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

FieldIs
actionA 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.
kindThe noun it happened to, so "everything that ever happened to my targets" is a filter and not a search.
severitylow, medium or high. High is money, credentials and data leaving the building.
actor, actorKind, actorRoleWho did it and what kind of principal they were.
targetType, target, targetIdThe 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.
fieldsField names an entry touched without repeating their values. It is how an entry says "a caller's number was involved" without printing one.
metadataContext the route attached — the buyer behind a target, the filter behind an export.
ip, requestId, atWhere it came from, which request it was, and when. Quote requestId to support and the exact request can be found.
One entry
{
  "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

actorKindMeans
userA member of your team, in the console. Named, which is why one person, one login matters.
api_keyOne of your API keys. It is named by its label, which is the argument for naming keys after what uses them.
publisher_memberSomebody at one of your publishers, in the publisher portal — rotating their own key, for instance.
systemThe platform itself: a target marked degraded after repeated failures, a number renewed, a webhook endpoint stood down.
staffA 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.

kindCovers
workspaceWorkspace settings — the time zone reports are bucketed in.
campaignCampaigns created and edited, status changes, tag filters.
routingA campaign's routing plan, saved whole.
buyer, targetCreated, edited, paused and resumed; a target's cap reset and its tests.
publisherA lane opened, or its payout terms changed.
keyA publisher's routing key issued or revoked, and the compatible ping URL switched.
number, poolNumbers bought, assigned, moved and released; pools created, edited, paused and deleted.
blocked_callerA caller blocked or unblocked.
callThe books, by hand: a conversion, a tag edit, an adjustment.
webhookEndpoints created, edited, paused and deleted; test sends, replays, secret rotations — and the platform marking one degraded.
api_keyKeys created, edited, rotated and revoked.
exportA CSV left the building: calls, reports, pings, or this log.
Other kinds appear too — 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.

FilterTakes
kindOne of the nouns above, or any other kind your log holds.
severitylow, medium or high.
actorKinduser, api_key, publisher_member, system or staff.
actorUserId, apiKeyIdEverything one colleague, or one key, ever did.
targetType + targetIdEverything that ever happened to one record. This is the filter that settles arguments.
qA substring of the action sentence. Wildcards are literals: searching 100% finds "100%", not everything.
from, toA window.
Every change ever made to one target
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.

The summary
{
  "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 changes column 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.

  1. Filter kind=publisher over the last month. Lane changes — a payout mode, a rate, a buffer — are filed under the publisher, not under the campaign.
  2. Find the entry and read its changes. payoutMode or revsharePct with a before and an after is the whole answer, and it is not a matter of opinion.
  3. Read actor and actorKind. A colleague, a key of yours, or our staff — each of those is a different conversation to have next.
  4. Check at against 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.
  5. 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.
  6. 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.

Next steps