Agent skill

Stuart Webhooks Skill

Receive and authenticate Stuart last-mile delivery webhooks. Use when setting up a Stuart webhook endpoint, checking the webhook authentication header, debugging 401s, or handling v3 topics like package_created, courier_assigned, package_delivered and package_canceled.

Install this skill

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


Stuart sends a JSON webhook to your endpoint as each package moves through a delivery: created, courier assigned, courier arriving, delivered, canceled and so on. Webhooks are not signed. Instead you choose a Webhook authentication header (its name must start with X-) and a Webhook authentication key in the Stuart dashboard or webhook API, and Stuart adds that one header to every request. Your endpoint authenticates the request by comparing it with the value you configured.

A static shared credential authenticates the sender, not the body, and gives no replay protection (there is no signature and no timestamp). Serve the endpoint over HTTPS, keep the value secret, and rotate it in Stuart and in your config if it leaks. Use event_id to deduplicate.

When to Use This Skill

  • How do I receive Stuart webhooks?
  • How do I verify the Stuart webhook authentication header and key?
  • Does Stuart sign webhook payloads? (No: use the shared header value)
  • How do I handle package_created, courier_assigned or package_delivered events?
  • Why does my Stuart webhook endpoint return 401?

Verification (core)

const crypto = require('crypto');

// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
  const ha = crypto.createHash('sha256').update(String(a)).digest();
  const hb = crypto.createHash('sha256').update(String(b)).digest();
  return crypto.timingSafeEqual(ha, hb);
}

function verifyStuartRequest(headers, name = process.env.STUART_WEBHOOK_HEADER || 'X-Webhook-Secret',
                             secret = process.env.STUART_WEBHOOK_SECRET) {
  if (!secret) return false;                          // never accept when unconfigured
  const received = headers[name.toLowerCase()];       // Node lowercases header names
  return Boolean(received) && safeEqual(received, secret); // WHOLE value, no 'Bearer ' parsing
}
  • The header name is whatever you entered as the authentication header (must start with X-); read it from STUART_WEBHOOK_HEADER (default X-Webhook-Secret). Look it up case-insensitively.
  • STUART_WEBHOOK_SECRET is the exact, whole header value, including any Bearer prefix you typed into the authentication key. Compare the whole value; don't strip or parse a prefix.
  • Return 401 for a missing header or a mismatch, 400 for a body that isn't a JSON object, 200 once handled.
  • Read the body raw (express.raw({ type: '*/*' })) and authenticate before parsing it.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Event Types (v3 topics)

Version 3 payloads carry topic, event_id, webhook_id, occurred_at and a details object (with details.package.id and details.package.reference).

TopicSent When
package_createdThe package is in the Stuart system
courier_assignedA courier accepted the package
courier_arrivingThe courier entered the pickup or dropoff geofence (details.task)
package_splitThe package was split; details.new_package.id is the new package
courier_waitingThe courier has waited 2 minutes at pickup or dropoff (details.task)
package_deliveringThe courier has the package and is heading to the dropoff
package_deliveredThe package was delivered
package_canceledThe package will not be delivered (details.cancelation)
package_returningAn undeliverable package is on its way back (details.return)
package_returnedThe package was returned to the pickup location
courier_reassigningThe courier was unassigned and a new one will be found (details.reassigning)
courier_movingCourier coordinates while heading to the dropoff (details.coordinates)

Environment Variables

STUART_WEBHOOK_HEADER=X-Webhook-Secret   # the authentication header name you set in Stuart
STUART_WEBHOOK_SECRET=your-secret-value  # the exact authentication key (whole header value)

Local Development

Stuart can't reach localhost. Tunnel with the Hookdeck CLI (no account required):

npx hookdeck-cli listen 3000 stuart --path /webhooks/stuart

Use the printed URL as the webhook URL in Stuart.

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 4, 2026

View on GitHub →