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):
| Reason | Status | Meaning |
|---|---|---|
bad_signature | rejected | The X-EAI-Signature header does not match the payload. |
no_secret | rejected | No webhook secret could be read for the connector (neither a webhook_secret credential nor spec.webhooks.secret_ref). |
timestamp_outside_window | rejected | The timestamp of the signature is more than 5 minutes away from the clock of the Control Plane. |
duplicate_delivery | duplicate | The same delivery id was already accepted. |
prompt_injection_blocked | rejected | The payload looks like a prompt injection. |
| empty | accepted | The delivery was accepted and published as an event. |
Actions
| Button or action | What it does | API call | Roles |
|---|---|---|---|
| Refresh (icon) | Reloads the list. | GET /v1/webhooks/deliveries | Any tenant member |
| Filter | Keeps the deliveries of one connector. | GET /v1/webhooks/deliveries with connector | Any tenant member |
| Row click or View details | Shows the delivery row. | None | Any tenant member |
| Retry (error state) | Sends the failed read again. | Same call | Same 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
- 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-Tenantheader of the sender. - Read the reason. The reason says which check failed. Read Signature valid, Status and Reason of the last row. Expected result:
acceptedwith an empty reason. If it fails:bad_signatureorno_secretmeans the secret differs or is missing; store it again with Credentials (kindwebhook_secret). - 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
- Look for duplicates. A replay reuses an old delivery. Read the rows with the status
duplicateor the reasontimestamp_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
| Symptom | Cause | Fix |
|---|---|---|
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_window | The clock of the sender drifts, or the delivery is replayed. | Synchronize the clock of the sender. |
Sender gets webhook_duplicate (409), status duplicate | The same delivery id was already accepted. | Send a new X-EAI-Delivery for each event. |
Reason prompt_injection_blocked | The payload carries text that looks like a prompt injection. | Clean the payload at the source. |