# Rocketlane Webhooks

Rocketlane (customer onboarding and project delivery) sends a `POST` with a JSON body to a
webhook you define in **Settings > Webhooks**. A **Send webhook** action in an automation
fires it. Payloads are **not signed**. The only authentication Rocketlane offers is HTTP
Basic auth, which the docs call optional but recommended.

> In Hookdeck: choose the **Rocketlane** source type. Choose Basic authentication and set a
> username and password on the Rocketlane webhook.

## When to Use This Skill

* How do I receive Rocketlane webhooks?
* How do I verify a Rocketlane webhook? (Basic auth, there is no signature)
* Why does my Rocketlane webhook endpoint return 401?
* How do I handle `TASK_CREATED`, `TASK_UPDATED` or `PROJECT_CREATED` events?
* Why does Rocketlane show my webhook delivery as failed?

## Verification (core)

Rocketlane sends `Authorization: Basic base64(username:password)` using the credentials you
entered on the webhook. Parse it strictly, split on the **first** colon, and compare the
username and the password each in constant time. Evaluate both before deciding:

```javascript
const crypto = require('crypto');

// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
const safeEqual = (a, b) =>
  crypto.timingSafeEqual(
    crypto.createHash('sha256').update(a).digest(),
    crypto.createHash('sha256').update(b).digest());

function verifyRocketlane(authorization, user = process.env.ROCKETLANE_WEBHOOK_USERNAME,
                          pass = process.env.ROCKETLANE_WEBHOOK_PASSWORD) {
  if (!user || !pass) return false;                       // never accept when unconfigured
  const m = /^basic[ \t]+([A-Za-z0-9+/]+={0,2})$/i.exec((authorization || '').trim());
  if (!m || m[1].length % 4) return false;                // strict base64 only
  const bytes = Buffer.from(m[1], 'base64');
  if (bytes.toString('base64') !== m[1]) return false;    // non-canonical padding/bits
  let decoded;
  try {
    decoded = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
  } catch {
    return false;                                         // not valid UTF-8
  }
  const i = decoded.indexOf(':');                         // first colon
  if (i < 0) return false;
  const userOk = safeEqual(decoded.slice(0, i), user);
  const passOk = safeEqual(decoded.slice(i + 1), pass);   // no short-circuit
  return userOk && passOk;
}

```

Return `401` when this fails, `400` for a body that isn't a JSON object, and `200` once
handled. Express must read the body raw (`express.raw({ type: '*/*' })`) and parse it only
after the check. In Python use `base64.b64decode(token, validate=True)` and
`hmac.compare_digest` on **bytes** (a `str` with non-ASCII characters raises). Full Python
and Next.js versions are in [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/rocketlane-webhooks/references/verification.md).

**Know what this does and does not prove.** A static shared credential authenticates the
*sender*, not the *body*, and gives **no replay protection** (no signature, no timestamp). Use
HTTPS only, and rotate the username and password if they leak. Dedupe on `eventId`.

**Credentials must arrive on the first request.** The docs don't say whether Rocketlane sends
Basic credentials preemptively, and Hookdeck never answers with a `401` plus
`WWW-Authenticate` challenge: it verifies the request as received.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/rocketlane-webhooks/examples/express/),
> [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/rocketlane-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/rocketlane-webhooks/examples/fastapi/).

## Event Types

The body has `eventType` and `resourceType`. These `eventType` values come from Rocketlane's
Event Payload page:

| `eventType` | Fires when |
| --- | --- |
| `TASK_CREATED` | A task is created |
| `TASK_UPDATED` | A task changes (adds `changeLog` with `changedFields`, `from`, `to`) |
| `TASK_DELETED` | A task is deleted |
| `PROJECT_CREATED`, `PROJECT_UPDATED`, `PROJECT_ARCHIVED`, `PROJECT_DELETED` | Project lifecycle |
| `PHASE_CREATED`, `PHASE_UPDATED`, `PHASE_DELETED` | Phase lifecycle |

The docs' payload-structure snippet prints `"TASK_UPDATED."` with a stray full stop; the real
sample payloads have none, and the examples match the plain strings. Rocketlane lists only
`TASK` as a supported `resourceType` and says only task triggers are supported for
webhooks automations today, so expect task events first.

## Delivery Rules

* Only `200` or `204` count as delivered. Any other status, or no response in **10 seconds**, is a failure.
* Order is not guaranteed to be chronological. The docs say Rocketlane delivers each event exactly once.

## Environment Variables

```bash
ROCKETLANE_WEBHOOK_USERNAME=rocketlane          # as entered on the Rocketlane webhook
ROCKETLANE_WEBHOOK_PASSWORD=replace-with-a-long-random-password

```

## Local Development

```bash
npx hookdeck-cli listen 3000 rocketlane --path /webhooks/rocketlane

```

No account required. Paste the printed URL as the webhook URL in Rocketlane (HTTPS only).

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/rocketlane-webhooks/references/overview.md) - Payload fields and event types
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/rocketlane-webhooks/references/setup.md) - Creating the webhook and automation in Rocketlane
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/rocketlane-webhooks/references/verification.md) - Basic auth check in Node, Python and Next.js, gotchas
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/rocketlane-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests