Skip to main content
Webhooks let your service react to changes in ION without polling. You register a receiver, which is a URL plus a shared secret then subscribe it to specific events. ION then posts a JSON message to your URL whenever a matching change is committed.

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.
Stick with polling when:
  • 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:
  • action is one of CREATE, UPDATE, or DELETE, the operation that fires the webhook.
  • resource is one of ION’s webhookable entities. The schema exposes the full list: use the API Playground and inspect the ResourceEnum type.
Common resources customers subscribe to:

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 the x-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:
Implement the same construction on your side and compare:
Always use 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.
ION times out a delivery that takes longer than 5 seconds to respond. If your processing is slow, acknowledge with a 200 right away and queue the work to run asynchronously downstream.

Idempotency and deduplication

Use event_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:
The sharedSecret in the response is your HMAC key for signature verification. It’s shown once, so store it like an API key.
Capture the sharedSecret at creation. It’s returned exactly once. If you lose it, you have to delete and recreate the receiver.
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:
A receiver can have multiple subscriptions.

Test the receiver

Before pointing production traffic at a new receiver:
  1. Register against your sandbox tenant. See the Sandbox guide.
  2. Trigger known events. Create, update, and delete a run step in the sandbox UI, then verify your receiver logs the deliveries.
  3. Inspect failed deliveries. Query webhookEvents filtered by subscription_id and status = "failed" to see retry history and the request and response bodies ION recorded.
This is the fastest way to debug “my receiver isn’t firing.” The cause is usually a non-200 response code, an SSL certificate issue, or a firewall.

Monitor deliveries in ION

The same delivery log is browsable in the app: in ION, go to Developer Platform > Webhook Deliveries. The Summary banner counts loaded deliveries by status, and each row shows one attempt: Timestamp, Status (succeeded, retrying, sending, or failed; a retried delivery passes through several), HTTP Status, Receiver Endpoint, and Response Body. The response body is usually the fastest clue when a delivery fails. Click Load More to page through older deliveries.