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_CASE are 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 2xx and 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_progress while 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 500 is retryable;
  • a 404 is terminal and does not consume the remaining ladder;
  • every attempt is a new HTTP request with a fresh webhook-timestamp and 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 202 with an event_id.
  • A duplicate with the same webhook-id and body returned 200 and the same event_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.