Webhook Events & Verification

Webhook Events & Verification

Register a webhook endpoint to receive real-time delivery status updates instead of polling.

Setup

$curl -X POST https://api.goautolane.com/dd/v1/webhooks \
> -H "Authorization: Bearer YOUR_API_KEY" \
> -H "x-retailer-id: a1b2c3d4-e5f6-7890-1234-567890abcdef" \
> -H "Content-Type: application/json" \
> -d '{
> "url": "https://your-server.com/webhooks/autolane",
> "events": ["pickup_status.changed", "tracking_link.created"]
> }'

The response includes a signing secret (prefixed alwh_). Store it securely. It is only shown once. Subsequent GET /dd/v1/webhooks responses return the secret masked as alwh_****.... If you need to rotate your secret later, call PUT /dd/v1/webhooks with { "rotate_secret": true } and the new secret is returned in full.

One webhook URL per organization

Each Autolane organization has a single webhook subscription. Re-registering with POST /dd/v1/webhooks while one already exists returns 409 WEBHOOK_EXISTS; use PUT /dd/v1/webhooks to update the URL or subscribed events instead. If you operate multiple sites under one organization, all of their delivery events arrive at the same URL. Note that external_delivery_id is unique per (retailer, external_delivery_id) in the API, so two sites under the same org could legitimately use the same external_delivery_id value for different deliveries; ensure your IDs are org-unique (for example, prefix them with your site identifier) before routing on that field alone. DSP aggregators that bridge multiple retailer organizations should register one webhook per partner organization’s API key.

Events

EventDescription
pickup_status.changedFires on each pickup status transition: ASSIGNED, EN_ROUTE_TO_STORE, ARRIVED_AT_STORE, WAITING_FOR_LOAD, LOADED, EN_ROUTE_TO_CUSTOMER, ARRIVED_AT_CUSTOMER, COMPLETED, FAILED, CANCELLED.
tracking_link.createdFires when a customer-facing tracking link is generated for a delivery (at the moment the pickup is scheduled).
pickup.vehicle_location.changedFires when the assigned autonomous vehicle’s location updates during the delivery. Subscribe to this event to drive live map UIs on your side.

Subscribe to additional events at any time by calling PUT /dd/v1/webhooks with an updated events array.

Payloads

When an event fires, Autolane sends a POST request to your registered URL with a JSON body.

pickup_status.changed

1{
2 "event": "pickup_status.changed",
3 "timestamp": "2026-04-10T12:00:00Z",
4 "data": {
5 "external_delivery_id": "ACME-12345",
6 "status": "EN_ROUTE_TO_CUSTOMER",
7 "previous_status": "ASSIGNED",
8 "estimated_arrival": "2026-04-10T12:30:00Z",
9 "customer": {
10 "phone": "+15551234567"
11 },
12 "delivery_address": {
13 "street": "123 Main St",
14 "unit": "4B",
15 "city": "Austin",
16 "state": "TX",
17 "zip": "78701"
18 }
19 }
20}

estimated_arrival is the estimated arrival time at the customer — the delivery ETA locked when the quote was created. It is null only when no delivery ETA is available for the delivery.

If you are also subscribed to pickup.vehicle_location.changed, this event additionally carries a vehicle_location field with the vehicle’s position at the moment the status changed. The field is null when no fresh location fix (within the last 5 minutes) is available, and is omitted entirely when you are not subscribed to the location event.

tracking_link.created

1{
2 "event": "tracking_link.created",
3 "timestamp": "2026-04-14T17:00:00Z",
4 "data": {
5 "external_delivery_id": "ACME-12345",
6 "tracking_url": "https://autola.ne/t/tok_abc123def456",
7 "customer": {
8 "phone": "+14155551234"
9 }
10 }
11}

pickup.vehicle_location.changed

1{
2 "event": "pickup.vehicle_location.changed",
3 "timestamp": "2026-05-11T15:00:30Z",
4 "data": {
5 "external_delivery_id": "ACME-12345",
6 "vehicle_location": {
7 "latitude": 30.2672,
8 "longitude": -97.7431,
9 "recorded_at": "2026-05-11T15:00:28Z"
10 }
11 }
12}

recorded_at is the timestamp on the underlying telemetry sample. It may lag the outer timestamp by a few seconds.

Events are not strictly ordered across event types. A pickup.vehicle_location.changed event may arrive shortly after a terminal pickup_status.changed (ARRIVED_AT_CUSTOMER, COMPLETED) for the same delivery. Use the timestamp field to reconcile relative ordering on your side.

Request headers

Each request includes these headers:

HeaderDescription
X-Autolane-SignatureHMAC-SHA256 hex digest of the raw body, signed with your secret
X-Autolane-Delivery-IdUUID for this delivery attempt. Stable across retries
Content-Typeapplication/json
User-AgentAutolane-Webhook/1.0

Your endpoint should return any 2xx status code to acknowledge receipt. Non-2xx responses (and request timeouts past 10 seconds) trigger retries.

Verifying Signatures

Use the signing secret from your webhook registration to verify that each delivery is authentically from Autolane. The X-Autolane-Signature header contains an HMAC-SHA256 hex digest of the raw request body, computed using your alwh_-prefixed secret.

Always verify signatures using a timing-safe comparison to prevent timing attacks:

1import { createHmac, timingSafeEqual } from 'crypto';
2import express from 'express';
3
4const app = express();
5
6// Capture raw body for webhook signature verification
7app.use(
8 express.json({
9 verify: (req, _res, buf) => {
10 req.rawBody = buf;
11 },
12 }),
13);
14
15function verifyWebhookSignature(rawBody, signature, secret) {
16 // secret is your alwh_-prefixed signing secret from webhook registration
17 const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
18 const providedBuffer = Buffer.from(signature ?? '', 'utf8');
19 const expectedBuffer = Buffer.from(expected, 'utf8');
20 if (providedBuffer.length !== expectedBuffer.length) return false;
21 return timingSafeEqual(providedBuffer, expectedBuffer);
22}
23
24app.post('/webhooks/autolane', (req, res) => {
25 const signature = req.get('x-autolane-signature');
26 const isValid = verifyWebhookSignature(
27 req.rawBody,
28 signature,
29 process.env.AUTOLANE_WEBHOOK_SECRET,
30 );
31
32 if (!isValid) {
33 return res.status(401).send('Invalid signature');
34 }
35
36 const event = req.body;
37 if (event.event === 'pickup_status.changed') {
38 console.log(`Delivery ${event.data.external_delivery_id} is now ${event.data.status}`);
39 } else if (event.event === 'tracking_link.created') {
40 console.log(`Tracking link created for ${event.data.external_delivery_id}`);
41 } else if (event.event === 'pickup.vehicle_location.changed') {
42 const { latitude, longitude } = event.data.vehicle_location;
43 console.log(`Vehicle at ${latitude}, ${longitude}`);
44 }
45
46 res.status(200).send('OK');
47});

Replay protection

The signature covers the request body only and does not include a timestamp. To reject replayed events at your endpoint, combine two payload-level signals:

  • The top-level timestamp field on every event is an ISO 8601 string. Reject events whose timestamp is more than a small tolerance (e.g., 5 minutes) outside your server clock.
  • The X-Autolane-Delivery-Id header is a UUID stable across retries for the same delivery attempt. Treat it as the dedupe key (see the next section).

Idempotency

Each delivery includes an X-Autolane-Delivery-Id header, a UUID that stays the same across retries. Store processed delivery IDs in a shared store (Redis, your database, etc.) and reject duplicates so retried deliveries don’t double-fire your business logic.

1import { createClient } from 'redis';
2
3const redis = createClient({ url: process.env.REDIS_URL });
4await redis.connect();
5
6const DEDUP_TTL_SECONDS = 60 * 60 * 24 * 7; // keep dedupe state for 7 days
7
8app.post('/webhooks/autolane', async (req, res) => {
9 const deliveryId = req.headers['x-autolane-delivery-id'];
10
11 // SET key value NX EX <ttl>: atomic "set if not exists with TTL".
12 // Returns null when the key already exists, indicating a duplicate.
13 const reserved = await redis.set(`autolane:webhook:${deliveryId}`, '1', {
14 NX: true,
15 EX: DEDUP_TTL_SECONDS,
16 });
17 if (reserved === null) {
18 return res.status(200).send('Already processed');
19 }
20
21 // Process the event...
22
23 res.status(200).send('OK');
24});

Note that external_delivery_id is the create-side idempotency key (see Idempotency in the Overview). It identifies the underlying delivery and stays the same across all events for that delivery. X-Autolane-Delivery-Id identifies one webhook event delivery (stable across the retries for that event) and changes from one event to the next.

Retries

Failed deliveries (non-2xx responses or request timeouts past 10 seconds) are retried with exponential backoff on a 30-second base:

AttemptApproximate delay after the previous attempt
1initial delivery
230 seconds
360 seconds
4120 seconds
5240 seconds

After 5 failed attempts the event is given up on. Failed attempts also increment a per-organization consecutive_failures counter; a successful delivery resets it to zero.

If the counter reaches 50 consecutive failures the webhook endpoint is automatically disabled and the notification email on your account receives a heads-up. Call PUT /dd/v1/webhooks (with any valid update body, or just { "url": "<your-current-url>" }) to re-enable the endpoint. Re-enabling resets the failure counter to zero.