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

EndpointSurfaceCredential
GET /api/v1/campaignsREST APIAPI key with campaigns:read
GET /api/v1/campaigns/:idREST APIAPI key with campaigns:read
POST /api/v1/campaigns/:id/statusREST APIAPI key with campaigns:write
POST /api/tracking/campaignsConsoleSession token
PATCH /api/tracking/campaigns/:idConsoleSession token
POST /api/tracking/campaigns/:id/statusConsoleSession token
The routing plan, the tag filters and the call simulator are in Routing plans. A campaign's publisher lanes are in Publishers.

The campaign object

FieldTypeMeaning
iduuidThe campaign's id, used in every path below.
kindstringAlways tracking here. A campaign of any other kind answers 404 on these routes.
name, descriptionstring, string or nullWhat you call it, and a line about it for your team.
statusstringdraft, live, paused, restricted or archived. A restricted campaign has been stopped by platform staff and only they can move it.
statusNotestring or nullThe note sent with the last status change. It is cleared when a campaign goes live.
verticalId, verticalstring, object or nullWhat the campaign sells. Fixed when the campaign is created.
routingModestringpriority, weighted or highest_bid.
recordCallsbooleanWhether calls are recorded.
repeatCallerPolicystringallow, block or same_target.
anonymousPolicystringallow or block.
unroutedActionstringhangup, message or forward, with unroutedMessage or unroutedForwardE164 beside it.
bufferSecondsintegerSeconds held back before settlement.
dialTimeoutSecondsintegerHow long one target may ring.
failoverBudgetSecondsintegerHow long the whole waterfall may take.
dedupeWindowSecondsintegerHow long a caller counts as a repeat caller.
dailyCap, concurrencyCapintegerCampaign-wide caps. 0 is unlimited.
hoursSchedule, timezoneobject or null, stringWhen the campaign is open. null is always open.
statesarray of stringsTwo-letter state codes accepted. Empty accepts every state.
routingKeyPrefixstringThe prefix the platform mints for this campaign's routing keys. Read-only.
publishedAttimestamp or nullWhen the campaign first went live.
countsobject{ numbers, routes, activeRoutes, publishers } — numbers that are not released, and lanes that are active.
todayobjectThe day's counts and money, on the workspace's calendar.
liveCallsintegerCalls on this campaign right now.
blockers, canGoLive, checklistarray, boolean, objectOnly on a single-campaign read and after a write. See The checklist.
createdAt, updatedAttimestampWhen 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".

  • blockers is 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.
  • canGoLive is true when blockers is empty.
  • checklist is the same ground covered as steps: each { key, label, done, blocking, optional, to, sentence }. A step with blocking: true stops a launch; an optional step is advice. to is a path inside the console, relative to its own base.

Statuses, and who may change them

StatusWhat it meansWho can move it
draftBuilt but never live. Numbers can point at it; calls are not routed.Console only — launching is a person's act.
liveTaking calls.A key may pause it. The console may do anything.
pausedNumbers still point at it; callers meet unroutedAction.A key may resume it. A resume still has to pass the go-live checks.
archivedOut 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.
restrictedStopped by platform staff.Staff only. Every other caller is refused.

Endpoints

List campaigns

GET/api/v1/campaigns

Every 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.

API key. Send Authorization: Bearer <api key>. See Authentication.Scopecampaigns:read

Query parameters

NameTypeDescription
qOptionalstringMatches name, case-insensitively, anywhere in the value.up to 120 characters
statusOptionalenumOne status.draft, live, paused, restricted, archived · default: every status except archived
routingModeOptionalenumOnly campaigns routing this way.priority, weighted, highest_bid
verticalIdOptionalstringOnly campaigns in this vertical.up to 80 characters
pageOptionalintegerPage number, from 1.default 1
pageSizeOptionalintegerRows per page.default 25 · at most 200; a larger value is clamped rather than refused
sortOptionalenumThe field to sort by. An unknown value falls back to the default rather than failing.createdAt, updatedAt, name, status · default createdAt
dirOptionalenumSort 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

{
  "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

StatusCodeWhen
400validation_errorA filter has the wrong type, or status or routingMode is not one of the listed values. details names the field.
401missing_key, invalid_keyNo bearer key was sent, or the key is malformed, unknown, or revoked with its grace period over. See Authentication.
402subscription_inactive, plan_upgrade_requiredThe workspace's subscription is not active, or its plan does not include call tracking and API access.
403insufficient_scope, ip_not_allowedThe key does not hold campaigns:read, or the request came from an address outside the key's allowlist.
429rate_limitedThe key is over its per-minute limit. Wait for Retry-After seconds.

Read a campaign

GET/api/v1/campaigns/:id

One 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.

API key. Send Authorization: Bearer <api key>. See Authentication.Scopecampaigns:read

Path parameters

NameTypeDescription
idRequireduuidThe 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. blockers holds 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

StatusCodeWhen
404not_foundNo 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.
401missing_key, invalid_keyNo bearer key was sent, or the key is malformed, unknown, or revoked with its grace period over. See Authentication.
402subscription_inactive, plan_upgrade_requiredThe workspace's subscription is not active, or its plan does not include call tracking and API access.
403insufficient_scope, ip_not_allowedThe key does not hold campaigns:read, or the request came from an address outside the key's allowlist.
429rate_limitedThe key is over its per-minute limit. Wait for Retry-After seconds.

Pause or resume a campaign

POST/api/v1/campaigns/:id/status

Switch 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.

API key. Send Authorization: Bearer <api key>. See Authentication.Scopecampaigns:write

Path parameters

NameTypeDescription
idRequireduuidThe campaign's id, as returned by List campaigns.

Request body

NameTypeDescription
statusRequiredenumThe status to move to.live or paused
noteOptionalstringWhy, 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 changedfalse when 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

StatusCodeWhen
400validation_errorstatus is missing, or is not live or paused.
404not_foundNo 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.
409conflictThe 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.
409conflictA resume was refused because the campaign is not ready. details.blockers is the same list Read a campaign returns.
401missing_key, invalid_keyNo bearer key was sent, or the key is malformed, unknown, or revoked with its grace period over. See Authentication.
402subscription_inactive, plan_upgrade_requiredThe workspace's subscription is not active, or its plan does not include call tracking and API access.
403insufficient_scope, ip_not_allowedThe key does not hold campaigns:write, or the request came from an address outside the key's allowlist.
429rate_limitedThe key is over its per-minute limit. Wait for Retry-After seconds.

Console API · Create a campaign

POST/api/tracking/campaigns

Console 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.

Console session. Called by the console with the signed-in user's session. Use the REST API with an API key for your own integrations.

Query parameters

NameTypeDescription
agencyIdOptionaluuidThe workspace to act in. Only needed when you hold an owner or admin seat on more than one.

Request body

NameTypeDescription
nameRequiredstringWhat you call the campaign.1 to 120 characters
verticalIdRequiredstringWhat 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
descriptionOptionalstringA line for your team about what this campaign is.up to 300 characters
routingModeOptionalenumHow the routing plan chooses between targets.priority, weighted, highest_bid
recordCallsOptionalbooleanWhether calls on this campaign are recorded. See Recordings for what that means for consent and retention.
repeatCallerPolicyOptionalenumWhat 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
anonymousPolicyOptionalenumWhether a caller who hides their number is accepted. block ends the call with the reason anonymous_blocked.allow, block
unroutedActionOptionalenumWhat 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
unroutedMessageOptionalstringThe sentence read out to the caller. Required when unroutedAction is message.up to 500 characters
unroutedForwardE164OptionalstringThe 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.
bufferSecondsOptionalintegerSeconds held back before a call is settled, so a publisher's post or a buyer's postback has time to arrive.0 to 3600
dialTimeoutSecondsOptionalintegerHow long one target may ring before the plan moves on.5 to 120
failoverBudgetSecondsOptionalintegerHow long the whole waterfall may take, across every attempt. It must be at least dialTimeoutSeconds.10 to 180
dedupeWindowSecondsOptionalintegerHow 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
dailyCapOptionalintegerCalls this campaign may take in one UTC day. Past it, callers end with the reason campaign_capped.0 to 1000000 · 0 is unlimited
concurrencyCapOptionalintegerHow many calls this campaign may have live at once.0 to 100000 · 0 is unlimited
hoursScheduleOptionalobject or nullWhen 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
timezoneOptionalstringAn IANA zone. It is folded into hoursSchedule.tz, so on a campaign with no schedule it has no effect.up to 64 characters
statesOptionalarray of stringsTwo-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

StatusCodeWhen
400validation_errorA field has the wrong type or is out of range. details names the field.
400bad_requestA 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.
402plan_upgrade_requiredThe workspace already holds as many campaigns as its plan allows. details.feature is maxTrackingCampaigns.
401unauthorizedNo session token, or one that has expired.
402subscription_inactiveThe workspace's subscription is not active, and this is a write. details carries status, planId and agencyId.
402plan_upgrade_requiredThe workspace's plan does not include call tracking. details.feature is callTracking.
403forbiddenYou named no workspace and hold no owner or admin seat on any.
404not_foundNo tracking campaign with that id in this workspace, or you named a workspace with ?agencyId= that is not yours.

Console API · Update a campaign

PATCH/api/tracking/campaigns/:id

Console 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.

Console session. Called by the console with the signed-in user's session. Use the REST API with an API key for your own integrations.

Path parameters

NameTypeDescription
idRequireduuidThe campaign's id, as returned by List campaigns.

Query parameters

NameTypeDescription
agencyIdOptionaluuidThe workspace to act in.

Request body

NameTypeDescription
nameOptionalstringA new name.1 to 120 characters
descriptionOptionalstringA line for your team about what this campaign is.up to 300 characters
routingModeOptionalenumHow the routing plan chooses between targets.priority, weighted, highest_bid
recordCallsOptionalbooleanWhether calls on this campaign are recorded. See Recordings for what that means for consent and retention.
repeatCallerPolicyOptionalenumWhat 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
anonymousPolicyOptionalenumWhether a caller who hides their number is accepted. block ends the call with the reason anonymous_blocked.allow, block
unroutedActionOptionalenumWhat 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
unroutedMessageOptionalstringThe sentence read out to the caller. Required when unroutedAction is message.up to 500 characters
unroutedForwardE164OptionalstringThe 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.
bufferSecondsOptionalintegerSeconds held back before a call is settled, so a publisher's post or a buyer's postback has time to arrive.0 to 3600
dialTimeoutSecondsOptionalintegerHow long one target may ring before the plan moves on.5 to 120
failoverBudgetSecondsOptionalintegerHow long the whole waterfall may take, across every attempt. It must be at least dialTimeoutSeconds.10 to 180
dedupeWindowSecondsOptionalintegerHow 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
dailyCapOptionalintegerCalls this campaign may take in one UTC day. Past it, callers end with the reason campaign_capped.0 to 1000000 · 0 is unlimited
concurrencyCapOptionalintegerHow many calls this campaign may have live at once.0 to 100000 · 0 is unlimited
hoursScheduleOptionalobject or nullWhen 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
timezoneOptionalstringAn IANA zone. It is folded into hoursSchedule.tz, so on a campaign with no schedule it has no effect.up to 64 characters
statesOptionalarray of stringsTwo-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

StatusCodeWhen
400validation_error, bad_requestAs for create, plus bad_request with "Nothing to update" when the body carried no change.
409conflictThe campaign is archived. Restore it to draft first.
401unauthorizedNo session token, or one that has expired.
402subscription_inactiveThe workspace's subscription is not active, and this is a write. details carries status, planId and agencyId.
402plan_upgrade_requiredThe workspace's plan does not include call tracking. details.feature is callTracking.
403forbiddenYou named no workspace and hold no owner or admin seat on any.
404not_foundNo 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

POST/api/tracking/campaigns/:id/status

Console 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.

Console session. Called by the console with the signed-in user's session. Use the REST API with an API key for your own integrations.

Path parameters

NameTypeDescription
idRequireduuidThe campaign's id, as returned by List campaigns.

Query parameters

NameTypeDescription
agencyIdOptionaluuidThe workspace to act in.

Request body

NameTypeDescription
statusRequiredenumThe status to move to.draft, live, paused, archived
noteOptionalstringWhy. 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

StatusCodeWhen
400validation_errorstatus is missing or is not one of the four.
402plan_upgrade_requiredRestoring an archived campaign to draft would take the workspace past the number of campaigns its plan allows.
409conflictThe 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.
401unauthorizedNo session token, or one that has expired.
402subscription_inactiveThe workspace's subscription is not active, and this is a write. details carries status, planId and agencyId.
402plan_upgrade_requiredThe workspace's plan does not include call tracking. details.feature is callTracking.
403forbiddenYou named no workspace and hold no owner or admin seat on any.
404not_foundNo tracking campaign with that id in this workspace, or you named a workspace with ?agencyId= that is not yours.