Gando

Gando

Developer Portal

gando.app
Developer portal

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-Signature header computed from the body with a secret only you and Gando know. Anyone can POST to 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).

Loading diagram…

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.

Loading diagram…

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.).

SymptomRoot causePartner actionGando support action
signature_mismatch — computed signature never matches X-Gando-SignatureWrong 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}/secretConfirm 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 toleranceClock skew, queue backlog, or a replayed/captured requestSync server clocks (NTP); if legitimate deliveries are being rejected, check for processing backlogs before the verification stepCheck GET /webhooks/{id}/deliveries for the original delivery timestamp vs. when it reached the partner
unknown_or_rotated_secret — signature fails right after a rotationPOST /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 belowConfirm rotation timestamp against when the partner's verification started failing
payload_altered_in_transit — signature fails, timestamp is fresh, secret is correctA 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 itProvide a raw-body capture from the partner's edge for comparison against what Gando sent
non_2xx_or_timeout — retries exhausted after 5 attemptsEndpoint down, slow, or returning an error status during the retry windowCheck endpoint health and response time during the failure window; make the handler idempotent so retries are safe rather than trying to eliminate themInvestigate 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:

  1. POST /api/partner/v1/webhooks with the same events, a new URL (or the same URL if your consumer can tell endpoints apart by id), and a fresh secret is returned.
  2. Deploy your consumer's verification code with the new secret, listening on the new endpoint.
  3. POST /api/partner/v1/webhooks/{id}/test against the new endpoint to confirm signature verification passes end-to-end before relying on it.
  4. 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.* and booking_deposit_session.* events → dedupe on data.id + data.status.
  • rental_operator.linked → dedupe on data.accountId (add data.linkedAt if 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

  1. Start your local webhook consumer, then expose it with ngrok: ngrok http <port>.
  2. Register the ngrok HTTPS URL against your Gando staging account: POST /api/partner/v1/webhooks with { "url": "https://<your-subdomain>.ngrok.app/webhooks/gando" }.
  3. Trigger a synthetic event: POST /api/partner/v1/webhooks/{id}/test sends a deposit.activated payload (the endpoint must be subscribed to deposit.activated or the deposit.status_changed wildcard).
  4. 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.