Skip to main content

Webhook deliveries

The Webhook deliveries screen lets you read the signed webhooks the connectors received, whether their signature was valid, and why a delivery was rejected. Portal path: /capabilities/webhooks. Developers and integrators use it to debug the webhooks of a business system; security officers use it to spot forged or replayed deliveries. The screen is read only and is drawn by portal/src/screens/capabilities.tsx.

What the screen shows​

  • Deliveries table, read with GET /v1/webhooks/deliveries (50 most recent rows; this route has no cursor). Columns: Connector, Delivery id, Signature valid, Status (accepted, rejected, duplicate), Reason, Received.
  • Filter Connector (connector name), then Filter.
  • When the list is empty: No webhook delivery, with the hint Deliveries appear when a connector receives a signed webhook.
  • A click on a row, or View details, shows every field of the row in a side panel.

The Reason column takes these values (services/control-plane/src/eai/domains/connectors/service.py):

ReasonStatusMeaning
bad_signaturerejectedThe X-EAI-Signature header does not match the payload.
no_secretrejectedNo webhook secret could be read for the connector (neither a webhook_secret credential nor spec.webhooks.secret_ref).
timestamp_outside_windowrejectedThe timestamp of the signature is more than 5 minutes away from the clock of the Control Plane.
duplicate_deliveryduplicateThe same delivery id was already accepted.
prompt_injection_blockedrejectedThe payload looks like a prompt injection.
emptyacceptedThe delivery was accepted and published as an event.

Actions​

Button or actionWhat it doesAPI callRoles
Refresh (icon)Reloads the list.GET /v1/webhooks/deliveriesAny tenant member
FilterKeeps the deliveries of one connector.GET /v1/webhooks/deliveries with connectorAny tenant member
Row click or View detailsShows the delivery row.NoneAny tenant member
Retry (error state)Sends the failed read again.Same callSame roles

A business system delivers a webhook with POST /v1/webhooks/{connector} and these headers:

X-EAI-Tenant: <tenant-slug>
X-EAI-Signature: t=<unix-time>,v1=<hex-hmac>
X-EAI-Delivery: <delivery-id>

<tenant-slug>: slug of your tenant. <unix-time>: time of the delivery in seconds (it may also come in a separate X-EAI-Timestamp header). <hex-hmac>: HMAC-SHA256, in hexadecimal, of <unix-time>. followed by the raw body, keyed with the webhook secret of the connector (sha256= is accepted instead of v1=). <delivery-id>: unique id of the delivery (when absent, the timestamp and the payload hash are used). The replay window is 5 minutes. An accepted delivery is published on the subject connectors.<connector>.webhook.

Roles​

The list only needs a tenant (services/control-plane/src/eai/domains/connectors/routes.py); the screen offers no write action. The webhook route itself is not called with a user session: it authenticates the sender by the signature, and accepts deliveries only for an active connector whose manifest enables webhooks. The webhook secret is stored with the Credentials action of the Connectors screen (kind webhook_secret), by tenant_admin or security_officer.

Typical tasks​

Debug a webhook that does not start anything​

  1. Filter on the connector. Each delivery is logged, accepted or not. Type the connector name in Connector and click Filter. Expected result: the deliveries of the connector, newest first. If it fails: No webhook delivery means nothing reached the Control Plane; check the URL and the X-EAI-Tenant header of the sender.
  2. Read the reason. The reason says which check failed. Read Signature valid, Status and Reason of the last row. Expected result: accepted with an empty reason. If it fails: bad_signature or no_secret means the secret differs or is missing; store it again with Credentials (kind webhook_secret).
  3. Check the event. An accepted delivery is published on the subject connectors.<connector>.webhook. Open the Triggers screen and check that a trigger listens to this subject. Expected result: a run starts on the Runs screen. If it fails: compare the filter of the trigger with the payload.

Spot replayed deliveries​

  1. Look for duplicates. A replay reuses an old delivery. Read the rows with the status duplicate or the reason timestamp_outside_window. Expected result: few or none. If it fails: many rows from one source point to a replay attempt; report it to the security team.

Troubleshooting​

SymptomCauseFix
Sender gets invalid_signature (401)Wrong signature, missing secret, or unknown or inactive tenant.Check the secret, the signed string <unix-time>.<body> and the X-EAI-Tenant header.
Sender gets tenant_required (400)The X-EAI-Tenant header is missing.Add the header with the slug of the tenant.
Sender gets connector_not_found (404)The connector does not exist, is not active, or its manifest does not enable webhooks.Enable webhooks in the manifest and set the connector to active.
Sender gets webhook_replay (401), reason timestamp_outside_windowThe clock of the sender drifts, or the delivery is replayed.Synchronize the clock of the sender.
Sender gets webhook_duplicate (409), status duplicateThe same delivery id was already accepted.Send a new X-EAI-Delivery for each event.
Reason prompt_injection_blockedThe payload carries text that looks like a prompt injection.Clean the payload at the source.