Webhook lifecycle
Everything a partner integration needs to receive Gando webhooks reliably: how deliveries and retries work, how to verify a request is genuinely from Gando, how to troubleshoot a failing endpoint, and how to rotate a signing secret without losing events.
This page covers the Partner Webhooks feature (/api/partner/v1/webhooks/*). For request/response schemas, see the Partner API Reference (tag Webhooks).
Concept
A Gando webhook is an HTTP POST that Gando sends to a URL you register, whenever something changes on a resource you care about — a deposit's status, a rental operator finishing partner connect, a booking session completing. It's the push counterpart to polling GET /api/partner/v1/deposits.
Two properties make a webhook trustworthy enough to act on automatically:
- HMAC-SHA256 signing — every request carries an
X-Gando-Signatureheader computed from the body with a secret only you and Gando know. Anyone canPOSTto your public endpoint pretending to be Gando; only a valid signature proves the request's integrity (the body wasn't altered) and authenticity (it was built with your secret). - A signed timestamp — the signature isn't computed over the body alone, it's computed over
"<timestamp>.<rawBody>". Without the timestamp, a captured request with a valid signature could be replayed indefinitely — the body is legitimate, but stale. Binding the timestamp into the signature lets you reject old requests even if the signature itself checks out, closing that replay window.
Delivery and retry timeline
Gando sends the event once, then retries on a fixed backoff schedule if your endpoint doesn't acknowledge it with a 2xx within the delivery timeout (10 seconds).
Full attempt history — status codes, errors, and timestamps — is available per endpoint via GET /api/partner/v1/webhooks/{id}/deliveries. Check it first whenever a delivery seems to be missing before assuming Gando never sent it.
Respond fast, process later. Verify the signature synchronously, enqueue the event, and return 2xx immediately. Any processing that takes longer than the delivery timeout should happen after you've acknowledged the request.
Troubleshooting decision tree
Signature failures are almost always one of five causes. Work through them in this order — each check is cheap and rules out the more expensive ones.
Recommended timestamp tolerance
Enforce a 5-minute window between X-Gando-Timestamp and your server's current time before accepting a delivery. This is the same tolerance Gando applies when verifying its own inbound PSP webhooks — tight enough to close the replay window, loose enough to absorb normal clock skew and queueing delay.
Webhook delivery error catalog
This table covers failures you'll observe while verifying an inbound Gando delivery — distinct from the Partner API error catalog, which covers error responses from calling the webhook management endpoints (webhook_not_found, webhook_access_denied, etc.).
| Symptom | Root cause | Partner action | Gando support action |
|---|---|---|---|
signature_mismatch — computed signature never matches X-Gando-Signature | Wrong secret, or signing string built incorrectly (must be HMAC_SHA256("<timestamp>.<rawBody>", secret), hex-encoded, prefixed sha256=) | Re-check the signing string construction against the reference examples on the Partner API Reference; confirm the secret in use via GET /webhooks/{id}/secret | Confirm on Gando's side which secret last signed deliveries to that endpoint id |
timestamp_out_of_tolerance — signature is otherwise valid, but X-Gando-Timestamp is older than your tolerance | Clock skew, queue backlog, or a replayed/captured request | Sync server clocks (NTP); if legitimate deliveries are being rejected, check for processing backlogs before the verification step | Check GET /webhooks/{id}/deliveries for the original delivery timestamp vs. when it reached the partner |
unknown_or_rotated_secret — signature fails right after a rotation | POST /webhooks/{id}/rotate-secret invalidates the previous secret immediately (Gando keeps one active secret per endpoint, no overlap window) | Fetch and deploy the new secret before rotating in production — see Secret rotation below | Confirm rotation timestamp against when the partner's verification started failing |
payload_altered_in_transit — signature fails, timestamp is fresh, secret is correct | A proxy, load balancer, WAF, or web framework parsed and re-serialized the JSON body before it reached your verification code, changing byte-for-byte content (key order, whitespace, escaping) | Verify the signature against the raw request body captured before any JSON parsing or middleware touches it | Provide a raw-body capture from the partner's edge for comparison against what Gando sent |
non_2xx_or_timeout — retries exhausted after 5 attempts | Endpoint down, slow, or returning an error status during the retry window | Check endpoint health and response time during the failure window; make the handler idempotent so retries are safe rather than trying to eliminate them | Investigate via GET /webhooks/{id}/deliveries; offer a manual re-send if the underlying issue is now fixed |
Secret rotation
POST /api/partner/v1/webhooks/{id}/rotate-secret generates a new signing secret and returns it once. Gando keeps exactly one active secret per webhook endpoint — the moment you rotate, every subsequent delivery is signed with the new secret and verification against the old one starts failing. There is no dual-secret grace window today.
To rotate without dropping events, don't rotate the secret on your only endpoint — stand up a second one:
POST /api/partner/v1/webhookswith the sameevents, a new URL (or the same URL if your consumer can tell endpoints apart by id), and a fresh secret is returned.- Deploy your consumer's verification code with the new secret, listening on the new endpoint.
POST /api/partner/v1/webhooks/{id}/testagainst the new endpoint to confirm signature verification passes end-to-end before relying on it.- Once confirmed,
DELETE /api/partner/v1/webhooks/{id}on the old endpoint.
This avoids any window where a delivery could arrive signed with a secret your consumer no longer accepts.
Replay protection
Gando does not currently attach a dedicated delivery or event id to outbound payloads — there's no X-Gando-Delivery-Id header or id field to key a dedup table on directly. Retries reuse the same status transition, so protect against double-processing using the entity and the status it transitioned to:
deposit.*andbooking_deposit_session.*events → dedupe ondata.id+data.status.rental_operator.linked→ dedupe ondata.accountId(adddata.linkedAtif you need per-link granularity).
Model it as an idempotent state upsert, not an append-only event log — a replayed retry for the same (id, status) pair should update the same row, not create a duplicate:
create table webhook_processed_events (
entity_id text not null,
status text not null,
processed_at timestamptz not null default now(),
primary key (entity_id, status)
);
insert into webhook_processed_events (entity_id, status)
values ($1, $2)
on conflict (entity_id, status) do nothing
returning entity_id;
If the insert returns no row, this (entity_id, status) pair was already processed — skip any side effects (emails, ledger writes) but still return 2xx so Gando stops retrying.
Testing locally
- Start your local webhook consumer, then expose it with ngrok:
ngrok http <port>. - Register the ngrok HTTPS URL against your Gando staging account:
POST /api/partner/v1/webhookswith{ "url": "https://<your-subdomain>.ngrok.app/webhooks/gando" }. - Trigger a synthetic event:
POST /api/partner/v1/webhooks/{id}/testsends adeposit.activatedpayload (the endpoint must be subscribed todeposit.activatedor thedeposit.status_changedwildcard). - Inspect the result via
GET /api/partner/v1/webhooks/{id}/deliveries— status code, response time, and any error your endpoint returned.
ngrok's free-tier URL changes every time the tunnel restarts — PATCH /api/partner/v1/webhooks/{id} with the new URL each session, or use a paid static domain if you're testing against the same endpoint repeatedly.