Exports
CSV exports for calls, reports, the ping ledger and the activity log — what each contains, how big it may be, and the quoting rules a parser needs.
After this page you can take any table in the console away as a file, write a job that pulls one on a schedule, and parse the result without being surprised. Anything you can see as a table, you can export — with the filters that are on screen, built on the server so it holds every matching row rather than the page you happen to be looking at.
Every export is also written to the activity log before its first byte, with the filter that produced it and the row count. A file of your callers' data leaving the platform is an act somebody can always account for afterwards.
The four exports
| Export | One row per | Cap | Credential |
|---|---|---|---|
| Calls, from the call log | Call, with its tags, reason, buyer, target, duration and books. | 100,000 | API key with calls:read, or a session |
| Report, from the report builder | Group, with the fourteen measures. | 5,000 | Session only |
| Pings, from the ping ledger | Ping, inbound or outbound, with its outcome, reject code and latency. | 50,000 | Session only |
| Activity, from the activity log | Change, with who made it and the values before and after. | 50,000 | Session only |
All four are reads, so they stay open to a workspace whose subscription has lapsed. Being unable to get your own history out because a card expired would be the wrong kind of gate.
The call export
The log exactly as it is filtered, streamed. It is the export people actually use, and the only one a program can pull.
curl -G "https://api.buy3.io/api/v1/calls/export.csv" \
-H "Authorization: Bearer $BUY3_API_KEY" \
--data-urlencode "from=2026-09-14T05:00:00Z" \
--data-urlencode "to=2026-09-21T05:00:00Z" \
--data-urlencode "tz=America/Chicago" \
--data-urlencode "campaignId=6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d" \
-o calls-2026-09-21.csvref,startedAt,startedAtLocal,status,outcome,callerNumber,dialedNumber,campaign,publisher,buyer,target,ringSeconds,talkSeconds,converted,revenueCents,payoutCents,profitCents,usageCents,reasonCode,reasonSentence,hasRecording,tags
CA-482137,2026-09-20T14:03:11.000Z,2026-09-20 09:03:11,completed,converted,+14155550142,+18885550100,Acme Health — Inbound,Northwind Media,Acme Health,Acme Health — Dallas floor,8,184,true,4200,2750,1450,22,,,true,"{""gclid"":""Cj0KCQjw"",""utm_source"":""google""}"
CA-482201,2026-09-20T21:14:02.000Z,2026-09-20 16:14:02,no_answer,unrouted,+14155550188,+18885550100,Acme Health — Inbound,Northwind Media,,,0,0,false,0,0,0,0,campaign_closed,The call arrived outside the campaign's hours of operation.,false,| Column | Is |
|---|---|
ref | The call's public reference. Quote it in a ticket. |
startedAt | A full ISO 8601 instant in UTC. |
startedAtLocal | The same moment as YYYY-MM-DD HH:MM:SS in the time zone the export was run in — a format a spreadsheet sorts correctly. |
status | The carrier-level status. |
outcome | One of converted, connected, unrouted, missed, live. |
callerNumber, dialedNumber | E.164. callerNumber is empty when the caller withheld it. |
campaign, publisher, buyer, target | Names as frozen on the call, so a buyer you have since renamed still reads as it did. Empty where the call had none. |
ringSeconds, talkSeconds | Seconds ringing before an answer, and seconds with the buyer. |
converted | true or false. |
revenueCents, payoutCents, profitCents, usageCents | Integer cents. The first three are books; the fourth was charged. |
reasonCode, reasonSentence | Only on an unrouted call: the code and the sentence the router stored at the time. |
hasRecording | true or false. |
tags | The call's tags as a JSON object in one cell, or empty when it has none. That is why the cell is quoted and holds doubled quotes. |
- It takes the same filters as the call log, so "export what I am looking at" is literally the same query.
- Rows are streamed in batches, newest first, so a hundred thousand of them never become one enormous allocation on a server that is also routing calls.
- Over the cap, the newest 100,000 rows are written. The activity entry records both the number that matched and the number exported, so you can tell a full file from a capped one.
- The file is named
calls-YYYY-MM-DD.csv, dated today rather than after the window.
The report export
The summary on screen, written out: the same query, the same rows, the same numbers — so a total added up in a spreadsheet matches the footer on the page.
publisherKey,publisher,dayKey,day,calls,connected,converted,unrouted,connectRate,conversionRate,revenueCents,payoutCents,profitCents,usageCents,avgTalkSeconds,totalTalkSeconds,rpcCents,epcCents
c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b,Northwind Media,2026-09-19,2026-09-19,212,171,96,14,0.8066,0.5614,403200,264000,139200,2871,201,34371,1902,1245
total,All calls,,,1388,1102,640,97,0.7939,0.5808,2688000,1760000,928000,18230,201,221502,1937,1268- Two columns per dimension:
<dimension>Keythen<dimension>. The key is the id a spreadsheet can join on; the column beside it is the label a person reads. - Then the fourteen measures in a fixed order. Rates are fractions and money is cents — the same numbers the API returns.
- The last line is the total row:
totalandAll callsin the first dimension's two columns, a second dimension's columns empty, and the measures for the whole window rather than the sum of the lines above. On a report that hit its cap the two differ, and the footer is the true one. - The cap is 5,000 data rows and the
limityou send to the JSON report is ignored. Group more coarsely or narrow the window. - The file is named after the grouping:
report-publisher-day-2026-09-20.csv.
The ping export
One file per direction, because inbound and outbound pings have different columns and a CSV with half its cells empty is two CSVs pretending to be one.
| Direction | Columns |
|---|---|
| Inbound — publishers pinging you | ref, at, outcome, reasonCode, reason, reasonSentence, campaign, publisher, callerNumber, callerState, elapsedMs, statusCode, payoutCents, clearingPriceCents, winningTarget, targetsSolicited, bidsReceived |
| Outbound — you pinging buyers | bidId, at, origin, auctionRef, campaign, publisher, target, buyer, status, won, bidCents, httpStatus, latencyMs, rejectCode, rejectReason, sentence, callId |
- Callers are left masked exactly as they are on screen. The export is not a way round a campaign's disclosure setting.
- The cap is 50,000 rows, streamed like the call export.
- The ledger itself is pruned after 30 days, so an older window exports nothing rather than failing. See Limits.
The activity export
The filtered history, with the changes column flattened into one readable cell rather than a blob of JSON: field: before → after; field: before → after.
id,at,severity,kind,actor,actorKind,action,targetType,target,targetId,changes,fields,ip,requestId
88213,2026-09-20T16:22:04.118Z,medium,call,Dana Whitfield,user,marked call CA-482137 as converted,call,CA-482137,0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11,converted: false → true; revenueCents: 0 → 5000,,203.0.113.41,req_0f1e2d3c4b5a6978
88214,2026-09-20T16:41:52.117Z,high,export,API key b3_live_Zk3q,api_key,exported 1388 calls from the call log,call,1388 calls,,,caller_number,203.0.113.41,req_1a2b3c4d5e6f7080- Columns are
id,at,severity,kind,actor,actorKind,action,targetType,target,targetId,changes,fields,ip,requestId. fieldsnames the fields an entry touched, never their values — it is how an entry says "a caller's number was involved" without repeating one.limitdefaults to 10,000 and may be raised to the cap of 50,000.- It redacts exactly as the log on screen does: a row written while our staff were acting on your account keeps its action and its changes, and loses their address and their internal notes.
The format
| Media type | text/csv; charset=utf-8, sent as an attachment with a filename. |
| Encoding | UTF-8 with a byte-order mark in front, so a spreadsheet opens an accented name as an accented name. Strip the mark before parsing if your reader does not. |
| Records | One per line. CRLF for calls, reports and pings; LF for activity. |
| Quoting | A cell is wrapped in double quotes only when it holds a comma, a double quote, a carriage return or a line feed. A double quote inside a quoted cell is doubled. |
| Empty cells | Written as nothing at all, so an absent value and an empty string look the same in the file. |
| Objects | Written as JSON — which is how the call export's tags column works. |
| Timestamps | A full ISO 8601 instant in UTC. Where a file carries a second, local column, it is the same moment in the export's time zone. |
The leading apostrophe is ours, not the data's
- In the call, report and ping exports a phone-shaped cell is left alone:
+18885550100arrives as it is, because an export whose every number carries a stray apostrophe cannot be pasted anywhere. - In the activity export a leading
+or-is always prefixed, phone-shaped or not. Actions and target names there are free text somebody typed. - This is why you should never compare an exported cell to a literal without trimming.
'=HYPERLINK(...)in a file was=HYPERLINK(...)in a tag.
Worked example: a month-end file you can defend
You invoice buyers on the first and pay publishers on the fifth. A report is live — it follows any conversion or adjustment made later — so the report you read on the first will not be the report you read on the tenth.
- Set the window as instants with your own offset, not bare dates:
from=2026-09-01T05:00:00Z,to=2026-10-01T05:00:00Zfor a workspace in Chicago. This is the single most common cause of a figure that will not reconcile. - Set
tzto the same zone, so days bucket on the calendar your buyer invoices on. - Export the report grouped by
buyerfor what you bill, and bypublisherfor what you pay. Two files, two conversations. - Export the call log over the same window and filters. It is the line-level evidence behind both, and it is the file a buyer asks for when they dispute a total.
- Keep all three files. When a call is corrected on the eighth, the report changes and your invoice does not — and the export is the record of what the figures were when you raised it.
- Check the activity log afterwards: each export is an entry with its filter and its row count, which is how you prove later which file came from which query.
