Events

Every event a webhook or pixel can subscribe to, the exact moment each one fires, and the payload it carries.

After this page you can pick the right event for what you want to know, write a handler against its payload before a real call arrives, and tell the three events that look alike apart. An event is something that happened in your workspace that a webhook or a pixel can be told about. An endpoint subscribes by name, or to everything with *.

There are ten, and this is all of them. Nine are about a call and carry the same payload core; the tenth is about your wallet and carries something else entirely.

EventFires whenCarries
call.incomingA call arrives, before anything is decided.The payload core
call.routedA target is chosen and its line is being dialled.attemptNo
call.failoverA target did not take the call and the next one is being tried.attemptNo, reason, sipCode, sentence, nextTargetId, nextTargetName
call.connectedA buyer answered.attemptNo
call.completedThe call ended and its books were written.reason
call.convertedThe call earned its price.reason
call.unroutedNo target took the call.reason, sentence, action
target.cappedA target or its buyer reached a cap with this call.scope, window, and the cap and the count
target.downA target was suspended after failing repeatedly.reason, consecutiveFailures, sentence
wallet.lowA caller was turned away because the wallet is empty.Not a call — see below

The envelope

Every webhook delivery is the same four fields. data is what changes.

What is POSTed to your URL
{
  "id": "req_5f0c2a91b7d34e10",
  "event": "call.converted",
  "createdAt": "2026-09-20T14:06:24.512Z",
  "data": { "…": "the payload core, plus this event's own fields" }
}
  • id identifies the event, not the attempt. Every retry and every replay of the same event carries the same id, and it is also the buy3-request-id header. De-duplicate on it.
  • createdAt is the moment this attempt was built, so it moves between retries. Order by what is in datastartedAt, answeredAt, endedAt — not by createdAt.
  • data is documented per event below. A fact a call does not have is null, never absent, so a field you read always exists.
  • The signature covers the whole body. See Verifying a delivery.

The payload core

The nine events about a call — the seven call.* plus target.capped and target.down — all carry the same twenty-six fields, built by one function on the server. A live event, the test delivery you send from the console and the sample the console prints all go through it, so a handler written against the sample cannot be surprised by the real thing. Each event then adds its own fields on top, listed with it below.

The payload core, on a finished call
{
  "callId": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11",
  "ref": "CA-482137",
  "status": "completed",
  "campaignId": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
  "campaignName": "Medicare — Inbound",
  "publisherId": "c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b",
  "publisherName": "Northwind Media",
  "publisherRef": "nw-88231",
  "buyerId": "f1e2d3c4-b5a6-4978-8a9b-0c1d2e3f4a5b",
  "buyerName": "Acme Insurance",
  "targetId": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
  "targetName": "Acme — Dallas floor",
  "targetExternalRef": "acme-dal-01",
  "callerNumber": "+14155550142",
  "callerState": "CA",
  "callerCity": "San Francisco",
  "callerZip": "94103",
  "dialedNumber": "+18885550100",
  "startedAt": "2026-09-20T14:03:11.000Z",
  "answeredAt": "2026-09-20T14:03:19.000Z",
  "endedAt": "2026-09-20T14:06:23.000Z",
  "talkSeconds": 184,
  "converted": true,
  "revenueCents": 4200,
  "payoutCents": 2750,
  "tags": {
    "gclid": "Cj0KCQjw",
    "utm_source": "google",
    "utm_campaign": "medicare-q3"
  }
}
FieldMeaning
callIdThe call's id. Use it on GET /api/v1/calls/:id to fetch the whole waterfall.
refThe call's public reference, as the console prints it. Quote this in a ticket.
statusThe call's status as the event fired — so routing on call.incoming and completed on call.completed.
campaignId, campaignNameThe campaign, as frozen on the call. A campaign you rename later still reads as it did.
publisherId, publisherNameThe publisher the call came from. Both null for your own media.
publisherRefThe publisher's own id for the call, as sent with their ping. null for a call that was dialled rather than won at auction.
buyerId, buyerNameThe buyer the call is with. null until a target is chosen, and on every unrouted call.
targetId, targetNameThe target the call is on right now — the router rewrites these as the call moves down the plan.
targetExternalRefYour own reference for that target, frozen on the plan entry the call is on.
callerNumberThe caller in E.164. null when they withheld it.
callerStateThe caller's state, from their area code.
callerCity, callerZipWhat a publisher sent with their ping, frozen at the claim. Both null on a call that was dialled rather than won.
dialedNumberThe tracking number that was dialled.
startedAt, answeredAt, endedAtISO 8601 instants in UTC. answeredAt is the buyer's leg answering, never the platform picking up the caller.
talkSecondsSeconds the caller spent talking to the buyer. 0 until there is a buyer on the line.
convertedWhether the call has earned its price. false until settlement says otherwise.
revenueCents, payoutCentsThe call's books in integer cents. Both 0 until the call settles. These are records — see books-only money.
tagsThe call's tags as a flat object of strings.
There is no usageCents here. What the platform charged is settled by a sweeper minutes after hangup, later than any of these events — read it from the call.

Call events, in the order a call lives them

A call that connects and converts fires call.incoming, call.routed, call.connected, call.completed and call.converted, with a call.failover for each target that did not take it. A call that reaches nobody fires call.incoming and call.unrouted, and later call.completed.

  1. 01

    call.incoming

    the call exists

  2. 02

    call.routed

    ringing a target

  3. 03

    call.failover

    and on to the next

  4. 04

    call.connected

    a buyer answered

  5. 05

    call.completed

    books written

  6. 06

    call.converted

    it earned its price

call.failover repeats once per target that did not take the call. An unrouted call leaves the line after call.incoming and fires call.unrouted instead.

call.incoming

A call arrived on one of your tracking numbers, before anything was decided about it. It is emitted as the call row is created, which is after the gates have run — so a call the gates refused fires this too, immediately followed by call.unrouted.

  • For a call that a publisher won at auction, this fires when the caller actually arrives, not when the publisher was quoted. The two can be a minute apart, and the failover budget starts at the second of them.
  • buyerId, targetId and answeredAt are always null here: nothing has been chosen yet.
  • tags already holds the tags the router observed — the dialled number, the publisher, the caller's area code and state — alongside anything a publisher or a DNI session sent. See Filters and tags.
  • Adds no fields of its own.
call.incoming
{
  "id": "req_1a2b3c4d5e6f7080",
  "event": "call.incoming",
  "createdAt": "2026-09-20T14:03:11.204Z",
  "data": {
    "callId": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11",
    "ref": "CA-482137",
    "status": "routing",
    "campaignId": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
    "campaignName": "Acme Health — Inbound",
    "publisherId": "c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b",
    "publisherName": "Northwind Media",
    "publisherRef": null,
    "buyerId": null,
    "buyerName": null,
    "targetId": null,
    "targetName": null,
    "targetExternalRef": null,
    "callerNumber": "+14155550142",
    "callerState": "CA",
    "callerCity": null,
    "callerZip": null,
    "dialedNumber": "+18885550100",
    "startedAt": "2026-09-20T14:03:11.000Z",
    "answeredAt": null,
    "endedAt": null,
    "talkSeconds": 0,
    "converted": false,
    "revenueCents": 0,
    "payoutCents": 0,
    "tags": {
      "dialed_number": "+18885550100",
      "publisher": "Northwind Media",
      "caller_area_code": "415",
      "caller_state": "CA",
      "utm_source": "google"
    }
  }
}

call.routed

A target was chosen and its line is being dialled. It fires once per dial attempt, so a call that fails over twice fires it three times. targetId, targetName and the buyer behind them are the entry being rung right now.

Extra fieldMeaning
attemptNoWhich attempt this is, counting from 1. It does not say how long the plan is.
call.routed
{
  "id": "req_2b3c4d5e6f708192",
  "event": "call.routed",
  "createdAt": "2026-09-20T14:03:12.880Z",
  "data": {
    "callId": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11",
    "ref": "CA-482137",
    "status": "ringing",
    "buyerId": "f1e2d3c4-b5a6-4978-8a9b-0c1d2e3f4a5b",
    "buyerName": "BlueSky Legal",
    "targetId": "d4c3b2a1-9e8f-4d7c-b6a5-4f3e2d1c0b9a",
    "targetName": "BlueSky Legal — Intake",
    "targetExternalRef": "bsl-intake",
    "answeredAt": null,
    "talkSeconds": 0,
    "attemptNo": 1,
    "…": "the rest of the payload core"
  }
}

call.failover

A target did not take the call and the next one in the plan is being tried. The target named in the core is the one that failed; nextTargetId and nextTargetName are where the call is going. Both are null when the plan is spent, which is the moment a call.unrouted follows.

Extra fieldMeaning
attemptNoThe attempt that just failed.
reasonOne word: no_answer, busy, rejected, failed, abandoned.
sipCodeThe SIP response from the buyer's side, or null when the carrier gave none. See SIP codes.
sentenceThe same thing in English, as the waterfall shows it.
nextTargetId, nextTargetNameThe next entry in the plan, or null if there is none.
call.failover
{
  "id": "req_3c4d5e6f708192a3",
  "event": "call.failover",
  "createdAt": "2026-09-20T14:03:31.019Z",
  "data": {
    "callId": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11",
    "ref": "CA-482137",
    "status": "routing",
    "targetId": "d4c3b2a1-9e8f-4d7c-b6a5-4f3e2d1c0b9a",
    "targetName": "BlueSky Legal — Intake",
    "attemptNo": 1,
    "reason": "no_answer",
    "sipCode": 480,
    "sentence": "BlueSky Legal — Intake did not answer (SIP 480 Temporarily Unavailable).",
    "nextTargetId": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
    "nextTargetName": "Acme Health — Dallas floor",
    "…": "the rest of the payload core"
  }
}

call.connected

A buyer answered and the caller is talking to them. This is the moment answeredAt is stamped, the moment the duration threshold starts running, and the moment the platform's per-minute clock starts. talkSeconds is still 0 — the call has only just begun.

Extra fieldMeaning
attemptNoThe attempt that answered.
call.connected
{
  "id": "req_4d5e6f708192a3b4",
  "event": "call.connected",
  "createdAt": "2026-09-20T14:03:19.332Z",
  "data": {
    "callId": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11",
    "ref": "CA-482137",
    "status": "in_progress",
    "buyerId": "f1e2d3c4-b5a6-4978-8a9b-0c1d2e3f4a5b",
    "buyerName": "Acme Health",
    "targetId": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
    "targetName": "Acme Health — Dallas floor",
    "answeredAt": "2026-09-20T14:03:19.000Z",
    "endedAt": null,
    "talkSeconds": 0,
    "converted": false,
    "revenueCents": 0,
    "payoutCents": 0,
    "attemptNo": 2,
    "…": "the rest of the payload core"
  }
}

call.completed

The call ended and its books were written. It is sent for every call, converted or not, including calls nobody answered, with the final duration and figures. This is the event to invoice from and the event to write into your own database.

reasonThe books say
durationThe buyer's leg talked for at least the frozen duration threshold. The usual conversion.
postbackThe buyer posted a conversion for the call.
manualSomebody marked it by hand — see Conversions and adjustments.
not_answeredNo buyer answered, so nothing was earned.
no_buyerThe call never reached a target at all. An unrouted call that the fallback number answered lands here.
under_threshold:41s_of_90sA buyer answered and hung up short. The two figures are the talk time and the threshold it had to beat.
revenueCents is the frozen price when it converted and 0 when it did not. payoutCents follows the publisher's own terms, which can be earned on a call the buyer did not pay for.
call.completed
{
  "id": "req_5e6f708192a3b4c5",
  "event": "call.completed",
  "createdAt": "2026-09-20T14:06:24.190Z",
  "data": {
    "callId": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11",
    "ref": "CA-482137",
    "status": "completed",
    "answeredAt": "2026-09-20T14:03:19.000Z",
    "endedAt": "2026-09-20T14:06:23.000Z",
    "talkSeconds": 184,
    "converted": true,
    "revenueCents": 4200,
    "payoutCents": 2750,
    "reason": "duration",
    "…": "the rest of the payload core"
  }
}

call.converted

The call earned its price. It fires beside call.completed when the call converted at settlement, and on its own, possibly an hour later, when a conversion arrives after the call had already settled — a buyer's postback, or a correction made by hand.

  • It carries the same reason vocabulary as call.completed, or the note somebody typed when they corrected the books by hand.
  • It fires only on a change. A call that is already converted and is adjusted again does not fire it a second time, so a conversion pixel cannot double-report.
  • A call marked not converted does not fire anything — an un-conversion is a correction to your books, not an event an ad network should be told about.
  • This is the event a conversion pixel usually hangs off.

call.unrouted

No target took the call. It fires the instant the router gives up, which for a gate refusal is before any target was dialled, and for a spent plan is after the last one failed. A call.completed follows when the call ends.

Extra fieldMeaning
reasonOne of the eleven unrouted reason codes.
sentenceThe sentence the router stored at the time, naming the campaign, the cap and the figure.
actionWhat the caller then heard: hangup, message or forward — the campaign's unrouted action.
call.unrouted
{
  "id": "req_6f708192a3b4c5d6",
  "event": "call.unrouted",
  "createdAt": "2026-09-20T14:03:11.402Z",
  "data": {
    "callId": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11",
    "ref": "CA-482137",
    "status": "rejected",
    "campaignName": "Acme Health — Inbound",
    "buyerId": null,
    "targetId": null,
    "answeredAt": null,
    "talkSeconds": 0,
    "converted": false,
    "reason": "campaign_closed",
    "sentence": "The call arrived outside the campaign's hours of operation.",
    "action": "message",
    "…": "the rest of the payload core"
  }
}

Target events

target.capped

A target or its buyer reached a cap with this call and stops taking calls until the window rolls over. It fires on the write that lands on the cap, not on every call afterwards — so it is news exactly once per window, rather than a stream of alerts about a target you already know is full.

Extra fieldMeaning
scopetarget or buyer — which of the two filled up.
windowhourly, daily, monthly, lifetime, or daily revenue for a money cap.
cap, countThe call cap and the counter that just reached it. Present on a call cap.
capCents, centsThe revenue cap and the day's revenue. Present on a daily revenue cap instead.
One call can fire this more than once — a call that fills a target's daily cap and its buyer's monthly cap at the same moment emits one event for each.
target.capped
{
  "id": "req_708192a3b4c5d6e7",
  "event": "target.capped",
  "createdAt": "2026-09-20T14:06:24.221Z",
  "data": {
    "callId": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11",
    "ref": "CA-482137",
    "buyerId": "f1e2d3c4-b5a6-4978-8a9b-0c1d2e3f4a5b",
    "buyerName": "Acme Health",
    "targetId": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
    "targetName": "Acme Health — Dallas floor",
    "scope": "target",
    "window": "daily",
    "cap": 50,
    "count": 50,
    "…": "the rest of the payload core"
  }
}

target.down

A target has failed three calls in a row. It is emitted for a number or sip target only — the ones the platform dials — on the third consecutive dial failure, once, not on every failure afterwards. A call that connects to it clears the counter. A bidding target that stops answering is not announced this way; watch its health instead, through Targets.

Extra fieldMeaning
reasonThe outcome of the failure that crossed the line — no_answer, busy, rejected, failed.
consecutiveFailuresHow many in a row it took. This is the threshold, so it is always 3.
sentence"BlueSky Legal — Intake has failed 3 calls in a row."
target.down
{
  "id": "req_8192a3b4c5d6e7f8",
  "event": "target.down",
  "createdAt": "2026-09-20T14:03:31.044Z",
  "data": {
    "callId": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11",
    "ref": "CA-482137",
    "targetId": "d4c3b2a1-9e8f-4d7c-b6a5-4f3e2d1c0b9a",
    "targetName": "BlueSky Legal — Intake",
    "reason": "no_answer",
    "consecutiveFailures": 3,
    "sentence": "BlueSky Legal — Intake has failed 3 calls in a row.",
    "…": "the rest of the payload core"
  }
}

This is a warning, not a suspension. Three dial failures do not take a dialled target out of the plan: the next call tries it again, and you get one event rather than a stream of them. What does take it out is the buyer's own equipment answering 503 with a Retry-After — then it is skipped with the suppressed reason code for exactly as long as that header asked for, and comes back on its own. Nothing has to be switched back on either way.

wallet.low

Your wallet is empty and a caller has just been turned away for it. This is the one event that is not about a call, and its payload has nothing in common with the others: four fields, and no call anywhere in them.

wallet.low
{
  "id": "req_92a3b4c5d6e7f809",
  "event": "wallet.low",
  "createdAt": "2026-09-20T14:03:11.118Z",
  "data": {
    "agencyId": "5d0e7c2a-1b3f-4e6d-9a8c-7f6e5d4c3b2a",
    "agencyName": "Northwind Media Group",
    "balanceCents": 0,
    "reason": "workspace_unfunded"
  }
}
FieldMeaning
agencyId, agencyNameThe workspace whose wallet is empty.
balanceCentsThe balance at that moment, in cents. It is zero or below, which is why routing stopped.
reasonworkspace_unfunded — the same reason code the refused calls carry.

Which endpoints hear an event

An event reaches one of your endpoints when all of these hold. They are applied at delivery time, so a change to an endpoint takes effect on the next event and never rewrites the log.

  1. The event happened in your workspace. There is no setting that fans an endpoint out beyond it.
  2. The endpoint is not paused. A paused endpoint hears nothing; events that fire while it is paused are not queued for it and cannot be replayed later.
  3. Its pattern matches: the exact name, or * for everything. Patterns are exact names only — there is no call.* family form, so an endpoint that wants six of the seven call events names all six.
  4. Its campaign narrowing, if it has one, equals the event's campaign. An endpoint pinned to a campaign does not hear an event that names no campaign at all.
  5. Its publisher narrowing, if it has one, equals the event's publisher — so an endpoint pinned to a publisher never hears a call from your own media.
  6. Your plan still includes webhooks. A workspace that downgrades keeps its endpoints exactly as they are and stops emitting to them; coming back on the plan starts them again with nothing to rebuild.

Each matching endpoint gets its own delivery with its own retries, so one partner being down never delays another.

Worked example: a handler that survives contact

Three rules make a receiver that never needs an emergency fix: answer first, de-duplicate on the request id, and ignore what you do not recognise.

A minimal Node 18+ receiver
import express from "express";

const app = express();

// The events this handler knows. Anything else is acknowledged and dropped —
// new events are added over time and a 500 on one you have never seen just
// buys you four retries of the same surprise.
const HANDLED = new Set(["call.connected", "call.completed", "call.converted", "call.unrouted"]);

app.post("/hooks/buy3", express.json(), async (req, res) => {
  const event = req.header("buy3-event");
  const requestId = req.header("buy3-request-id");

  // Answer first, work afterwards: a delivery that takes longer than the
  // timeout counts as failed even when your handler finishes the job.
  res.status(204).end();

  if (!HANDLED.has(event)) return;
  if (await alreadySeen(requestId)) return;   // retries and replays reuse it
  await remember(requestId);

  const call = req.body.data;
  console.log(event, call.ref, call.converted, call.revenueCents);
});
  • Answer first. Any 2xx is success. A delivery that has not been answered inside the timeout counts as failed and is retried, even though your handler finished the job — and now you have processed it twice.
  • De-duplicate on buy3-request-id. Retries and replays reuse it deliberately, so a receiver that remembers the ids it has handled is safe against both.
  • Ignore unknown names. Events are added over time. A handler that throws on one it has not seen turns a new feature into four retries and a degraded endpoint.
  • Do not infer order from arrival. Two events a millisecond apart can land the other way round. call.incoming always describes the earlier moment whatever time it arrives; order on the payload's own instants.

Next steps