When to use webhooks vs polling
Use webhooks when:- Your system can accept inbound HTTP.
- You want low-latency reactions to changes.
- You want to avoid polling for changes.
- Your system can’t accept inbound HTTP.
- You only need data on a schedule.
- You need a pull-only model for compliance.
How delivery works
ION fires a webhook on the change itself, not on the API call that made it. Any committed change to a subscribed(resource, action) triggers a delivery, no matter where it came from: the GraphQL API, the ION UI, or an automated action.
Once a matching change commits, ION builds one delivery per matching subscription and POSTs the JSON body to your receiver URL with the x-ion-signature header. Delivery is at-least-once: if your receiver returns a non-success response, ION retries.
Event types
A subscription is a(resource, action) pair where:
actionis one ofCREATE,UPDATE, orDELETE, the operation that fires the webhook.resourceis one of ION’s webhookable entities. The schema exposes the full list: use the API Playground and inspect theResourceEnumtype.
Payload shape
Every delivery is a JSON POST with this shape:data.old and data.new contain only the fields that changed, and one side
is always null on a CREATE or DELETE, so don’t assume both are populated.
If you need the full current state of the entity, requery it through the API
using data.id. The webhook is a change notification, not a full snapshot.Signature verification
Every delivery includes an HMAC signature in thex-ion-signature header. Verify it before trusting the payload. Anyone can reach your endpoint. Only you and ION hold the secret.
The signature signs the receiver URL plus the data field’s keys and values (alphabetically sorted, null values excluded), not the raw body:
hmac.compare_digest or your language’s equivalent. A constant-time comparison prevents timing attacks.
Custom headers
Custom headers are useful when your receiver sits behind an API gateway that requires its own auth, such as an internal API key. Register them once per receiver via
WebhookHeader mutations and ION attaches them to every delivery.
Delivery guarantees and retries
- At-least-once. ION redelivers if your receiver returns anything other than the configured
expectedResponseCode(default 200). - Retries follow exponential backoff. Persistent failures eventually mark the event as failed. The failed event is preserved so you can inspect it by querying
webhookEvents. - No ordering guarantees between independent transactions. Two updates to the same row in one transaction always arrive before any update in a later transaction, but parallel transactions can interleave.
- No exactly-once. Build your receiver to be idempotent.
Idempotency and deduplication
Useevent_id as your idempotency key. Maintain a recent-events store on your side and short-circuit re-deliveries:
event_id is unique per delivery and stable across retries.
Register a receiver
Webhook receivers are registered via the GraphQL API. Create the receiver first:sharedSecret in the response is your HMAC key for signature verification. It’s shown once, so store it like an API key.
The webhookUri must be stable. Every change requires re-registering the
receiver. A fixed Ngrok tunnel works for local testing. A tunnel URL that
rotates on each restart does not work.
Subscribe to events
Add one or more subscriptions to the receiver. Each subscription is one(resource, action) tuple:
Test the receiver
Before pointing production traffic at a new receiver:- Register against your sandbox tenant. See the Sandbox guide.
- Trigger known events. Create, update, and delete a run step in the sandbox UI, then verify your receiver logs the deliveries.
- Inspect failed deliveries. Query
webhookEventsfiltered bysubscription_idandstatus = "failed"to see retry history and the request and response bodies ION recorded.