Reservations

Reservations API

The Reservations API lets fleet partners reserve an arrival window at an Autolane stall so a runner can load an order into an autonomous vehicle or sidewalk robot. Discover reservable sites, zones, and retailers, create and manage reservations, and register webhook endpoints that receive a signed event each time a reservation changes status.

Authentication

All requests require an Autolane API key in the Authorization header:

$curl https://api-sandbox.goautolane.com/rs/v1/retailers \
> -H "Authorization: Bearer YOUR_API_KEY"

Create API keys in the Autolane Portal under Integrations > API Keys. Keys are environment-specific: a sandbox key only works against the sandbox host, and a production key only against the production host. Toggle dev mode in the portal to choose which environment a new key targets. See Authentication for the full walk-through.

Your key must also belong to an organization enrolled as an Autolane fleet partner; other keys receive 403 NOT_FLEET_PARTNER even with the right permission. Contact Autolane to enroll.

Permissions

Each endpoint checks a permission on your key:

PermissionGrants
retailers:readGET /rs/v1/retailers
reservations:readGET /rs/v1/reservations and GET /rs/v1/reservations/{id}
reservations:writeCreate, move, and cancel reservations; the sandbox-only advance endpoint
webhooks:manageEvery /rs/v1/webhooks endpoint

Webhook endpoints receive whole reservations, so pointing one at a URL costs the scope that reads them. Registering an endpoint with POST /rs/v1/webhooks needs reservations:read as well as webhooks:manage. The same holds for a PUT /rs/v1/webhooks/{id} that changes url or subscribed_events, or sets is_active to true. What counts is the change, not the field you send: a value you send back unchanged costs nothing. Setting is_active to false and rotating the secret need webhooks:manage alone, so you can always stop a stream or replace a leaked secret. That also means a key holding webhooks:manage alone can mint a new secret and read it in full, and that secret is the key that signs reservation events into your endpoint; grant the scope accordingly. A 403 names the fields that asked for more.

Hosts

EnvironmentBase URL
Productionhttps://api.goautolane.com
Sandboxhttps://api-sandbox.goautolane.com

Sandbox site

The sandbox contains one synthetic site so you can integrate before any real configuration exists. Its ids are stable and safe to reference in tests:

EntityNameId
SiteSandbox Mall00000000-0000-4000-8000-000000000001
ZoneSandbox AV Zone A00000000-0000-4000-8000-000000000002
RetailerSandbox Coffee Co.00000000-0000-4000-8000-000000000003

Real sites and retailers configured for sandbox testing appear alongside it in the same response.

Reservation lifecycle

A reservation moves through requested, confirmed, arrived, completed, or canceled:

StatusMeaning
requestedAccepted, not yet confirmed. In this version a create confirms synchronously, so you will not observe it.
confirmedThe window is booked and a stall is assigned. The create response already carries this status.
arrivedThe vehicle is at the stall.
completedThe order is loaded and the reservation is done. Terminal.
canceledTerminal. canceled_by says who canceled: partner, runner, ops, or system, and cancel_reason carries the reason text.

Rules that follow from the lifecycle:

  • The arrival window can change (PATCH /rs/v1/reservations/{id}) only while the reservation is requested or confirmed. Once it is arrived, completed, or canceled, the window is fixed. Moving a window keeps the assigned stall and changes nothing else, and the new window is checked against the same rules as create.
  • Cancel (DELETE /rs/v1/reservations/{id}) works from any status except completed and is safe to retry: canceling an already canceled reservation returns 200 with the same reservation. A cancel through the API records canceled_by: partner, and the optional reason query parameter (up to 500 characters) is stored on the reservation and echoed back as cancel_reason.
  • Every status change fires a reservation.status_changed webhook. Moving a window fires nothing, because the window is not a status and you already know about the change. A cancel retry that finds the reservation already canceled returns 200 and fires nothing: treat the 200 as your confirmation and never wait on the webhook.

Creating a reservation

POST /rs/v1/reservations books an arrival window in a zone for one retailer order:

$curl -X POST https://api-sandbox.goautolane.com/rs/v1/reservations \
> -H "Authorization: Bearer YOUR_API_KEY" \
> -H "Content-Type: application/json" \
> -H "Idempotency-Key: order-8471" \
> -d '{
> "zone_id": "00000000-0000-4000-8000-000000000002",
> "retailer_id": "00000000-0000-4000-8000-000000000003",
> "order_number": "8471",
> "end_user_name": "Jordan Lee",
> "end_user_phone": "+15125550123",
> "arrival_window_start": "2026-09-01T17:00:00Z",
> "arrival_window_end": "2026-09-01T17:30:00Z",
> "open_link_url": "https://fleet.example.com/vehicles/veh-42/open"
> }'

Create-time rules:

  • Zone and retailer. zone_id and retailer_id come from GET /rs/v1/retailers, and the retailer must be actively supported in the zone. Poll discovery to keep your picker current; zones with no supported retailers are omitted from its response.
  • Arrival window. The window must start in the future, start no more than 24 hours ahead, and be between 5 and 120 minutes wide. Those three limits are deployment defaults and can be tuned per environment.
  • Phone. end_user_phone must be E.164, for example +15125550123.
  • Open link. open_link_url is the HTTPS endpoint our backend calls to open the vehicle door or trunk. It must use https:// and resolve to a public address; we check that at create and reject anything else with 422 INVALID_OPEN_LINK.
  • Stall assignment. A stall is assigned by soft hold: the active stall holding the fewest reservations that overlap your window and are not yet completed or canceled. Capacity never rejects a booking, so a busy zone stacks reservations on its least loaded stall.

In this version a reservation is confirmed synchronously, so the 201 response already carries status: "confirmed". That confirm is a real status change, so a successful create also fires a reservation.status_changed webhook carrying confirmed for the reservation you just got a 201 for. Expect it; it is not a duplicate.

Idempotent retries

Send the optional Idempotency-Key header (1 to 255 characters) to make retries safe. Use one key per reservation you intend to create, and resend the same value when you retry. A retry with a key you already used returns the stored reservation with 200 instead of creating a second one. A retry whose zone, retailer, order number, end user name, end user phone, or open link differs from the stored reservation is rejected with 422 IDEMPOTENCY_KEY_REUSED; the arrival window is excluded from that comparison because it can be changed later with PATCH.

If create answers 503 URL_VALIDATION_UNAVAILABLE, our capacity to validate open_link_url is exhausted; that is not a problem with your URL. Retry the same request, reusing your Idempotency-Key if you sent one.

Errors

Every error response has one shape:

1{
2 "success": false,
3 "error": "Arrival window must start in the future",
4 "code": "WINDOW_IN_PAST"
5}

code is stable and machine-readable; branch on it, never on the error text. The codes:

CodeStatusMeaning
VALIDATION_ERROR400Malformed request: invalid JSON, a field that fails validation, an id that is not a UUID, or an empty update body
INVALID_API_KEY401Missing or invalid API key
WRONG_ENV_KEY403A sandbox key on the production host, or a production key on the sandbox host
PERMISSION_DENIED403The key lacks a permission the call needs; the message names what is missing
NOT_FLEET_PARTNER403The organization is not enrolled as an Autolane fleet partner
RESERVATION_NOT_FOUND404No such reservation for your organization. Another organization’s reservation returns this same 404, never a 403, so ids cannot be probed
WEBHOOK_NOT_FOUND404No such webhook endpoint for your organization
NOT_FOUND404The advance endpoint on production, which answers 404 before your key is even checked
INVALID_STATE409The reservation’s status does not allow the action: moving a window once the reservation is arrived, completed, or canceled, canceling a completed reservation, or advancing past the end
ZONE_NOT_FOUND422zone_id does not name a reservable zone
RETAILER_NOT_SUPPORTED_IN_ZONE422The retailer is not currently supported in that zone
ZONE_HAS_NO_STALLS422The zone has no active stalls to hold
WINDOW_IN_PAST422The arrival window starts in the past
WINDOW_TOO_FAR_OUT422The window starts more than 24 hours ahead
WINDOW_TOO_SHORT422The window is under 5 minutes wide
WINDOW_TOO_LONG422The window is over 120 minutes wide
INVALID_OPEN_LINK422open_link_url is not a public HTTPS endpoint
IDEMPOTENCY_KEY_REUSED422The Idempotency-Key was already used with a different body; the arrival window is excluded from the comparison
INVALID_WEBHOOK_URL422The webhook URL is not a public HTTPS endpoint
WEBHOOK_LIMIT_REACHED422The organization already holds 10 webhook endpoints
INTERNAL_ERROR500Something failed on our side
URL_VALIDATION_UNAVAILABLE503Our capacity to validate your URL is exhausted, not a problem with your URL. Retry the same request

Webhooks

Register HTTPS endpoints that receive reservation events. Autolane signs every delivery with the secret belonging to the endpoint it is sent to.

Registering an endpoint

Registration needs reservations:read as well as webhooks:manage (see Permissions):

$curl -X POST https://api-sandbox.goautolane.com/rs/v1/webhooks \
> -H "Authorization: Bearer YOUR_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{
> "url": "https://your-server.com/webhooks/autolane",
> "subscribed_events": ["reservation.status_changed"]
> }'

The URL must resolve to a public address; we check that at registration. The response carries the endpoint’s signing secret (prefixed whsec_) in full for the only time. Store it before you move on: every later read masks it as whsec_…cdef, showing only the last four characters, and the only way to get a usable secret again is to rotate it.

An organization may hold at most 10 endpoints, and deactivated endpoints still count against that limit because endpoints cannot be deleted. To move traffic to a new URL once you are at the limit, point an existing endpoint at it with PUT /rs/v1/webhooks/{id}.

Unlike creating a reservation, this endpoint ignores the Idempotency-Key header. A create you retry after a lost response can register a second endpoint that then receives every event alongside the first, turning each status change into two deliveries. Duplicates show up in GET /rs/v1/webhooks and can be switched off with PUT.

Event payload

The only event today is reservation.status_changed. Each delivery is a POST to your URL:

1{
2 "event_id": "0d9f2b1a-6c3e-4f7a-9b2d-8e1c5a4f3b21",
3 "event_type": "reservation.status_changed",
4 "occurred_at": "2026-09-01T17:04:12Z",
5 "data": {
6 "reservation": {
7 "reservation_id": "7f3e9c2b-1a5d-4e8f-b6c4-2d9a8e7f1c35",
8 "status": "arrived",
9 "zone_id": "00000000-0000-4000-8000-000000000002",
10 "stall_id": "b4d1f8a2-3c6e-4b9d-8f2a-5e7c1d9b3a64",
11 "stall_label": "Blue 3",
12 "retailer_id": "00000000-0000-4000-8000-000000000003",
13 "order_number": "8471",
14 "end_user_name": "Jordan Lee",
15 "end_user_phone": "+15125550123",
16 "arrival_window_start": "2026-09-01T17:00:00Z",
17 "arrival_window_end": "2026-09-01T17:30:00Z",
18 "open_link_url": "https://fleet.example.com/vehicles/veh-42/open",
19 "canceled_by": null,
20 "cancel_reason": null,
21 "created_at": "2026-09-01T14:22:05Z",
22 "updated_at": "2026-09-01T17:04:12Z"
23 }
24 }
25}

The reservation object is the same shape the reservation endpoints return, as it stood at the moment the event occurred.

Each request carries these headers:

HeaderDescription
X-Autolane-Signaturet=<unix-seconds>,v1=<hex>; see Verifying signatures
X-Autolane-Delivery-IdNames one delivery; the same value arrives again when that delivery retries
Content-Typeapplication/json

Verifying signatures

Every delivery carries X-Autolane-Signature: t=<unix-seconds>,v1=<hex>. The hex is an HMAC-SHA256 over the exact string "<t>.<raw request body>". The key is the endpoint secret as a whole string, whsec_ prefix included, taken as UTF-8 bytes: do not strip the prefix, and do not hex-decode the 48 characters after it. Verify by recomputing that HMAC over the raw bytes you received, before any JSON parsing, and comparing constant-time.

A matching hex is not enough. Check that t is close to your own clock and reject the delivery when it is not. Five minutes either way is a sound tolerance and leaves room for clock skew. Skip this step and a captured delivery stays valid forever. The timestamp is signed along with the body, so once the hex matches you know t came from us and not from whoever replayed it.

1import { createHmac, timingSafeEqual } from 'crypto';
2import express from 'express';
3
4const app = express();
5
6// Verification needs the raw bytes exactly as received, before any JSON parsing
7app.use(
8 express.json({
9 verify: (req, _res, buf) => {
10 req.rawBody = buf;
11 },
12 }),
13);
14
15const TOLERANCE_SECONDS = 5 * 60;
16
17function verifySignature(rawBody, header, secret) {
18 if (!header) return false;
19
20 // Header format: t=<unix-seconds>,v1=<hex>
21 const parts = Object.fromEntries(
22 header.split(',').map((pair) => pair.split('=', 2)),
23 );
24 const t = Number(parts.t);
25 const provided = parts.v1;
26 if (!Number.isInteger(t) || !provided) return false;
27
28 // Reject stale timestamps: a matching HMAC alone would let a
29 // captured delivery verify forever.
30 const nowSeconds = Math.floor(Date.now() / 1000);
31 if (Math.abs(nowSeconds - t) > TOLERANCE_SECONDS) return false;
32
33 // The key is the whole secret string, whsec_ prefix included, as UTF-8
34 const expected = createHmac('sha256', secret)
35 .update(`${t}.`)
36 .update(rawBody)
37 .digest('hex');
38
39 const providedBuffer = Buffer.from(provided, 'utf8');
40 const expectedBuffer = Buffer.from(expected, 'utf8');
41 if (providedBuffer.length !== expectedBuffer.length) return false;
42 return timingSafeEqual(providedBuffer, expectedBuffer);
43}
44
45app.post('/webhooks/autolane', (req, res) => {
46 const isValid = verifySignature(
47 req.rawBody,
48 req.get('x-autolane-signature'),
49 process.env.AUTOLANE_WEBHOOK_SECRET, // the whsec_... secret from registration
50 );
51 if (!isValid) {
52 return res.status(401).send('Invalid signature');
53 }
54
55 const { reservation } = req.body.data;
56 console.log(`Reservation ${reservation.reservation_id} is now ${reservation.status}`);
57
58 res.status(200).send('OK');
59});

A rejection spends a retry attempt, and the retry schedule below runs out: a delivery your clock rejected 8 times is dropped for good. There is no replay endpoint, so if you have rejected deliveries or suspect your clock is off, read current state from GET /rs/v1/reservations instead of waiting for us to send it again.

Delivery is at-least-once: dedup on two keys

Every delivery carries X-Autolane-Delivery-Id, and every payload carries event_id. They are different keys and you need both:

  • The delivery id names one delivery. Delivery is at-least-once, so the same delivery id can arrive more than once, and a repeat is not a new event. Dedup on the delivery id to drop retries.
  • The event id names one status change. Two of your endpoints subscribed to the same event (a retried create can leave two of them on one URL) turn a single change into two deliveries carrying two delivery ids and one shared event id. Dedup on the event id to collapse one event that reached you through more than one endpoint.

Ordering

Deliveries are not ordered. Each one retries on its own schedule and several are sent at once, so a later event can land before an earlier one. Order by occurred_at, never by arrival. Every payload carries the reservation as it stood at that moment, so applying deliveries in the order they turn up can walk a reservation backwards.

Retries

Up to 8 attempts per delivery. Any non-2xx response fails an attempt, and redirects are not followed, so a 3xx fails too. Each attempt is given 10 seconds. Backoff doubles between attempts (2, 4, 8 and so on, in minutes) and is capped at 60 minutes; after the eighth attempt the delivery is dropped. Acknowledge with a 2xx quickly and do heavy processing after you respond.

Managing endpoints

PUT /rs/v1/webhooks/{id} changes the URL, the subscribed events, or the active flag, and rotates the secret. Every field is optional, but the body must carry at least one of them. Concurrent PUTs are last write wins: every field you send is written whether or not it changed, so a body built from a stale GET puts back what it read.

  • Pausing. Set is_active to false to stop deliveries without giving up the slot. While an endpoint is inactive no new event is queued for it, but a delivery already queued gets no promise either way: it may be discarded, or let through if you switch back on quickly. Do not build on either outcome.
  • Repointing. Point an existing endpoint at a new URL to reuse a slot, but repoint only to a host you control: the URL is read when a delivery is sent, not when it is queued, so every pending delivery built up under the old URL goes to the new host, and a backlog spanning about three hours of the retry schedule can arrive as a burst.
  • Rotating the secret. Send rotate_secret: true to mint a new secret. The response returns it in full exactly once, and new deliveries are signed with it at once, but a delivery already picked up for sending keeps the old secret for up to 90 seconds. Accept both signatures for a few minutes after a rotation rather than cutting the old one off. Rotation is not idempotent, and this endpoint ignores Idempotency-Key: a PUT you retry after a lost response rotates a second time, and the secret from the first rotation is gone for good. If a rotate response goes missing, rotate again and use the secret you get back.

Sandbox walkthrough

POST /rs/v1/reservations/{id}/advance exists only on the sandbox host. It walks a reservation one lifecycle step per call (confirmed to arrived to completed) so you can drive the whole lifecycle without a vehicle, and each step fires the same reservation.status_changed webhook a real arrival would. On production the route always answers 404 NOT_FOUND, before your key is even checked.

Drive a reservation end to end against the sandbox:

1. Create a reservation using the sandbox zone and retailer (see Creating a reservation for the full request). The 201 response carries status: "confirmed", and your webhook endpoint receives a reservation.status_changed event carrying confirmed.

2. Advance to arrived:

$curl -X POST https://api-sandbox.goautolane.com/rs/v1/reservations/RESERVATION_ID/advance \
> -H "Authorization: Bearer YOUR_API_KEY"

The response carries status: "arrived" and your endpoint receives the matching event.

3. Advance to completed: run the same call again. The response carries status: "completed", the final event arrives, and the reservation is done.

4. Advance past the end: a third call answers 409 INVALID_STATE, because a completed reservation has no next step. The same 409 comes back for a canceled reservation.

To exercise the cancel path instead, stop after step 1 or 2 and call DELETE /rs/v1/reservations/RESERVATION_ID; the event that follows carries canceled with canceled_by: "partner".