Webhook Events & Verification

Webhook Events & Verification

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

Webhook registration is account-scoped: a single webhook configuration is shared across every store in your organization. The x-retailer-id header is not used on /dd/v1/webhooks — only your API key is required.

Setup

$curl -X POST https://api.goautolane.com/dd/v1/webhooks \
> -H "Authorization: Bearer YOUR_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{
> "url": "https://your-server.com/webhooks/autolane",
> "events": ["pickup_status.changed", "tracking_link.created", "pickup.vehicle_location.changed"]
> }'

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

URL requirements

The url you register must:

  • Use https:// (plain http:// is rejected).
  • Use a hostname, not a bare IP literal (IPv4 or IPv6).
  • Resolve to a publicly routable address — loopback, RFC1918 private, link-local, multicast, and CGNAT ranges are rejected.

Invalid URLs are rejected at registration with 400:

1{
2 "success": false,
3 "error": "…",
4 "code": "INVALID_WEBHOOK_URL",
5 "detail": "BLOCKED_TARGET"
6}

detail is one of INVALID_URL, NOT_HTTPS, BARE_IP_REJECTED, DNS_RESOLUTION_FAILED, or BLOCKED_TARGET. The same validation runs again at dispatch time on every delivery attempt, so a hostname whose DNS record later flips to a private address will stop delivering rather than be exploited.

Events

EventDescription
pickup_status.changedFired when a delivery transitions to: EN_ROUTE_TO_STORE, ARRIVED_AT_STORE, WAITING_FOR_LOAD, LOADED, ASSIGNED, EN_ROUTE_TO_CUSTOMER, ARRIVED_AT_CUSTOMER, COMPLETED, FAILED, or CANCELLED. Includes an optional vehicle_location field when you’re also subscribed to pickup.vehicle_location.changed.
tracking_link.createdFired when a customer tracking link is generated for a delivery.
pickup.vehicle_location.changedFired on significant vehicle movement (~50 m) and at a 60-second heartbeat while a delivery is in transit (EN_ROUTE_TO_STORE through EN_ROUTE_TO_CUSTOMER). Cancels automatically on terminal status (ARRIVED_AT_CUSTOMER, COMPLETED, FAILED, CANCELLED).

Payloads

When an event fires, Autolane sends a POST request to your registered URL with a JSON body. Use the external_delivery_id field to correlate events with the order_id you supplied when creating the delivery.

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 "vehicle_location": {
20 "latitude": 30.2672,
21 "longitude": -97.7431,
22 "recorded_at": "2026-04-10T11:59:58Z"
23 }
24 }
25}

The vehicle_location field is only present if your webhook is subscribed to pickup.vehicle_location.changed. When subscribed, it’s an object with a fresh fix (≤ 5 minutes old), or null when no fresh fix is available or no vehicle is assigned yet. If your webhook isn’t subscribed to the location event, the field is omitted entirely.

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://track.goautolane.com/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 when the underlying telemetry sample was taken (ISO 8601). It may lag the outer timestamp by a few seconds — use timestamp to order events relative to each other, and recorded_at if you need the precise moment of the GPS fix.

The event fires on two triggers while a delivery is in the active window (EN_ROUTE_TO_STORE, ARRIVED_AT_STORE, WAITING_FOR_LOAD, LOADED, EN_ROUTE_TO_CUSTOMER):

  1. Movement-driven: the vehicle has moved enough since the last fix (~50 m).
  2. Heartbeat: at least 60 seconds have elapsed since the last emit, even if the vehicle is stationary.

The event stops firing automatically once the delivery transitions to ARRIVED_AT_CUSTOMER, COMPLETED, FAILED, or CANCELLED. No action is required on your end to unsubscribe per-delivery.

Event ordering note

Webhook delivery is at-least-once with no cross-type ordering guarantee. A pickup.vehicle_location.changed event may arrive shortly after a terminal pickup_status.changed for the same delivery — reconcile relative ordering using the timestamp field rather than receive-order.

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 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(
44 `Delivery ${event.data.external_delivery_id} at (${latitude}, ${longitude})`,
45 );
46 }
47
48 res.status(200).send('OK');
49});

Idempotency

Each delivery includes an X-Autolane-Delivery-Id header — a UUID that stays the same across retries. Store processed delivery IDs and reject duplicates to ensure idempotent handling.

1const processedDeliveries = new Set();
2
3app.post('/webhooks/autolane', (req, res) => {
4 const deliveryId = req.headers['x-autolane-delivery-id'];
5
6 if (processedDeliveries.has(deliveryId)) {
7 return res.status(200).send('Already processed');
8 }
9
10 // Process the event...
11 processedDeliveries.add(deliveryId);
12
13 res.status(200).send('OK');
14});

Retries

Failed deliveries (non-2xx responses or timeouts) are retried up to 7 times with exponential backoff.