API reference
Ingestion API
POST /ingest/v1/{source_public_id}: authentication modes, signatures, deduplication, responses.
On this page
POST {DEADHOOK_INGEST_URL}
This is the public ingestion endpoint for provider webhooks. It accepts the raw provider request, authenticates it, records the Event, and creates Delivery work for each active Destination.
Authentication
The Source's configured authentication method determines the credential:
| Method | Required credential |
|---|---|
| Standard Webhooks | webhook-id, webhook-timestamp, webhook-signature |
| Generic bearer | Authorization: Bearer <source-secret> |
| Stripe | Stripe-Signature |
| Shopify | X-Shopify-Hmac-SHA256, X-Shopify-Webhook-Id |
The Source locator in the URL is not a secret. Always sign the exact raw bytes before any parse/reserialize step.
Standard Webhooks signature
signed_content = webhook-id + "." + webhook-timestamp + "." + raw_body
signature = "v1," + base64(HMAC-SHA256(base64_decode(secret_without_whsec_), signed_content))
DeadHook accepts space-separated signatures and checks the previous Source secret during rotation.
Timestamp tolerance is 300 seconds. The verified webhook-id is required and becomes the
deduplication ID.
Example headers:
webhook-id: msg_01jexample
webhook-timestamp: 1760000000
webhook-signature: v1,MEUCIQ...
Content-Type: application/json
Generic bearer
Authorization: Bearer <source-secret>
X-Event-ID: order_123
X-Event-ID is optional. When provided it is the deduplication ID; when omitted, requests without a
provider event ID are treated as separate Events even if their bodies are identical.
Stripe
Stripe-Signature: t=1760000000,v1=<hex-hmac>
Content-Type: application/json
DeadHook verifies the signature over timestamp + "." + raw body and only then parses the JSON Event
id.
Shopify
X-Shopify-Hmac-Sha256: <base64-hmac>
X-Shopify-Webhook-Id: <delivery-id>
X-Shopify-Topic: orders/create
The HMAC is over the raw body with the Shopify app secret.
Content type and size
Accepted by default:
application/jsonapplication/*+jsontext/plainapplication/octet-stream
The body limit is 256 KiB. Oversized requests return 413 payload_too_large.
Success responses
New Event:
HTTP/1.1 202 Accepted
Content-Type: application/json
{"event_id":"6f6c0f2e-...","status":"accepted"}
Exact duplicate: same Source and upstream Event ID with the same body:
HTTP/1.1 200 OK
{"event_id":"6f6c0f2e-...","status":"duplicate"}
202 means DeadHook accepted and recorded the Event and queued its initial Delivery work. It does
not mean any Destination was contacted or succeeded.
Deduplication
- Same Source + upstream Event ID + same body → same
event_id,200 duplicate, no new Delivery. - Same upstream Event ID + different body →
409 upstream_event_conflict. - Different upstream Event ID + identical body → a new Event; bodies are not used as event identity.
- Without an upstream ID (generic bearer), no body-based deduplication is performed.
Errors
| Status | Code | Meaning |
|---|---|---|
400 |
invalid_request |
Missing or malformed header, JSON, or content type. |
401 |
invalid_signature |
Signature or bearer credential did not verify. |
403 |
source_disabled |
Source is disabled. |
404 |
source_not_found |
The ingestion URL does not match an available Source. |
409 |
upstream_event_conflict |
Same upstream ID, different body. |
409 |
destination_not_configured |
No active Destination is available. |
409 |
plan_limit_reached |
Monthly accepted-Event quota is exhausted. |
409 |
destination_limit_exceeded |
Destination capacity for this Source is exhausted. |
413 |
payload_too_large |
Body is over the limit. |
415 |
unsupported_media_type |
Content type is not accepted. |
429 |
rate_limited |
Request rate limit exceeded; honor Retry-After. |
503 |
unavailable |
Ingestion is temporarily unavailable; retry with backoff. |
Do not retry 400, 401, 403, 404, 409, 413, or 415 unchanged. A 409 plan_limit_reached
means the monthly accepted-Event quota is exhausted; retrying immediately will not help. Retry 429
and 503 with jitter and respect Retry-After.