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_idto 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_assignedorpackage_deliveredevents? - 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 fromSTUART_WEBHOOK_HEADER(defaultX-Webhook-Secret). Look it up case-insensitively. STUART_WEBHOOK_SECRETis the exact, whole header value, including anyBearerprefix you typed into the authentication key. Compare the whole value; don't strip or parse a prefix.- Return
401for a missing header or a mismatch,400for a body that isn't a JSON object,200once 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).
| Topic | Sent When |
|---|---|
package_created | The package is in the Stuart system |
courier_assigned | A courier accepted the package |
courier_arriving | The courier entered the pickup or dropoff geofence (details.task) |
package_split | The package was split; details.new_package.id is the new package |
courier_waiting | The courier has waited 2 minutes at pickup or dropoff (details.task) |
package_delivering | The courier has the package and is heading to the dropoff |
package_delivered | The package was delivered |
package_canceled | The package will not be delivered (details.cancelation) |
package_returning | An undeliverable package is on its way back (details.return) |
package_returned | The package was returned to the pickup location |
courier_reassigning | The courier was unassigned and a new one will be found (details.reassigning) |
courier_moving | Courier 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
- references/overview.md - What Stuart webhooks are, topics and payload shape
- references/setup.md - Configure the webhook and authentication header
- references/verification.md - Header check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests