Activity log
A permanent, read-only record of every configuration change in the workspace: who or what made it, when, from where, and the value before and after.
After this page you can answer "who changed this, and when?" about anything in the workspace, settle a dispute with a buyer or a publisher from a record neither of you can rewrite, and find the one entry that explains why something started behaving differently on Thursday.
Every change to how your workspace is configured writes a row: a cap raised, a payout changed, a key rotated, a number moved, a webhook paused, a call's books corrected. Each row says who or what made the change, when, and what the value was before and after.
What a row records
| Field | Is |
|---|---|
action | A sentence in the past tense — "raised the daily cap on Acme Health — Dallas floor". It is written by the route that made the change, so it says what happened rather than which columns moved. |
kind | The noun it happened to, so "everything that ever happened to my targets" is a filter and not a search. |
severity | low, medium or high. High is money, credentials and data leaving the building. |
actor, actorKind, actorRole | Who did it and what kind of principal they were. |
targetType, target, targetId | The thing that changed, its label at the time, and its id. |
changes | { field: [before, after] } — every field that moved, with both values. This is the part a dispute is settled on. |
fields | Field names an entry touched without repeating their values. It is how an entry says "a caller's number was involved" without printing one. |
metadata | Context the route attached — the buyer behind a target, the filter behind an export. |
ip, requestId, at | Where it came from, which request it was, and when. Quote requestId to support and the exact request can be found. |
{
"id": "88213",
"at": "2026-09-20T16:22:04.118Z",
"kind": "target",
"action": "raised the daily cap on Acme Health — Dallas floor",
"severity": "medium",
"actor": "Dana Whitfield",
"actorKind": "user",
"actorRole": "admin",
"actorUserId": "9e8d7c6b-5a49-4382-b716-05f4e3d2c1b0",
"apiKeyId": null,
"target": "Acme Health — Dallas floor",
"targetType": "target",
"targetId": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
"changes": {
"dailyCap": [50, 120],
"status": ["paused", "active"]
},
"fields": [],
"metadata": { "buyerId": "f1e2d3c4-b5a6-4978-8a9b-0c1d2e3f4a5b" },
"ip": "203.0.113.41",
"requestId": "req_0f1e2d3c4b5a6978"
}Who can be an actor
actorKind | Means |
|---|---|
user | A member of your team, in the console. Named, which is why one person, one login matters. |
api_key | One of your API keys. It is named by its label, which is the argument for naming keys after what uses them. |
publisher_member | Somebody at one of your publishers, in the publisher portal — rotating their own key, for instance. |
system | The platform itself: a target marked degraded after repeated failures, a number renewed, a webhook endpoint stood down. |
staff | A member of our support team, acting on your workspace. It is recorded in your log, not only in ours. |
For every other actor the address is kept, and metadata is kept minus a small set of internal keys — margins, provider costs, treasury and risk fields. A tenant route has no business writing any of them; that filter is the net underneath.
What is logged
One kind per noun, and the list below is exactly what this product's own routes write. A status change is filed under the thing whose status changed, not under a generic "status", so a filter on target really does return everything that ever happened to your targets.
kind | Covers |
|---|---|
workspace | Workspace settings — the time zone reports are bucketed in. |
campaign | Campaigns created and edited, status changes, tag filters. |
routing | A campaign's routing plan, saved whole. |
buyer, target | Created, edited, paused and resumed; a target's cap reset and its tests. |
publisher | A lane opened, or its payout terms changed. |
key | A publisher's routing key issued or revoked, and the compatible ping URL switched. |
number, pool | Numbers bought, assigned, moved and released; pools created, edited, paused and deleted. |
blocked_caller | A caller blocked or unblocked. |
call | The books, by hand: a conversion, a tag edit, an adjustment. |
webhook | Endpoints created, edited, paused and deleted; test sends, replays, secret rotations — and the platform marking one degraded. |
api_key | Keys created, edited, rotated and revoked. |
export | A CSV left the building: calls, reports, pings, or this log. |
billing and wallet from your plan and deposits, member from your publishers' portal seats. The summary lists the kinds your own log actually holds, so an empty week still has a working filter menu.Finding one entry
The tiles above the table — Configuration changes, High severity, People and programs — are counted over the whole window rather than over the page you happen to be holding, so a "high severity" count of 18 means 18.
| Filter | Takes |
|---|---|
kind | One of the nouns above, or any other kind your log holds. |
severity | low, medium or high. |
actorKind | user, api_key, publisher_member, system or staff. |
actorUserId, apiKeyId | Everything one colleague, or one key, ever did. |
targetType + targetId | Everything that ever happened to one record. This is the filter that settles arguments. |
q | A substring of the action sentence. Wildcards are literals: searching 100% finds "100%", not everything. |
from, to | A window. |
curl -G "https://api.buy3.io/api/tracking/activity" \
-H "Authorization: Bearer $BUY3_SESSION_TOKEN" \
--data-urlencode "targetType=target" \
--data-urlencode "targetId=a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d" \
--data-urlencode "pageSize=50"GET /api/tracking/activity/summary?days=7 answers the tiles and the filter menus in one request, counted server-side.
{
"days": 7,
"since": "2026-09-13T16:40:00.000Z",
"total": 418,
"configChanges": 96,
"severity": { "low": 302, "medium": 98, "high": 18 },
"kinds": { "call": 181, "target": 64, "campaign": 41, "webhook": 22, "export": 9, "api_key": 4 },
"actorKinds": { "user": 331, "api_key": 71, "system": 12, "staff": 4 },
"actors": [
{ "name": "Dana Whitfield", "kind": "user", "role": "admin", "entries": 240 },
{ "name": "API key b3_live_Zk3q", "kind": "api_key", "role": "system", "entries": 71 }
]
}configChanges counts the entries that say from what to what — the ones a dispute between you and a buyer is settled on, as opposed to the many that merely record that something happened.
Why it cannot be changed
A publisher and a buyer trust your numbers partly because there is a record neither of you can rewrite. So the log is protected in the database itself, not only in the application: an update to a row is refused outright, whoever asks, and a delete is refused outside the platform's own retention process.
- There is no write route to this log, and there is nothing for one to do. It is read-only by construction rather than by policy.
- You cannot edit an action, remove an embarrassing entry or backdate one. Neither can we, through the console.
- It follows that the log is only as useful as the attribution is honest — which is the whole argument for individual logins and per-job API keys.
Taking it away
- Export CSV downloads the filtered set, up to 50,000 entries; the default is 10,000 and may be raised to the cap.
- The
changescolumn is flattened into one readable cell —field: before → after; field: before → after— rather than a blob of JSON. - A lapsed workspace may still export its history. The plan gate is about changing the account, and the record of an export changes nothing.
- Format, quoting and the leading apostrophe are covered in Exports.
Worked example: "your payout changed and nobody told us"
Northwind Media say their rate on one campaign dropped three weeks ago. You think it did not. The log answers this in about a minute.
- Filter
kind=publisherover the last month. Lane changes — a payout mode, a rate, a buffer — are filed under the publisher, not under the campaign. - Find the entry and read its
changes.payoutModeorrevsharePctwith a before and an after is the whole answer, and it is not a matter of opinion. - Read
actorandactorKind. A colleague, a key of yours, or our staff — each of those is a different conversation to have next. - Check
atagainst the calls they are disputing. A call's terms are frozen when the call arrives, so calls that started before the change kept the old terms; only calls after it carry the new ones. - If the change was a mistake, correct the lane and use an adjustment on the affected calls. The adjustment is itself logged, with your reason.
- Send them the entry. "Changed on the 2nd at 14:12 by Dana, from 40% to 35%, and here are the calls on each side of it" ends the conversation; "our records show" does not.
What a publisher sees
Each publisher has an activity log of their own in the portal, reading the same table scoped to their rows: their lanes, their keys, their own people. They do not see your buyers, your targets, your campaigns' internals or anybody else's rows — and they see the entries where you changed something about them, which is the point.
