Skip to main content
Webhooks are HTTP POST requests OptimalDial sends to a URL you control whenever something interesting happens to one of your uploads. They’re the right way to learn about state changes — polling works, but webhooks arrive within seconds and don’t burn rate-limit budget. This guide covers everything on the receiving side: what we send, how to verify it, how we retry, and the small set of decisions that separate a brittle integration from a sturdy one. For the configuration endpoints (registering URLs, listing deliveries) see the Webhooks reference.

Event types

Pings are sent only during POST /api/v1/webhooks and PATCH /api/v1/webhooks/{id} (when the URL changes). They are never re-sent later — your receiver must be live before you register.

Event envelope

Every event has the same outer shape. The data object is event-specific.
Sample bodies for each event type:
Note: the created payload has total_rows (everything you submitted), valid_row_count (contacts that will be processed), and valid_number_count (the phone numbers you were charged for). On an upload that maps several phone columns the last two differ, so total_rows - valid_row_count is not an invalid count. It does not include download URLs — those only appear once processing finishes.

Headers we send

Every delivery includes: You should log X-OptimalDial-Delivery-Id with every received webhook — it’s the fastest way for us to debug a delivery problem with you.

Signature verification

The signature is HMAC-SHA256 of f"{timestamp}.{body}" keyed by your endpoint’s secret, in Stripe-compatible format. Verifying it has three parts:
  1. Parse the t and v1 values out of the X-OptimalDial-Signature header.
  2. Reject if the timestamp is more than 5 minutes off from your server clock (replay protection).
  3. Recompute the HMAC and compare it to v1 with a constant-time comparison.
Always verify against the raw request body bytes, before any framework parses or re-serialises the JSON. Pretty-printing or re-ordering keys breaks the signature.
If you’re rolling your own verifier in another language, stick to these rules and you’ll match our wire format exactly:
  • Sign over f"{timestamp}.{body}" — the dot is literal.
  • HMAC-SHA256, output as lowercase hex.
  • Reject when |now - timestamp| > 300.
  • Use a constant-time comparator (hmac.compare_digest in Python, crypto.timingSafeEqual in Node, etc.).

Retry policy

Each delivery has its own retry schedule. We attempt up to 6 times with the following backoff between attempts: After the 6th attempt fails, the delivery is marked exhausted and we stop trying. Total wall-clock window from first attempt to last is approximately 14 hours and 36 minutes. Per-attempt timeout: 10 seconds. If your endpoint hasn’t responded in that window, we count the attempt as failed and move it onto the next backoff.

Which responses count as success vs. retryable vs. permanent

Auto-disable after sustained failure

We keep a consecutive_failures counter on each endpoint. Every failed attempt (whether the delivery eventually succeeded on retry or got permanently rejected) increments it; every successful delivery resets it to 0. After 20 consecutive failures we set is_active: false and stamp disabled_at on the endpoint. No further events go out until you re-enable it via PATCH /api/v1/webhooks/{id} with {"is_active": true} (which also resets the counter and clears disabled_at). This is a backstop for outages — if your receiver is down for a sustained period, we’d rather pause than carpet-bomb you with retries when you come back. Anything queued during the outage that exhausted its retries is gone; reconcile by listing uploads with GET /api/v1/uploads and looking at their statuses.

URL safety

When you register a webhook endpoint we validate the URL before accepting it:
  • Scheme must be https://. Plain http:// is rejected unless the host appears in the server’s WEBHOOK_ALLOW_HTTP_HOSTS allow-list (used for local development on the OptimalDial side; never enabled in production).
  • DNS resolution must return a public IP. We reject:
    • Private ranges (RFC 1918, RFC 4193 unique-local IPv6).
    • Loopback (127.0.0.0/8, ::1).
    • Link-local (169.254.0.0/16, fe80::/10).
    • Multicast and reserved/unspecified addresses.
    • Cloud metadata endpoints: 169.254.169.254 (AWS, GCP, Azure), 100.100.100.200 (Alibaba), fd00:ec2::254 (IPv6 metadata).
  • Redirects are not followed. Whatever URL you register is the URL we POST to.
These rules apply on every delivery as well as on registration — if your DNS later flips to a private IP, the delivery will fail and the failure counter will tick up.

Best practices

  1. Idempotency. Each event carries an id (also in X-OptimalDial-Event-Id). We may send the same event more than once if a previous attempt was unacknowledged — store the IDs you’ve already processed and de-dupe on receipt. A 24-hour TTL on the de-dupe set is plenty.
  2. Respond fast, process async. Return 2xx as soon as you’ve persisted the event ID and queued the work. Real processing (downloading large CSVs, mutating your DB) should not happen inside the request handler — you have 10 seconds total before we count the attempt as failed.
  3. Always verify the signature first. Even before parsing the body. An unverified webhook is just an arbitrary HTTP request to a URL someone scraped from your DNS records.
  4. Log the delivery ID. X-OptimalDial-Delivery-Id is the fastest way for us to find the row in our delivery table when you ask “did you actually try to send me this?”.
  5. Don’t 5xx on unknown event types. If we add a new event type, you don’t want every delivery to start failing — return 2xx and ignore unknown types until you’re ready to handle them.
  6. Use one secret per endpoint. Each registered endpoint has its own secret. Don’t share them, and don’t try to re-use a deleted endpoint’s secret on a new one.
  7. Re-fetch download URLs. The signed URLs in upload.completed payloads are short-lived. If you can’t process the file immediately, store the upload.id and call GET /api/v1/uploads/{id}/download/processed later for a fresh URL.
  8. Handle webhook.ping like any other event. It arrives at registration time and carries no business meaning — just respond 2xx so the registration completes.