Skip to main content
A webhook is a signed HTTPS POST that Nexio sends to your server when a run reaches a terminal status, or when Nexio attaches a correction to a finished run. Use webhooks to learn about results without polling. They are delivered at least once and in no guaranteed order, so treat each delivery as a signal. GET /api/v1/runs/{run_id} is the source of truth for a run; if a webhook and the run disagree, the run wins. Corrections are the exception: after Nexio records a correction, GET keeps returning the original output (see run.superseded). Webhooks are one of three event mechanisms on the platform. Events overview explains how they differ from platform events and inbound events. Credential. Every route on this page accepts an organization API key or a capability-scoped key (nxsk_v1_...) that holds webhooks:manage. A scoped key without it gets 403 insufficient_capability. An endpoint belongs to the environment of the key that created it, and you cannot choose another. See Authentication and Environments.

How it works

  1. You register an HTTPS endpoint and the events it wants. Nexio returns a signing secret once.
  2. When a run in that environment reaches a terminal status, Nexio creates one delivery for each active endpoint that lists the event.
  3. Nexio posts the event body with X-Nexio-* headers and an HMAC-SHA256 signature over the exact bytes.
  4. Your receiver verifies the signature, records X-Nexio-Delivery, and answers 2xx within 10 seconds.
  5. If the receiver fails, Nexio retries on the endpoint’s retry policy. A delivery that cannot succeed ends in dead_letter, and you can resend it after you fix the receiver.

Create an endpoint

POST /api/v1/webhooks registers an endpoint and returns its signing secret. The secret is shown only in this response. Store it in your secret manager before you do anything else.
Response 201 Created:
Unknown fields are refused with 400 invalid_request. An environment holds at most 10 active endpoints; creating or re-enabling an eleventh active endpoint gets 409 webhook_limit_exceeded. The secret is whsec_ followed by 64 hex characters. To change an endpoint later, call Update webhook endpoint with any of url, events, description, active, auth_token, or payload_mode. Send null or an empty string to clear description or auth_token. Setting active to false cancels the endpoint’s pending deliveries. A new url applies to new deliveries only, because each delivery keeps the URL it was created with, including when you resend it. You can also manage endpoints in the portal under Settings, then Webhooks.

Event catalog

An endpoint receives only the events it lists. There are four. What a run.superseded delivery carries depends on your organization’s delivery setup. In one setup, each correction is its own event, and a full body carries the corrected output. In the other, the event is keyed to the run, a full body carries the run’s stored (original) output, and a later correction of the same run sends no new delivery to an endpoint that already has one. Treat run.superseded as a signal that a correction exists. Webhook events has a full example body and the trigger rules for each event.

Envelope

Every delivery body has the same envelope. The machine-readable schema is WebhookEvent in the OpenAPI document.

Payload modes

Each endpoint has a payload_mode. The envelope, headers, and signature are the same in both modes. Only data.run changes. A thin body is smaller than 1024 bytes. Nexio cuts an error longer than 256 bytes to 256 characters plus a short truncation note, and replaces it with a fixed message if the body would still reach 1024 bytes. Fetch the run for the full error. A full body has no fixed ceiling, because it carries the whole run output. A thin run.completed delivery:

Headers

Nexio does not follow redirects. A 3xx answer is a permanent failure.

Verify each delivery

The signature is a hex HMAC-SHA256, keyed with the endpoint secret, over this exact byte string:
  1. Read the raw request bytes before any JSON parsing.
  2. Split X-Nexio-Signature on commas. Take the one t value and every v1 value. Refuse a header with two t values.
  3. Require t to equal X-Nexio-Timestamp, and require it to be decimal digits.
  4. Refuse the delivery when abs(now - t) is more than 300 seconds. Exactly 300 is accepted.
  5. Compute the HMAC with every secret you currently accept (two during a rotation) and compare against every v1 value in constant time. Accept if any pair matches.
  6. Record X-Nexio-Delivery with an atomic insert. See Deduplicate.
  7. Parse the JSON only after the signature passes.
Never verify a re-serialized JSON object. Whitespace, key order, Unicode escapes, and number spelling can change while the parsed value stays the same, and the signature will not match.

Reference verifiers

The Python, JavaScript, and Go verifiers below are the files Nexio’s own delivery tests run against the signature test file, a JSON list of signed requests with the expected accept or refuse result for each. CI checks that the published copies match the tested source byte for byte. The TypeScript tab is a typed port of the JavaScript verifier for Node 20 and later with the same checks; it is not one of the tested or byte-checked files. Each keeps seen delivery IDs in memory to show the deduplication contract; replace that set with a durable, shared store before production.

Wire a verifier into a receiver

These receivers use the reference verifiers above. They read the raw body, verify, answer 2xx for a duplicate, and hand the work to a queue so the response goes out well inside the 10 second timeout.

Deduplicate on X-Nexio-Delivery

Delivery is at least once. The same delivery can arrive more than once, for example when your receiver committed its work but the response timed out. X-Nexio-Delivery stays the same across every retry and every resend of one delivery, so it is the key to deduplicate on.
  • Insert the delivery ID into a table with a unique constraint before you apply side effects, in the same transaction when you can.
  • When the insert conflicts, answer 2xx and do nothing else. An error answer makes Nexio retry a delivery you already handled.
  • Do not deduplicate on the body id alone. Two endpoints receive the same event id with different delivery IDs.
Order is not guaranteed. A run.superseded delivery can arrive before the run.completed for the same run. Compare with the run you already stored before you overwrite anything. GET /api/v1/runs/{run_id} returns the original output, not the correction.

Retry policies

Every endpoint has a retry policy, fixed when the endpoint was created. New endpoints get standard-v3. The receiver timeout is 10 seconds on every policy. An attempt that has not answered in 10 seconds counts as a transport failure. Without Retry-After, standard-v3 gives up after about 3.3 days (about 4 days at maximum jitter), and standard-v2 after about 16 hours at maximum jitter. How Nexio classifies each answer: On standard-v3 and standard-v2, a 429 or 503 with a valid Retry-After (seconds or an HTTP date) replaces the base wait when it is longer, capped at 12 hours per retry. Jitter applies after that. legacy-v1 ignores Retry-After.

Permanent failures

A permanent failure ends the delivery in dead_letter on that attempt. It does not use the rest of the attempt budget. A receiver that answers 400, 401, 403, or 404 by mistake, or a load balancer that redirects, dead-letters every delivery on the first try. If your receiver is briefly unable to accept work, answer 503 (with Retry-After if you know when to come back) so Nexio retries.

Delivery states

Each delivery has one status. This is the only place the model is defined; other pages link here. Each attempt is also recorded with its own outcome (success, retry, dead_letter, permanent_failure, or late_result_dropped), status code, and duration. Read them with Get webhook delivery. The API never returns the signed body bytes or your receiver’s response body.

Automatic disable

Each time a delivery ends in dead_letter, Nexio looks at the endpoint’s 10 most recent finished deliveries (success or dead_letter), counting only those that finished since the endpoint was last changed (created, updated, re-enabled, or its secret rotated). If there are 10 and all are dead_letter, Nexio deactivates the endpoint. It sets active to false, deactivated_by to system, and deactivated_reason to a message that names the delivery that tipped it, and it cancels the endpoint’s pending deliveries. Nexio skips the check while another delivery to the endpoint is in_progress; the next dead letter checks again. The portal shows the reason on the endpoint page. To recover:
  1. Fix the receiver.
  2. Send a test delivery from the portal and confirm it passes.
  3. Set active to true in the portal or with Update webhook endpoint. Re-enabling clears the deactivation fields.
  4. Reconcile the runs that finished while the endpoint was off by polling, and resend any dead_letter deliveries you need.

Rotate the secret

POST /api/v1/webhooks/{endpointID}/rotate-secret returns a new secret once. The old secret keeps working for an overlap that Nexio sets from the environment’s kind. You cannot change it. During the overlap every delivery carries two v1 values, one per secret. Accept both secrets until previous_secret_expires_at, then drop the old one.
Response 200 OK:
A zero-downtime rotation: call rotate, add the returned secret to your receiver’s accepted list alongside the old one, then remove the old secret after previous_secret_expires_at. The new secret exists only once rotate returns it. During the overlap each delivery carries two v1 signatures, one per secret, so your receiver keeps verifying with the old secret until you install the new one.

Resend a dead-letter delivery

After you fix the receiver, find the deliveries that dead-lettered and resend them.
One page of the list response:
Resend response 200 OK:
What a resend does:
  • It works only on a dead_letter delivery whose endpoint is active and not deleted. Anything else, including an unknown delivery ID, gets 409 delivery_not_resendable.
  • It keeps the delivery ID, so your deduplication still recognizes it, and it sends the same body bytes that were stored when the delivery was created. Each attempt is signed again with a fresh timestamp and the endpoint’s current secret.
  • It posts to the URL stored on the delivery. If you fixed the receiver by changing the endpoint’s url, a resend still goes to the old URL; reconcile those runs by polling instead.
  • It starts a new generation: resend_generation goes up by one, attempt_count goes back to 0, and next_attempt_trigger becomes manual_resend.
  • The new generation has the delivery’s own attempt limit, the max_attempts it already shows (8 on standard-v3). A resend does not switch the delivery to a different policy.
  • Nexio records which key asked for the resend.

Send a test delivery

The portal can send a test delivery to an endpoint: open Settings, then Webhooks, open the endpoint, and go to its Settings tab. In Send test delivery, pick Small (about 1 KB) or Large (about 16 KB; the body is at least 16,000 bytes, to check that your gateway accepts large bodies), then choose Send. A test delivery is a run.completed event with data.run.output.test set to true and the header X-Nexio-Test: true. It is signed like a real delivery, with the same headers, so it proves your verification. It creates no delivery record and is not retried. Test sends are limited to 6 per minute per organization on each Nexio API server. There is no public API route for test deliveries. Verify a test delivery normally in your receiver, then skip business side effects when X-Nexio-Test is true.

Reconcile by polling

Webhooks tell you that something changed. Polling tells you what is true now. Keep every run_id you submit, and reconcile with GET /api/v1/runs/{run_id} when:
  • a delivery is in dead_letter or cancelled,
  • the endpoint was deactivated for any period,
  • a run you submitted has produced no delivery after your expected run time,
  • you receive a thin body and need the output.
Polling does not return corrections: GET keeps returning the original output after Nexio records one. See Runs for polling and statuses.

Platform event deliveries to the same endpoint

Nexio can also configure an automation for your organization that posts platform events to one of your registered webhook endpoints (the deliver_webhook effect, described in Automations). Those deliveries use the same headers and the same signature scheme, so the verifiers above work unchanged. The body is different:
How to tell the two apart: A simple rule: a body with a data.run object is a run webhook; a body with dispatch_id is a platform event delivery. Deduplicate both on X-Nexio-Delivery. is_replay is true when the delivery comes from a replay (Nexio re-running the automation) rather than from the event’s first dispatch.

Limits

Common errors

The full error envelope is described in Errors.

Webhook events

Trigger rules and a full example body for each event.

Runs

Run statuses, polling, and the run object inside every delivery.

Create webhook endpoint

The endpoint reference for every webhook route.

Events overview

How webhooks relate to platform events and inbound events.
Last modified on September 25, 2026