Receive and verify signed webhooks
Create an endpoint, validate signatures, handle retries and troubleshoot delivery failures.
3 min read
Screenshots show the real application in an isolated demo environment with fictional example data. Setup notices and controls may differ by environment, plan and role. Click an image to view it full-size, and always use the values generated in your own workspace.
Webhooks notify your server
API requests ask the app to do something; webhooks notify your HTTPS server that an event occurred. Set up an endpoint that accepts JSON POST requests and choose only the events your integration needs.
Examples include campaign.created, campaign.updated, campaign.published, campaign.paused, campaign.expired, campaign.viewed, campaign.clicked, conversion.created, recipient.started, recipient.first_opened and recipient.extended. Event availability does not make every event proof of a human interaction.
Create and test an endpoint
- 1Open API & webhooks and add the endpoint with at least one supported event.
- 2Use a public HTTPS address on port 443. Local/private addresses and redirecting endpoints are not supported by the delivery safety checks.
- 3Securely store the endpoint signing secret on your receiver.
- 4Use Send test only when you are ready: a signed sample can trigger real automation at the destination.
- 5Inspect delivery history, status and attempts, then test an actual event using a separate test campaign.
Validate the signature before processing
Read the X-CountdownFlow-Signature header. Its format is t=TIMESTAMP,v1=SIGNATURE. The signature is an HMAC-SHA256 of the timestamp, a dot and the exact raw HTTP body, using the webhook signing secret.
Reject stale timestamps according to your replay policy and compare signatures with a timing-safe function. Do not decode and re-encode JSON before verification: that changes the signed bytes. The X-CountdownFlow-Event header identifies the event type, but a header alone is not authentication.
// Receiver-side PHP example. Supply the secret from secure server storage.
$raw = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_COUNTDOWNFLOW_SIGNATURE'] ?? '';
$secret = getenv('COUNTDOWNFLOW_WEBHOOK_SECRET');
if (!$secret || !preg_match('/^t=(\d+),v1=([a-f0-9]{64})$/', $header, $m)) {
http_response_code(401); exit;
}
if (abs(time() - (int) $m[1]) > 300) {
http_response_code(401); exit;
}
$expected = hash_hmac('sha256', $m[1].'.'.$raw, $secret);
if (!hash_equals($expected, $m[2])) {
http_response_code(401); exit;
}
$event = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
// Deduplicate the payload ID and enqueue trusted processing in your system.
http_response_code(200);
Your browser blocked clipboard access. Select the text and copy it manually.
Retries and duplicate-safe processing
A delivery can be retried. Retries share the logical payload ID, so record that identifier and make your handler idempotent before doing things such as granting access or creating an order.
Verify and acknowledge promptly, then process heavier work in your own queue. A timeout or non-successful response may lead to retry. Redirects are not followed. Use the delivery screen to understand failures rather than exposing response bodies or credentials in support tickets.
Changes and secret rotation
Disabling an endpoint also disables its tests. If you rotate the signing secret, update the receiver securely and verify the new configuration. Do not confuse these application webhooks with the platform’s internal billing-provider webhook setup.