Skip to main content
When a request reaches a terminal state, SuperDial sends an HTTP POST to your webhook endpoint with the result. Webhooks are the recommended way to consume request results.

When webhooks fire

A webhook is sent once per request, when its state becomes one of:
StateMeaning
SUCCESSThe request completed and produced a full result. Read the structured output from the request’s results field via GET /v1/requests/{requestId}.
PARTIALThe request captured some required fields but not all. The full GET returns results partially populated, missingFields listing the gaps, and the error object describing why.
FAILUREThe request captured nothing usable. The full GET returns results empty and the error object describing the cause.
No webhook is sent while a request is still PROCESSING.

Configuration

You can set your webhook endpoint in two places:

Account default

Ask your account team to set an account-level webhook URL. Once configured, this becomes the default for every request you create — no per-request webhookUrl field needed.

Per-request override

Pass a webhookUrl field in the body of POST /v1/requests to direct a single request to a different URL. The per-request value always wins over the account default.
{
  "schemaId": "fWxzG4nqtpHsJxS5Lm3q",
  "inputs": { "payerName": "Sample Insurance Co", "memberId": "TEST123456789", "phoneNumber": "2125551234" },
  "webhookUrl": "https://your-app.example.com/webhooks/superdial/requests"
}
If neither is configured, no webhook is sent and you must poll GET /v1/requests/{requestId} for results.

Payload

{
  "requestId": "8bF7xK2mP9qR4sT6uV0w",
  "requestBatchId": "pH9kJ2lM4nB6vC8xZ7Qr",
  "state": "SUCCESS",
  "internalId": "claim_internal_456",
  "internalTag": "march-batch"
}
FieldTypeAlways present?Notes
requestIdstringYesThe request’s ID. Use it to fetch the full result via GET /v1/requests/{requestId}.
requestBatchIdstringYesThe batch this request belongs to.
statestringYesOne of SUCCESS, PARTIAL, or FAILURE (uppercase).
internalIdstringOnly if supplied at create timeEchoed from your input. When you didn’t supply one, the key is absent from the JSON object entirely — not null, not empty string.
internalTagstringOnly if supplied at create timeEchoed from your input. When you didn’t supply one, the key is absent from the JSON object entirely — not null, not empty string.
The webhook body is intentionally compact. To get the full result (results, missingFields, modality, data_completeness, and the error object on non-SUCCESS), call GET /v1/requests/{requestId} after receiving the webhook.
By the time you receive the webhook, the request is fully readable — for phone-backed requests, the call enrichment fields (transcript, recordingDownloadUrl, callDuration, callSummary) are available on the GET /v1/requests/{requestId} response.

Signature verification

Every webhook delivery includes an X-Webhook-Signature header. Verify it on every delivery — without verification, anyone who learns your endpoint URL can forge events. The signature is HMAC-SHA256 of the raw request body. The signing secret is your account’s webhook secret if you’ve set one up on the portal, otherwise it is your production API key. The same production-side secret is used for sandbox webhooks too — your sandbox API key will fail verification. Compute the HMAC over the raw body bytes exactly as received — do not parse and re-serialize the JSON, since that changes the byte sequence and breaks the signature. The header value is the bare 64-character lowercase hex digest, with no sha256= prefix or other framing. Example:
X-Webhook-Signature: 3f5b2a1c8d9e4f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a

Python

import hmac
import hashlib

def verify_webhook_signature(raw_body: bytes, signature_header: str, signing_secret: str) -> bool:
    expected = hmac.new(
        signing_secret.encode("utf-8"),
        raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header)

Node.js

import crypto from "node:crypto";

function verifyWebhookSignature(rawBody, signatureHeader, signingSecret) {
  const expected = crypto
    .createHmac("sha256", signingSecret)
    .update(rawBody)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(signatureHeader, "hex"),
  );
}
Both samples use a constant-time comparator (hmac.compare_digest / crypto.timingSafeEqual); use the equivalent in your language rather than == to avoid timing-attack risk. The signature doesn’t include a timestamp, so it can’t be used to detect replays on its own. Use requestId as a dedup key — see Idempotency below.

Delivery and retries

We send the webhook when the request reaches a terminal state. On a 5xx response or connection failure we automatically retry up to 3 more times (4 attempts total) with exponential backoff (0.5s → 1s → 2s). A 4xx response is treated as terminal — we don’t retry, since re-posting the same payload is unlikely to succeed. If all four attempts fail, the failure is recorded and no further attempts are made. Fall back to polling GET /v1/requests/{requestId} to recover. For high-reliability ingestion:
  • Ack with 2xx as soon as you’ve durably enqueued the event for processing.
  • Track the requestIds you’ve submitted on your side. Any request whose terminal outcome you haven’t received within the expected window should trigger a fallback poll.

Idempotency

In rare cases the same requestId may arrive more than once. Always treat requestId as a dedup key:
if already_processed(payload["requestId"]):
    return 200  # idempotent ack
If you also passed internalId when creating the request, you have two layers of dedup keys — pick whichever fits your system. Return HTTP 200 as quickly as possible. Don’t do synchronous work that could exceed our 10-second timeout — push processing to a background job and ack immediately. 5xx responses and connection failures trigger automatic retries; 4xx responses don’t (see Delivery and retries above). If all retries are exhausted, fall back to polling GET /v1/requests/{requestId} to recover. After acking, call GET /v1/requests/{requestId} to fetch the full payload (results, missingFields, modality, and the error object on PARTIAL / FAILURE).