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

EndpointSurfaceCredential
GET /api/v1/pingsREST APIAPI key with pings:read
GET /api/v1/pings/:idREST APIAPI key with pings:read
GET /api/tracking/pings/summaryConsoleSession token
GET /api/tracking/pings/export.csvConsoleSession token
The console calls the first two as well, at /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.
Base URL
https://api.buy3.io/api/v1

Two ledgers, one at a time

direction chooses the ledger. They hold different rows with different columns, so no answer ever mixes them.

directionOne row isRead 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.
outboundYour ping to one buyer's bidding endpoint: one bid request, one answer.A buyer's endpoint is slow, erroring or never bidding.
  • outcome filters the inbound ledger and status the outbound one. Sending the wrong one answers 400 rather than quietly ignoring it.
  • One inbound ping usually has several outbound rows under it: one per external bidding target the auction asked. auction.id and auction.ref join 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 — follow callId instead.

The window, and how long rows last

  • from and to take an ISO 8601 instant. A bare 2026-09-14 is 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 no to, 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

FieldTypeMeaning
iduuidThe auction's id.
directionstringAlways inbound.
refstring | nullThe public reference, such as AU-100482. It is what a publisher quotes at you, and you can read a ping by it.
atISO 8601When the ping arrived.
environmentstring | nullWhich environment the ping was scored in.
outcomestringHow it ended — see below.
reasonobject | null{ code, reason, sentence } for a ping that got no bid. null when the outcome was won or cached.
campaign, publisherobject | null{ id, name }.
callerNumberstring | nullMasked to the last four digits unless the campaign discloses caller ids.
callerMaskedbooleanWhether the number above was masked.
callerStatestring | nullThe two-letter state the publisher sent.
elapsedMsinteger | nullHow long you took to answer. This is the number a publisher's platform times out on.
tmaxMsinteger | nullThe time budget the auction was given.
statusCodeinteger | nullThe HTTP status you answered with. A no-bid is a 200.
payoutCentscents | nullWhat you offered the publisher.
clearingPriceCentscents | nullWhat the winning bid cleared at.
winningTargetobject | null{ bidId, id, name }.
targetsConsidered, targetsSolicited, bidsReceivedintegerHow many targets were in the plan, how many were actually asked, and how many answered with a bid.

An outbound ping

FieldTypeMeaning
iduuidThe bid's id. This is what GET /api/v1/pings/:id?direction=outbound takes.
directionstringAlways outbound.
atISO 8601When you sent the bid request.
originstringpublisher — 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.
auctionobject{ id, ref } — the auction this bid belongs to.
campaign, publisher, target, buyerobject | null{ id, name }.
statusstringWhat became of the bid — see below.
wonbooleanThis bid took the call.
bidCentscents | nullThe bid as it counted, after any modifier.
rawBidCentscents | nullThe bid exactly as the buyer returned it.
httpStatusinteger | nullWhat the buyer's endpoint answered. null means nothing answered.
latencyMsinteger | nullHow long the buyer took.
rejectCode, rejectReasoninteger, string | nullWhy the endpoint declined, when it said.
sentencestringThe same fact in plain words, for a person.
callIduuid | nullThe call this bid became.

Outcomes and statuses

Inbound outcomeMeans
wonA bid was returned to the publisher.
no_bidNo bid. reason carries the code and a sentence.
cachedThe publisher had already asked about this caller and the earlier answer had not expired, so it was repeated.
rate_limitedThe publisher's key was over one of its limits.
blockedThe request was refused before the auction — a blocked caller, an address off the allowlist, a lane that is not live.
errorThe auction could not be scored.
Outbound statusMeans
bidThe endpoint returned a bid.
no_bid, rejectedThe endpoint declined.
timeoutThe endpoint did not answer inside the auction's budget.
errorThe request failed, or the answer could not be read.
won, claimedThis bid took the call.
lostAnother bid took it. sentence says why this one did not.
expiredThe bid was never claimed in time.
voidThe 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 ending token, password, secret or signature — 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

List pings

GET/api/v1/pings

One 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.

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

Query parameters

NameTypeDescription
directionOptionalstringWhich ledger to read. The two cannot be mixed in one answer.inbound or outbound · default inbound
fromOptionaldateThe start of the window. A ping belongs to the moment it arrived.default: midnight six days ago, on the workspace's clock
toOptionaldateThe end of the window.default: now
campaignIdOptionaluuidOnly pings on this campaign.
publisherIdOptionaluuidOnly pings from this publisher.
targetIdOptionaluuidOnly pings this target took part in. Inbound, that means every auction it was asked to bid in, whether it won or lost.
outcomeOptionalstringInbound only. How the auction ended.won, no_bid, cached, rate_limited, blocked or error
statusOptionalstringOutbound only. What became of the bid.bid, no_bid, rejected, timeout, error, won, lost, expired, claimed or void
reasonCodeOptionalintegerOnly pings refused under this reject code.0–99999
qOptionalstringAn auction reference such as AU-100482, an auction id or a bid id.up to 60 characters
pageOptionalintegerThe page to return.from 1, default 1
pageSizeOptionalintegerRows 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. from and to echo 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

StatusCodeWhen
400validation_errordirection, 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.
400bad_requeststatus 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.
401missing_keyNo Authorization: Bearer header was sent.
401invalid_keyThe key is malformed, unknown, or revoked with its grace window over.
402subscription_inactiveThe workspace's subscription is not active. On the REST API that stops reads as well as writes.
402plan_upgrade_requiredThe plan does not include call tracking, or does not include API access. details.feature says which.
403ip_not_allowedThe key has an IP allowlist and the request came from outside it.
403insufficient_scopeThe key does not hold pings:read. details lists the scopes required and the ones held.
429rate_limitedThe key is over its per-minute limit. Wait Retry-After seconds.

Read one ping

GET/api/v1/pings/:id

One 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.

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

Path parameters

NameTypeDescription
idRequiredstringFor 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

NameTypeDescription
directionOptionalstringWhich 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. ping is the ledger row plus everything the publisher sent, trace is the auction as it was written, bids is up to 100 bids by rank, and call is the call this became. The example is abbreviated.
  • 200 (outbound)An outbound ping. trace and bids are always empty here — the trace belongs to the auction, which auction.id names.
{
  "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

StatusCodeWhen
400validation_errordirection is neither inbound nor outbound.
404not_foundNo 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.
401missing_keyNo Authorization: Bearer header was sent.
401invalid_keyThe key is malformed, unknown, or revoked with its grace window over.
402subscription_inactiveThe workspace's subscription is not active. On the REST API that stops reads as well as writes.
402plan_upgrade_requiredThe plan does not include call tracking, or does not include API access. details.feature says which.
403ip_not_allowedThe key has an IP allowlist and the request came from outside it.
403insufficient_scopeThe key does not hold pings:read. details lists the scopes required and the ones held.
429rate_limitedThe key is over its per-minute limit. Wait Retry-After seconds.

Summarise the ping ledger

GET/api/tracking/pings/summary

The 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.

Console session. Called by the console with the signed-in user's session. Use the REST API with an API key for your own integrations.

Query parameters

NameTypeDescription
fromOptionaldateThe start of the window. A ping belongs to the moment it arrived.default: midnight six days ago, on the workspace's clock
toOptionaldateThe end of the window.default: now
campaignIdOptionaluuidOnly pings on this campaign.
publisherIdOptionaluuidOnly pings from this publisher.
targetIdOptionaluuidOnly pings this target took part in. Inbound, that means every auction it was asked to bid in, whether it won or lost.
directionOptionalstringValidated 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

  • 200inbound counts the window's pings; won counts cached answers too. Latencies are the median and the 95th percentile in milliseconds, and are null until there is something to measure. noBidReasons holds at most 25 entries, targets and publishers at 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

StatusCodeWhen
400validation_errorA filter has the wrong type, or direction, outcome or status is not an allowed word.
400bad_requeststatus 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.
401unauthorizedNo session token, or one that has expired.
402plan_upgrade_requiredThe workspace's plan does not include call tracking. details.feature is callTracking.
403forbiddenYou named no workspace and hold no owner or admin seat on any.
404not_foundAn agencyId that is not a workspace you administer answers the same 404.

Export pings as CSV

GET/api/tracking/pings/export.csv

The 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.

Console session. Called by the console with the signed-in user's session. Use the REST API with an API key for your own integrations.

Query parameters

NameTypeDescription
directionOptionalstringWhich ledger to read. The two cannot be mixed in one answer.inbound or outbound · default inbound
fromOptionaldateThe start of the window. A ping belongs to the moment it arrived.default: midnight six days ago, on the workspace's clock
toOptionaldateThe end of the window.default: now
campaignIdOptionaluuidOnly pings on this campaign.
publisherIdOptionaluuidOnly pings from this publisher.
targetIdOptionaluuidOnly pings this target took part in. Inbound, that means every auction it was asked to bid in, whether it won or lost.
outcomeOptionalstringInbound only. How the auction ended.won, no_bid, cached, rate_limited, blocked or error
statusOptionalstringOutbound only. What became of the bid.bid, no_bid, rejected, timeout, error, won, lost, expired, claimed or void
reasonCodeOptionalintegerOnly pings refused under this reject code.0–99999
qOptionalstringAn 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.csv

Responses

  • 200Content-Type: text/csv; charset=utf-8, sent as an attachment named pings-<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 carries bidId, 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,0

Errors

StatusCodeWhen
400validation_errorAs on the list. A bad query is refused before anything is written to the activity log.
400bad_requeststatus 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.
401unauthorizedNo session token, or one that has expired.
402plan_upgrade_requiredThe workspace's plan does not include call tracking. details.feature is callTracking.
403forbiddenYou named no workspace and hold no owner or admin seat on any.
404not_foundAn agencyId that is not a workspace you administer answers the same 404.