WebhooksOverview

Webhooks

We POST a signed JSON body to a URL you own when something happens to one of your bookings. That is the whole mechanism. Everything below is detail about what we send, how you prove it came from us, and what we do when your endpoint does not answer.

If you would rather pull than be pushed, GET /v1/events serves the same events on demand and needs no endpoint at all.

Register an endpoint

curl -sS -X POST "https://{your-base-url}/v1/webhooks" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://hooks.example.com/drivecars",
    "description": "Production booking sync",
    "event_types": ["booking.confirmed", "booking.cancelled"]
  }'

Requires the webhooks:manage scope.

{
  "id": 42,
  "url": "https://hooks.example.com/drivecars",
  "description": "Production booking sync",
  "event_types": ["booking.confirmed", "booking.cancelled"],
  "status": "active",
  "consecutive_failures": 0,
  "last_success_at": null,
  "last_failure_at": null,
  "created_at": "2026-09-09T10:00:00.000Z",
  "updated_at": "2026-09-09T10:00:00.000Z",
  "secret": "whsec_0f3b8a1c2d4e5f60718293a4b5c6d7e8"
}

secret is returned exactly once, here. It is encrypted at rest on our side and no other response — the list, the single-endpoint read, a later PATCH — has a field for it. If you lose it, your only route back is rotating it. Store it before you close the terminal.

Which URLs are accepted

Your URL is validated at registration, again on any PATCH that changes it, and again on every single delivery attempt. That last one is not redundancy for its own sake: a hostname that resolved somewhere safe when you registered it can resolve somewhere else tomorrow.

A URL is rejected with 400 unsafe_url if it:

RuleWhy
is not https://Plain HTTP puts your event payloads on the wire in the clear.
uses a port other than 443Non-standard ports are the usual shape of an internal service.
carries a username or passwordhttps://user:pass@host/ — credentials in a URL we would store.
is an IP literalhttps://203.0.113.10/hook — we require a hostname.
is localhostOurs, not yours.
ends in .internal, .amazonaws.com, or .drivecars.aiInternal namespaces.
resolves to a private, loopback, link-local, CGNAT, or multicast addressIncluding IPv6 and IPv4-in-IPv6 forms of the same.

The last rule checks every address the hostname resolves to, not just the first, and the connection is then pinned to the address that was checked. A 3xx from your endpoint is never followed — it counts as a failed delivery like any other non-2xx.

The practical consequence for you: you cannot point a DriveCars webhook at localhost while developing. Use a tunnelling service that gives you a public HTTPS hostname, or use GET /v1/events instead, which needs no inbound connectivity at all.

What arrives

POST /drivecars HTTP/1.1
Content-Type: application/json
DriveCars-Signature: t=1788987793,v1=8bcb785698d21745...
DriveCars-Event-Id: evt_9c1f4a7b2e8d05364f1a9b7c3d2e5f80
DriveCars-Event-Type: booking.confirmed
DriveCars-Delivery-Id: 5591
 
{"id":"evt_9c1f4a7b2e8d05364f1a9b7c3d2e5f80","type":"booking.confirmed","data":{  }}

The body has exactly three keys:

Key
idThe event id, evt_ + 32 hex characters. Stable across retries and across a replay.
typeOne of the event types.
dataThe payload. For booking events, the same booking object GET /v1/bookings/{id} returns.

Note what is not in the delivered body: no created_at, and no livemode. Those two fields exist on the same event when you read it back from GET /v1/events — the delivered body is the narrower shape. If you need either, read the event by id.

What we expect back

Answer 2xx, quickly. Anything else is a failure and we will retry.

Do the minimum inside the request — verify the signature, write the event somewhere durable, return 200. Do the real work afterwards. Our connection timeout is 10 seconds, and a slow handler turns into a retry, which turns into a second copy of the event.

Two responses mean something specific:

  • 410 Gone — we stop permanently. The delivery dead-letters on the spot and the endpoint is disabled. Return it only when you mean “this URL is never coming back”.
  • Anything else non-2xx, including 404 and 401 — an ordinary failure, and we retry on the schedule.

Before you go live

  • Verify every signature. An unverified webhook endpoint is an unauthenticated write path into your system.
  • Deduplicate on id. Delivery is at-least-once. You will eventually see the same event twice.
  • Do not assume order. Events for one booking can arrive out of sequence after a retry.