Prove the endpoint is yours
Delivered during registration, and again on every re-registration, to the URL
you gave PUT /v1/webhooks/endpoint. Nothing real is ever sent to an endpoint
that has not answered this.
Answer 2xx with a JSON body echoing the same challenge string back:
{ "challenge": "<the value we sent>" }
Anything else leaves the endpoint pending_verification and no booking
notification is sent. Register again to retry.
It is signed exactly like a real delivery
Same headers, same scheme. That is deliberate: your verification code is exercised here, before a guest’s arrival depends on it.
Verify the signature over the raw request body, before any JSON parsing:
expected = HMAC_SHA256(secret, "v1:" + X-Signature-Timestamp + ":" + raw_body)
compared in hex, in constant time, against X-Signature, with
X-Signature-Timestamp inside five minutes of now. JSON.parse followed by
JSON.stringify does not round-trip byte-identically — key order, whitespace and
1.0 → 1 all move — so re-serialising the body will make every signature look
wrong.
Respond quickly: we wait five seconds, inside your own registration request.
Treat the challenge value as single-use. It is not a credential and it is not reused, so nothing needs to store it.
Authorizations
A short-lived partner token, obtained by exchanging your API key at
POST /token. Send it as Authorization: Bearer <access_token>.
Tokens live for fifteen minutes by default and never for more than an hour. Mint one per batch of work, not one per request.
The token carries the workspace and the environment its key was issued for. You cannot change either by asking: the exchange ignores everything in the request body for exactly that reason.
Never send it as a cookie or in a query string, and never store it — store the API key, mint tokens from it.
Body
Response
Echo { "challenge": "<value>" }. Any 2xx is accepted; the BODY is what
we check.
An opaque, single-use, unguessable string. Echo it back verbatim in a
2xx JSON response to activate the endpoint.