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/json
  • application/*+json
  • text/plain
  • application/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.