Register a webhook destination

A destination is a URL plus a signing secret, registered once and reused by every rule that points at it. Create it before any rule that needs it — the response gives you an ID that rules take as webhook_destination_id, and a signing secret that is shown once and then never again.

Creating one

Request

curl -X POST \
  https://countdownshare.com/api/v1/webhook-destinations \
  -H "Authorization: Bearer $COUNTDOWNSHARE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Order automation",
    "url": "https://example.com/webhooks/countdownshare",
    "description": "Releases stock reservations when a hold expires",
    "enabled": true
  }'

201 Created

HTTP/1.1 201 Created

{
  "data": {
    "id": "0f4726a4-c05b-4eba-97d2-30ba8e59446e",
    "name": "Order automation",
    "url": "https://example.com/webhooks/countdownshare",
    "enabled": true,
    "signing_secret_prefix": "whsec_1a2b",
    "signing_secret": "whsec_1a2b3c4d5e6f...",
    "secret_rotated_at": "2029-12-01T10:00:00.000Z",
    "created_at": "2029-12-01T10:00:00.000Z",
    "updated_at": "2029-12-01T10:00:00.000Z"
  },
  "request_id": "7b82b7f7-4d13-497b-9f20-58d46fd7a510"
}

# signing_secret appears HERE and on rotation. Nowhere else, ever.
FieldTypeRequiredDescription
namestringYes1–100 characters. Internal label for this destination.
urlstringYesWhere deliveries are sent. Production requires HTTPS.
descriptionstringNoUp to 500 characters. A note about the receiving service.
enabledbooleanNoAccept deliveries when true. Defaults to true.
signing_secret is in this response and in the response to a rotation. It appears nowhere else — not in a later GET, not in the dashboard, not on request. Store it before you do anything else with the response.

Storing the signing secret

Every delivery is signed with this secret, and verifying that signature is what distinguishes a real delivery from anyone who has guessed your URL. Treat it exactly like an API key.

It belongs in your secret store or an environment variable your webhook handler reads. It does not belong in a config file in the repository, a database row your application logs, or a message to a colleague.

signing_secret_prefix comes back on every read and is safe to display. It is the first few characters, enough to confirm which secret a destination is currently using without exposing the whole thing — useful in your own admin screens.

Lost the secret? You do not need to recreate the destination. Rotate it — the ID, the URL, and every rule pointing at it stay exactly as they are.

What URLs are accepted

SandboxProduction
Schemehttp:// or https://https:// only
Private and loopback addressesRejectedRejected
Link-local addressesRejectedRejected
Public tunnel URLsAcceptedAccepted

Sandbox accepts plain HTTP so you can point at a local tunnel while developing. http://localhost:3000 is still rejected — loopback and private ranges are blocked in both environments, so use the public URL the tunnel gives you rather than the address it forwards to.

The private-address block is deliberate and not configurable. A webhook destination that could point at internal infrastructure is a server-side request forgery primitive, and allowing it would make this API a tool for reaching things it should not reach.

Testing before you rely on it

A test delivery is a genuine signed request to your registered URL. It is the fastest way to separate a broken handler from a rule that never matched, and it works before any timer exists.

Send a test delivery

curl -X POST \
  https://countdownshare.com/api/v1/webhook-destinations/$DESTINATION_ID/test \
  -H "Authorization: Bearer $COUNTDOWNSHARE_API_KEY"

# Sends a real, signed delivery to the registered URL.
# Use it to prove your verification works before a timer depends on it.

It produces a real delivery record, so you can inspect what happened with GET /webhook-deliveries — including the response your endpoint returned and how long it took. Verification itself is covered under verify a delivery.

Rotating the secret

POST /webhook-destinations/{destination_id}/rotate-secret replaces the secret and returns the new one once. The switch is immediate: deliveries after the rotation are signed with the new secret, and there is no grace period on our side.

Because of that, the safe sequence has your handler accepting both for a short window. Deploy the dual-accept code first, then rotate, then remove the old secret.

Accept both during the changeover

// Accept either secret during the changeover window.
// Verify against the new one first; fall back to the old one.
export function verifyWithRotation(rawBody, headers) {
  const secrets = [
    process.env.COUNTDOWNSHARE_WEBHOOK_SECRET,
    process.env.COUNTDOWNSHARE_WEBHOOK_SECRET_PREVIOUS,
  ].filter(Boolean);

  return secrets.some((secret) => verify(rawBody, headers, secret));
}
  1. 01

    Deploy dual verification

    Your handler accepts the current secret and a "previous" slot that is still empty.

  2. 02

    Rotate

    Call the endpoint. Move the old value into the previous slot and the new one into the current slot.

  3. 03

    Deploy the new configuration

    Deliveries now verify against the new secret; in-flight retries signed with the old one still pass.

  4. 04

    Clear the previous slot

    After a day, once no retries can still be carrying the old signature.

Step four matters because retries can span roughly fifteen hours. A delivery signed with the old secret an hour before rotation may still be retrying afterwards — clearing the previous slot too early rejects it.

Managing destinations

MethodEndpointPurpose
GET/webhook-destinationsList destinations and their IDs
GET/webhook-destinations/{destination_id}Get one, without its secret
PATCH/webhook-destinations/{destination_id}Change name, URL, description, or enabled state
DELETE/webhook-destinations/{destination_id}Delete one that no rule depends on

Disabling instead of deleting

Setting enabled: false stops deliveries while leaving the destination and every rule pointing at it intact. That is what you want during an incident or a maintenance window — flip it back and the rules resume without being recreated.

Deleting requires that no rule references the destination. Remove or repoint the rules first; the API will not silently orphan them.

Changing the URL

Editable via PATCH, and the secret does not change with it. Migrating to a new endpoint is therefore a single field update, with no rule changes and no rotation — as long as the new URL verifies against the same secret.

One destination per receiving service, not per timer. Destinations are capped by plan while rules are unlimited, so a hundred timers pointing at one order-processing endpoint is one destination and a hundred rules. See webhook rules.