Agent skill

Onfleet Webhooks Skill

Receive and verify Onfleet last-mile delivery webhooks. Use when setting up Onfleet webhook handlers, answering the GET ?check= validation handshake, debugging X-Onfleet-Signature verification (HMAC-SHA512 keyed with the HEX-DECODED org secret), or handling triggers like taskStarted, taskEta, taskCompleted, taskFailed, workerDuty and routePlanCompleted.

Install this skill

npx skills add hookdeck/webhook-skills --skill onfleet-webhooks


Onfleet 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.

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)

HeaderX-Onfleet-Signature
AlgorithmHMAC-SHA512, hex digest
KeyThe organisation's webhook secret, hex-decoded to bytes
Signed contentThe 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.

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:

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/, examples/nextjs/, 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)

IDtriggerNameIDtriggerName
0taskStarted16workerDeleted
1taskEta (threshold, seconds)17SMSRecipientOptOut
2taskArrival (threshold, meters)18autoDispatchJobCompleted
3taskCompleted19taskBatchCreateJobCompleted
4taskFailed20routeOptimizationJobCompleted
5workerDuty22routePlanCreated
6taskCreated23routePlanStarted
7taskUpdated24routePlanCompleted
8taskDeleted25workerUpdated
9taskAssigned26routePlanUpdated
10taskUnassigned27routePlanUnassigned
12taskDelayed (threshold, seconds)28routePlanAssigned
13taskCloned29routePlanDelayed
14smsRecipientResponseMissed30predictedTaskDelay (threshold, seconds)
15workerCreated

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 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

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

Local Development

# 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

Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Oct 19, 2026

View on GitHub →