Start here
Quickstart
Send your first signed webhook and inspect its delivery in DeadHook.
On this page
This walkthrough creates a Source and Destination, sends a signed Event to the complete ingestion URL from your Source configuration, inspects the Delivery, then forces a failure and recovers through replay.
Placeholders. Values in
UPPER_SNAKE_CASEare specific to your account. Never paste a real Source or Destination secret into documentation, chat, or source control.
Before you begin
You need:
- a DeadHook account and an Organization;
- a Project (create one if the Organization has none);
- a public HTTPS endpoint that can return
2xxand print request headers/body; - Python 3.9+ or any HTTP client for the signing example.
Open Projects and create a Project if needed:
Name: Quickstart project
Slug: quickstart-project
Environment: development
Projects are resource filters inside an Organization. They help organize work; the Organization is the access boundary. See Project scoping.
1. Create a Source
Open Sources → Create Source and choose:
Project: Quickstart project
Name: Quickstart source
Authentication: Standard Webhooks
The Source is created with a public ingestion URL and a signing secret. The secret is shown once; you will store it in step 4.
2. Add a Destination
Open the Source and add a Destination:
URL: https://example.com/webhooks/deadhook
Timeout: 10000 ms
Capture response body: off
Replace example.com with a real public HTTPS endpoint you control. The Destination signing secret is
also shown once; store it on your receiving service to verify outbound requests. It is separate from
the Source secret.
3. Copy the Source ingestion URL
Open the Source overview and copy the complete Ingestion URL. It already contains the correct host and Source locator. Do not construct it from separate pieces.
Store the complete value:
export DEADHOOK_INGEST_URL='PASTE_THE_COMPLETE_INGESTION_URL_FROM_YOUR_SOURCE'
The exact URL is specific to your Source. Treat the complete URL as an integration endpoint; its locator is not a credential. Authentication comes from the signature.
4. Store the Source signing secret
When the Source was created, DeadHook showed a signing secret exactly once. Copy that secret now:
export DEADHOOK_SOURCE_SECRET='whsec_REPLACE_WITH_THE_ONE_TIME_SOURCE_SECRET'
If the secret is no longer available, rotate it from the Source page and store the new value. The previous secret remains valid only during the documented rotation overlap.
5. Run the Python example
Save this as send_webhook.py and run it with the complete ingestion URL and Source secret from steps 3
and 4:
import base64
import hashlib
import hmac
import json
import os
import time
import urllib.request
ingest_url = os.environ["DEADHOOK_INGEST_URL"]
secret = os.environ["DEADHOOK_SOURCE_SECRET"]
# Standard Webhooks secrets are base64 after the whsec_ prefix.
key = base64.b64decode(secret.removeprefix("whsec_"))
message_id = f"msg_quickstart_{int(time.time())}"
timestamp = str(int(time.time()))
body = json.dumps(
{"type": "order.created", "data": {"order_id": "ord_123", "amount": 4200}},
separators=(",", ":"),
).encode()
signed = message_id.encode() + b"." + timestamp.encode() + b"." + body
signature = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
request = urllib.request.Request(
ingest_url,
data=body,
method="POST",
headers={
"Content-Type": "application/json",
"webhook-id": message_id,
"webhook-timestamp": timestamp,
"webhook-signature": f"v1,{signature}",
},
)
with urllib.request.urlopen(request) as response:
print(response.status)
print(response.read().decode())
Expected first response:
HTTP/1.1 202 Accepted
Content-Type: application/json
{"event_id":"6f6c0f2e-0000-4000-8000-000000000001","status":"accepted"}
The event_id is a UUID generated by DeadHook. The exact value differs on every new Event.
Sending the same webhook-id with the same body again returns 200 OK and the same
event_id:
{ "event_id": "6f6c0f2e-0000-4000-8000-000000000001", "status": "duplicate" }
Sending the same webhook-id with a different body returns 409 Conflict with code
upstream_event_conflict. A tampered signature returns 401 Unauthorized with code
invalid_signature.
6. Inspect the Event and Delivery
Open Events in the selected Organization. Filter to your Project or Source. The Event shows:
accepted/in_progresswhile a Delivery is still active;- one or more Delivery rows, one per active Destination;
- each attempt's HTTP status, duration, result, and error code;
- the Destination response only if capture was enabled.
If your Destination returned 2xx, the Delivery is delivered. If it returned a retryable failure,
you will see retry_scheduled and the next attempt time.
7. Force a failure and observe retries
Point the Destination at an endpoint that returns 500, or temporarily update the Destination URL in
the UI. Send a new Event.
With the default policy:
- attempts use the retry ladder documented in Automatic retry policy;
- a
500is retryable; - a
404is terminal and does not consume the remaining ladder; - every attempt is a new HTTP request with a fresh
webhook-timestampand signature; - after 8 attempts or 24 hours, the Delivery becomes
dead_letter.
Restore the Destination to a 2xx endpoint.
8. Recover with replay
Open Dead Letter Queue, select the Delivery, and choose Replay. Provide a reason. Replay creates a new dispatch cycle for that Delivery and records an audit event.
Replay is available only when the Delivery's state and the Destination allow it. A Destination that is archived blocks replay until it is restored; a payload that is no longer retained cannot be replayed.
Replay does not guarantee Destination success; the new cycle can fail again.
What success looks like
- The complete ingestion URL was copied from the Source, not assembled by hand.
- The Source and Destination secrets were stored once and not committed.
- First Event returned
202with anevent_id. - A duplicate with the same
webhook-idand body returned200and the sameevent_id. - The Event appears in Event Explorer.
- The Delivery attempt history shows the request and its result.
- A forced failure scheduled a retry or became a dead letter according to policy.
- Replay was accepted with a reason and recorded.
Next: Configuring Sources, Delivery lifecycle, or the Ingestion API reference.