API reference
Publishers
Read the publishers who send you calls and the lanes that join them to your campaigns; set payout terms, caps and keys through the console.
A publisher is a partner who sends you calls. A publisher joins one of your campaigns through a lane — the pairing of the two — and the lane is where the commercial relationship lives: the payout terms, the caps, the states and hours the publisher may send, and the key they authenticate with. One publisher can have a lane on several campaigns, on different terms. The concepts are in Publishers overview.
Two surfaces
| Endpoint | Surface | Credential |
|---|---|---|
GET /api/v1/publishers | REST API | API key with publishers:read |
GET /api/v1/publishers/:id | REST API | API key with publishers:read |
GET /api/tracking/campaigns/:id/publishers | Console | Session token |
GET /api/tracking/campaigns/:id/publishers/:publisherId | Console | Session token |
PUT /api/tracking/campaigns/:id/publishers/:publisherId | Console | Session token |
DELETE /api/tracking/campaigns/:id/publishers/:publisherId | Console | Session token |
POST /api/tracking/campaigns/:id/publishers/:publisherId/compat | Console | Session token |
PATCH /api/publishers/keys/:keyId | Console | Session token |
POST /api/publishers/keys/:keyId/compat-token/rotate | Console | Session token |
POST /api/publishers/keys/:keyId/signing-secret/rotate | Console | Session token |
The publisher object
| Field | Type | Meaning |
|---|---|---|
id | uuid | The publisher's id. |
name | string | What you call them. |
status | string | onboarding, live, paused, review or terminated. Only a live publisher's pings are accepted. |
statusNote | string or null | The note behind the current status. |
routingPrefix | string | The short code that identifies this publisher in routing keys and references. |
source | string or null | How they came to you, for your own reporting. |
contactName, contactEmail, contactPhone, contactIm | string or null | Who to reach, and how. |
timezone | string | The publisher's own zone, shown beside their figures. |
quality | integer or null | A rolling quality score from 0 to 100, or null when there is not enough traffic to judge. |
portalEnabled | boolean | Whether this publisher can sign in to the publisher portal. |
payoutSchedule | string | weekly, net_15 or net_30. |
payoutMethod | string | ach, wire or paypal. |
joinedAt, createdAt, updatedAt | timestamp or null | When they joined, and when the record was created and last changed. |
lanes | array | Every lane this publisher has onto your tracking campaigns. See The lane. |
The lane
A lane is one publisher's standing on one campaign. It is created when you attach the publisher and it holds everything that is specific to that pairing.
| Field | Type | Meaning |
|---|---|---|
campaignId, campaign | uuid, object | Which campaign. The REST API names it as { id, name, status }; the console returns publisherId and a publisher block instead, because it reads lanes under the campaign. |
active | boolean | false stops the lane taking calls without closing it. |
payoutMode | string | inherit, fixed or revshare. See Payout terms. |
payoutCents | integer or null | The fixed amount per converted call, when payoutMode is fixed. |
revsharePct | number or null | The share of revenue, from 0 to 100, when payoutMode is revshare. |
minPayoutCents, maxPayoutCents | integer or null | A floor and a ceiling on a revenue-share payout. |
bufferSeconds, effectiveBufferSeconds | integer or null, integer or null | The lane's own settlement buffer, and the one actually used — the lane's, or the campaign's when the lane has none. |
dedupeWindowSeconds | integer | How long a caller counts as a duplicate from this publisher. |
hourlyCap, dailyCap, monthlyCap, concurrencyCap | integer | How much this publisher may send. 0 is unlimited. |
dailyPayoutCapCents | integer | The most this lane may earn the publisher in a day. 0 is unlimited. |
blockWhenCapped | boolean | true refuses a ping once a cap is full; false lets it through unpaid. |
states, hoursSchedule, timezone | array or null, object or null, string or null | Where and when this publisher may send. |
notesPublic, notesInternal | string or null | notesPublic is shown to the publisher in their portal. notesInternal is yours. |
usage | object | { liveCalls, hour, day, month, dayPayoutCents } — the counters behind the caps. |
key | object or null | The lane's routing key: { id, prefix, state, protocol, environment, requireSignature, lastUsedAt, useCount, issuedAt }. The key itself is never here — only its prefix. |
numbers | integer | Tracking numbers attributed to this publisher on this campaign. |
compat | object | Whether the Ringba-compatible single-step ping is switched on. The console also returns its url; the REST API returns { enabled } only. |
assignedAt, termsFrozenAt | timestamp or null | When the lane was created, and when its terms were last frozen. |
Payout terms
A lane's payout is fixed — an amount in cents for each converted call — or a revenue share, a percentage of what the buyer paid, with an optional floor and ceiling. inherit takes the campaign's own terms. Whatever the mode, the terms in force when a call starts are frozen onto that call: changing a lane never re-prices a call that has already arrived, and termsFrozenAt records when the terms last moved. The guide is Payout terms.
Endpoints
- GET
/api/v1/publishersList publishers - GET
/api/v1/publishers/:idRead a publisher - GET
/api/tracking/campaigns/:id/publishersConsole API · List a campaign's lanes - GET
/api/tracking/campaigns/:id/publishers/:publisherIdConsole API · Read one lane - PUT
/api/tracking/campaigns/:id/publishers/:publisherIdConsole API · Attach a publisher, or change their terms - DELETE
/api/tracking/campaigns/:id/publishers/:publisherIdConsole API · Detach a publisher - POST
/api/tracking/campaigns/:id/publishers/:publisherId/compatConsole API · Switch the single-step ping URL on or off - PATCH
/api/publishers/keys/:keyIdConsole API · Change a publisher key's settings - POST
/api/publishers/keys/:keyId/compat-token/rotateConsole API · Rotate the single-step ping URL - POST
/api/publishers/keys/:keyId/signing-secret/rotateConsole API · Rotate a key's signing secret
List publishers
/api/v1/publishersEvery publisher in your workspace, filtered and paged, each with its lanes onto your tracking campaigns. This is the read a reporting integration needs — who feeds this workspace, and on what terms per campaign. Terminated publishers are history: they are left out unless you ask for them by name.
Query parameters
| Name | Type | Description |
|---|---|---|
qOptional | string | Matches name, routingPrefix or contactName, case-insensitively, anywhere in the value.up to 120 characters |
statusOptional | enum | One status.onboarding, live, paused, review, terminated · default: every status except terminated |
pageOptional | integer | Page number, from 1.default 1 |
pageSizeOptional | integer | Rows per page.default 25 · at most 200; a larger value is clamped rather than refused |
sortOptional | enum | The field to sort by. An unknown value falls back to the default rather than failing.createdAt, updatedAt, name, status · default createdAt |
dirOptional | enum | Sort direction.asc, desc · default desc |
Example request
curl "https://api.buy3.io/api/v1/publishers?status=live&pageSize=25" \
-H "Authorization: Bearer $BUY3_API_KEY"Responses
- 200A page of publisher objects, each with its
lanes. A publisher with no lane onto one of your tracking campaigns has an empty array.
{
"data": [
{
"id": "c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b",
"name": "Northwind Media",
"status": "live",
"statusNote": null,
"routingPrefix": "NW",
"source": "referral",
"contactName": "Sam Okafor",
"contactEmail": "sam@example.com",
"contactPhone": "+1 555 0177",
"contactIm": null,
"timezone": "America/New_York",
"quality": 82,
"portalEnabled": true,
"payoutSchedule": "net_15",
"payoutMethod": "ach",
"joinedAt": "2026-07-05T13:11:02.000Z",
"createdAt": "2026-07-04T17:45:00.000Z",
"updatedAt": "2026-09-19T08:12:00.000Z",
"lanes": [
{
"campaignId": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
"active": true,
"payoutMode": "fixed",
"payoutCents": 2750,
"revsharePct": null,
"minPayoutCents": null,
"maxPayoutCents": null,
"bufferSeconds": null,
"effectiveBufferSeconds": 0,
"dedupeWindowSeconds": 86400,
"hourlyCap": 0,
"dailyCap": 400,
"monthlyCap": 0,
"concurrencyCap": 25,
"dailyPayoutCapCents": 0,
"blockWhenCapped": true,
"states": [
"TX",
"OK",
"NM"
],
"hoursSchedule": null,
"timezone": "America/Chicago",
"notesPublic": "Send health cover calls only. No aged leads.",
"notesInternal": "Renegotiate the payout in October.",
"usage": {
"liveCalls": 1,
"hour": 6,
"day": 142,
"month": 2988,
"dayPayoutCents": 390500
},
"key": {
"id": "1f2e3d4c-5b6a-4798-8c9d-0e1f2a3b4c5e",
"prefix": "b3k_live_7Qx4",
"state": "live",
"protocol": "rtb_v1",
"environment": "production",
"requireSignature": false,
"lastUsedAt": "2026-09-20T18:19:55.000Z",
"useCount": 18422,
"issuedAt": "2026-07-05T13:11:02.000Z"
},
"numbers": 2,
"campaign": {
"id": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
"name": "Health cover — inbound",
"status": "live"
},
"compat": {
"enabled": false
},
"assignedAt": "2026-07-05T13:11:02.000Z",
"termsFrozenAt": "2026-09-01T00:00:00.000Z"
}
]
}
],
"page": 1,
"pageSize": 25,
"total": 2,
"totalPages": 1
}Errors
| Status | Code | When |
|---|---|---|
| 400 | validation_error | A filter has the wrong type, or status is not one of the five. details names the field. |
| 401 | missing_key, invalid_key | No bearer key was sent, or the key is malformed, unknown, or revoked with its grace period over. See Authentication. |
| 402 | subscription_inactive, plan_upgrade_required | The workspace's subscription is not active, or its plan does not include call tracking and API access. |
| 403 | insufficient_scope, ip_not_allowed | The key does not hold publishers:read, or the request came from an address outside the key's allowlist. |
| 429 | rate_limited | The key is over its per-minute limit. Wait for Retry-After seconds. |
Read a publisher
/api/v1/publishers/:idOne publisher with every lane it holds onto your tracking campaigns, oldest lane first. Each lane carries its terms, its caps and its counters, so one request answers "what has this publisher agreed to, and where are they against it today".
Path parameters
| Name | Type | Description |
|---|---|---|
idRequired | uuid | The publisher's id, as returned by List publishers. |
Example request
curl "https://api.buy3.io/api/v1/publishers/c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b" \
-H "Authorization: Bearer $BUY3_API_KEY"Responses
- 200The publisher and its lanes.
{
"publisher": {
"id": "c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b",
"name": "Northwind Media",
"status": "live",
"statusNote": null,
"routingPrefix": "NW",
"source": "referral",
"contactName": "Sam Okafor",
"contactEmail": "sam@example.com",
"contactPhone": "+1 555 0177",
"contactIm": null,
"timezone": "America/New_York",
"quality": 82,
"portalEnabled": true,
"payoutSchedule": "net_15",
"payoutMethod": "ach",
"joinedAt": "2026-07-05T13:11:02.000Z",
"createdAt": "2026-07-04T17:45:00.000Z",
"updatedAt": "2026-09-19T08:12:00.000Z",
"lanes": [
{
"campaignId": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
"active": true,
"payoutMode": "fixed",
"payoutCents": 2750,
"revsharePct": null,
"minPayoutCents": null,
"maxPayoutCents": null,
"bufferSeconds": null,
"effectiveBufferSeconds": 0,
"dedupeWindowSeconds": 86400,
"hourlyCap": 0,
"dailyCap": 400,
"monthlyCap": 0,
"concurrencyCap": 25,
"dailyPayoutCapCents": 0,
"blockWhenCapped": true,
"states": [
"TX",
"OK",
"NM"
],
"hoursSchedule": null,
"timezone": "America/Chicago",
"notesPublic": "Send health cover calls only. No aged leads.",
"notesInternal": "Renegotiate the payout in October.",
"usage": {
"liveCalls": 1,
"hour": 6,
"day": 142,
"month": 2988,
"dayPayoutCents": 390500
},
"key": {
"id": "1f2e3d4c-5b6a-4798-8c9d-0e1f2a3b4c5e",
"prefix": "b3k_live_7Qx4",
"state": "live",
"protocol": "rtb_v1",
"environment": "production",
"requireSignature": false,
"lastUsedAt": "2026-09-20T18:19:55.000Z",
"useCount": 18422,
"issuedAt": "2026-07-05T13:11:02.000Z"
},
"numbers": 2,
"campaign": {
"id": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
"name": "Health cover — inbound",
"status": "live"
},
"compat": {
"enabled": false
},
"assignedAt": "2026-07-05T13:11:02.000Z",
"termsFrozenAt": "2026-09-01T00:00:00.000Z"
}
]
}
}Errors
| Status | Code | When |
|---|---|---|
| 404 | not_found | No publisher with that id in your workspace. A malformed id, an id that does not exist and another workspace's publisher all answer exactly the same, before any query runs. |
| 401 | missing_key, invalid_key | No bearer key was sent, or the key is malformed, unknown, or revoked with its grace period over. See Authentication. |
| 402 | subscription_inactive, plan_upgrade_required | The workspace's subscription is not active, or its plan does not include call tracking and API access. |
| 403 | insufficient_scope, ip_not_allowed | The key does not hold publishers:read, or the request came from an address outside the key's allowlist. |
| 429 | rate_limited | The key is over its per-minute limit. Wait for Retry-After seconds. |
Console API · List a campaign's lanes
/api/tracking/campaigns/:id/publishersConsole API. These routes take a signed-in owner's or admin's session token, not an API key, and they may change with the console. Every lane onto this campaign, oldest assignment first, each with its terms, caps, counters, key prefix and the state of its single-step ping URL. Only publishers your workspace owns are listed.
Path parameters
| Name | Type | Description |
|---|---|---|
idRequired | uuid | The campaign's id. |
Query parameters
| Name | Type | Description |
|---|---|---|
agencyIdOptional | uuid | The workspace to act in. Only needed when you hold an owner or admin seat on more than one. |
Example request
curl "https://api.buy3.io/api/tracking/campaigns/6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d/publishers" \
-H "Authorization: Bearer $BUY3_SESSION_TOKEN"Responses
- 200The campaign's lanes.
{
"lanes": [
{
"campaignId": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
"publisherId": "c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b",
"publisher": {
"id": "c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b",
"name": "Northwind Media",
"status": "live",
"routingPrefix": "NW"
},
"active": true,
"payoutMode": "fixed",
"payoutCents": 2750,
"revsharePct": null,
"minPayoutCents": null,
"maxPayoutCents": null,
"bufferSeconds": null,
"effectiveBufferSeconds": 0,
"dedupeWindowSeconds": 86400,
"hourlyCap": 0,
"dailyCap": 400,
"monthlyCap": 0,
"concurrencyCap": 25,
"dailyPayoutCapCents": 0,
"blockWhenCapped": true,
"states": [
"TX",
"OK",
"NM"
],
"hoursSchedule": null,
"timezone": "America/Chicago",
"notesPublic": "Send health cover calls only. No aged leads.",
"notesInternal": "Renegotiate the payout in October.",
"usage": {
"liveCalls": 1,
"hour": 6,
"day": 142,
"month": 2988,
"dayPayoutCents": 390500
},
"key": {
"id": "1f2e3d4c-5b6a-4798-8c9d-0e1f2a3b4c5e",
"prefix": "b3k_live_7Qx4",
"state": "live",
"protocol": "rtb_v1",
"environment": "production",
"requireSignature": false,
"lastUsedAt": "2026-09-20T18:19:55.000Z",
"useCount": 18422,
"issuedAt": "2026-07-05T13:11:02.000Z"
},
"numbers": 2,
"compat": {
"enabled": false,
"url": null
},
"assignedAt": "2026-07-05T13:11:02.000Z",
"termsFrozenAt": "2026-09-01T00:00:00.000Z"
}
]
}Errors
| Status | Code | When |
|---|---|---|
| 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 | No tracking campaign with that id in this workspace, or you named a workspace with ?agencyId= that is not yours. |
Console API · Read one lane
/api/tracking/campaigns/:id/publishers/:publisherIdConsole API. One publisher's standing on one campaign, with the single-step ping URL in full when it is switched on — this is the page that URL is copied from.
Path parameters
| Name | Type | Description |
|---|---|---|
idRequired | uuid | The campaign's id. |
publisherIdRequired | uuid | The publisher's id. |
Query parameters
| Name | Type | Description |
|---|---|---|
agencyIdOptional | uuid | The workspace to act in. |
Example request
curl "https://api.buy3.io/api/tracking/campaigns/6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d/publishers/c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b" \
-H "Authorization: Bearer $BUY3_SESSION_TOKEN"Responses
- 200The lane.
{
"lane": {
"campaignId": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
"publisherId": "c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b",
"publisher": {
"id": "c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b",
"name": "Northwind Media",
"status": "live",
"routingPrefix": "NW"
},
"active": true,
"payoutMode": "fixed",
"payoutCents": 2750,
"revsharePct": null,
"minPayoutCents": null,
"maxPayoutCents": null,
"bufferSeconds": null,
"effectiveBufferSeconds": 0,
"dedupeWindowSeconds": 86400,
"hourlyCap": 0,
"dailyCap": 400,
"monthlyCap": 0,
"concurrencyCap": 25,
"dailyPayoutCapCents": 0,
"blockWhenCapped": true,
"states": [
"TX",
"OK",
"NM"
],
"hoursSchedule": null,
"timezone": "America/Chicago",
"notesPublic": "Send health cover calls only. No aged leads.",
"notesInternal": "Renegotiate the payout in October.",
"usage": {
"liveCalls": 1,
"hour": 6,
"day": 142,
"month": 2988,
"dayPayoutCents": 390500
},
"key": {
"id": "1f2e3d4c-5b6a-4798-8c9d-0e1f2a3b4c5e",
"prefix": "b3k_live_7Qx4",
"state": "live",
"protocol": "rtb_v1",
"environment": "production",
"requireSignature": false,
"lastUsedAt": "2026-09-20T18:19:55.000Z",
"useCount": 18422,
"issuedAt": "2026-07-05T13:11:02.000Z"
},
"numbers": 2,
"compat": {
"enabled": false,
"url": null
},
"assignedAt": "2026-07-05T13:11:02.000Z",
"termsFrozenAt": "2026-09-01T00:00:00.000Z"
}
}Errors
| Status | Code | When |
|---|---|---|
| 404 | not_found | Publisher not found when the publisher is not yours; Publisher lane not found when they have no lane on this campaign. |
| 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 | No tracking campaign with that id in this workspace, or you named a workspace with ?agencyId= that is not yours. |
Console API · Attach a publisher, or change their terms
/api/tracking/campaigns/:id/publishers/:publisherIdConsole API. One request creates a lane or updates the one that is there. Creating a lane issues its routing key in the same transaction, and that key is returned in full exactly once — in this response, as key. It is never readable again; afterwards only its prefix comes back. Updating an existing lane never touches the key. A lane's terms take effect on the next call: calls already in progress keep the terms they started under.
Path parameters
| Name | Type | Description |
|---|---|---|
idRequired | uuid | The campaign's id. |
publisherIdRequired | uuid | The publisher's id. |
Query parameters
| Name | Type | Description |
|---|---|---|
agencyIdOptional | uuid | The workspace to act in. |
Request body
| Name | Type | Description |
|---|---|---|
activeOptional | boolean | false stops the lane taking calls without closing it. |
payoutModeOptional | enum | How the publisher is paid.inherit, fixed, revshare |
payoutCentsOptional | integer or null | The amount per converted call. Required when payoutMode is fixed.0 to 10000000 |
revsharePctOptional | number or null | The publisher's share of revenue. Required when payoutMode is revshare.0 to 100 |
minPayoutCentsOptional | integer or null | A floor under a revenue-share payout. It must not exceed maxPayoutCents.0 to 10000000 |
maxPayoutCentsOptional | integer or null | A ceiling over a revenue-share payout.0 to 10000000 |
bufferSecondsOptional | integer or null | The lane's own settlement buffer. null uses the campaign's.0 to 3600 |
dedupeWindowSecondsOptional | integer | How long a caller from this publisher counts as a duplicate.0 to 7776000 · 0 switches it off |
hourlyCapOptional | integer | Calls this publisher may send in one UTC clock hour.0 to 1000000 · 0 is unlimited |
dailyCapOptional | integer | Calls in one UTC day.0 to 1000000 · 0 is unlimited |
monthlyCapOptional | integer | Calls in one UTC month.0 to 10000000 · 0 is unlimited |
concurrencyCapOptional | integer | How many of this publisher's calls may be live at once.0 to 10000 · 0 is unlimited |
dailyPayoutCapCentsOptional | integer | The most the lane may earn the publisher in a UTC day.0 to 1000000000 · 0 is unlimited |
blockWhenCappedOptional | boolean | true refuses the publisher's pings once a cap is full, so they can send the call elsewhere. false accepts them unpaid. |
statesOptional | array of strings or null | Two-letter state codes this publisher may send from.at most 62 codes |
hoursScheduleOptional | object or null | When this publisher may send, as { tz, days }. null is always open. |
timezoneOptional | string or null | An IANA zone for the lane's hours.up to 64 characters |
notesPublicOptional | string or null | A note the publisher can read in their portal.up to 4000 characters |
notesInternalOptional | string or null | A note only your team can read.up to 4000 characters |
Example request
curl -X PUT "https://api.buy3.io/api/tracking/campaigns/6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d/publishers/c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b" \
-H "Authorization: Bearer $BUY3_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"payoutMode": "fixed",
"payoutCents": 2750,
"dailyCap": 400,
"concurrencyCap": 25,
"blockWhenCapped": true,
"states": [
"TX",
"OK",
"NM"
],
"notesPublic": "Send health cover calls only. No aged leads."
}'Responses
- 201The lane was created.
keyis the publisher's routing key in full — store it now, it cannot be read again — andkeyNotesays the same thing in words. - 200The lane already existed and its terms were updated. No key is returned, and the existing key is untouched.
{
"lane": {
"campaignId": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
"publisherId": "c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b",
"publisher": {
"id": "c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b",
"name": "Northwind Media",
"status": "live",
"routingPrefix": "NW"
},
"active": true,
"payoutMode": "fixed",
"payoutCents": 2750,
"revsharePct": null,
"minPayoutCents": null,
"maxPayoutCents": null,
"bufferSeconds": null,
"effectiveBufferSeconds": 0,
"dedupeWindowSeconds": 86400,
"hourlyCap": 0,
"dailyCap": 400,
"monthlyCap": 0,
"concurrencyCap": 25,
"dailyPayoutCapCents": 0,
"blockWhenCapped": true,
"states": [
"TX",
"OK",
"NM"
],
"hoursSchedule": null,
"timezone": "America/Chicago",
"notesPublic": "Send health cover calls only. No aged leads.",
"notesInternal": "Renegotiate the payout in October.",
"usage": {
"liveCalls": 1,
"hour": 6,
"day": 142,
"month": 2988,
"dayPayoutCents": 390500
},
"key": {
"id": "1f2e3d4c-5b6a-4798-8c9d-0e1f2a3b4c5e",
"prefix": "b3k_live_7Qx4",
"state": "live",
"protocol": "rtb_v1",
"environment": "production",
"requireSignature": false,
"lastUsedAt": "2026-09-20T18:19:55.000Z",
"useCount": 18422,
"issuedAt": "2026-07-05T13:11:02.000Z"
},
"numbers": 2,
"compat": {
"enabled": false,
"url": null
},
"assignedAt": "2026-07-05T13:11:02.000Z",
"termsFrozenAt": "2026-09-01T00:00:00.000Z"
},
"created": true,
"key": "b3k_live_7Qx4mV0c1SxamPLeKEy",
"keyNote": "This is the only time this key is shown. Send it to the publisher and store it somewhere safe."
}Errors
| Status | Code | When |
|---|---|---|
| 400 | validation_error | A field has the wrong type or is out of range, or a required companion field is missing — payoutCents for a fixed lane, revsharePct for a revshare one, or a minimum above the maximum. |
| 404 | not_found | Publisher not found — the publisher is not one of yours. |
| 409 | conflict | The campaign is archived, or the publisher is terminated and cannot be given a new lane. |
| 401 | unauthorized | No session token, or one that has expired. |
| 402 | subscription_inactive | The workspace's subscription is not active, and this is a write. details carries status, planId and agencyId. |
| 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 | No tracking campaign with that id in this workspace, or you named a workspace with ?agencyId= that is not yours. |
Console API · Detach a publisher
/api/tracking/campaigns/:id/publishers/:publisherIdConsole API. Closes the lane and revokes every active key for the pair, with no grace window — the publisher's next ping is refused — and switches off the single-step ping URL. The publisher's tracking numbers on the campaign are not touched: they keep ringing, and the response counts them so you can decide what to do about it. Calls already settled keep their payouts.
Path parameters
| Name | Type | Description |
|---|---|---|
idRequired | uuid | The campaign's id. |
publisherIdRequired | uuid | The publisher's id. |
Query parameters
| Name | Type | Description |
|---|---|---|
agencyIdOptional | uuid | The workspace to act in. |
Example request
curl -X DELETE "https://api.buy3.io/api/tracking/campaigns/6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d/publishers/c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b" \
-H "Authorization: Bearer $BUY3_SESSION_TOKEN"Responses
- 200What was removed, how many keys were revoked, and how many of the publisher's numbers still point at the campaign.
{
"removed": true,
"campaignId": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
"publisherId": "c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b",
"revokedKeys": 1,
"numbersStillAssigned": 2,
"note": "Two tracking numbers are still attributed to this publisher on this campaign. Reassign or release them."
}Errors
| Status | Code | When |
|---|---|---|
| 404 | not_found | Publisher lane not found — this publisher has no lane on this campaign. |
| 401 | unauthorized | No session token, or one that has expired. |
| 402 | subscription_inactive | The workspace's subscription is not active, and this is a write. details carries status, planId and agencyId. |
| 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 | No tracking campaign with that id in this workspace, or you named a workspace with ?agencyId= that is not yours. |
Console API · Switch the single-step ping URL on or off
/api/tracking/campaigns/:id/publishers/:publisherId/compatConsole API. Turns the Ringba-compatible single-step ping on or off for this lane. Switching it on mints a token if there is none, and widens a legacy key's protocol so both doors work. Switching it off forgets the token: turning it on again gives a new URL, and the old one never works again. The token is never written to the activity log.
Path parameters
| Name | Type | Description |
|---|---|---|
idRequired | uuid | The campaign's id. |
publisherIdRequired | uuid | The publisher's id. |
Query parameters
| Name | Type | Description |
|---|---|---|
agencyIdOptional | uuid | The workspace to act in. |
Request body
| Name | Type | Description |
|---|---|---|
enabledRequired | boolean | true opens the door and returns the URL; false closes it and forgets the token. |
Example request
curl -X POST "https://api.buy3.io/api/tracking/campaigns/6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d/publishers/c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b/compat" \
-H "Authorization: Bearer $BUY3_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"enabled": true
}'Responses
- 200Whether the door is open, the URL in full when it is, and the protocol the lane's key now speaks.
{
"enabled": true,
"url": "https://api.buy3.io/api/rtb/compat/ringba/7f3a9c1e5b2d48a06e1f7c3b9d5a2e84c6b1f0d37a9e2c58.json",
"protocol": "rtb_v1"
}Errors
| Status | Code | When |
|---|---|---|
| 404 | not_found | Publisher lane not found — this publisher has no lane on this campaign. |
| 409 | conflict | The lane has no live routing key to hang the URL on. |
| 409 | compat_needs_unsigned_key | You asked to switch the URL on for a key that requires signed requests, which a Ringba-style platform cannot send. details carries the keyPrefix. Turn signature enforcement off on the key first — see Change a key's settings. |
| 401 | unauthorized | No session token, or one that has expired. |
| 402 | subscription_inactive | The workspace's subscription is not active, and this is a write. details carries status, planId and agencyId. |
| 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 | No tracking campaign with that id in this workspace, or you named a workspace with ?agencyId= that is not yours. |
Console API · Change a publisher key's settings
/api/publishers/keys/:keyIdConsole API. Everything about a live key that is configuration rather than credential: its label, which protocol it speaks, its IP allowlist, its own rate limits and whether it must sign its requests. The key itself is never returned — only its prefix and the flags. Every write here is filed under both your workspace and the publisher, so it appears in your activity log and in the publisher's portal.
Path parameters
| Name | Type | Description |
|---|---|---|
keyIdRequired | uuid | The key's id, as lane.key.id. A missing, malformed or foreign key is the same 404 Key not found. |
Request body
| Name | Type | Description |
|---|---|---|
labelOptional | string or null | A name for the key, so a publisher with several can tell them apart.up to 120 characters |
protocolOptional | enum | Which ping protocol this key may use.legacy, rtb_v1, both |
environmentOptional | enum | What the key is for.production, test, sandbox |
reserveLaneOptional | boolean | Whether a winning bid on this key holds a concurrency slot until it is claimed. |
requireSignatureOptional | boolean | Whether every request on this key must be signed. A signing secret has to exist first. See Keys and security. |
allowCidrsOptional | array of strings or null | Addresses this key may be used from. An empty array or null means no allowlist.at most 64 entries · an IPv4 address, an IPv4 CIDR block or an IPv6 address |
rateLimitPerMinuteOptional | integer | The key's own per-minute limit.1 to 100000 |
rateLimitPerHourOptional | integer | The key's own per-hour limit.1 to 5000000 |
rateLimitPerDayOptional | integer | The key's own per-day limit.1 to 100000000 |
maxLiveBidsPerCallerOptional | integer | How many live bids this key may hold for one caller at a time.1 to 100 |
allowSelfRotateOptional | boolean | Whether the publisher may rotate this key themselves from their portal. |
compatEnabledOptional | boolean | The same switch as the lane's compat route: true mints the single-step token when there is none, false switches the door off and forgets the token. |
Example request
curl -X PATCH "https://api.buy3.io/api/publishers/keys/1f2e3d4c-5b6a-4798-8c9d-0e1f2a3b4c5e" \
-H "Authorization: Bearer $BUY3_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"label": "Northwind — production",
"allowCidrs": [
"203.0.113.0/24"
],
"rateLimitPerMinute": 600
}'Responses
- 200The key's settings as they now stand.
compatUrlis the single-step URL when that door is open. The bearer key, the signing secret and the bare compat token are never served.
{
"key": {
"id": "1f2e3d4c-5b6a-4798-8c9d-0e1f2a3b4c5e",
"publisherId": "c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b",
"campaignId": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
"keyPrefix": "b3k_live_7Qx4",
"label": "Northwind — production",
"protocol": "rtb_v1",
"environment": "production",
"reserveLane": true,
"active": true,
"requireSignature": false,
"allowSelfRotate": false,
"allowCidrs": [
"203.0.113.0/24"
],
"rateLimitPerMinute": 600,
"rateLimitPerHour": 20000,
"rateLimitPerDay": 300000,
"maxLiveBidsPerCaller": 3,
"useCount": 18422,
"lastUsedAt": "2026-09-20T18:19:55.000Z",
"issuedAt": "2026-07-05T13:11:02.000Z",
"graceUntil": null,
"revokedAt": null,
"rotatedFromId": null,
"createdAt": "2026-07-05T13:11:02.000Z",
"signingSecretLast4": null,
"hasSigningSecret": false,
"compatEnabled": false,
"compatUrl": null
}
}Errors
| Status | Code | When |
|---|---|---|
| 400 | bad_request | No fields to update, an entry in allowCidrs that is not an address or block — details.allowCidrs names them — or requireSignature: true on a key that has no signing secret yet. |
| 404 | not_found | Key not found — the id is missing, malformed, or belongs to a publisher you do not own. |
| 409 | conflict | The key has been revoked. |
| 409 | compat_needs_unsigned_key | compatEnabled: true on a key that requires signed requests. details carries the keyPrefix. |
| 409 | compat_enabled | requireSignature: true on a key whose single-step URL is switched on. The two cannot both be on. details carries the keyPrefix. |
| 401 | unauthorized | No session token, or one that has expired. |
| 402 | subscription_inactive | The workspace's subscription is not active, and this is a write. details carries status, planId and agencyId. |
| 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 | No key with that id belongs to a publisher you own, or you named a workspace with ?agencyId= that is not yours. |
Console API · Rotate the single-step ping URL
/api/publishers/keys/:keyId/compat-token/rotateConsole API. Mints a new single-step ping URL without touching the bearer key. There is no grace window: the previous URL stops working the moment this returns, so send the new one to the publisher before they next ping, or they will be refused. It takes no body. The token is never written to the activity log.
Path parameters
| Name | Type | Description |
|---|---|---|
keyIdRequired | uuid | The key's id. |
Example request
curl -X POST "https://api.buy3.io/api/publishers/keys/1f2e3d4c-5b6a-4798-8c9d-0e1f2a3b4c5e/compat-token/rotate" \
-H "Authorization: Bearer $BUY3_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'Responses
- 201The key, with
compatUrlset to the new URL, and a note repeating that the old one is dead.
{
"key": {
"id": "1f2e3d4c-5b6a-4798-8c9d-0e1f2a3b4c5e",
"publisherId": "c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b",
"campaignId": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
"keyPrefix": "b3k_live_7Qx4",
"protocol": "both",
"compatEnabled": true,
"compatUrl": "https://api.buy3.io/api/rtb/compat/ringba/2b8e4f1a7c3d59e06b1f8a4c2d7e5039f6a1b8c4d2e7f0a3.json",
"…": "…"
},
"note": "The previous ping URL stopped working immediately. Send the new one to the publisher."
}Errors
| Status | Code | When |
|---|---|---|
| 404 | not_found | Key not found. |
| 409 | conflict | The key has been revoked — its replacement holds the ping URL now — or the single-step door is switched off. Switch it on first. |
| 401 | unauthorized | No session token, or one that has expired. |
| 402 | subscription_inactive | The workspace's subscription is not active, and this is a write. details carries status, planId and agencyId. |
| 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 | No key with that id belongs to a publisher you own, or you named a workspace with ?agencyId= that is not yours. |
Console API · Rotate a key's signing secret
/api/publishers/keys/:keyId/signing-secret/rotateConsole API. Mints a new signing secret for the key and returns it once. There is no grace window — the secret is one value, overwritten in place — so the publisher's next signed request has to use the new one. By default this also switches signature enforcement on. To rotate without an outage: switch requireSignature off, rotate with enforce: false, let the publisher deploy the secret, then switch requireSignature back on.
Path parameters
| Name | Type | Description |
|---|---|---|
keyIdRequired | uuid | The key's id. |
Request body
| Name | Type | Description |
|---|---|---|
enforceOptional | boolean | Whether to switch requireSignature on at the same time. false mints the secret and leaves enforcement as it is.default true |
Example request
curl -X POST "https://api.buy3.io/api/publishers/keys/1f2e3d4c-5b6a-4798-8c9d-0e1f2a3b4c5e/signing-secret/rotate" \
-H "Authorization: Bearer $BUY3_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"enforce": false
}'Responses
- 200The key, and
signingSecretin full — the only time it is shown.previousSecretValidis alwaysfalse: there is no overlap.
{
"key": {
"id": "1f2e3d4c-5b6a-4798-8c9d-0e1f2a3b4c5e",
"keyPrefix": "b3k_live_7Qx4",
"requireSignature": true,
"hasSigningSecret": true,
"signingSecretLast4": "9c1f",
"…": "…"
},
"signingSecret": "b3s_sAmPLeSigNingSecret000000",
"effective": "immediately",
"previousSecretValid": false,
"note": "This is the only time this secret is shown. The previous one stopped working immediately."
}Errors
| Status | Code | When |
|---|---|---|
| 404 | not_found | Key not found. |
| 409 | conflict | The key has been revoked. |
| 409 | compat_enabled | enforce is true and the key's single-step URL is switched on. The two cannot both be on. details carries the keyPrefix. |
| 401 | unauthorized | No session token, or one that has expired. |
| 402 | subscription_inactive | The workspace's subscription is not active, and this is a write. details carries status, planId and agencyId. |
| 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 | No key with that id belongs to a publisher you own, or you named a workspace with ?agencyId= that is not yours. |
