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.| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | 1–100 characters. Internal label for this destination. |
url | string | Yes | Where deliveries are sent. Production requires HTTPS. |
description | string | No | Up to 500 characters. A note about the receiving service. |
enabled | boolean | No | Accept 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.
What URLs are accepted
| Sandbox | Production | |
|---|---|---|
Scheme | http:// or https:// | https:// only |
Private and loopback addresses | Rejected | Rejected |
Link-local addresses | Rejected | Rejected |
Public tunnel URLs | Accepted | Accepted |
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.
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));
}- 01
Deploy dual verification
Your handler accepts the current secret and a "previous" slot that is still empty.
- 02
Rotate
Call the endpoint. Move the old value into the previous slot and the new one into the current slot.
- 03
Deploy the new configuration
Deliveries now verify against the new secret; in-flight retries signed with the old one still pass.
- 04
Clear the previous slot
After a day, once no retries can still be carrying the old signature.
Managing destinations
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /webhook-destinations | List 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.