API reference
Pings
Read the ping ledger: every inbound auction and every outbound bid request, with its request, response, latency and reject code.
Every ping, in both directions, is a row you can read. A publisher asking what you will pay for a caller is one row; you asking a buyer's bidding endpoint what they will pay is another. This group lists them, opens one with everything behind it, sums them up and exports them. The concepts are in The ping ledger.
The rows that matter most are the ones that did not become calls. Every "why are you not bidding on my traffic" is a refused ping with a reject code on it, and every "your buyer never answers" is an outbound ping with a latency and an HTTP status on it.
Two surfaces
| Endpoint | Surface | Credential |
|---|---|---|
GET /api/v1/pings | REST API | API key with pings:read |
GET /api/v1/pings/:id | REST API | API key with pings:read |
GET /api/tracking/pings/summary | Console | Session token |
GET /api/tracking/pings/export.csv | Console | Session token |
/api/tracking/pings and /api/tracking/pings/:id. They are the same handlers, so the query and the answer are identical. An API key cannot run the summary or the export.https://api.buy3.io/api/v1Two ledgers, one at a time
direction chooses the ledger. They hold different rows with different columns, so no answer ever mixes them.
direction | One row is | Read it when |
|---|---|---|
inbound (default) | A publisher's ping to you: one auction, whatever came of it. | A publisher says their pings are not getting bids. |
outbound | Your ping to one buyer's bidding endpoint: one bid request, one answer. | A buyer's endpoint is slow, erroring or never bidding. |
outcomefilters the inbound ledger andstatusthe outbound one. Sending the wrong one answers400rather than quietly ignoring it.- One inbound ping usually has several outbound rows under it: one per external bidding target the auction asked.
auction.idandauction.refjoin them. - The router also pings buyers for calls that arrived on a tracking number rather than through an auction. Those rows are in the outbound ledger with
origin: "call", and there is no inbound row to join them to — followcallIdinstead.
The window, and how long rows last
fromandtotake an ISO 8601 instant. A bare2026-09-14is read as midnight UTC.- With no
from, the window starts at midnight six days ago on the workspace's own clock: the last seven days, today included. With noto, it ends now. - The widest window is 92 days, and a wider one answers
400. - Ping rows are pruned well before that — see Limits for the retention period. Read a ping while it is young, or export it.
An inbound ping
| Field | Type | Meaning |
|---|---|---|
id | uuid | The auction's id. |
direction | string | Always inbound. |
ref | string | null | The public reference, such as AU-100482. It is what a publisher quotes at you, and you can read a ping by it. |
at | ISO 8601 | When the ping arrived. |
environment | string | null | Which environment the ping was scored in. |
outcome | string | How it ended — see below. |
reason | object | null | { code, reason, sentence } for a ping that got no bid. null when the outcome was won or cached. |
campaign, publisher | object | null | { id, name }. |
callerNumber | string | null | Masked to the last four digits unless the campaign discloses caller ids. |
callerMasked | boolean | Whether the number above was masked. |
callerState | string | null | The two-letter state the publisher sent. |
elapsedMs | integer | null | How long you took to answer. This is the number a publisher's platform times out on. |
tmaxMs | integer | null | The time budget the auction was given. |
statusCode | integer | null | The HTTP status you answered with. A no-bid is a 200. |
payoutCents | cents | null | What you offered the publisher. |
clearingPriceCents | cents | null | What the winning bid cleared at. |
winningTarget | object | null | { bidId, id, name }. |
targetsConsidered, targetsSolicited, bidsReceived | integer | How many targets were in the plan, how many were actually asked, and how many answered with a bid. |
An outbound ping
| Field | Type | Meaning |
|---|---|---|
id | uuid | The bid's id. This is what GET /api/v1/pings/:id?direction=outbound takes. |
direction | string | Always outbound. |
at | ISO 8601 | When you sent the bid request. |
origin | string | publisher — a publisher's ping started this auction, and the inbound row carries auction.ref. call — the router pinged for a call that had already arrived, and there is no inbound row. |
auction | object | { id, ref } — the auction this bid belongs to. |
campaign, publisher, target, buyer | object | null | { id, name }. |
status | string | What became of the bid — see below. |
won | boolean | This bid took the call. |
bidCents | cents | null | The bid as it counted, after any modifier. |
rawBidCents | cents | null | The bid exactly as the buyer returned it. |
httpStatus | integer | null | What the buyer's endpoint answered. null means nothing answered. |
latencyMs | integer | null | How long the buyer took. |
rejectCode, rejectReason | integer, string | null | Why the endpoint declined, when it said. |
sentence | string | The same fact in plain words, for a person. |
callId | uuid | null | The call this bid became. |
Outcomes and statuses
Inbound outcome | Means |
|---|---|
won | A bid was returned to the publisher. |
no_bid | No bid. reason carries the code and a sentence. |
cached | The publisher had already asked about this caller and the earlier answer had not expired, so it was repeated. |
rate_limited | The publisher's key was over one of its limits. |
blocked | The request was refused before the auction — a blocked caller, an address off the allowlist, a lane that is not live. |
error | The auction could not be scored. |
Outbound status | Means |
|---|---|
bid | The endpoint returned a bid. |
no_bid, rejected | The endpoint declined. |
timeout | The endpoint did not answer inside the auction's budget. |
error | The request failed, or the answer could not be read. |
won, claimed | This bid took the call. |
lost | Another bid took it. sentence says why this one did not. |
expired | The bid was never claimed in time. |
void | The bid was withdrawn. |
What a ping detail hides
- Credentials. Every stored request and response is filtered on the way out: a value under a key that reads as a credential —
authorization,x-api-key, anything endingtoken,password,secretorsignature— comes back as•••, and so does everything under it. - Callers. A caller's number is masked to its last four digits — in the column, inside the stored request body and inside the trace — unless the campaign discloses caller ids. The export masks them exactly as the API does.
- Other people's bids. An auction can include a target that is not yours. Such a bid still appears, with its rank, status and latency, because it is part of the story — but with no buyer, no price, no bodies and no destination.
Endpoints
- GET
/api/v1/pingsList pings - GET
/api/v1/pings/:idRead one ping - GET
/api/tracking/pings/summarySummarise the ping ledger - GET
/api/tracking/pings/export.csvExport pings as CSV
List pings
/api/v1/pingsOne ledger, newest first, in the standard list envelope with direction, from and to beside it. Changes nothing and is not recorded in the activity log. The console calls the same handler at /api/tracking/pings.
Query parameters
| Name | Type | Description |
|---|---|---|
directionOptional | string | Which ledger to read. The two cannot be mixed in one answer.inbound or outbound · default inbound |
fromOptional | date | The start of the window. A ping belongs to the moment it arrived.default: midnight six days ago, on the workspace's clock |
toOptional | date | The end of the window.default: now |
campaignIdOptional | uuid | Only pings on this campaign. |
publisherIdOptional | uuid | Only pings from this publisher. |
targetIdOptional | uuid | Only pings this target took part in. Inbound, that means every auction it was asked to bid in, whether it won or lost. |
outcomeOptional | string | Inbound only. How the auction ended.won, no_bid, cached, rate_limited, blocked or error |
statusOptional | string | Outbound only. What became of the bid.bid, no_bid, rejected, timeout, error, won, lost, expired, claimed or void |
reasonCodeOptional | integer | Only pings refused under this reject code.0–99999 |
qOptional | string | An auction reference such as AU-100482, an auction id or a bid id.up to 60 characters |
pageOptional | integer | The page to return.from 1, default 1 |
pageSizeOptional | integer | Rows per page. A larger value is clamped, not refused.1–200 · default 50 |
Example request
curl "https://api.buy3.io/api/v1/pings?direction=inbound&outcome=no_bid&publisherId=c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b&pageSize=100" \
-H "Authorization: Bearer $BUY3_API_KEY"Responses
- 200A page of the chosen ledger.
fromandtoecho the window that was used, so a client can show what it actually asked for.
{
"data": [
{
"id": "3f2e1d0c-9b8a-4765-8432-10fedcba9876",
"direction": "inbound",
"ref": "AU-100482",
"at": "2026-09-20T14:03:09.412Z",
"environment": "production",
"outcome": "won",
"reason": null,
"campaign": {
"id": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
"name": "Medicare — Inbound"
},
"publisher": {
"id": "c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b",
"name": "Northwind Media"
},
"callerNumber": "••••••0142",
"callerMasked": true,
"callerState": "CA",
"elapsedMs": 412,
"tmaxMs": 1000,
"statusCode": 200,
"payoutCents": 2750,
"clearingPriceCents": 4200,
"winningTarget": {
"bidId": "7c6b5a49-3827-4615-9048-372615049382",
"id": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
"name": "Acme Health — Dallas floor"
},
"targetsConsidered": 6,
"targetsSolicited": 3,
"bidsReceived": 2
},
{
"id": "8e7d6c5b-4a39-4827-9615-04938271650a",
"direction": "inbound",
"ref": "AU-100483",
"at": "2026-09-20T14:04:55.031Z",
"environment": "production",
"outcome": "no_bid",
"reason": {
"code": 1008,
"reason": "state_not_targeted",
"sentence": "That state is not being bought."
},
"campaign": {
"id": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
"name": "Medicare — Inbound"
},
"publisher": {
"id": "c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b",
"name": "Northwind Media"
},
"callerNumber": "••••••7733",
"callerMasked": true,
"callerState": "VT",
"elapsedMs": 38,
"tmaxMs": 1000,
"statusCode": 200,
"payoutCents": null,
"clearingPriceCents": null,
"winningTarget": null,
"targetsConsidered": 6,
"targetsSolicited": 0,
"bidsReceived": 0
}
],
"page": 1,
"pageSize": 50,
"total": 2,
"totalPages": 1,
"direction": "inbound",
"from": "2026-09-14T04:00:00.000Z",
"to": "2026-09-20T18:22:09.412Z"
}Errors
| Status | Code | When |
|---|---|---|
| 400 | validation_error | direction, outcome or status is not one of the allowed words, reasonCode is out of range, or an id filter is not a UUID. details names the field. |
| 400 | bad_request | status was sent with direction=inbound, or outcome with direction=outbound. Each ledger has its own word, and accepting the other one would hand back an unfiltered list that looks filtered. from is later than to, one of them is not a date, or the window is wider than 92 days. |
| 401 | missing_key | No Authorization: Bearer header was sent. |
| 401 | invalid_key | The key is malformed, unknown, or revoked with its grace window over. |
| 402 | subscription_inactive | The workspace's subscription is not active. On the REST API that stops reads as well as writes. |
| 402 | plan_upgrade_required | The plan does not include call tracking, or does not include API access. details.feature says which. |
| 403 | ip_not_allowed | The key has an IP allowlist and the request came from outside it. |
| 403 | insufficient_scope | The key does not hold pings:read. details lists the scopes required and the ones held. |
| 429 | rate_limited | The key is over its per-minute limit. Wait Retry-After seconds. |
Read one ping
/api/v1/pings/:idOne ping with everything behind it: the request as it was received or sent, the answer, the auction's trace as it was written, every bid in rank order and the call it became. Changes nothing. The console calls the same handler at /api/tracking/pings/:id.
Path parameters
| Name | Type | Description |
|---|---|---|
idRequired | string | For an inbound ping, the auction's id or its public reference such as AU-100482, which is not case-sensitive. For an outbound ping, the bid's id.up to 60 characters |
Query parameters
| Name | Type | Description |
|---|---|---|
directionOptional | string | Which ledger the id belongs to.inbound or outbound · default inbound |
Example request
curl "https://api.buy3.io/api/v1/pings/AU-100482" \
-H "Authorization: Bearer $BUY3_API_KEY"Responses
- 200An inbound ping.
pingis the ledger row plus everything the publisher sent,traceis the auction as it was written,bidsis up to 100 bids by rank, andcallis the call this became. The example is abbreviated. - 200 (outbound)An outbound ping.
traceandbidsare always empty here — the trace belongs to the auction, whichauction.idnames.
{
"ping": {
"id": "3f2e1d0c-9b8a-4765-8432-10fedcba9876",
"direction": "inbound",
"ref": "AU-100482",
"at": "2026-09-20T14:03:09.412Z",
"environment": "production",
"outcome": "won",
"reason": null,
"campaign": {
"id": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
"name": "Medicare — Inbound"
},
"publisher": {
"id": "c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b",
"name": "Northwind Media"
},
"callerNumber": "••••••0142",
"callerMasked": true,
"callerState": "CA",
"elapsedMs": 412,
"tmaxMs": 1000,
"statusCode": 200,
"payoutCents": 2750,
"clearingPriceCents": 4200,
"winningTarget": {
"bidId": "7c6b5a49-3827-4615-9048-372615049382",
"id": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
"name": "Acme Health — Dallas floor"
},
"targetsConsidered": 6,
"targetsSolicited": 3,
"bidsReceived": 2,
"callerZip": "94103",
"callerCity": "San Francisco",
"publisherRef": "nw-88231",
"externalCallId": "nw-88231",
"tags": {
"sub_id": "fb-42",
"utm_source": "facebook"
},
"sipOk": false,
"sourceIp": "203.0.113.24",
"userAgent": "NorthwindDialer/2.4",
"requestId": "5d1c0d2e-8a4f-4c0b-9e1d-2f3a4b5c6d7e",
"requestBody": {
"CID": "••••••0142",
"state": "CA",
"zipcode": "94103",
"sub_id": "fb-42"
},
"responseBody": {
"bidId": "7c6b5a49-3827-4615-9048-372615049382",
"bidAmount": 27.5,
"expireInSeconds": 30
}
},
"trace": [
{
"step": "auction",
"targets": 6,
"solicited": 3,
"bids": 2
}
],
"bids": [
{
"id": "7c6b5a49-3827-4615-9048-372615049382",
"rank": 1,
"status": "claimed",
"targetKind": "external_rtb",
"own": true,
"target": {
"id": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
"name": "Acme Health — Dallas floor"
},
"latencyMs": 212,
"buyer": {
"id": "f1e2d3c4-b5a6-4978-8a9b-0c1d2e3f4a5b",
"name": "Acme Health"
},
"bidCents": 4200,
"rawBidCents": 4200,
"payoutCents": 2750,
"durationThresholdSeconds": 90,
"httpStatus": 200,
"rejectCode": null,
"rejectReason": null,
"sentence": "The bid won and the caller was sent to this target.",
"callId": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11"
},
{
"id": "5b4a3928-1706-4958-8372-615049382716",
"rank": 2,
"status": "lost",
"targetKind": "external_rtb",
"own": false,
"target": null,
"latencyMs": 184
}
],
"call": {
"id": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11",
"ref": "CA-482137",
"status": "completed",
"startedAt": "2026-09-20T14:03:11.000Z",
"answeredAt": "2026-09-20T14:03:19.000Z",
"talkSeconds": 184,
"converted": true
}
}Errors
| Status | Code | When |
|---|---|---|
| 400 | validation_error | direction is neither inbound nor outbound. |
| 404 | not_found | No such ping in the workspace. Another workspace's ping, an id that is not a UUID or a reference, an id longer than 60 characters, and direction=outbound with anything but a bid id all answer exactly as a missing one does. |
| 401 | missing_key | No Authorization: Bearer header was sent. |
| 401 | invalid_key | The key is malformed, unknown, or revoked with its grace window over. |
| 402 | subscription_inactive | The workspace's subscription is not active. On the REST API that stops reads as well as writes. |
| 402 | plan_upgrade_required | The plan does not include call tracking, or does not include API access. details.feature says which. |
| 403 | ip_not_allowed | The key has an IP allowlist and the request came from outside it. |
| 403 | insufficient_scope | The key does not hold pings:read. details lists the scopes required and the ones held. |
| 429 | rate_limited | The key is over its per-minute limit. Wait Retry-After seconds. |
Summarise the ping ledger
/api/tracking/pings/summaryThe window added up four ways: how the inbound ledger did overall, every refusal ranked, how each buying target behaved, and how each publisher's pings turned into calls. It is the page behind "which buyer is timing out" and "which publisher sends traffic nobody wants". Changes nothing. This is a console route: an API key cannot call it.
Query parameters
| Name | Type | Description |
|---|---|---|
fromOptional | date | The start of the window. A ping belongs to the moment it arrived.default: midnight six days ago, on the workspace's clock |
toOptional | date | The end of the window.default: now |
campaignIdOptional | uuid | Only pings on this campaign. |
publisherIdOptional | uuid | Only pings from this publisher. |
targetIdOptional | uuid | Only pings this target took part in. Inbound, that means every auction it was asked to bid in, whether it won or lost. |
directionOptional | string | Validated as on the list, so a misplaced status or outcome is still refused. It does not change the answer: the summary always covers the inbound ledger and the bids under it.inbound or outbound · default inbound |
Example request
curl "https://api.buy3.io/api/tracking/pings/summary?from=2026-09-14T04:00:00Z&campaignId=6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d" \
-H "Authorization: Bearer $BUY3_SESSION_TOKEN"Responses
- 200
inboundcounts the window's pings;woncountscachedanswers too. Latencies are the median and the 95th percentile in milliseconds, and arenulluntil there is something to measure.noBidReasonsholds at most 25 entries,targetsandpublishersat most 100 each. Rates are fractions to four places.
{
"from": "2026-09-14T04:00:00.000Z",
"to": "2026-09-20T18:22:09.412Z",
"inbound": {
"pings": 4820,
"won": 1388,
"noBid": 3301,
"rateLimited": 94,
"blocked": 37,
"errors": 0,
"winRate": 0.288,
"p50ElapsedMs": 121,
"p95ElapsedMs": 604,
"payoutOfferedCents": 3817000
},
"noBidReasons": [
{
"code": 1008,
"reason": "state_not_targeted",
"sentence": "That state is not being bought.",
"outcome": "no_bid",
"count": 1902,
"share": 0.5762
},
{
"code": 1011,
"reason": "no_bids",
"sentence": "No target returned a bid.",
"outcome": "no_bid",
"count": 806,
"share": 0.2442
}
],
"targets": [
{
"target": {
"id": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
"name": "Acme Health — Dallas floor"
},
"buyer": {
"id": "f1e2d3c4-b5a6-4978-8a9b-0c1d2e3f4a5b",
"name": "Acme Health"
},
"pings": 1519,
"bids": 1204,
"wins": 902,
"declined": 298,
"timeouts": 17,
"errors": 0,
"bidRate": 0.7926,
"timeoutRate": 0.0112,
"p50LatencyMs": 198,
"p95LatencyMs": 612,
"avgBidCents": 4108
}
],
"publishers": [
{
"publisher": {
"id": "c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b",
"name": "Northwind Media"
},
"pings": 2904,
"won": 901,
"calls": 764,
"ghostBids": 137,
"winRate": 0.3103,
"pingToCallRate": 0.2631,
"wonToCallRate": 0.8479
}
]
}Errors
| Status | Code | When |
|---|---|---|
| 400 | validation_error | A filter has the wrong type, or direction, outcome or status is not an allowed word. |
| 400 | bad_request | status was sent with direction=inbound, or outcome with direction=outbound. Each ledger has its own word, and accepting the other one would hand back an unfiltered list that looks filtered. from is later than to, one of them is not a date, or the window is wider than 92 days. |
| 401 | unauthorized | No session token, or one that has expired. |
| 402 | plan_upgrade_required | The workspace's plan does not include call tracking. details.feature is callTracking. |
| 403 | forbidden | You named no workspace and hold no owner or admin seat on any. |
| 404 | not_found | An agencyId that is not a workspace you administer answers the same 404. |
Export pings as CSV
/api/tracking/pings/export.csvThe filtered ledger as a file, streamed. One ledger per file — the two have different columns — capped at 50,000 rows, with no paging. The export is written to the activity log before the first byte (export, medium severity) with the direction, the filters, the window and the row count. Callers stay masked exactly as they are in the API: an export is not a way round a campaign's disclosure setting. It is a read, so a lapsed workspace can still run it. This is a console route: an API key cannot call it.
Query parameters
| Name | Type | Description |
|---|---|---|
directionOptional | string | Which ledger to read. The two cannot be mixed in one answer.inbound or outbound · default inbound |
fromOptional | date | The start of the window. A ping belongs to the moment it arrived.default: midnight six days ago, on the workspace's clock |
toOptional | date | The end of the window.default: now |
campaignIdOptional | uuid | Only pings on this campaign. |
publisherIdOptional | uuid | Only pings from this publisher. |
targetIdOptional | uuid | Only pings this target took part in. Inbound, that means every auction it was asked to bid in, whether it won or lost. |
outcomeOptional | string | Inbound only. How the auction ended.won, no_bid, cached, rate_limited, blocked or error |
statusOptional | string | Outbound only. What became of the bid.bid, no_bid, rejected, timeout, error, won, lost, expired, claimed or void |
reasonCodeOptional | integer | Only pings refused under this reject code.0–99999 |
qOptional | string | An auction reference such as AU-100482, an auction id or a bid id.up to 60 characters |
Example request
curl "https://api.buy3.io/api/tracking/pings/export.csv?direction=outbound&from=2026-09-14T04:00:00Z&status=timeout" \
-H "Authorization: Bearer $BUY3_SESSION_TOKEN" \
-o pings.csvResponses
- 200
Content-Type: text/csv; charset=utf-8, sent as an attachment namedpings-<direction>-<YYYY-MM-DD>.csv. UTF-8 with a byte-order mark, RFC 4180 quoting — the file's rules are in The file. The inbound columns are shown; the outbound file carriesbidId, at, origin, auctionRef, campaign, publisher, target, buyer, status, won, bidCents, httpStatus, latencyMs, rejectCode, rejectReason, sentence, callId.
ref,at,outcome,reasonCode,reason,reasonSentence,campaign,publisher,callerNumber,callerState,elapsedMs,statusCode,payoutCents,clearingPriceCents,winningTarget,targetsSolicited,bidsReceived
AU-100482,2026-09-20T14:03:09.412Z,won,,,,Medicare — Inbound,Northwind Media,••••••0142,CA,412,200,2750,4200,Acme Health — Dallas floor,3,2
AU-100483,2026-09-20T14:04:55.031Z,no_bid,1008,state_not_targeted,That state is not being bought.,Medicare — Inbound,Northwind Media,••••••7733,VT,38,200,,,,0,0Errors
| Status | Code | When |
|---|---|---|
| 400 | validation_error | As on the list. A bad query is refused before anything is written to the activity log. |
| 400 | bad_request | status was sent with direction=inbound, or outcome with direction=outbound. Each ledger has its own word, and accepting the other one would hand back an unfiltered list that looks filtered. from is later than to, one of them is not a date, or the window is wider than 92 days. |
| 401 | unauthorized | No session token, or one that has expired. |
| 402 | plan_upgrade_required | The workspace's plan does not include call tracking. details.feature is callTracking. |
| 403 | forbidden | You named no workspace and hold no owner or admin seat on any. |
| 404 | not_found | An agencyId that is not a workspace you administer answers the same 404. |
