# Onfleet Webhooks

[Onfleet](https://onfleet.com) is a last-mile delivery platform. Each Onfleet
webhook targets one trigger (task started, task completed, worker on duty,
and so on). When you create a webhook, Onfleet first sends an unsigned
`GET ?check=<random_string>` request to validate the URL. After that, every
event arrives as a signed `POST` with an `X-Onfleet-Signature` header.

## When to Use This Skill

* How do I receive Onfleet webhooks?
* Why won't Onfleet save my webhook URL? (the `GET ?check=` handshake)
* How do I verify the `X-Onfleet-Signature` header?
* Why does my Onfleet signature never match? (the secret is hex)
* How do I handle `taskCompleted`, `taskFailed`, `taskEta` or `workerDuty`?

## Validation Handshake

When a webhook is created, Onfleet sends `GET <your url>?check=<random_string>`.
Reply `200` with the exact string as a plain-text body. Onfleet doesn't
accept a `204`, and the request is not signed, so don't run signature
verification on it. The string changes on every attempt.

```javascript
app.get('/webhooks/onfleet', (req, res) => {
  const check = req.query.check;
  if (typeof check !== 'string') {
    return res.status(400).type('text/plain').send('Missing check parameter');
  }
  res.status(200).type('text/plain').send(check);
});

```

## Signature Verification (core)

|  |  |
| --- | --- |
| Header | `X-Onfleet-Signature` |
| Algorithm | HMAC-SHA512, hex digest |
| Key | The organisation's webhook secret, hex-decoded to bytes |
| Signed content | The raw request body bytes. No timestamp, no prefix |

The secret is a hex string. Use it as an HMAC key only after hex-decoding it.
If you pass the string as-is, you get a well-formed digest that never matches.

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

function verifyOnfleetSignature(rawBody, signatureHeader, secretHex) {
  if (!signatureHeader) {
    return { valid: false, error: 'Missing X-Onfleet-Signature header' };
  }

  const expected = crypto
    .createHmac('sha512', Buffer.from(secretHex, 'hex'))
    .update(rawBody)
    .digest('hex');

  const received = Buffer.from(signatureHeader.trim().toLowerCase(), 'utf8');
  const computed = Buffer.from(expected, 'utf8');

  // timingSafeEqual throws on unequal lengths, so check first.
  if (received.length !== computed.length) {
    return { valid: false, error: 'Invalid signature' };
  }
  if (!crypto.timingSafeEqual(received, computed)) {
    return { valid: false, error: 'Invalid signature' };
  }
  return { valid: true };
}

```

Python:

```python
expected = hmac.new(bytes.fromhex(secret_hex), raw_body, hashlib.sha512).hexdigest()
received = signature_header.strip().lower().encode("utf-8")
valid = hmac.compare_digest(received, expected.encode("utf-8"))

```

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

The examples also check that the secret is valid hex before using it, because
`Buffer.from(hex)` silently truncates at the first non-hex character. If it
isn't, they fail closed with a `500`. They acknowledge with `200` first and
then process the event, and cover every documented trigger in their tests.

## Webhook Triggers (`triggerName` / `triggerId`)

| ID | `triggerName` | ID | `triggerName` |
| --- | --- | --- | --- |
| 0 | `taskStarted` | 16 | `workerDeleted` |
| 1 | `taskEta` (threshold, seconds) | 17 | `SMSRecipientOptOut` |
| 2 | `taskArrival` (threshold, meters) | 18 | `autoDispatchJobCompleted` |
| 3 | `taskCompleted` | 19 | `taskBatchCreateJobCompleted` |
| 4 | `taskFailed` | 20 | `routeOptimizationJobCompleted` |
| 5 | `workerDuty` | 22 | `routePlanCreated` |
| 6 | `taskCreated` | 23 | `routePlanStarted` |
| 7 | `taskUpdated` | 24 | `routePlanCompleted` |
| 8 | `taskDeleted` | 25 | `workerUpdated` |
| 9 | `taskAssigned` | 26 | `routePlanUpdated` |
| 10 | `taskUnassigned` | 27 | `routePlanUnassigned` |
| 12 | `taskDelayed` (threshold, seconds) | 28 | `routePlanAssigned` |
| 13 | `taskCloned` | 29 | `routePlanDelayed` |
| 14 | `smsRecipientResponseMissed` | 30 | `predictedTaskDelay` (threshold, seconds) |
| 15 | `workerCreated` |  |  |

IDs 11 and 21 aren't documented. For trigger 30, the trigger table says
`predictedTaskDelay` but the sample payload says `taskPredictedDelay`, so
accept both names or route on `triggerId`. See
[references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/onfleet-webhooks/references/overview.md) for payload shapes.

## Handler Rules

1. Answer `GET ?check=` with `200` plain text. Otherwise Onfleet won't save the webhook.
2. Verify the raw bytes, then parse. Re-serialised JSON won't match.
3. Respond within 10 seconds. Onfleet retries timed-out requests every 30
  seconds and disables the webhook after 300 consecutive failures.
4. Treat every field as optional. Fields that don't apply to a trigger
  arrive as `null`, and `data` can be `{}`.
5. Mind the units. `time` is milliseconds; `etaSeconds` and `delay` are seconds.
6. Acknowledge unknown triggers. Log them and return `200`.

## Environment Variables

```bash
# Organisation-wide webhook secret (hex). Not your API key.
# Dashboard: Configuration -> API & Webhook -> Show Secret
ONFLEET_WEBHOOK_SECRET=your_hex_webhook_secret

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 onfleet --path /webhooks/onfleet

```

Run your handler first, so the `GET ?check=` handshake succeeds when you create
the webhook in Onfleet.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/onfleet-webhooks/references/overview.md): Triggers, payload shape, units, naming quirks
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/onfleet-webhooks/references/setup.md): Find the secret, create webhooks (dashboard or API), thresholds
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/onfleet-webhooks/references/verification.md): HMAC-SHA512 details, common mistakes, debugging