API reference

Calls

List and filter calls, watch the calls in progress, export the log as CSV, read one call with its full waterfall, stream its recording, and post conversions, tags and adjustments.

With these endpoints a program can do everything the call log does: find any call, explain why it went where it went, and correct its books. A call is one inbound call to one of your tracking numbers, or one call a publisher won at auction and sent you — routed or not. Only tracking calls appear here; any other call's id answers 404.

The call object

A row of the list. The campaign, publisher, buyer and target objects are snapshots frozen on the call, so a buyer renamed or deleted since still reads as it was; id is null when the record has since been deleted, and the whole object is null when the call never had one — a call on your own media has no publisher, an unrouted call has no buyer or target.

FieldTypeMeaning
iduuidThe call's id, used in every path below.
refstring or nullThe public reference, such as CA-482137. It is what people quote to each other.
startedAttimestampWhen the call arrived.
statusstringThe call's status: initiated, routing, reserved, ringing, waiting, in_progress, completed, missed, rejected, abandoned, no_agent, no_answer, busy, failed or terminated.
outcomestringOne word per call, most specific first: live (not ended), converted, unrouted, connected (a buyer answered), missed.
callerNumberstring or nullThe caller, in E.164. null when the caller hid their number.
dialedNumberstring or nullThe tracking number that was dialled.
campaign, publisher, buyer, targetobject or null{ id, name }, frozen on the call.
talkSecondsintegerSeconds the caller talked to the buyer. Counted from the moment the buyer's leg answered.
ringSecondsintegerSeconds spent ringing.
convertedbooleanWhether the call earned its price — see Conversion.
revenueCentsintegerWhat the buyer owes you for the call. A record, not a transfer.
payoutCentsintegerWhat you owe the publisher for the call. A record, not a transfer.
profitCentsintegerrevenueCents less payoutCents.
usageCentsintegerWhat the platform charged for the call's connected minutes, at your plan's per-minute rate. See pricing.
reasonCodestring or nullThe unrouted reason code the call ended under, else null.
reasonSentencestring or nullThat reason in words. The router's own sentence — it names the cap and the figure — when one was stored.
hasRecordingbooleanWhether a recording exists.
tagsobjectThe call's tags, as string values.

The call in detail

Get a call and the three writes return the list row plus these fields.

FieldTypeMeaning
directionstringinbound or outbound.
attributionstring or nullHow the call was tied to its source: tracking_number, routing_key, ping_window, direct or rtb_bid.
routingModestring or nullThe routing mode in force when the call was routed.
answeredAttimestamp or nullWhen the buyer's leg answered. Never set by the platform picking up the caller.
endedAttimestamp or nullWhen the call ended. null while it is in progress.
hungUpBystring or nullcaller, agent (the far end — the buyer, on a tracking call), platform or unknown.
endedReasonstring or nullThe cause the call ended under. For an unrouted call it is the reason code.
attemptCountintegerHow many targets were dialled.
callerState, callerCitystring or nullThe state comes from the caller's area code. The city is what a publisher sent with their ping.
dialedNumberId, numberPoolId, dniSessionIdstring or nullThe tracking number, and the pool and visitor session when the call came through DNI.
deliveryModestring or nullHow the call was delivered to the buyer.
tagSourcesobjectWhere each tag came from: publisher, dni, number, system, api or user.
settlementModestringbooks_only for a tracking call.
settledAt, convertedAttimestamp or nullWhen the books were written, and when the call converted.
conversionSourcestring or nullduration, postback, ivr, manual or qc.
frozenobjectThe terms the books were written on: { revenueCents, payoutCents, durationThresholdSeconds }. A call that booked nothing on a priced target is explained by comparing the two.
usageRateCentsnumber or nullThe per-minute rate frozen on the call when it started — a rate, with two decimal places.
correctionsobject or nullManual figures kept on the call: any of converted, revenueCents, payoutCents. They survive every later recompute.
adjustmentsarrayThe history of adjustments, oldest first, at most 50.
conversionNotestring or nullThe note sent with the last conversion.
recordingsarray{ id, format, channels, durationSeconds, createdAt, expiresAt, url }. url is the path of Stream a recording, relative to the API's origin.

The waterfall

Get a call also returns story: the call explained, from records written while the call happened. It is the same account the console draws — see The call waterfall.

PartWhat it holds
summary{ outcome, sentence } — the whole call in one sentence.
stepsIn order, each { at, step, title, sentence, tone, detail }. step is one of incoming, gates, plan, skipped, ping, dial, result, connected, unrouted, ended or settled; tone is info, success, warning or danger. There is one skipped step per target passed over, with its skip reason.
attemptsOne entry per dial: who was rung, for how long, the outcome and the SIP result in words. Destinations are masked — "the number ending 0188".
pingsThe buyers' bidding endpoints that were asked for a price, with status, bid, latency and reject reason. Empty when the plan has no RTB targets.
moneyThe books: converted, revenueCents, payoutCents, profitCents, usageCents, usageRateCents, the frozen terms and the adjustments.
eventsThe carrier's event trail as { id, at, source, type }, without payloads.
New step kinds may be added. Render sentence for a step you do not recognise rather than failing.

Books-only writes

Three endpoints change what your reports say about a call: Post a conversion, Add or remove tags and Adjust a call's books. None of them moves money. Revenue and payout on a tracking call are records, not transfers — your buyers pay you, and you pay your publishers, outside the platform. Each write is recorded in the activity log under the key's name, and each is safe to retry. The guide is Conversions and adjustments.

Endpoints

List calls

GET/api/v1/calls

The call log, filtered and paged. stats is computed over the same filter as the rows, across every page, so the totals are always about the list you asked for; total is stats.count. Both rates are fractions and are null when their denominator is zero: connectRate is connected ÷ calls, conversionRate is converted ÷ connected.

API key. Send Authorization: Bearer <api key>. See Authentication.Scopecalls:read

Query parameters

NameTypeDescription
fromOptionaldateStart of the window, as an ISO 8601 instant. A bare date is read as midnight UTC.default: the start of the day six days ago in tz, so the window covers the last 7 calendar days
toOptionaldateEnd of the window.default: now · must not be before from · the window may span at most 366 days
tzOptionalstringIANA time zone for the default window and for local timestamps, such as America/Chicago. An unrecognised zone is a 400.up to 64 characters · default: the workspace's time zone
campaignIdOptionaluuidCalls on one campaign.
publisherIdOptionaluuidCalls from one publisher.
buyerIdOptionaluuidCalls taken by one buyer.
targetIdOptionaluuidCalls taken by one target.
numberIdOptionaluuidCalls that arrived on one tracking number.
statusOptionalenumThe call's status.initiated, routing, reserved, ringing, waiting, in_progress, completed, missed, rejected, abandoned, no_agent, no_answer, busy, failed, terminated
outcomeOptionalenumOne word for how the call ended. As a filter, connected is every call a buyer answered, converted or not.connected, converted, unrouted, missed, live
minTalkOptionalintegerLeast talk time, in seconds.0 to 86400; must not exceed maxTalk
maxTalkOptionalintegerMost talk time, in seconds.0 to 86400
callerOptionalstringA caller's number. A full number — E.164, or 10 or 11 digits — is an exact match; anything shorter matches the end of the number, so 0142 finds the caller ending 0142. In a query string, write the + of an E.164 number as %2B.up to 32 characters · at least 4 digits
tagKeyOptionalstringCalls that carry this tag, whatever its value.letters, digits, ., - and _; up to 64 characters
tagValueOptionalstringCalls whose tagKey tag has exactly this value. Needs tagKey.up to 512 characters
reasonOptionalstringCalls that ended unrouted for this reason. Only the unrouted reason codes are accepted; a skip reason such as capped is a 400, because no call ends on one.
hasRecordingOptionalbooleantrue for calls with a recording, false for calls without.
pageOptionalintegerPage number, from 1.default 1
pageSizeOptionalintegerRows per page.default 25 · at most 200; a larger value is clamped
sortOptionalenumThe field to sort by. An unknown value falls back to the default.startedAt, talkSeconds, billedCents (revenue) · default startedAt
dirOptionalenumSort direction.asc, desc · default desc

Example request

curl "https://api.buy3.io/api/v1/calls?outcome=converted&from=2026-09-14T05:00:00Z&tz=America/Chicago&pageSize=1" \
  -H "Authorization: Bearer $BUY3_API_KEY"

Responses

  • 200A page of call objects, with stats for the whole filtered set and the window that was applied. usageCents is left out of the example rows and of stats.
{
  "data": [
    {
      "id": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11",
      "ref": "CA-482137",
      "startedAt": "2026-09-19T14:03:11.000Z",
      "status": "completed",
      "outcome": "converted",
      "callerNumber": "+14155550142",
      "dialedNumber": "+18885550100",
      "campaign": {
        "id": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
        "name": "Health cover — inbound"
      },
      "publisher": {
        "id": "c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b",
        "name": "Northwind Media"
      },
      "buyer": {
        "id": "f1e2d3c4-b5a6-4978-8a9b-0c1d2e3f4a5b",
        "name": "Acme Health"
      },
      "target": {
        "id": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
        "name": "Acme Health — Dallas floor"
      },
      "talkSeconds": 184,
      "ringSeconds": 8,
      "converted": true,
      "revenueCents": 4200,
      "payoutCents": 2750,
      "profitCents": 1450,
      "reasonCode": null,
      "reasonSentence": null,
      "hasRecording": true,
      "tags": {
        "gclid": "Cj0KCQjw",
        "utm_source": "search"
      }
    }
  ],
  "page": 1,
  "pageSize": 1,
  "total": 96,
  "totalPages": 96,
  "stats": {
    "count": 96,
    "connected": 96,
    "converted": 96,
    "unrouted": 0,
    "connectRate": 1,
    "conversionRate": 1,
    "revenueCents": 403200,
    "payoutCents": 264000,
    "profitCents": 139200,
    "avgTalkSeconds": 201,
    "totalTalkSeconds": 19296
  },
  "from": "2026-09-14T05:00:00.000Z",
  "to": "2026-09-20T18:22:09.412Z",
  "timezone": "America/Chicago"
}

Errors

StatusCodeWhen
401missing_key, invalid_keyNo bearer key was sent, or the key is malformed, unknown or revoked. See Authentication.
402subscription_inactive, plan_upgrade_requiredThe workspace's subscription is not active, or its plan does not include call tracking and API access.
403insufficient_scope, ip_not_allowedThe key does not hold calls:read, or the request came from outside the key's IP allowlist.
429rate_limitedThe key is over its per-minute limit. Wait for Retry-After seconds.
400validation_errorA filter has the wrong type: an id that is not a UUID, a status or outcome that is not in the list, a talk time out of range. details names the field.
400bad_requestfrom is after to, the window is wider than 366 days, tz is not a time zone, tagValue was sent without tagKey, tagKey is not a valid key, reason is not an unrouted reason, minTalk exceeds maxTalk, or caller has fewer than 4 digits. details names the field.

List calls in progress

GET/api/v1/calls/live

The calls on the phone right now, built to be polled every few seconds. It takes no filters on purpose: a call in progress on a campaign you have filtered out is still in progress. It returns calls that have not ended, started in the last six hours, with the status routing, ringing or in_progress — newest first, at most 200. target and buyer follow the call down the routing plan, so they name who is being rung or spoken to at this moment. The response is sent with Cache-Control: no-store. The guide is Live calls.

API key. Send Authorization: Bearer <api key>. See Authentication.Scopecalls:read

Example request

curl "https://api.buy3.io/api/v1/calls/live" \
  -H "Authorization: Bearer $BUY3_API_KEY"

Responses

  • 200count is the number of calls in live; connected is how many of them a buyer has answered. elapsedSeconds and talkSeconds are measured at at, the moment the response was built.
{
  "live": [
    {
      "id": "2c4e6a80-1b3d-4f5a-9c7e-8d0b2a4c6e81",
      "ref": "CA-482291",
      "status": "in_progress",
      "startedAt": "2026-09-20T18:21:02.000Z",
      "answeredAt": "2026-09-20T18:21:14.000Z",
      "elapsedSeconds": 67,
      "talkSeconds": 55,
      "callerNumber": "+14155550142",
      "callerState": "CA",
      "dialedNumber": "+18885550100",
      "campaign": {
        "id": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
        "name": "Health cover — inbound"
      },
      "publisher": {
        "id": "c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b",
        "name": "Northwind Media"
      },
      "target": {
        "id": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
        "name": "Acme Health — Dallas floor"
      },
      "buyer": {
        "id": "f1e2d3c4-b5a6-4978-8a9b-0c1d2e3f4a5b",
        "name": "Acme Health"
      },
      "attemptNo": 1
    }
  ],
  "count": 1,
  "connected": 1,
  "at": "2026-09-20T18:22:09.412Z"
}

Errors

StatusCodeWhen
401missing_key, invalid_keyNo bearer key was sent, or the key is malformed, unknown or revoked. See Authentication.
402subscription_inactive, plan_upgrade_requiredThe workspace's subscription is not active, or its plan does not include call tracking and API access.
403insufficient_scope, ip_not_allowedThe key does not hold calls:read, or the request came from outside the key's IP allowlist.
429rate_limitedThe key is over its per-minute limit. Wait for Retry-After seconds.

Export calls as CSV

GET/api/v1/calls/export.csv

The filtered call log as one CSV file, streamed from the server — far faster than paging for a large pull. It takes the same filters as List calls, without paging or sorting, and returns at most 100,000 rows, newest first. The file is UTF-8 with a byte-order mark and RFC 4180 quoting; tags is one cell holding a JSON object, and startedAtLocal is YYYY-MM-DD HH:MM:SS in the request's time zone. A cell that a spreadsheet would read as a formula is prefixed with an apostrophe. The export is written to the activity log before the first byte is sent, at high severity, with the filter and the row count under the key's name — a file of callers' numbers leaving the platform is an act with a name on it. If the stream fails part-way the connection is dropped rather than ending a truncated file cleanly, so a broken download fails visibly. See Exports.

API key. Send Authorization: Bearer <api key>. See Authentication.Scopecalls:read

Query parameters

NameTypeDescription
fromOptionaldateStart of the window, as an ISO 8601 instant. A bare date is read as midnight UTC.default: the start of the day six days ago in tz, so the window covers the last 7 calendar days
toOptionaldateEnd of the window.default: now · must not be before from · the window may span at most 366 days
tzOptionalstringIANA time zone for the default window and for local timestamps, such as America/Chicago. An unrecognised zone is a 400.up to 64 characters · default: the workspace's time zone
campaignIdOptionaluuidCalls on one campaign.
publisherIdOptionaluuidCalls from one publisher.
buyerIdOptionaluuidCalls taken by one buyer.
targetIdOptionaluuidCalls taken by one target.
numberIdOptionaluuidCalls that arrived on one tracking number.
statusOptionalenumThe call's status.initiated, routing, reserved, ringing, waiting, in_progress, completed, missed, rejected, abandoned, no_agent, no_answer, busy, failed, terminated
outcomeOptionalenumOne word for how the call ended. As a filter, connected is every call a buyer answered, converted or not.connected, converted, unrouted, missed, live
minTalkOptionalintegerLeast talk time, in seconds.0 to 86400; must not exceed maxTalk
maxTalkOptionalintegerMost talk time, in seconds.0 to 86400
callerOptionalstringA caller's number. A full number — E.164, or 10 or 11 digits — is an exact match; anything shorter matches the end of the number, so 0142 finds the caller ending 0142. In a query string, write the + of an E.164 number as %2B.up to 32 characters · at least 4 digits
tagKeyOptionalstringCalls that carry this tag, whatever its value.letters, digits, ., - and _; up to 64 characters
tagValueOptionalstringCalls whose tagKey tag has exactly this value. Needs tagKey.up to 512 characters
reasonOptionalstringCalls that ended unrouted for this reason. Only the unrouted reason codes are accepted; a skip reason such as capped is a 400, because no call ends on one.
hasRecordingOptionalbooleantrue for calls with a recording, false for calls without.

Example request

curl "https://api.buy3.io/api/v1/calls/export.csv?outcome=converted&from=2026-09-01T05:00:00Z&tz=America/Chicago" \
  -H "Authorization: Bearer $BUY3_API_KEY" \
  -o calls.csv

Responses

  • 200Content-Type: text/csv; charset=utf-8, with Content-Disposition: attachment; filename="calls-YYYY-MM-DD.csv". The first line is the header below, then one line per call.
ref,startedAt,startedAtLocal,status,outcome,callerNumber,dialedNumber,campaign,publisher,buyer,target,ringSeconds,talkSeconds,converted,revenueCents,payoutCents,profitCents,usageCents,reasonCode,reasonSentence,hasRecording,tags

Errors

StatusCodeWhen
401missing_key, invalid_keyNo bearer key was sent, or the key is malformed, unknown or revoked. See Authentication.
402subscription_inactive, plan_upgrade_requiredThe workspace's subscription is not active, or its plan does not include call tracking and API access.
403insufficient_scope, ip_not_allowedThe key does not hold calls:read, or the request came from outside the key's IP allowlist.
429rate_limitedThe key is over its per-minute limit. Wait for Retry-After seconds.
400validation_errorA filter has the wrong type: an id that is not a UUID, a status or outcome that is not in the list, a talk time out of range. details names the field.
400bad_requestfrom is after to, the window is wider than 366 days, tz is not a time zone, tagValue was sent without tagKey, tagKey is not a valid key, reason is not an unrouted reason, minTalk exceeds maxTalk, or caller has fewer than 4 digits. details names the field.

Get a call

GET/api/v1/calls/:id

Everything about one call: the list row, the detail fields, its recordings and storythe waterfall, with every check it passed, every target considered and why each was skipped, and every dial attempt with its result in words. Recording links point at /api/v1, so the key that read the call can follow them.

API key. Send Authorization: Bearer <api key>. See Authentication.Scopecalls:read

Path parameters

NameTypeDescription
idRequireduuidThe call's id. The public reference (CA-482137) is not accepted here — find a call by reference with the list's filters and read its id.

Example request

curl "https://api.buy3.io/api/v1/calls/0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11" \
  -H "Authorization: Bearer $BUY3_API_KEY"

Responses

  • 200recordingsReason says why recordings is empty: not_saved (the call was answered and nothing was kept — the campaign does not record, for instance), never_answered, or null when there are recordings. usageCents and usageRateCents are left out of the example, on the call and in story.money.
{
  "call": {
    "id": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11",
    "ref": "CA-482137",
    "startedAt": "2026-09-19T14:03:11.000Z",
    "status": "completed",
    "outcome": "converted",
    "callerNumber": "+14155550142",
    "dialedNumber": "+18885550100",
    "campaign": {
      "id": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
      "name": "Health cover — inbound"
    },
    "publisher": {
      "id": "c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b",
      "name": "Northwind Media"
    },
    "buyer": {
      "id": "f1e2d3c4-b5a6-4978-8a9b-0c1d2e3f4a5b",
      "name": "Acme Health"
    },
    "target": {
      "id": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
      "name": "Acme Health — Dallas floor"
    },
    "talkSeconds": 184,
    "ringSeconds": 8,
    "converted": true,
    "revenueCents": 4200,
    "payoutCents": 2750,
    "profitCents": 1450,
    "reasonCode": null,
    "reasonSentence": null,
    "hasRecording": true,
    "tags": {
      "gclid": "Cj0KCQjw",
      "utm_source": "search"
    },
    "direction": "inbound",
    "attribution": "tracking_number",
    "routingMode": "priority",
    "answeredAt": "2026-09-19T14:03:38.000Z",
    "endedAt": "2026-09-19T14:06:42.000Z",
    "hungUpBy": "caller",
    "endedReason": "normal_clearing",
    "attemptCount": 2,
    "callerState": "CA",
    "callerCity": null,
    "dialedNumberId": "7c0e5a3b-1d2f-4e6a-8b9c-0d1e2f3a4b5c",
    "numberPoolId": null,
    "dniSessionId": null,
    "deliveryMode": "pstn_did",
    "tagSources": {
      "gclid": "dni",
      "utm_source": "dni"
    },
    "settlementMode": "books_only",
    "settledAt": "2026-09-19T14:06:43.000Z",
    "convertedAt": "2026-09-19T14:06:43.000Z",
    "conversionSource": "duration",
    "frozen": {
      "revenueCents": 4200,
      "payoutCents": 2750,
      "durationThresholdSeconds": 90
    },
    "corrections": null,
    "adjustments": [],
    "conversionNote": null,
    "recordings": [
      {
        "id": "3f2e1d0c-9b8a-4c7d-a6e5-f4d3c2b1a098",
        "format": "mp3",
        "channels": 2,
        "durationSeconds": 184,
        "createdAt": "2026-09-19T14:06:50.000Z",
        "expiresAt": null,
        "url": "/api/v1/calls/0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11/recordings/3f2e1d0c-9b8a-4c7d-a6e5-f4d3c2b1a098/audio"
      }
    ]
  },
  "story": {
    "summary": {
      "outcome": "converted",
      "sentence": "Connected to Acme Health — Dallas floor (Acme Health) on the second attempt and talked for 3 minutes 4 seconds. It converted for $42.00, with $27.50 owed to the publisher."
    },
    "steps": [
      {
        "at": "2026-09-19T14:03:11.000Z",
        "step": "incoming",
        "title": "Call received",
        "sentence": "A call from +14155550142 (CA) arrived on +18885550100 for the campaign Health cover — inbound, from the publisher Northwind Media. It carried 2 tags.",
        "tone": "info",
        "detail": {
          "callerNumber": "+14155550142",
          "callerState": "CA",
          "dialedNumber": "+18885550100",
          "numberId": "7c0e5a3b-1d2f-4e6a-8b9c-0d1e2f3a4b5c",
          "publisherId": "c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b",
          "poolId": null,
          "sessionId": null,
          "tags": {
            "gclid": "Cj0KCQjw",
            "utm_source": "search"
          },
          "tagSources": {
            "gclid": "dni",
            "utm_source": "dni"
          }
        }
      },
      {
        "at": "2026-09-19T14:03:11.000Z",
        "step": "gates",
        "title": "Checks passed",
        "sentence": "The call passed every check before routing.",
        "tone": "success",
        "detail": {
          "passed": [
            "workspace_inactive",
            "workspace_unfunded",
            "campaign_not_live",
            "caller_blocked",
            "anonymous_blocked",
            "campaign_closed",
            "campaign_capped",
            "repeat_blocked"
          ],
          "failed": null
        }
      },
      {
        "at": "2026-09-19T14:03:11.000Z",
        "step": "plan",
        "title": "Routing plan",
        "sentence": "2 targets were considered, in priority order: 2 could take the call. First in line: BlueSky Legal — overflow (BlueSky Legal).",
        "tone": "info",
        "detail": {
          "mode": "priority",
          "eligible": [
            {
              "targetId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
              "targetName": "BlueSky Legal — overflow",
              "buyerId": "e5d4c3b2-a1f0-4e9d-8c7b-6a5f4e3d2c1b",
              "buyerName": "BlueSky Legal",
              "priority": 1,
              "weight": 1,
              "revenueCents": 3500,
              "source": "static"
            },
            {
              "targetId": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
              "targetName": "Acme Health — Dallas floor",
              "buyerId": "f1e2d3c4-b5a6-4978-8a9b-0c1d2e3f4a5b",
              "buyerName": "Acme Health",
              "priority": 2,
              "weight": 1,
              "revenueCents": 4200,
              "source": "static"
            }
          ],
          "skipped": []
        }
      },
      {
        "at": "2026-09-19T14:03:12.000Z",
        "step": "dial",
        "title": "Attempt 1 · BlueSky Legal — overflow",
        "sentence": "Rang BlueSky Legal — overflow (BlueSky Legal) on the number ending 0147 — no answer after 18 seconds (SIP 480, the line was unavailable).",
        "tone": "warning",
        "detail": {
          "attemptNo": 1,
          "outcome": "no_answer",
          "rangSeconds": 18,
          "endedAt": "2026-09-19T14:03:30.000Z",
          "targetId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
          "targetName": "BlueSky Legal — overflow",
          "buyerId": "e5d4c3b2-a1f0-4e9d-8c7b-6a5f4e3d2c1b",
          "buyerName": "BlueSky Legal",
          "deliveryMode": "pstn_did",
          "destination": "the number ending 0147",
          "sipCode": 480,
          "sipText": "the line was unavailable"
        }
      },
      {
        "at": "2026-09-19T14:03:30.000Z",
        "step": "dial",
        "title": "Attempt 2 · Acme Health — Dallas floor",
        "sentence": "Rang Acme Health — Dallas floor (Acme Health) on the number ending 0188 — answered.",
        "tone": "success",
        "detail": {
          "attemptNo": 2,
          "outcome": "answered",
          "rangSeconds": 8,
          "endedAt": "2026-09-19T14:03:38.000Z",
          "targetId": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
          "targetName": "Acme Health — Dallas floor",
          "buyerId": "f1e2d3c4-b5a6-4978-8a9b-0c1d2e3f4a5b",
          "buyerName": "Acme Health",
          "deliveryMode": "pstn_did",
          "destination": "the number ending 0188",
          "sipCode": null,
          "sipText": null
        }
      },
      {
        "at": "2026-09-19T14:03:38.000Z",
        "step": "connected",
        "title": "Connected",
        "sentence": "Acme Health — Dallas floor (Acme Health) answered on the second attempt, 27 seconds after the call arrived.",
        "tone": "success",
        "detail": {
          "attemptNo": 2,
          "targetId": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
          "targetName": "Acme Health — Dallas floor",
          "secondsToConnect": 27
        }
      },
      {
        "at": "2026-09-19T14:06:42.000Z",
        "step": "ended",
        "title": "Call ended",
        "sentence": "The call ended after 3 minutes 4 seconds of talk time — the caller hung up.",
        "tone": "info",
        "detail": {
          "talkSeconds": 184,
          "hungUpBy": "caller",
          "hasRecording": true
        }
      },
      {
        "at": "2026-09-19T14:06:43.000Z",
        "step": "settled",
        "title": "Converted",
        "sentence": "The call converted: $42.00 in revenue from Acme Health, $27.50 owed to the publisher Northwind Media — $14.50 profit.",
        "tone": "success",
        "detail": {
          "converted": true,
          "revenueCents": 4200,
          "payoutCents": 2750,
          "profitCents": 1450,
          "durationThresholdSeconds": 90,
          "conversionSource": "duration",
          "corrected": false
        }
      }
    ],
    "attempts": [
      {
        "attemptNo": 1,
        "startedAt": "2026-09-19T14:03:12.000Z",
        "endedAt": "2026-09-19T14:03:30.000Z",
        "rangSeconds": 18,
        "outcome": "no_answer",
        "result": "no answer after 18 seconds (SIP 480, the line was unavailable)",
        "tone": "warning",
        "targetId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
        "targetName": "BlueSky Legal — overflow",
        "buyerId": "e5d4c3b2-a1f0-4e9d-8c7b-6a5f4e3d2c1b",
        "buyerName": "BlueSky Legal",
        "agentName": null,
        "deliveryMode": "pstn_did",
        "destination": "the number ending 0147",
        "sipCode": 480,
        "sipText": "the line was unavailable",
        "lossCode": null,
        "whisperAccepted": null
      },
      {
        "attemptNo": 2,
        "startedAt": "2026-09-19T14:03:30.000Z",
        "endedAt": "2026-09-19T14:03:38.000Z",
        "rangSeconds": 8,
        "outcome": "answered",
        "result": "answered",
        "tone": "success",
        "targetId": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
        "targetName": "Acme Health — Dallas floor",
        "buyerId": "f1e2d3c4-b5a6-4978-8a9b-0c1d2e3f4a5b",
        "buyerName": "Acme Health",
        "agentName": null,
        "deliveryMode": "pstn_did",
        "destination": "the number ending 0188",
        "sipCode": null,
        "sipText": null,
        "lossCode": null,
        "whisperAccepted": null
      }
    ],
    "pings": [],
    "money": {
      "settled": true,
      "settledAt": "2026-09-19T14:06:43.000Z",
      "settlementMode": "books_only",
      "converted": true,
      "convertedAt": "2026-09-19T14:06:43.000Z",
      "conversionSource": "duration",
      "revenueCents": 4200,
      "payoutCents": 2750,
      "profitCents": 1450,
      "durationThresholdSeconds": 90,
      "frozen": {
        "revenueCents": 4200,
        "payoutCents": 2750
      },
      "adjustments": []
    },
    "events": [
      {
        "id": "918273",
        "at": "2026-09-19T14:03:11.000Z",
        "source": "telnyx",
        "type": "call.initiated"
      },
      {
        "id": "918301",
        "at": "2026-09-19T14:06:42.000Z",
        "source": "telnyx",
        "type": "call.hangup"
      }
    ]
  },
  "recordingsReason": null
}

Errors

StatusCodeWhen
401missing_key, invalid_keyNo bearer key was sent, or the key is malformed, unknown or revoked. See Authentication.
402subscription_inactive, plan_upgrade_requiredThe workspace's subscription is not active, or its plan does not include call tracking and API access.
403insufficient_scope, ip_not_allowedThe key does not hold calls:read, or the request came from outside the key's IP allowlist.
429rate_limitedThe key is over its per-minute limit. Wait for Retry-After seconds.
404not_foundNo tracking call with that id in your workspace. A malformed id and another workspace's call answer exactly the same.

Stream a recording

GET/api/v1/calls/:id/recordings/:rid/audio

The audio of one recording. It is streamed through the platform: the carrier's own link never leaves the server, so there is no public URL to a recording and nothing to leak by forwarding one. Take the path from call.recordings[].url. See Recordings.

API key. Send Authorization: Bearer <api key>. See Authentication.Scopecalls:read

Path parameters

NameTypeDescription
idRequireduuidThe call's id. The public reference (CA-482137) is not accepted here — find a call by reference with the list's filters and read its id.
ridRequireduuidThe recording's id, from call.recordings.

Example request

curl "https://api.buy3.io/api/v1/calls/0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11/recordings/3f2e1d0c-9b8a-4c7d-a6e5-f4d3c2b1a098/audio" \
  -H "Authorization: Bearer $BUY3_API_KEY" \
  -o CA-482137.mp3

Responses

  • 200The audio bytes, with Content-Type: audio/*, Content-Length, Cache-Control: private, max-age=3600 and Content-Disposition: inline; filename="<ref>.<format>".
(binary audio)

Errors

StatusCodeWhen
401missing_key, invalid_keyNo bearer key was sent, or the key is malformed, unknown or revoked. See Authentication.
402subscription_inactive, plan_upgrade_requiredThe workspace's subscription is not active, or its plan does not include call tracking and API access.
403insufficient_scope, ip_not_allowedThe key does not hold calls:read, or the request came from outside the key's IP allowlist.
429rate_limitedThe key is over its per-minute limit. Wait for Retry-After seconds.
404not_foundNo such call, no such recording on that call, or a recording with no audio stored against it. A malformed id or rid answers the same.
410errorThe carrier no longer holds the audio.
502upstream_errorThe carrier refused the request or could not be reached. Safe to retry.

Post a conversion

POST/api/v1/calls/:id/conversion

Tell the books that a call sold — or that it did not. Use it when the sale happens in your own system, after the call, and only you know which calls paid. Books only: no wallet moves. converted: false is the one way a converted call stops being one; the platform never revokes a conversion on its own. The revenue booked is 0 when converted is false, revenueCents when you send it, and otherwise the call's frozen price — or an earlier manual figure, which stands. The publisher's payout is recomputed on the terms frozen on the call. The call's conversionSource becomes manual, a call.converted webhook fires when the call newly converts, and the change is written to the activity log under the key's name. Safe to retry: it sets the books, so a repeat changes nothing.

API key. Send Authorization: Bearer <api key>. See Authentication.Scopecalls:write

Path parameters

NameTypeDescription
idRequireduuidThe call's id. The public reference (CA-482137) is not accepted here — find a call by reference with the list's filters and read its id.

Request body

NameTypeDescription
convertedRequiredbooleanWhether the call converted.
revenueCentsOptionalintegerWhat the buyer owes for this call, overriding the frozen price. The figure is kept through any later recompute.0 to 100000000 · must be 0 or omitted when converted is false
noteOptionalstringWhy, in your own words. Returned as conversionNote and kept in the activity log.up to 500 characters

Example request

curl -X POST "https://api.buy3.io/api/v1/calls/0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11/conversion" \
  -H "Authorization: Bearer $BUY3_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "converted": true,
    "revenueCents": 5500,
    "note": "Policy bound — CRM sale 88231"
  }'

Responses

  • 200The call in detail, with the books before and after. The example shows call cut down to the fields that changed; the response carries all of them.
{
  "call": {
    "id": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11",
    "ref": "CA-482137",
    "outcome": "converted",
    "converted": true,
    "revenueCents": 5500,
    "payoutCents": 2750,
    "profitCents": 2750,
    "convertedAt": "2026-09-20T15:40:12.000Z",
    "conversionSource": "manual",
    "corrections": {
      "converted": true,
      "revenueCents": 5500
    },
    "conversionNote": "Policy bound — CRM sale 88231"
  },
  "before": {
    "converted": false,
    "revenueCents": 0,
    "payoutCents": 0,
    "convertedAt": null
  },
  "after": {
    "converted": true,
    "revenueCents": 5500,
    "payoutCents": 2750,
    "convertedAt": "2026-09-20T15:40:12.000Z"
  }
}

Errors

StatusCodeWhen
401missing_key, invalid_keyNo bearer key was sent, or the key is malformed, unknown or revoked. See Authentication.
402subscription_inactive, plan_upgrade_requiredThe workspace's subscription is not active, or its plan does not include call tracking and API access.
403insufficient_scope, ip_not_allowedThe key does not hold calls:write, or the request came from outside the key's IP allowlist.
429rate_limitedThe key is over its per-minute limit. Wait for Retry-After seconds.
400validation_errorconverted is missing or not a boolean, revenueCents is out of range, or note is too long.
400bad_requestrevenueCents is above 0 while converted is false. details.revenueCents says so.
404not_foundNo tracking call with that id in your workspace. A malformed id and another workspace's call answer exactly the same.
409conflictThe call is still in progress, it does not settle books-only, it changed while this was being saved, or its books could not be recomputed. Wait for the call to end, then send the request again.

Add or remove tags

POST/api/v1/calls/:id/tags

Attach what you learnt about a call after it arrived — a CRM id, a disposition, a lead score — so reports can group by it and pixels can send it. Tags are merged into what the call already carries, in one statement, so two writers in the same second both keep their tags. Send null as a value to remove a key. Every tag set here is stamped with the source api, so it can never pass for something the publisher sent or the platform measured. It works on a call that is still in progress. Written to the activity log, at low severity.

API key. Send Authorization: Bearer <api key>. See Authentication.Scopecalls:write

Path parameters

NameTypeDescription
idRequireduuidThe call's id. The public reference (CA-482137) is not accepted here — find a call by reference with the list's filters and read its id.

Request body

NameTypeDescription
tagsRequiredobjectKeys and values to set. A value is a string, a number or a boolean, stored as a string and cut at 512 characters — or null to remove the key.1 to 40 keys per request · key: letters, digits, ., - and _, up to 64 characters · a call carries at most 80 tags

Example request

curl -X POST "https://api.buy3.io/api/v1/calls/0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11/tags" \
  -H "Authorization: Bearer $BUY3_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tags": {
      "crm_id": "88231",
      "disposition": "sold",
      "lead_score": null
    }
  }'

Responses

  • 200The call's full tag set after the merge, and where each tag came from.
{
  "id": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11",
  "tags": {
    "gclid": "Cj0KCQjw",
    "utm_source": "search",
    "crm_id": "88231",
    "disposition": "sold"
  },
  "tagSources": {
    "gclid": "dni",
    "utm_source": "dni",
    "crm_id": "api",
    "disposition": "api"
  }
}

Errors

StatusCodeWhen
401missing_key, invalid_keyNo bearer key was sent, or the key is malformed, unknown or revoked. See Authentication.
402subscription_inactive, plan_upgrade_requiredThe workspace's subscription is not active, or its plan does not include call tracking and API access.
403insufficient_scope, ip_not_allowedThe key does not hold calls:write, or the request came from outside the key's IP allowlist.
429rate_limitedThe key is over its per-minute limit. Wait for Retry-After seconds.
400validation_errortags is missing or is not an object.
400bad_requesttags is empty or has more than 40 keys, a key has characters that are not allowed, a value is an object or an array, or the call would carry more than 80 tags. details.tags says which.
404not_foundNo tracking call with that id in your workspace. A malformed id and another workspace's call answer exactly the same.

Adjust a call's books

POST/api/v1/calls/:id/adjust

Correct what a call's revenue or payout says, with a reason: the buyer short-paid, you agreed a make-good with the publisher. Books only: no wallet moves. A number sets and keeps the figure through any later recompute. An explicit JSON null clears a kept correction, and the call books what its frozen terms say again — the buyer's price if it converted, nothing if it did not. A field you do not send is left alone; this is the one endpoint where null and "not sent" differ, and an empty string does not count as either. Each adjustment is appended to the call's adjustments — who, when, why, from what, to what — and written to the activity log under the key's name. A call takes at most 50 adjustments. Safe to retry: a repeat changes no figure, and adds one more line of history.

API key. Send Authorization: Bearer <api key>. See Authentication.Scopecalls:write

Path parameters

NameTypeDescription
idRequireduuidThe call's id. The public reference (CA-482137) is not accepted here — find a call by reference with the list's filters and read its id.

Request body

NameTypeDescription
revenueCentsOptionalinteger or nullWhat the buyer owes for this call. null clears a kept correction.0 to 100000000
payoutCentsOptionalinteger or nullWhat you owe the publisher for this call. null clears a kept correction.0 to 100000000
reasonRequiredstringWhy the books are being changed. Kept with the adjustment.3 to 500 characters

Example request

curl -X POST "https://api.buy3.io/api/v1/calls/0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11/adjust" \
  -H "Authorization: Bearer $BUY3_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "revenueCents": 3000,
    "reason": "Buyer short-paid — invoice 2291"
  }'

Responses

  • 200The call in detail and the adjustment that was recorded. adjustment.cleared is present only when a side was cleared, and lists which. The example shows call cut down to the fields that changed.
{
  "call": {
    "id": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11",
    "ref": "CA-482137",
    "converted": true,
    "revenueCents": 3000,
    "payoutCents": 2750,
    "profitCents": 250,
    "corrections": {
      "revenueCents": 3000
    }
  },
  "adjustment": {
    "at": "2026-09-21T09:12:44.103Z",
    "by": {
      "kind": "api_key",
      "id": "5b1c9a70-2e4d-4f3a-9c8b-7d6e5f4a3b2c",
      "name": "API key b3_live_Qx7m"
    },
    "reason": "Buyer short-paid — invoice 2291",
    "before": {
      "revenueCents": 4200,
      "payoutCents": 2750
    },
    "after": {
      "revenueCents": 3000,
      "payoutCents": 2750
    }
  }
}

Errors

StatusCodeWhen
401missing_key, invalid_keyNo bearer key was sent, or the key is malformed, unknown or revoked. See Authentication.
402subscription_inactive, plan_upgrade_requiredThe workspace's subscription is not active, or its plan does not include call tracking and API access.
403insufficient_scope, ip_not_allowedThe key does not hold calls:write, or the request came from outside the key's IP allowlist.
429rate_limitedThe key is over its per-minute limit. Wait for Retry-After seconds.
400validation_errorreason is missing, shorter than 3 characters or longer than 500, or a figure is out of range.
400bad_requestNeither revenueCents nor payoutCents was sent — as a number, or as an explicit null. The message is "Say what to adjust".
404not_foundNo tracking call with that id in your workspace. A malformed id and another workspace's call answer exactly the same.
409conflictThe call is still in progress, it does not settle books-only, it has already been adjusted 50 times, or its books could not be recomputed.