Webhooks
Webhooks let an Odeva app receive an HTTP POST whenever something happens in an organisation, instead of polling the API. Each subscription is scoped to one organisation and to a list of event types. Deliveries are signed so a receiver can verify they came from Odeva.
Use webhooks when an external service needs to react to bookings — issuing invoices, opening doors, syncing a CRM, kicking off internal automation. For one-off lookups or interactive flows, call the GraphQL API directly.
1. Create a subscription
Section titled “1. Create a subscription”Create a subscription with the createWebhookSubscription mutation. The mutation returns the signingSecret exactly once at creation time, so store it before moving on.
mutation CreateWebhook($input: CreateWebhookSubscriptionInput!) { createWebhookSubscription(input: $input) { webhookSubscription { id name endpointUrl eventTypes status } signingSecret errors }}{ "input": { "name": "Reservations to billing", "endpointUrl": "https://hooks.example.com/odeva", "eventTypes": ["reservation.created", "reservation.cancelled"], "appInstallationId": "AppInstallation-123" }}Notes:
endpointUrlmust be a public HTTP or HTTPS URL. Delivery to loopback, link-local, and private IP ranges is blocked.appInstallationIdis optional. If you call the mutation with an app API key, the subscription is attached to that installation automatically and is removed when the app is uninstalled.- The signing secret is prefixed with
whsec_and is only readable from the create response. Rotate by deleting and recreating the subscription.
2. Event types
Section titled “2. Event types”The supported event types are returned by the webhookEventTypes query and currently include:
reservation.createdreservation.confirmedreservation.cancelledreservation.checked_inreservation.checked_outapp.installedapp.uninstalled
A subscription may listen to any subset. app.uninstalled is always delivered to subscriptions attached to the installation being removed, even after the subscription is disabled, so the receiver can clean up on its side.
3. Receive a delivery
Section titled “3. Receive a delivery”Each delivery is a POST to the subscription endpoint with Content-Type: application/json.
Headers:
X-Odeva-Event-Id— the platform event ID.X-Odeva-Event-Type— the event type, e.g.reservation.confirmed.X-Odeva-Delivery-Id— the delivery ID. Use this as your idempotency key.X-Odeva-Timestamp— Unix seconds when the request was signed.X-Odeva-Signature—sha256=<hex>HMAC of the signed payload.User-Agent—Odeva-Webhooks/1.0.
Body:
{ "id": "PlatformEvent-7f9c…", "type": "reservation.confirmed", "version": 1, "organization_id": "Organization-42", "occurred_at": "2026-05-26T09:14:31Z", "subject": { "type": "Reservation", "id": "Reservation-1234" }, "payload": { "...": "event-specific fields" }, "correlation_id": "optional-trace-id"}Respond with any 2xx status code as soon as you have accepted the event. Long-running work belongs in a background queue on your side — Odeva treats responses slower than 10 seconds as failures.
4. Verify the signature
Section titled “4. Verify the signature”The signature is an HMAC-SHA256 of "{timestamp}.{body}" using the subscription’s signing secret.
import crypto from "node:crypto";
export function verifyOdevaSignature(req: { headers: Record<string, string>; rawBody: string;}, signingSecret: string): boolean { const timestamp = req.headers["x-odeva-timestamp"]; const signature = req.headers["x-odeva-signature"]; if (!timestamp || !signature) return false;
const expected = "sha256=" + crypto .createHmac("sha256", signingSecret) .update(`${timestamp}.${req.rawBody}`) .digest("hex");
const a = Buffer.from(signature); const b = Buffer.from(expected); return a.length === b.length && crypto.timingSafeEqual(a, b);}Reject requests with a missing or mismatched signature. Reject timestamps that are far outside your tolerance window (5 minutes is a reasonable default) to limit replay attacks. Use the raw request body for the HMAC — re-serialising the parsed JSON will change the bytes and break verification.
5. Retries and delivery state
Section titled “5. Retries and delivery state”A delivery is considered successful when your endpoint returns a 2xx response. On any other response, network error, or timeout, Odeva retries up to 5 attempts with delays of 1 minute, 5 minutes, 15 minutes, and 1 hour. After the final failed attempt the delivery is marked failed and the subscription’s lastFailureAt and lastError are updated.
Inspect recent deliveries with the webhookDeliveries query, optionally filtered by subscriptionId. Each WebhookDelivery exposes status, attemptCount, responseStatus, a truncated responseBody, lastError, and nextAttemptAt.
Use retryWebhookDelivery(id: ID!) to retry a failed delivery manually after fixing your endpoint, or updateWebhookSubscription to change the endpoint URL, event types, or status without recreating the subscription.
6. Required scope
Section titled “6. Required scope”The webhook queries and mutations require the webhooks:manage scope on the API key calling them. Request that scope when you register the app, alongside any other scopes the integration needs.