API reference
Campaigns
Read your campaigns and their go-live checklist, pause and resume them from a program, and create and edit them through the console.
A campaign is one stream of calls with one set of rules: who may call, when, how many, what happens when nobody can take the call, and which routing plan decides where it goes. Every tracking number, every publisher lane and every call belongs to exactly one campaign. The concepts are in Campaigns overview.
Two surfaces
| Endpoint | Surface | Credential |
|---|---|---|
GET /api/v1/campaigns | REST API | API key with campaigns:read |
GET /api/v1/campaigns/:id | REST API | API key with campaigns:read |
POST /api/v1/campaigns/:id/status | REST API | API key with campaigns:write |
POST /api/tracking/campaigns | Console | Session token |
PATCH /api/tracking/campaigns/:id | Console | Session token |
POST /api/tracking/campaigns/:id/status | Console | Session token |
The campaign object
| Field | Type | Meaning |
|---|---|---|
id | uuid | The campaign's id, used in every path below. |
kind | string | Always tracking here. A campaign of any other kind answers 404 on these routes. |
name, description | string, string or null | What you call it, and a line about it for your team. |
status | string | draft, live, paused, restricted or archived. A restricted campaign has been stopped by platform staff and only they can move it. |
statusNote | string or null | The note sent with the last status change. It is cleared when a campaign goes live. |
verticalId, vertical | string, object or null | What the campaign sells. Fixed when the campaign is created. |
routingMode | string | priority, weighted or highest_bid. |
recordCalls | boolean | Whether calls are recorded. |
repeatCallerPolicy | string | allow, block or same_target. |
anonymousPolicy | string | allow or block. |
unroutedAction | string | hangup, message or forward, with unroutedMessage or unroutedForwardE164 beside it. |
bufferSeconds | integer | Seconds held back before settlement. |
dialTimeoutSeconds | integer | How long one target may ring. |
failoverBudgetSeconds | integer | How long the whole waterfall may take. |
dedupeWindowSeconds | integer | How long a caller counts as a repeat caller. |
dailyCap, concurrencyCap | integer | Campaign-wide caps. 0 is unlimited. |
hoursSchedule, timezone | object or null, string | When the campaign is open. null is always open. |
states | array of strings | Two-letter state codes accepted. Empty accepts every state. |
routingKeyPrefix | string | The prefix the platform mints for this campaign's routing keys. Read-only. |
publishedAt | timestamp or null | When the campaign first went live. |
counts | object | { numbers, routes, activeRoutes, publishers } — numbers that are not released, and lanes that are active. |
today | object | The day's counts and money, on the workspace's calendar. |
liveCalls | integer | Calls on this campaign right now. |
blockers, canGoLive, checklist | array, boolean, object | Only on a single-campaign read and after a write. See The checklist. |
createdAt, updatedAt | timestamp | When the campaign was created and last changed. |
The checklist
A single-campaign read carries three extra fields that answer "is this campaign ready, and if not, what is missing".
blockersis a list of sentences. It is empty when the campaign can go live, and otherwise holds exactly what a go-live request would be refused with — no active tracking number, or no active route to an active target.canGoLiveistruewhenblockersis empty.checklistis the same ground covered as steps: each{ key, label, done, blocking, optional, to, sentence }. A step withblocking: truestops a launch; anoptionalstep is advice.tois a path inside the console, relative to its own base.
Statuses, and who may change them
| Status | What it means | Who can move it |
|---|---|---|
draft | Built but never live. Numbers can point at it; calls are not routed. | Console only — launching is a person's act. |
live | Taking calls. | A key may pause it. The console may do anything. |
paused | Numbers still point at it; callers meet unroutedAction. | A key may resume it. A resume still has to pass the go-live checks. |
archived | Out of use. It can only be restored to draft. | Console only. A campaign must be paused before it can be archived, and no number may still point at it. |
restricted | Stopped by platform staff. | Staff only. Every other caller is refused. |
Endpoints
- GET
/api/v1/campaignsList campaigns - GET
/api/v1/campaigns/:idRead a campaign - POST
/api/v1/campaigns/:id/statusPause or resume a campaign - POST
/api/tracking/campaignsConsole API · Create a campaign - PATCH
/api/tracking/campaigns/:idConsole API · Update a campaign - POST
/api/tracking/campaigns/:id/statusConsole API · Move a campaign through its statuses
List campaigns
/api/v1/campaignsEvery tracking campaign in your workspace, filtered and paged, each with its counts, its day so far and its live calls. Archived campaigns are left out unless you ask for them by name. The checklist fields are not on a list row — read one campaign for those.
Query parameters
| Name | Type | Description |
|---|---|---|
qOptional | string | Matches name, case-insensitively, anywhere in the value.up to 120 characters |
statusOptional | enum | One status.draft, live, paused, restricted, archived · default: every status except archived |
routingModeOptional | enum | Only campaigns routing this way.priority, weighted, highest_bid |
verticalIdOptional | string | Only campaigns in this vertical.up to 80 characters |
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/campaigns?status=live&pageSize=25" \
-H "Authorization: Bearer $BUY3_API_KEY"Responses
- 200A page of campaign objects, without
blockers,canGoLiveorchecklist.
{
"data": [
{
"id": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
"kind": "tracking",
"name": "Health cover — inbound",
"description": "Inbound calls from comparison sites and our own search ads.",
"status": "live",
"statusNote": null,
"verticalId": "health-cover",
"vertical": {
"id": "health-cover",
"name": "Health cover",
"short": "HC"
},
"routingMode": "priority",
"recordCalls": true,
"repeatCallerPolicy": "allow",
"anonymousPolicy": "allow",
"unroutedAction": "message",
"unroutedMessage": "Sorry, nobody is available right now. Please try again later.",
"unroutedForwardE164": null,
"bufferSeconds": 0,
"dialTimeoutSeconds": 18,
"failoverBudgetSeconds": 45,
"dedupeWindowSeconds": 86400,
"dailyCap": 0,
"concurrencyCap": 0,
"hoursSchedule": {
"tz": "America/Chicago",
"days": {
"mon": [
[
"08:00",
"18:00"
]
],
"tue": [
[
"08:00",
"18:00"
]
],
"wed": [
[
"08:00",
"18:00"
]
],
"thu": [
[
"08:00",
"18:00"
]
],
"fri": [
[
"08:00",
"17:00"
]
],
"sat": [
[
"09:00",
"13:00"
]
],
"sun": []
}
},
"timezone": "America/Chicago",
"states": [
"TX",
"OK",
"NM"
],
"routingKeyPrefix": "NWC-TRK-HEALTH-4F2A1",
"publishedAt": "2026-07-05T13:11:02.000Z",
"createdAt": "2026-07-02T15:40:00.000Z",
"updatedAt": "2026-09-19T09:22:10.000Z",
"counts": {
"numbers": 4,
"routes": 3,
"activeRoutes": 3,
"publishers": 2
},
"today": {
"calls": 212,
"connected": 171,
"converted": 96,
"connectRate": 0.8066,
"conversionRate": 0.5614,
"revenueCents": 403200,
"payoutCents": 264000
},
"liveCalls": 3
}
],
"page": 1,
"pageSize": 25,
"total": 6,
"totalPages": 1
}Errors
| Status | Code | When |
|---|---|---|
| 400 | validation_error | A filter has the wrong type, or status or routingMode is not one of the listed values. 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 campaigns: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 campaign
/api/v1/campaigns/:idOne campaign with every setting, plus blockers, canGoLive and the full checklist. Read this before resuming a campaign from a program: it tells you whether the resume would be accepted, and why not if it would not.
Path parameters
| Name | Type | Description |
|---|---|---|
idRequired | uuid | The campaign's id, as returned by List campaigns. |
Example request
curl "https://api.buy3.io/api/v1/campaigns/6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d" \
-H "Authorization: Bearer $BUY3_API_KEY"Responses
- 200The campaign, with its readiness.
- 200 (not ready)The same shape for a campaign that could not go live.
blockersholds the sentences a go-live request would be refused with.
{
"campaign": {
"id": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
"kind": "tracking",
"name": "Health cover — inbound",
"description": "Inbound calls from comparison sites and our own search ads.",
"status": "live",
"statusNote": null,
"verticalId": "health-cover",
"vertical": {
"id": "health-cover",
"name": "Health cover",
"short": "HC"
},
"routingMode": "priority",
"recordCalls": true,
"repeatCallerPolicy": "allow",
"anonymousPolicy": "allow",
"unroutedAction": "message",
"unroutedMessage": "Sorry, nobody is available right now. Please try again later.",
"unroutedForwardE164": null,
"bufferSeconds": 0,
"dialTimeoutSeconds": 18,
"failoverBudgetSeconds": 45,
"dedupeWindowSeconds": 86400,
"dailyCap": 0,
"concurrencyCap": 0,
"hoursSchedule": {
"tz": "America/Chicago",
"days": {
"mon": [
[
"08:00",
"18:00"
]
],
"tue": [
[
"08:00",
"18:00"
]
],
"wed": [
[
"08:00",
"18:00"
]
],
"thu": [
[
"08:00",
"18:00"
]
],
"fri": [
[
"08:00",
"17:00"
]
],
"sat": [
[
"09:00",
"13:00"
]
],
"sun": []
}
},
"timezone": "America/Chicago",
"states": [
"TX",
"OK",
"NM"
],
"routingKeyPrefix": "NWC-TRK-HEALTH-4F2A1",
"publishedAt": "2026-07-05T13:11:02.000Z",
"createdAt": "2026-07-02T15:40:00.000Z",
"updatedAt": "2026-09-19T09:22:10.000Z",
"counts": {
"numbers": 4,
"routes": 3,
"activeRoutes": 3,
"publishers": 2
},
"today": {
"calls": 212,
"connected": 171,
"converted": 96,
"connectRate": 0.8066,
"conversionRate": 0.5614,
"revenueCents": 403200,
"payoutCents": 264000
},
"liveCalls": 3,
"blockers": [],
"canGoLive": true,
"checklist": {
"ready": true,
"warnings": [],
"steps": [
{
"key": "buyer",
"label": "Add a buyer",
"done": true,
"blocking": false,
"optional": false,
"to": "buyers",
"sentence": "You have buyers to sell these calls to."
},
{
"key": "target",
"label": "Add a target",
"done": true,
"blocking": false,
"optional": false,
"to": "targets",
"sentence": "At least one destination is set up."
},
{
"key": "routing",
"label": "Build the routing plan",
"done": true,
"blocking": true,
"optional": false,
"to": "campaigns/6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d?tab=routing",
"sentence": "Three active routes point at active targets."
},
{
"key": "number",
"label": "Point a number at it",
"done": true,
"blocking": true,
"optional": false,
"to": "numbers",
"sentence": "Four tracking numbers send their calls here."
},
{
"key": "publishers",
"label": "Invite publishers",
"done": true,
"blocking": false,
"optional": true,
"to": "publishers",
"sentence": "Two publishers have a lane on this campaign."
},
{
"key": "funded",
"label": "Fund the wallet",
"done": true,
"blocking": true,
"optional": false,
"to": "wallet",
"sentence": "The workspace wallet can pay for connected minutes."
},
{
"key": "live",
"label": "Set it live",
"done": true,
"blocking": false,
"optional": false,
"to": "campaigns/6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
"sentence": "The campaign is live and taking calls."
}
]
}
}
}Errors
| Status | Code | When |
|---|---|---|
| 404 | not_found | No tracking campaign with that id in your workspace. A malformed id, an id that does not exist, another workspace's campaign and a campaign of another kind 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 campaigns: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. |
Pause or resume a campaign
/api/v1/campaigns/:id/statusSwitch a campaign between live and paused, and nothing else. A paused campaign keeps its numbers, its plan and its lanes; callers who ring it meet its unroutedAction, and the call is recorded as unrouted with the reason campaign_not_live. A resume is still judged by the go-live checks, so a campaign whose plan has since emptied is refused rather than switched on into nothing. The change is recorded in the activity log under the key's name.
Path parameters
| Name | Type | Description |
|---|---|---|
idRequired | uuid | The campaign's id, as returned by List campaigns. |
Request body
| Name | Type | Description |
|---|---|---|
statusRequired | enum | The status to move to.live or paused |
noteOptional | string | Why, for the activity log. It is kept as statusNote, and cleared when the campaign goes live.up to 500 characters |
Example request
curl -X POST "https://api.buy3.io/api/v1/campaigns/6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d/status" \
-H "Authorization: Bearer $BUY3_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"status": "paused",
"note": "Daily budget reached"
}'Responses
- 200The campaign, and
changed—falsewhen it was already in that status, which is what a retry answers.
{
"campaign": {
"id": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
"kind": "tracking",
"name": "Health cover — inbound",
"description": "Inbound calls from comparison sites and our own search ads.",
"status": "paused",
"statusNote": "Daily budget reached",
"verticalId": "health-cover",
"vertical": {
"id": "health-cover",
"name": "Health cover",
"short": "HC"
},
"routingMode": "priority",
"recordCalls": true,
"repeatCallerPolicy": "allow",
"anonymousPolicy": "allow",
"unroutedAction": "message",
"unroutedMessage": "Sorry, nobody is available right now. Please try again later.",
"unroutedForwardE164": null,
"bufferSeconds": 0,
"dialTimeoutSeconds": 18,
"failoverBudgetSeconds": 45,
"dedupeWindowSeconds": 86400,
"dailyCap": 0,
"concurrencyCap": 0,
"hoursSchedule": {
"tz": "America/Chicago",
"days": {
"mon": [
[
"08:00",
"18:00"
]
],
"tue": [
[
"08:00",
"18:00"
]
],
"wed": [
[
"08:00",
"18:00"
]
],
"thu": [
[
"08:00",
"18:00"
]
],
"fri": [
[
"08:00",
"17:00"
]
],
"sat": [
[
"09:00",
"13:00"
]
],
"sun": []
}
},
"timezone": "America/Chicago",
"states": [
"TX",
"OK",
"NM"
],
"routingKeyPrefix": "NWC-TRK-HEALTH-4F2A1",
"publishedAt": "2026-07-05T13:11:02.000Z",
"createdAt": "2026-07-02T15:40:00.000Z",
"updatedAt": "2026-09-20T19:02:18.000Z",
"counts": {
"numbers": 4,
"routes": 3,
"activeRoutes": 3,
"publishers": 2
},
"today": {
"calls": 212,
"connected": 171,
"converted": 96,
"connectRate": 0.8066,
"conversionRate": 0.5614,
"revenueCents": 403200,
"payoutCents": 264000
},
"liveCalls": 3,
"blockers": [],
"canGoLive": true,
"checklist": {
"ready": true,
"warnings": [],
"steps": [
{
"key": "buyer",
"label": "Add a buyer",
"done": true,
"blocking": false,
"optional": false,
"to": "buyers",
"sentence": "You have buyers to sell these calls to."
},
{
"key": "target",
"label": "Add a target",
"done": true,
"blocking": false,
"optional": false,
"to": "targets",
"sentence": "At least one destination is set up."
},
{
"key": "routing",
"label": "Build the routing plan",
"done": true,
"blocking": true,
"optional": false,
"to": "campaigns/6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d?tab=routing",
"sentence": "Three active routes point at active targets."
},
{
"key": "number",
"label": "Point a number at it",
"done": true,
"blocking": true,
"optional": false,
"to": "numbers",
"sentence": "Four tracking numbers send their calls here."
},
{
"key": "publishers",
"label": "Invite publishers",
"done": true,
"blocking": false,
"optional": true,
"to": "publishers",
"sentence": "Two publishers have a lane on this campaign."
},
{
"key": "funded",
"label": "Fund the wallet",
"done": true,
"blocking": true,
"optional": false,
"to": "wallet",
"sentence": "The workspace wallet can pay for connected minutes."
},
{
"key": "live",
"label": "Set it live",
"done": true,
"blocking": false,
"optional": false,
"to": "campaigns/6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
"sentence": "The campaign is live and taking calls."
}
]
}
},
"changed": true
}Errors
| Status | Code | When |
|---|---|---|
| 400 | validation_error | status is missing, or is not live or paused. |
| 404 | not_found | No tracking campaign with that id in your workspace. A malformed id, an id that does not exist, another workspace's campaign and a campaign of another kind all answer exactly the same, before any query runs. |
| 409 | conflict | The campaign is draft, archived or restricted. The API pauses a live campaign and resumes a paused one; launching, archiving and restoring are console acts. details carries the current status and the allowed list. |
| 409 | conflict | A resume was refused because the campaign is not ready. details.blockers is the same list Read a campaign returns. |
| 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 campaigns:write, 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 · Create a campaign
/api/tracking/campaignsConsole API. This route takes a signed-in owner's or admin's session token, not an API key, and it may change with the console — treat the REST endpoints above as the stable surface. A campaign is always created as a draft; going live is a separate request, once the checklist is clear. kind, status and routingKeyPrefix are set by the server and cannot be sent. How many campaigns a workspace may hold depends on its plan; see pricing.
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. |
Request body
| Name | Type | Description |
|---|---|---|
nameRequired | string | What you call the campaign.1 to 120 characters |
verticalIdRequired | string | What the campaign sells — a platform vertical, or one of your own that is active. Create only: a campaign cannot change vertical afterwards.1 to 80 characters |
descriptionOptional | string | A line for your team about what this campaign is.up to 300 characters |
routingModeOptional | enum | How the routing plan chooses between targets.priority, weighted, highest_bid |
recordCallsOptional | boolean | Whether calls on this campaign are recorded. See Recordings for what that means for consent and retention. |
repeatCallerPolicyOptional | enum | What to do with a caller who has reached this campaign before. allow routes them normally, block turns them away, same_target sends them back to whoever took the first call.allow, block, same_target |
anonymousPolicyOptional | enum | Whether a caller who hides their number is accepted. block ends the call with the reason anonymous_blocked.allow, block |
unroutedActionOptional | enum | What a caller hears when no target takes the call: hang up, play a message, or forward to a number of your own.hangup, message, forward |
unroutedMessageOptional | string | The sentence read out to the caller. Required when unroutedAction is message.up to 500 characters |
unroutedForwardE164Optional | string | The number to forward to, in E.164. Required when unroutedAction is forward. A forwarded call is still an unrouted call: it books nothing and is never counted as connected. |
bufferSecondsOptional | integer | Seconds held back before a call is settled, so a publisher's post or a buyer's postback has time to arrive.0 to 3600 |
dialTimeoutSecondsOptional | integer | How long one target may ring before the plan moves on.5 to 120 |
failoverBudgetSecondsOptional | integer | How long the whole waterfall may take, across every attempt. It must be at least dialTimeoutSeconds.10 to 180 |
dedupeWindowSecondsOptional | integer | How long a caller counts as a repeat caller for. It is the window repeatCallerPolicy is measured over — not a target's own duplicate window, which is set on the target.0 to 7776000 · 0 switches it off |
dailyCapOptional | integer | Calls this campaign may take in one UTC day. Past it, callers end with the reason campaign_capped.0 to 1000000 · 0 is unlimited |
concurrencyCapOptional | integer | How many calls this campaign may have live at once.0 to 100000 · 0 is unlimited |
hoursScheduleOptional | object or null | When the campaign accepts calls: { tz, days: { mon: [["08:00", "18:00"]], … } }. null is always open. A day with an empty list is closed. See Hours and caps.at most 6 windows a day · 24-hour clock; 24:00 is allowed as a closing time only · a schedule that closes all seven days is refused — send null instead |
timezoneOptional | string | An IANA zone. It is folded into hoursSchedule.tz, so on a campaign with no schedule it has no effect.up to 64 characters |
statesOptional | array of strings | Two-letter state codes this campaign accepts, from the caller's area code. An empty array accepts every state.at most 62 codes · upper-cased and de-duplicated for you |
Example request
curl -X POST "https://api.buy3.io/api/tracking/campaigns" \
-H "Authorization: Bearer $BUY3_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Health cover — inbound",
"verticalId": "health-cover",
"routingMode": "priority",
"unroutedAction": "message",
"unroutedMessage": "Sorry, nobody is available right now. Please try again later.",
"dedupeWindowSeconds": 86400,
"states": [
"TX",
"OK",
"NM"
]
}'Responses
- 201The campaign, as a draft, with the checklist of what is still missing.
{
"campaign": {
"id": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
"kind": "tracking",
"name": "Health cover — inbound",
"description": "Inbound calls from comparison sites and our own search ads.",
"status": "draft",
"statusNote": null,
"verticalId": "health-cover",
"vertical": {
"id": "health-cover",
"name": "Health cover",
"short": "HC"
},
"routingMode": "priority",
"recordCalls": true,
"repeatCallerPolicy": "allow",
"anonymousPolicy": "allow",
"unroutedAction": "message",
"unroutedMessage": "Sorry, nobody is available right now. Please try again later.",
"unroutedForwardE164": null,
"bufferSeconds": 0,
"dialTimeoutSeconds": 18,
"failoverBudgetSeconds": 45,
"dedupeWindowSeconds": 86400,
"dailyCap": 0,
"concurrencyCap": 0,
"hoursSchedule": null,
"timezone": "America/Chicago",
"states": [
"TX",
"OK",
"NM"
],
"routingKeyPrefix": "NWC-TRK-HEALTH-4F2A1",
"publishedAt": null,
"createdAt": "2026-07-02T15:40:00.000Z",
"updatedAt": "2026-09-19T09:22:10.000Z",
"counts": {
"numbers": 0,
"routes": 0,
"activeRoutes": 0,
"publishers": 0
},
"today": {
"calls": 0,
"connected": 0,
"converted": 0,
"connectRate": null,
"conversionRate": null,
"revenueCents": 0,
"payoutCents": 0
},
"liveCalls": 0,
"blockers": [
"No tracking number points at this campaign — buy or assign one first",
"The routing plan has no active target — add one, or every caller goes unrouted"
],
"canGoLive": false,
"checklist": {
"ready": false,
"warnings": [],
"steps": [
{
"key": "buyer",
"label": "Add a buyer",
"done": false,
"blocking": false,
"optional": false,
"to": "buyers",
"sentence": "You have buyers to sell these calls to."
},
{
"key": "target",
"label": "Add a target",
"done": false,
"blocking": false,
"optional": false,
"to": "targets",
"sentence": "At least one destination is set up."
}
]
}
}
}Errors
| Status | Code | When |
|---|---|---|
| 400 | validation_error | A field has the wrong type or is out of range. details names the field. |
| 400 | bad_request | A cross-field rule failed: unroutedMessage missing for a message action, unroutedForwardE164 missing for a forward action, failoverBudgetSeconds below dialTimeoutSeconds, a timezone that is not an IANA zone, an hoursSchedule that closes all seven days or holds an unknown day, a state code that is not two letters, or a verticalId that is not yours and not a platform vertical. details names each field. |
| 402 | plan_upgrade_required | The workspace already holds as many campaigns as its plan allows. details.feature is maxTrackingCampaigns. |
| 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 · Update a campaign
/api/tracking/campaigns/:idConsole API. Change any setting of the create form except verticalId, which is fixed, and status, which has its own route. Send only what you are changing. Cross-field rules are judged on the campaign as it will be after the change, so you can send unroutedAction and unroutedMessage together, or change one of them against the other's stored value. A setting takes effect on the next call; calls already in progress keep the rules they started under.
Path parameters
| Name | Type | Description |
|---|---|---|
idRequired | uuid | The campaign's id, as returned by List campaigns. |
Query parameters
| Name | Type | Description |
|---|---|---|
agencyIdOptional | uuid | The workspace to act in. |
Request body
| Name | Type | Description |
|---|---|---|
nameOptional | string | A new name.1 to 120 characters |
descriptionOptional | string | A line for your team about what this campaign is.up to 300 characters |
routingModeOptional | enum | How the routing plan chooses between targets.priority, weighted, highest_bid |
recordCallsOptional | boolean | Whether calls on this campaign are recorded. See Recordings for what that means for consent and retention. |
repeatCallerPolicyOptional | enum | What to do with a caller who has reached this campaign before. allow routes them normally, block turns them away, same_target sends them back to whoever took the first call.allow, block, same_target |
anonymousPolicyOptional | enum | Whether a caller who hides their number is accepted. block ends the call with the reason anonymous_blocked.allow, block |
unroutedActionOptional | enum | What a caller hears when no target takes the call: hang up, play a message, or forward to a number of your own.hangup, message, forward |
unroutedMessageOptional | string | The sentence read out to the caller. Required when unroutedAction is message.up to 500 characters |
unroutedForwardE164Optional | string | The number to forward to, in E.164. Required when unroutedAction is forward. A forwarded call is still an unrouted call: it books nothing and is never counted as connected. |
bufferSecondsOptional | integer | Seconds held back before a call is settled, so a publisher's post or a buyer's postback has time to arrive.0 to 3600 |
dialTimeoutSecondsOptional | integer | How long one target may ring before the plan moves on.5 to 120 |
failoverBudgetSecondsOptional | integer | How long the whole waterfall may take, across every attempt. It must be at least dialTimeoutSeconds.10 to 180 |
dedupeWindowSecondsOptional | integer | How long a caller counts as a repeat caller for. It is the window repeatCallerPolicy is measured over — not a target's own duplicate window, which is set on the target.0 to 7776000 · 0 switches it off |
dailyCapOptional | integer | Calls this campaign may take in one UTC day. Past it, callers end with the reason campaign_capped.0 to 1000000 · 0 is unlimited |
concurrencyCapOptional | integer | How many calls this campaign may have live at once.0 to 100000 · 0 is unlimited |
hoursScheduleOptional | object or null | When the campaign accepts calls: { tz, days: { mon: [["08:00", "18:00"]], … } }. null is always open. A day with an empty list is closed. See Hours and caps.at most 6 windows a day · 24-hour clock; 24:00 is allowed as a closing time only · a schedule that closes all seven days is refused — send null instead |
timezoneOptional | string | An IANA zone. It is folded into hoursSchedule.tz, so on a campaign with no schedule it has no effect.up to 64 characters |
statesOptional | array of strings | Two-letter state codes this campaign accepts, from the caller's area code. An empty array accepts every state.at most 62 codes · upper-cased and de-duplicated for you |
Example request
curl -X PATCH "https://api.buy3.io/api/tracking/campaigns/6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d" \
-H "Authorization: Bearer $BUY3_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"dailyCap": 500,
"hoursSchedule": {
"tz": "America/Chicago",
"days": {
"mon": [
[
"08:00",
"18:00"
]
],
"tue": [
[
"08:00",
"18:00"
]
],
"wed": [
[
"08:00",
"18:00"
]
],
"thu": [
[
"08:00",
"18:00"
]
],
"fri": [
[
"08:00",
"17:00"
]
],
"sat": [
[
"09:00",
"13:00"
]
],
"sun": []
}
}
}'Responses
- 200The campaign as it now stands, with its checklist recomputed.
{
"campaign": {
"id": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
"kind": "tracking",
"name": "Health cover — inbound",
"description": "Inbound calls from comparison sites and our own search ads.",
"status": "live",
"statusNote": null,
"verticalId": "health-cover",
"vertical": {
"id": "health-cover",
"name": "Health cover",
"short": "HC"
},
"routingMode": "priority",
"recordCalls": true,
"repeatCallerPolicy": "allow",
"anonymousPolicy": "allow",
"unroutedAction": "message",
"unroutedMessage": "Sorry, nobody is available right now. Please try again later.",
"unroutedForwardE164": null,
"bufferSeconds": 0,
"dialTimeoutSeconds": 18,
"failoverBudgetSeconds": 45,
"dedupeWindowSeconds": 86400,
"dailyCap": 500,
"concurrencyCap": 0,
"hoursSchedule": {
"tz": "America/Chicago",
"days": {
"mon": [
[
"08:00",
"18:00"
]
],
"tue": [
[
"08:00",
"18:00"
]
],
"wed": [
[
"08:00",
"18:00"
]
],
"thu": [
[
"08:00",
"18:00"
]
],
"fri": [
[
"08:00",
"17:00"
]
],
"sat": [
[
"09:00",
"13:00"
]
],
"sun": []
}
},
"timezone": "America/Chicago",
"states": [
"TX",
"OK",
"NM"
],
"routingKeyPrefix": "NWC-TRK-HEALTH-4F2A1",
"publishedAt": "2026-07-05T13:11:02.000Z",
"createdAt": "2026-07-02T15:40:00.000Z",
"updatedAt": "2026-09-20T19:05:44.000Z",
"counts": {
"numbers": 4,
"routes": 3,
"activeRoutes": 3,
"publishers": 2
},
"today": {
"calls": 212,
"connected": 171,
"converted": 96,
"connectRate": 0.8066,
"conversionRate": 0.5614,
"revenueCents": 403200,
"payoutCents": 264000
},
"liveCalls": 3,
"blockers": [],
"canGoLive": true,
"checklist": {
"ready": true,
"warnings": [],
"steps": [
{
"key": "buyer",
"label": "Add a buyer",
"done": true,
"blocking": false,
"optional": false,
"to": "buyers",
"sentence": "You have buyers to sell these calls to."
},
{
"key": "target",
"label": "Add a target",
"done": true,
"blocking": false,
"optional": false,
"to": "targets",
"sentence": "At least one destination is set up."
},
{
"key": "routing",
"label": "Build the routing plan",
"done": true,
"blocking": true,
"optional": false,
"to": "campaigns/6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d?tab=routing",
"sentence": "Three active routes point at active targets."
},
{
"key": "number",
"label": "Point a number at it",
"done": true,
"blocking": true,
"optional": false,
"to": "numbers",
"sentence": "Four tracking numbers send their calls here."
},
{
"key": "publishers",
"label": "Invite publishers",
"done": true,
"blocking": false,
"optional": true,
"to": "publishers",
"sentence": "Two publishers have a lane on this campaign."
},
{
"key": "funded",
"label": "Fund the wallet",
"done": true,
"blocking": true,
"optional": false,
"to": "wallet",
"sentence": "The workspace wallet can pay for connected minutes."
},
{
"key": "live",
"label": "Set it live",
"done": true,
"blocking": false,
"optional": false,
"to": "campaigns/6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
"sentence": "The campaign is live and taking calls."
}
]
}
}
}Errors
| Status | Code | When |
|---|---|---|
| 400 | validation_error, bad_request | As for create, plus bad_request with "Nothing to update" when the body carried no change. |
| 409 | conflict | The campaign is archived. Restore it to draft 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 tracking campaign with that id in this workspace, or you named a workspace with ?agencyId= that is not yours. |
Console API · Move a campaign through its statuses
/api/tracking/campaigns/:id/statusConsole API. The whole state machine: launch a draft, pause, resume, archive, and restore an archived campaign to draft. Going live is judged against the same checks Read a campaign reports, and a refusal hands back the whole list rather than the first problem. restricted is staff's and cannot be set here.
Path parameters
| Name | Type | Description |
|---|---|---|
idRequired | uuid | The campaign's id, as returned by List campaigns. |
Query parameters
| Name | Type | Description |
|---|---|---|
agencyIdOptional | uuid | The workspace to act in. |
Request body
| Name | Type | Description |
|---|---|---|
statusRequired | enum | The status to move to.draft, live, paused, archived |
noteOptional | string | Why. It is kept as statusNote and cleared when the campaign goes live.up to 500 characters |
Example request
curl -X POST "https://api.buy3.io/api/tracking/campaigns/6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d/status" \
-H "Authorization: Bearer $BUY3_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"status": "live"
}'Responses
- 200The campaign and whether anything
changed.
{
"campaign": {
"id": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
"kind": "tracking",
"name": "Health cover — inbound",
"description": "Inbound calls from comparison sites and our own search ads.",
"status": "live",
"statusNote": null,
"verticalId": "health-cover",
"vertical": {
"id": "health-cover",
"name": "Health cover",
"short": "HC"
},
"routingMode": "priority",
"recordCalls": true,
"repeatCallerPolicy": "allow",
"anonymousPolicy": "allow",
"unroutedAction": "message",
"unroutedMessage": "Sorry, nobody is available right now. Please try again later.",
"unroutedForwardE164": null,
"bufferSeconds": 0,
"dialTimeoutSeconds": 18,
"failoverBudgetSeconds": 45,
"dedupeWindowSeconds": 86400,
"dailyCap": 0,
"concurrencyCap": 0,
"hoursSchedule": {
"tz": "America/Chicago",
"days": {
"mon": [
[
"08:00",
"18:00"
]
],
"tue": [
[
"08:00",
"18:00"
]
],
"wed": [
[
"08:00",
"18:00"
]
],
"thu": [
[
"08:00",
"18:00"
]
],
"fri": [
[
"08:00",
"17:00"
]
],
"sat": [
[
"09:00",
"13:00"
]
],
"sun": []
}
},
"timezone": "America/Chicago",
"states": [
"TX",
"OK",
"NM"
],
"routingKeyPrefix": "NWC-TRK-HEALTH-4F2A1",
"publishedAt": "2026-09-20T19:07:00.000Z",
"createdAt": "2026-07-02T15:40:00.000Z",
"updatedAt": "2026-09-19T09:22:10.000Z",
"counts": {
"numbers": 4,
"routes": 3,
"activeRoutes": 3,
"publishers": 2
},
"today": {
"calls": 212,
"connected": 171,
"converted": 96,
"connectRate": 0.8066,
"conversionRate": 0.5614,
"revenueCents": 403200,
"payoutCents": 264000
},
"liveCalls": 3,
"blockers": [],
"canGoLive": true,
"checklist": {
"ready": true,
"warnings": [],
"steps": [
{
"key": "buyer",
"label": "Add a buyer",
"done": true,
"blocking": false,
"optional": false,
"to": "buyers",
"sentence": "You have buyers to sell these calls to."
},
{
"key": "target",
"label": "Add a target",
"done": true,
"blocking": false,
"optional": false,
"to": "targets",
"sentence": "At least one destination is set up."
},
{
"key": "routing",
"label": "Build the routing plan",
"done": true,
"blocking": true,
"optional": false,
"to": "campaigns/6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d?tab=routing",
"sentence": "Three active routes point at active targets."
},
{
"key": "number",
"label": "Point a number at it",
"done": true,
"blocking": true,
"optional": false,
"to": "numbers",
"sentence": "Four tracking numbers send their calls here."
},
{
"key": "publishers",
"label": "Invite publishers",
"done": true,
"blocking": false,
"optional": true,
"to": "publishers",
"sentence": "Two publishers have a lane on this campaign."
},
{
"key": "funded",
"label": "Fund the wallet",
"done": true,
"blocking": true,
"optional": false,
"to": "wallet",
"sentence": "The workspace wallet can pay for connected minutes."
},
{
"key": "live",
"label": "Set it live",
"done": true,
"blocking": false,
"optional": false,
"to": "campaigns/6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
"sentence": "The campaign is live and taking calls."
}
]
}
},
"changed": true
}Errors
| Status | Code | When |
|---|---|---|
| 400 | validation_error | status is missing or is not one of the four. |
| 402 | plan_upgrade_required | Restoring an archived campaign to draft would take the workspace past the number of campaigns its plan allows. |
| 409 | conflict | The state machine refused the move. details carries what to act on: blockers when a campaign is not ready to go live, numbers when numbers still point at a campaign you are archiving. A live campaign must be paused before it is archived, an archived campaign can only go back to draft, and a restricted campaign can only be moved by staff. |
| 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. |
