Skip to content

Webhooks

outboundIQ can POST a JSON payload to one or more URLs of your choice every time we finish cleaning a dial. Use webhooks to forward dial events to your CRM, your data warehouse, or any internal pipeline.

Webhooks are managed per company under Settings → Webhooks in the workspace UI.

You can configure up to 3 webhooks per company. Each webhook has:

  • A short name (for your own bookkeeping).
  • A destination URL (HTTPS only).
  • Optional custom headers (for example an X-API-Key your receiver expects).
  • An enabled / disabled toggle. Disabled webhooks receive nothing.

The first time you create a webhook we generate a signing secret and display it once. Save it somewhere safe — you will not be able to view it again. If you lose it, use Rotate signing secret to generate a new one (the old one will stop working immediately).

Every delivery is an HTTP POST with a Content-Type: application/json body.

In addition to any custom headers you configured, every request includes:

HeaderValue
Content-Typeapplication/json
User-AgentoutboundIQ-webhooks/1.0
X-OutboundIQ-EventThe event type. Currently always dial.
X-OutboundIQ-Delivery-IdA unique UUID per delivery. Use it for idempotency on your side.
X-OutboundIQ-Signaturesha256=<hex> — HMAC-SHA256 of the raw body using your signing secret.
type DialWebhookPayload = {
event: "dial";
// UUID — also sent as the X-OutboundIQ-Delivery-Id header
deliveryId: string;
// Your company slug
companySlug: string;
// Dial / call ID. For inbound calls this is prefixed with "IB_".
callId: string | null;
// "outbound" | "inbound" | "manual" | "sms" | "transfer" | ...
callDirection: string;
// EST/EDT, format: "YYYY-MM-DD HH:MM:SS"
timestamp: string;
// Outbound caller ID (the number we dialed FROM), 10-digit US.
ani: string | null;
// Prospect / customer phone (the number we dialed TO), 10-digit US.
phone: string | null;
// Human-readable campaign name, post-translation.
campaign: string | null;
// Internal campaign identifier.
campaignInternalId: string | null;
// The agent who handled the call.
agent: string | null;
// Disposition / call result, post-translation.
disposition: string | null;
// Original disposition identifier from the dialer.
dispositionId: string | null;
// Was contact made with the prospect?
contact: boolean;
// Did the call result in a successful outcome?
success: boolean;
// Was the disposition a system disposition (auto-set by the dialer)?
isSystemDispo: boolean;
// Total dial attempts on this lead, when known.
totalDialAttempts: number | null;
// "five9" | "five9ca" | "calltools" | "ringcx" | "newbridge" | ...
dialerFamily: string;
};
{
"event": "dial",
"deliveryId": "550e8400-e29b-41d4-a716-446655440000",
"companySlug": "abcde123-4567-8901-2345-67890abcdef0",
"callId": "AB12345",
"callDirection": "outbound",
"timestamp": "2026-04-10 14:32:15",
"ani": "5551234567",
"phone": "5559876543",
"campaign": "Q2 Outbound Push",
"campaignInternalId": "abc-123",
"agent": "Jane Doe",
"disposition": "Sale",
"dispositionId": "42",
"contact": true,
"success": true,
"isSystemDispo": false,
"totalDialAttempts": 3,
"dialerFamily": "five9"
}

Always verify the X-OutboundIQ-Signature header before trusting the payload. Compute HMAC-SHA256 of the raw request body using your signing secret and compare it to the sha256=<hex> value.

import { createHmac, timingSafeEqual } from "node:crypto";
function verifySignature(rawBody, header, secret) {
if (!header || !header.startsWith("sha256=")) return false;
const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
const provided = header.slice("sha256=".length);
const a = Buffer.from(expected, "hex");
const b = Buffer.from(provided, "hex");
if (a.length !== b.length) return false;
return timingSafeEqual(a, b);
}
import hmac, hashlib
def verify_signature(raw_body: bytes, header: str, secret: str) -> bool:
if not header or not header.startswith("sha256="):
return False
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header[len("sha256="):])

If your endpoint returns a non-2xx status or fails to respond within 10 seconds, we retry the delivery once after about 500ms. If the retry also fails we log the failure and give up — we do not redeliver later.

Each webhook is delivered independently. If you have multiple webhooks for the same dial and one fails, the others still receive their delivery.

To handle missed deliveries, treat the X-OutboundIQ-Delivery-Id as an idempotency key on your side and reconcile periodically against the Dials API.

Disabling a webhook stops deliveries immediately. Deleting it removes the webhook entirely and rotates out the signing secret. If all your webhooks are disabled or deleted, your company drops out of the webhook pipeline entirely until you enable or create one again.