API reference

Routing plans

Read and replace a campaign's routing plan as one document, manage the tag filters that reject or re-price a call, and simulate the waterfall a call would take.

A campaign's routing plan is its ordered list of targets: who is tried, in what order, and on what terms. The API treats the plan as one document — you read it whole and you replace it whole — so a plan is never left half-edited between two requests, and one change is one line in the activity log with the whole plan before and after. The concepts are in Routing plans.

Two surfaces

EndpointSurfaceCredential
GET /api/v1/campaigns/:id/routingREST APIAPI key with campaigns:read
PUT /api/v1/campaigns/:id/routingREST APIAPI key with campaigns:write
GET /api/tracking/campaigns/:id/filtersConsoleSession token
POST /api/tracking/campaigns/:id/filtersConsoleSession token
PATCH /api/tracking/campaigns/:id/filters/:filterIdConsoleSession token
DELETE /api/tracking/campaigns/:id/filters/:filterIdConsoleSession token
POST /api/tracking/campaigns/:id/simulateConsoleSession token
Tag filters are deliberately not on the REST API: a filter can change what a call is worth, and a key that could write one could quietly re-price your whole campaign.

The plan

FieldTypeMeaning
modestringpriority, weighted or highest_bid — see Three modes.
failoverBudgetSecondsintegerHow long the whole waterfall may take, across every attempt. It is the campaign's, shown here because it shapes the plan.
dialTimeoutSecondsintegerHow long one target may ring before the plan moves on.
routesarrayThe routes, ordered by priority ascending, then weight descending, then oldest first.

A route

FieldTypeMeaning
iduuidThe route's own id. It survives a replace that does not change the route.
targetIduuidThe target this route sends calls to. It must belong to your workspace.
targetobject or nullA summary of the target as it stands now: { id, name, kind, type, status, health, revenueCents, durationThresholdSeconds, buyer }. It carries no destination and no credential, on either surface.
priorityintegerLower is tried first. Routes with the same priority are one tier.
weightintegerThe route's share of calls among the routes it is level with. Only read in weighted mode.
activebooleanfalse keeps the route on the plan and out of routing. Use it to park a buyer without losing their place.
revenueOverrideCentsinteger or nullWhat a converted call is worth on this campaign, whatever the target says. null uses the target's own price.
maxBidCentsinteger or nullThe most you will pay a bidding target on this campaign. A bid above it is clamped down; it has no meaning on a fixed-price target.
durationThresholdSecondsinteger or nullThe talk time at which a call converts on this campaign. null uses the target's own.
effectiveRevenueCents, effectiveDurationThresholdSecondsinteger or nullThe override, or the target's own — the figures actually used. effectiveRevenueCents is null for a bidding target, where the bid is the price.
sharePctnumber or nullThe share of calls this route should get, to one decimal place. Only in weighted mode, and only for active routes; null otherwise.
createdAt, updatedAttimestampWhen the route was added and last changed.

Three modes

modeHow the next target is chosen
priorityLowest priority first, then the next, and so on down the plan. A tie is broken by weight and then by age. This is the mode to use when you have a preferred buyer.
weightedWithin a priority tier, calls are shared out in proportion to weight. sharePct on each route tells you what that works out at.
highest_bidBidding targets are asked for a price as the call arrives and the best bid wins, above any floor. Fixed-price targets in the plan are ranked by their revenue.
Whatever the mode, a target that is paused, capped, closed, out of area, at its concurrency limit or inside its duplicate window is skipped, and the waterfall records why.

Replacing the plan

Replace the routing plan takes the whole list of routes and diffs it against what is stored, in one transaction. That has three consequences worth knowing before you write one.

  • A route you leave out is removed. Read the plan, change what you mean to change, and send it all back. There is no endpoint that edits one route.
  • An unchanged route keeps its id and its updatedAt. The diff is by value, so re-sending a plan is not the same as rebuilding it.
  • Sending the same plan twice writes nothing. The answer's saved counts say so — { added: 0, updated: 0, removed: 0, unchanged: n } — and no activity entry is written. That is what makes the endpoint safe to retry after a timeout.

Endpoints

Read the routing plan

GET/api/v1/campaigns/:id/routing

The campaign's plan in full: the mode, the two timing budgets, and every route in the order it will be tried, each with a summary of its target as it stands right now. Read this before writing a plan — the object you get back is the object Replace the routing plan expects, minus the fields it computes for you.

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

Path parameters

NameTypeDescription
idRequireduuidThe campaign's id.

Example request

curl "https://api.buy3.io/api/v1/campaigns/6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d/routing" \
  -H "Authorization: Bearer $BUY3_API_KEY"

Responses

  • 200The plan. An empty routes array means every caller on this campaign meets its unroutedAction.
{
  "mode": "priority",
  "failoverBudgetSeconds": 45,
  "dialTimeoutSeconds": 18,
  "routes": [
    {
      "id": "d0c1b2a3-9e8f-4d7c-b6a5-4e3f2d1c0b9a",
      "targetId": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
      "target": {
        "id": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
        "name": "Acme Health — Dallas floor",
        "kind": "static",
        "type": "number",
        "status": "active",
        "health": "healthy",
        "revenueCents": 4200,
        "durationThresholdSeconds": 90,
        "buyer": {
          "id": "f1e2d3c4-b5a6-4978-8a9b-0c1d2e3f4a5b",
          "name": "Acme Health",
          "status": "active"
        }
      },
      "priority": 1,
      "weight": 1,
      "active": true,
      "revenueOverrideCents": null,
      "maxBidCents": null,
      "durationThresholdSeconds": null,
      "effectiveRevenueCents": 4200,
      "effectiveDurationThresholdSeconds": 90,
      "sharePct": null,
      "createdAt": "2026-07-03T09:00:00.000Z",
      "updatedAt": "2026-09-19T09:22:10.000Z"
    },
    {
      "id": "c9b8a7d6-5e4f-4c3b-a2d1-0f9e8d7c6b5a",
      "targetId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
      "target": {
        "id": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
        "name": "BlueSky Legal — overflow",
        "kind": "static",
        "type": "number",
        "status": "active",
        "health": "healthy",
        "revenueCents": 3200,
        "durationThresholdSeconds": 60,
        "buyer": {
          "id": "e5d4c3b2-a1f0-4e9d-8c7b-6a5f4e3d2c1b",
          "name": "BlueSky Legal",
          "status": "active"
        }
      },
      "priority": 2,
      "weight": 1,
      "active": true,
      "revenueOverrideCents": 3500,
      "maxBidCents": null,
      "durationThresholdSeconds": null,
      "effectiveRevenueCents": 3500,
      "effectiveDurationThresholdSeconds": 60,
      "sharePct": null,
      "createdAt": "2026-07-03T09:00:00.000Z",
      "updatedAt": "2026-09-19T09:22:10.000Z"
    }
  ]
}

Errors

StatusCodeWhen
404not_foundNo tracking campaign with that id in your workspace. A malformed id, an id that does not exist and another workspace's campaign 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.

Replace the routing plan

PUT/api/v1/campaigns/:id/routing

Send the whole plan. It is diffed against what is stored and saved in one transaction, so there is no moment when the campaign has half a plan. Routes you leave out are removed. The response carries the plan as it now stands and a saved block counting what actually changed — the same plan twice answers all zeroes but unchanged, writes nothing and is not recorded, which is what makes a retry safe. A change is recorded in the activity log under the key's name, with the plan before and after.

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

Path parameters

NameTypeDescription
idRequireduuidThe campaign's id.

Request body

NameTypeDescription
modeOptionalenumHow the plan chooses between targets. Leave it out to keep the mode the campaign already has.priority, weighted, highest_bid
routesRequiredarrayThe whole plan, in order. An empty array empties the plan — which is refused on a live campaign.at most 100 routes
routes[].targetIdRequireduuidOne of your own targets. It must not be archived, and each target may appear only once in a plan.
routes[].priorityOptionalintegerLower is tried first.0 to 1000 · default: the route's position in the array, counting from 1
routes[].weightOptionalintegerThe route's share within its priority tier, in weighted mode.1 to 10000 · default 1
routes[].activeOptionalbooleanfalse keeps the route on the plan but out of routing.default true
routes[].revenueOverrideCentsOptionalinteger or nullWhat a converted call is worth on this campaign. null uses the target's own revenue.0 to 100000000
routes[].maxBidCentsOptionalinteger or nullThe most you will pay a bidding target on this campaign.0 to 100000000
routes[].durationThresholdSecondsOptionalinteger or nullThe talk time at which a call converts on this campaign. null uses the target's own.0 to 7200

Example request

curl -X PUT "https://api.buy3.io/api/v1/campaigns/6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d/routing" \
  -H "Authorization: Bearer $BUY3_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "priority",
    "routes": [
      {
        "targetId": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
        "priority": 1
      },
      {
        "targetId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
        "priority": 2,
        "revenueOverrideCents": 3500
      }
    ]
  }'

Responses

  • 200The plan as it now stands, plus saved: how many routes were added, updated, removed and left alone.
  • 200 (a replay)The same plan sent twice. Nothing was written and nothing was recorded.
{
  "mode": "priority",
  "failoverBudgetSeconds": 45,
  "dialTimeoutSeconds": 18,
  "routes": [
    {
      "id": "d0c1b2a3-9e8f-4d7c-b6a5-4e3f2d1c0b9a",
      "targetId": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
      "target": {
        "id": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
        "name": "Acme Health — Dallas floor",
        "kind": "static",
        "type": "number",
        "status": "active",
        "health": "healthy",
        "revenueCents": 4200,
        "durationThresholdSeconds": 90,
        "buyer": {
          "id": "f1e2d3c4-b5a6-4978-8a9b-0c1d2e3f4a5b",
          "name": "Acme Health",
          "status": "active"
        }
      },
      "priority": 1,
      "weight": 1,
      "active": true,
      "revenueOverrideCents": null,
      "maxBidCents": null,
      "durationThresholdSeconds": null,
      "effectiveRevenueCents": 4200,
      "effectiveDurationThresholdSeconds": 90,
      "sharePct": null,
      "createdAt": "2026-07-03T09:00:00.000Z",
      "updatedAt": "2026-09-19T09:22:10.000Z"
    },
    {
      "id": "c9b8a7d6-5e4f-4c3b-a2d1-0f9e8d7c6b5a",
      "targetId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
      "target": {
        "id": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
        "name": "BlueSky Legal — overflow",
        "kind": "static",
        "type": "number",
        "status": "active",
        "health": "healthy",
        "revenueCents": 3200,
        "durationThresholdSeconds": 60,
        "buyer": {
          "id": "e5d4c3b2-a1f0-4e9d-8c7b-6a5f4e3d2c1b",
          "name": "BlueSky Legal",
          "status": "active"
        }
      },
      "priority": 2,
      "weight": 1,
      "active": true,
      "revenueOverrideCents": 3500,
      "maxBidCents": null,
      "durationThresholdSeconds": null,
      "effectiveRevenueCents": 3500,
      "effectiveDurationThresholdSeconds": 60,
      "sharePct": null,
      "createdAt": "2026-07-03T09:00:00.000Z",
      "updatedAt": "2026-09-19T09:22:10.000Z"
    }
  ],
  "saved": {
    "added": 1,
    "updated": 1,
    "removed": 0,
    "unchanged": 0
  }
}

Errors

StatusCodeWhen
400bad_request"Validation failed", with details keyed by row — "routes[2].weight", "routes[0].targetId". A target named twice, an archived target, a target that cannot take a tracking call, or a number out of range each name the exact route that carries it.
400validation_errorroutes is missing, is not an array, or holds more than 100 entries.
404not_foundNo tracking campaign with that id in your workspace. A malformed id, an id that does not exist and another workspace's campaign all answer exactly the same, before any query runs.
404not_foundTarget not found — a targetId that does not exist, or belongs to another workspace. The two are indistinguishable on purpose.
409conflictThe campaign is archived, or the plan you sent would leave a live campaign with no active route to an active target.
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 · List tag filters

GET/api/tracking/campaigns/:id/filters

Console 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. A tag filter reads one tag on the call and does one of three things: require the tag, reject the call, or adjust what the call is worth. Filters are evaluated in position order, lowest first. Each one comes back with a derived effect and a sentence describing it in plain words, so a list can be shown without re-deriving the logic. See Filters and tags.

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.

Query parameters

NameTypeDescription
agencyIdOptionaluuidThe 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/filters" \
  -H "Authorization: Bearer $BUY3_SESSION_TOKEN"

Responses

  • 200Every filter on the campaign, ordered by position and then by age.
{
  "filters": [
    {
      "id": "7e6d5c4b-3a29-4180-9e7f-0a1b2c3d4e5f",
      "campaignId": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
      "position": 10,
      "tagKey": "utm_source",
      "comparison": "equals",
      "value": "search",
      "valueList": null,
      "caseSensitive": false,
      "adjustment": "add",
      "adjustmentCents": 500,
      "adjustmentFactor": null,
      "active": true,
      "effect": "adjust",
      "sentence": "When utm_source equals search, add 500 cents to the call's revenue.",
      "createdAt": "2026-08-04T14:02:00.000Z",
      "updatedAt": "2026-09-01T10:30:00.000Z"
    }
  ]
}

Errors

StatusCodeWhen
401unauthorizedNo session token, or one that has expired.
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 · Add a tag filter

POST/api/tracking/campaigns/:id/filters

Console API. A filter says exactly one of three things, and the combination of comparison and adjustment is what decides which. Require: comparison: "required" with adjustment: "none" — the tag has to be there with that value. Reject: any other comparison with adjustment: "reject" — a call that matches is turned away. Adjust: add, multiply or override — a call that matches is re-priced. required may only pair with none, and none may only pair with required. An operand the comparison does not read is stored as null rather than kept.

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.

Query parameters

NameTypeDescription
agencyIdOptionaluuidThe workspace to act in.

Request body

NameTypeDescription
tagKeyRequiredstringThe tag to read.1 to 64 characters · letters, digits, _, . and -
comparisonRequiredenumHow the tag is tested.equals, not_equals, contains, not_contains, begins_with, not_begins_with, greater_than, less_than, exists, not_exists, regex, not_regex, in_list, not_in_list, required
valueOptionalstringWhat to compare against. Required unless the comparison is in_list, not_in_list, exists or not_exists. A number for greater_than and less_than; a pattern that compiles for regex and not_regex.up to 512 characters
valueListOptionalarray of stringsThe list to test membership against. Required for in_list and not_in_list.at most 500 items · each up to 512 characters
caseSensitiveOptionalbooleanWhether the comparison respects case.
adjustmentOptionalenumWhat happens when the filter matches.reject, none, add, multiply, override · default: none for required, reject for everything else
adjustmentCentsOptionalintegerThe amount for add (which may be negative) or override.−10000000 to 10000000 · non-zero for add · at least 0 for override
adjustmentFactorOptionalnumberThe multiplier for multiply.0 to 100 · must not be 1 for multiply
positionOptionalintegerWhere in the order this filter is evaluated. Lowest first.0 to 999
activeOptionalbooleanfalse keeps the filter without applying it.

Example request

curl -X POST "https://api.buy3.io/api/tracking/campaigns/6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d/filters" \
  -H "Authorization: Bearer $BUY3_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "tagKey": "utm_source",
    "comparison": "equals",
    "value": "search",
    "adjustment": "add",
    "adjustmentCents": 500,
    "position": 10
  }'

Responses

  • 201The filter that was created, with its derived effect and sentence.
{
  "filter": {
    "id": "7e6d5c4b-3a29-4180-9e7f-0a1b2c3d4e5f",
    "campaignId": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
    "position": 10,
    "tagKey": "utm_source",
    "comparison": "equals",
    "value": "search",
    "valueList": null,
    "caseSensitive": false,
    "adjustment": "add",
    "adjustmentCents": 500,
    "adjustmentFactor": null,
    "active": true,
    "effect": "adjust",
    "sentence": "When utm_source equals search, add 500 cents to the call's revenue.",
    "createdAt": "2026-08-04T14:02:00.000Z",
    "updatedAt": "2026-09-01T10:30:00.000Z"
  }
}

Errors

StatusCodeWhen
400validation_errorA field has the wrong type or is out of range, an operand the comparison needs is missing, a regex does not compile, or required was paired with an adjustment other than none. details names the field.
409conflictThe campaign is archived, or it already has 100 active filters. details carries the active count and the limit.
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 · Change a tag filter

PATCH/api/tracking/campaigns/:id/filters/:filterId

Console API. Change any field of the create form; none of them is required. The rules are judged on the filter as it will be, so you can change a comparison and the operand it needs in one request. Re-activating a filter on a campaign that already has its hundred active ones is refused.

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.
filterIdRequireduuidThe filter's id.

Query parameters

NameTypeDescription
agencyIdOptionaluuidThe workspace to act in.

Request body

NameTypeDescription
any create fieldOptionalEvery field of Add a tag filter.at least one field

Example request

curl -X PATCH "https://api.buy3.io/api/tracking/campaigns/6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d/filters/7e6d5c4b-3a29-4180-9e7f-0a1b2c3d4e5f" \
  -H "Authorization: Bearer $BUY3_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "adjustmentCents": 750
  }'

Responses

  • 200The filter as it now stands.
{
  "filter": {
    "id": "7e6d5c4b-3a29-4180-9e7f-0a1b2c3d4e5f",
    "campaignId": "6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d",
    "position": 10,
    "tagKey": "utm_source",
    "comparison": "equals",
    "value": "search",
    "valueList": null,
    "caseSensitive": false,
    "adjustment": "add",
    "adjustmentCents": 750,
    "adjustmentFactor": null,
    "active": true,
    "effect": "adjust",
    "sentence": "When utm_source equals search, add 750 cents to the call's revenue.",
    "createdAt": "2026-08-04T14:02:00.000Z",
    "updatedAt": "2026-09-20T19:12:03.000Z"
  }
}

Errors

StatusCodeWhen
400validation_errorA field has the wrong type, or the filter as it would be does not make sense.
400bad_requestNothing to update — the body carried no change.
404not_foundFilter not found — no filter with that id on this campaign.
409conflictRe-activating the filter would take the campaign past 100 active filters.
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 · Delete a tag filter

DELETE/api/tracking/campaigns/:id/filters/:filterId

Console API. Remove a filter from the campaign. Calls already settled keep the price they were settled at — a filter that added revenue does not un-add it from past calls. To stop a filter without losing it, set active: false instead.

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.
filterIdRequireduuidThe filter's id.

Query parameters

NameTypeDescription
agencyIdOptionaluuidThe workspace to act in.

Example request

curl -X DELETE "https://api.buy3.io/api/tracking/campaigns/6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d/filters/7e6d5c4b-3a29-4180-9e7f-0a1b2c3d4e5f" \
  -H "Authorization: Bearer $BUY3_SESSION_TOKEN"

Responses

  • 200Confirmation and the id that was removed.
{
  "deleted": true,
  "id": "7e6d5c4b-3a29-4180-9e7f-0a1b2c3d4e5f"
}

Errors

StatusCodeWhen
404not_foundFilter not found — no filter with that id on this campaign.
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 · Simulate a call

POST/api/tracking/campaigns/:id/simulate

Console API. The waterfall a call would take, without a call. Nothing is dialled, no buyer is pinged, no counter moves, nothing is written and nothing is recorded. Unlike a real call it evaluates every gate rather than stopping at the first failure, and it resolves the plan anyway — so one request tells you both what would turn the caller away and where the call would have gone. This is the fastest way to answer "why would a Texas caller at 7pm not reach anybody". See Simulate a call.

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.

Query parameters

NameTypeDescription
agencyIdOptionaluuidThe workspace to act in.

Request body

NameTypeDescription
callerIdOptionalstringThe caller's number — E.164, eleven digits or ten. Leave it out to simulate a caller who has hidden their number.up to 32 characters
tagsOptionalobjectThe tags the call would carry, so tag filters and per-target tag rules are exercised. Values are converted to strings.at most 40 keys
publisherIdOptionaluuidSimulate the call as if it came from this publisher. It must be one of yours.
atOptionaldateThe moment to simulate, so you can test hours of operation without waiting for them.default: now

Example request

curl -X POST "https://api.buy3.io/api/tracking/campaigns/6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d/simulate" \
  -H "Authorization: Bearer $BUY3_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "callerId": "+15555550142",
    "tags": {
      "utm_source": "search"
    },
    "at": "2026-09-20T19:15:00Z"
  }'

Responses

  • 200wouldRoute is true only when no gate failed and the plan has an eligible target. gates.failed is the first gate a real call would have ended on; gates.checks lists all eight with their verdicts. skipped has one entry per target passed over, with its skip reason. A bidding target is never actually pinged: it appears in plan with wouldPing: true and revenueCents: null.
{
  "wouldRoute": true,
  "reason": null,
  "firstChoice": {
    "linkId": "d0c1b2a3-9e8f-4d7c-b6a5-4e3f2d1c0b9a",
    "targetId": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
    "targetName": "Acme Health — Dallas floor",
    "targetKind": "static",
    "buyerId": "f1e2d3c4-b5a6-4978-8a9b-0c1d2e3f4a5b",
    "buyerName": "Acme Health",
    "deliveryMode": "pstn_did",
    "destination": "the number ending 0188",
    "priority": 1,
    "weight": 1,
    "revenueCents": 4200,
    "durationThresholdSeconds": 90,
    "source": "static",
    "wouldPing": false
  },
  "mode": "priority",
  "plan": [
    {
      "linkId": "d0c1b2a3-9e8f-4d7c-b6a5-4e3f2d1c0b9a",
      "targetId": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
      "targetName": "Acme Health — Dallas floor",
      "targetKind": "static",
      "buyerId": "f1e2d3c4-b5a6-4978-8a9b-0c1d2e3f4a5b",
      "buyerName": "Acme Health",
      "deliveryMode": "pstn_did",
      "destination": "the number ending 0188",
      "priority": 1,
      "weight": 1,
      "revenueCents": 4200,
      "durationThresholdSeconds": 90,
      "source": "static",
      "wouldPing": false
    }
  ],
  "skipped": [
    {
      "targetId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
      "targetName": "BlueSky Legal — overflow",
      "buyerId": "e5d4c3b2-a1f0-4e9d-8c7b-6a5f4e3d2c1b",
      "buyerName": "BlueSky Legal",
      "reason": "state",
      "sentence": "The caller's state is not one this target accepts."
    }
  ],
  "gates": {
    "passed": [
      "workspace_inactive",
      "workspace_unfunded",
      "campaign_not_live",
      "caller_blocked",
      "anonymous_blocked",
      "campaign_closed",
      "campaign_capped",
      "repeat_blocked"
    ],
    "failed": null,
    "checks": [
      {
        "code": "campaign_closed",
        "label": "Outside campaign hours",
        "passed": true,
        "sentence": null,
        "owner": "campaign"
      },
      {
        "code": "campaign_capped",
        "label": "Campaign cap reached",
        "passed": true,
        "sentence": null,
        "owner": "campaign"
      }
    ]
  },
  "caller": {
    "e164": "+15555550142",
    "state": "TX",
    "anonymous": false
  },
  "tags": {
    "utm_source": "search"
  },
  "publisherId": null,
  "at": "2026-09-20T19:15:00.000Z"
}

Errors

StatusCodeWhen
400bad_requesttags is not an object or holds more than 40 keys. details.tags says so.
404not_foundPublisher not foundpublisherId is not one of yours.
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.