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.

Deliveries are batched. A single request contains one or more dial events for the same company (up to 100 per request). When traffic is light you may get batches of one; under load expect tens. Always loop over the dials array.

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.batch.
X-OutboundIQ-Delivery-IdA unique UUID per HTTP delivery. Use it for idempotency on your side.
X-OutboundIQ-Signaturesha256=<hex> — HMAC-SHA256 of the raw body using your signing secret.
type DialBatchWebhookPayload = {
event: "dial.batch";
// UUID — also sent as the X-OutboundIQ-Delivery-Id header
deliveryId: string;
// ISO 8601 UTC timestamp of when we sent this batch
deliveredAt: string;
// 1..100 dial events, all for the same company
dials: DialEvent[];
};
type DialEvent = {
// 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;
// ISO 8601 UTC, e.g. "2026-04-10T18:32:15.000Z"
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;
};
{
"event": "dial.batch",
"deliveryId": "550e8400-e29b-41d4-a716-446655440000",
"deliveredAt": "2026-04-10T18:32:16.842Z",
"dials": [
{
"companySlug": "abcde123-4567-8901-2345-67890abcdef0",
"callId": "AB12345",
"callDirection": "outbound",
"timestamp": "2026-04-10T18:32:15.000Z",
"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
}
]
}

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. Failures are at the batch level: if a delivery fails after retry, every dial in that batch is dropped.

Each webhook is delivered independently. If you have multiple webhooks for the same company 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.