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:
| Rule | Why |
|---|---|
is not https:// | Plain HTTP puts your event payloads on the wire in the clear. |
| uses a port other than 443 | Non-standard ports are the usual shape of an internal service. |
| carries a username or password | https://user:pass@host/ — credentials in a URL we would store. |
| is an IP literal | https://203.0.113.10/hook — we require a hostname. |
is localhost | Ours, not yours. |
ends in .internal, .amazonaws.com, or .drivecars.ai | Internal namespaces. |
| resolves to a private, loopback, link-local, CGNAT, or multicast address | Including 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 | |
|---|---|
id | The event id, evt_ + 32 hex characters. Stable across retries and across a replay. |
type | One of the event types. |
data | The 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
404and401— 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.