Routing plans

Build a campaign's routing plan: choose between priority, weighted and highest-bid ordering, understand how failover walks the plan inside its budget, decide what an unrouted caller gets and read the plan's live indicators.

After this guide you will be able to put targets on a campaign in the order you mean, predict which one a given call reaches first, know exactly how long a caller can be kept waiting while targets are tried, and choose what happens when nobody answers.

A routing plan exists because selling a call is rarely one decision. You have a buyer you prefer, a second who takes what the first cannot, perhaps a few who bid in real time, and an office line for whatever is left. The plan is that list, the campaign's routing mode is how the list is put in order for each call, and failover is what moves a call down it.

What a plan is made of

A plan is a list of routes. Each route attaches one target to the campaign and says how that target is treated here. The target itself — its destination, price, hours and caps — lives under Targets and can be on many campaigns' plans, with different route settings on each.

FieldRangeMeaning
targetIdone of your targetsRequired. Not archived, and each target appears once in a plan.
priority0–1000Lower is tried first. Routes with the same priority form a tier. Left out, it takes the route's position in the list, starting at 1.
weight1–10 000The route's share of first place among the routes it is drawn against. Default 1.
activetrue or falseSwitch a route off without losing its settings. An inactive route is not judged at all: it never appears as skipped. Default true.
revenueOverrideCents0–100 000 000 or nullWhat a converted call is worth on this campaign, when that differs from the target's own revenue. null uses the target's. Ignored for a target that bids.
durationThresholdSeconds0–7200 or nullThe talk time at which a call converts on this campaign. null uses the target's own.
maxBidCents0–100 000 000 or nullA ceiling on a bidding target's price for this campaign. See External RTB targets for where it applies.
A plan holds at most 100 routes. The response adds effectiveRevenueCents and effectiveDurationThresholdSeconds: the override if there is one, otherwise the target's own figure.

How a call walks the plan

  1. 01

    Judge

    every active route, for this caller, now

  2. 02

    Ping

    bidding targets, in parallel, one budget

  3. 03

    Order

    by the campaign's routing mode

  4. 04

    Reserve and dial

    a slot on the target, its buyer and its line

  5. 05

    Fail over

    release, move down, inside the budget

  6. 06

    Unrouted action

    when the plan or the budget is spent

The order is fixed when the call arrives and stored on the call. Editing the plan never re-routes a call that is already ringing.

The plan is only reached once the call has passed the campaign's gates — plan active, wallet funded, campaign live, caller allowed, inside hours, under the campaign's caps. Those are covered in Campaigns overview. A call that fails a gate never looks at the plan.

How a target is judged

Every active route is tested against this caller at this second. The first test a target fails is its skip reason; the call carries on with the rest. A skip is not an error — it is the plan doing its job — and each one is written into the call's waterfall with a sentence that names the target, the cap and the figure.

Tested, in this orderSkip reason
The target is activepaused
Its buyer is activebuyer_paused
It has somewhere to send the call: a phone number or SIP address, or a bid URL for a bidding targetno_destination
It has not been switched off after repeated failuressuppressed
The call is inside the target's hoursclosed
The caller's state is one the target accepts. A caller whose state is unknown is not skippedstate
The target has a free concurrent slotconcurrency
Its buyer has a free concurrent slotbuyer_capped
The target's hourly, daily, monthly and lifetime caps have roomcapped
The buyer's hourly, daily, monthly and daily revenue caps have roombuyer_capped
The target's daily revenue cap has roomspend_capped
This caller has not already reached the target inside its duplicate windowduplicate
The call's tags pass the campaign's filterstag_filter
For a bidding target: the buyer answered the ping with a usable bidno_bid, timeout, error
The full sentences are in Reason codes.

Nothing is reserved at this stage. A cap read here is a judgement — "this target is full, do not bother" — and the guarantee is made later, at the dial. Two calls arriving together can both be told a target has room; exactly one gets the last slot, and the other sees concurrency against that target and moves down its plan.

The three routing modes

ModeHow the eligible routes are orderedUse it when
priorityLowest priority first. Inside a tier, the order is a weighted draw.You have a preferred buyer and others who catch what they cannot take.
weightedOne weighted draw across every eligible route. priority is ignored.You are splitting traffic on purpose: a test, or a contractual share.
highest_bidHighest price first, then lowest priority, then highest weight.Targets bid in real time, or fixed prices differ enough that price should decide.
The mode is a campaign setting, routingMode. It is saved together with the routes, because it decides what their order means.

A weighted draw works like this: one route is drawn for first place with a probability of its weight over the total, it is taken out, and the draw is repeated for second place, and so on. A route with twice the weight is twice as likely to be first — and, having lost the first draw, twice as likely to be second. The draw is random for every call, so the proportions hold over many calls, not inside any particular four.

Example 1: a priority ladder with a weighted tier

Acme Health is your main buyer and BlueSky Legal has asked for a quarter of the calls. Acme's overflow line takes anything neither of them answers, at a lower price on this campaign.

The plan
{
  "mode": "priority",
  "routes": [
    {
      "targetId": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
      "priority": 1,
      "weight": 3
    },
    {
      "targetId": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f",
      "priority": 1,
      "weight": 1
    },
    {
      "targetId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
      "priority": 2,
      "weight": 1,
      "revenueOverrideCents": 2500
    }
  ]
}
  • Tier 1 holds two routes with weights 3 and 1. When both are eligible, Acme Health — Dallas floor is first in line for about three calls in four and BlueSky Legal — Intake for about one in four.
  • Whichever of them was not drawn first is second. A call that Dallas does not answer fails over to BlueSky before it ever reaches tier 2, and the other way round.
  • Acme Health — Overflow is always last, and a call that converts there books 2500 cents, not the target's own revenue, because of the override.
  • If Dallas reaches its daily cap at three in the afternoon, it is skipped with capped for the rest of the window and BlueSky is first for every tier-1 call. Weights share out first place among the routes that are eligible, not among the routes on the page.

Example 2: a pure weighted split

You are testing BlueSky against Acme and want 60 per cent of calls to try Acme first, whatever order the routes are listed in.

The plan
{
  "mode": "weighted",
  "routes": [
    {
      "targetId": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
      "weight": 60
    },
    {
      "targetId": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f",
      "weight": 40
    }
  ]
}
  • The routes get priorities 1 and 2 from their position, and in weighted mode those are ignored: both routes go into one draw.
  • The saved plan reports sharePct on each active route — here 60 and 40. It is the route's weight over the total weight of the active routes, to one decimal place, and it is only reported in weighted mode. In the other two modes weight only settles order inside a tier or breaks a tie, and a percentage would claim a split that does not happen.
  • A failed dial still fails over. The 40 per cent of calls that try BlueSky first go to Acme next if BlueSky does not answer.

Example 3: highest bid with a ring tree

A ring tree is what a highest-bid plan becomes for one call: the buyers who want it, rung in price order until one answers. Here two targets bid in real time, one pays a fixed price, and the overflow line earns nothing on this campaign.

The plan
{
  "mode": "highest_bid",
  "routes": [
    {
      "targetId": "d3e4f5a6-b7c8-4d9e-8f1a-2b3c4d5e6f7a",
      "priority": 1
    },
    {
      "targetId": "e4f5a6b7-c8d9-4e0f-9a2b-3c4d5e6f7a8b",
      "priority": 1
    },
    {
      "targetId": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
      "priority": 2
    },
    {
      "targetId": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
      "priority": 3,
      "revenueOverrideCents": 0
    }
  ]
}

A call arrives from a Texas caller. Both bidding targets pass every test above, so both are pinged at the same moment, under one time budget:

TargetWhat happenedPrice for this call
BlueSky Legal — RTBAnswered the ping in 310 ms with a bid of $41.00 and a phone number to send the call to.4100
Acme Health — RTBDid not answer inside its ping time-out. Skipped with timeout: "The bidding endpoint of the buyer Acme Health did not answer within 1,500 ms."
Acme Health — Dallas floorFixed price. It "bids" its revenue.4200
Acme Health — OverflowFixed price, overridden to nothing on this campaign.0
  1. The order is by price: Dallas floor (4200), then BlueSky RTB (4100), then Overflow (0). A fixed price can outrank a live bid — price decides, not how the price was arrived at.
  2. Dallas is dialled first. It rings out. Its slot is released and the call fails over.
  3. BlueSky RTB is dialled on the number its bid named. It answers.
  4. The call now carries BlueSky's price and BlueSky's duration threshold — the one its bid stated, or the target's own if the bid stated none. If the caller talks past that threshold, the call converts for 4100.
  • Two equal prices are ordered by priority, lowest first, then by weight, highest first. That is what the priorities in the plan above are for.
  • A bid below the target's minimum bid is treated as no bid at all: the target is skipped with no_bid.
  • A price changed by a tag filter is the price the plan orders on.
  • In highest_bid mode a simulation does not ping anybody, so bidding targets show wouldPing: true, have no price and sort last.

Failover

Failover exists because a plan that stops at the first unanswered phone wastes the call. The router treats every route below the first as a fallback, and moves down for any of these reasons:

Result of the attemptWhat it means
no_answerThe target rang until its ring time-out, or the buyer's side gave up. SIP 408, 480 or 487.
busyThe line was busy. SIP 486 or 600.
rejectedThe buyer declined the call. SIP 403 or 603.
failedThe destination could not be reached or is not a working number, or the carrier would not place the call.
Skipped at the dialThe target, its buyer or its line lost its last free slot to another call between the plan being drawn and the dial. Shown as concurrency or buyer_capped. It does not count as an attempt.
The SIP answer is kept beside the outcome when the carrier gave one. SIP codes says what each means.
  1. 1

    Capacity is reserved before the phone rings

    A slot is taken on the target, then on its buyer, then on the target's line. Each is a single conditional update — "add one, if there is room" — so two calls cannot both take the last slot. It is all three or none: if the buyer is full, the target's slot is handed straight back.

  2. 2

    The terms are frozen and the target is dialled

    The target, buyer, price and duration threshold are written onto the call, a call.routed event is sent, and the call is transferred. A phone number or SIP target rings for its own ring time-out (ringTimeoutSeconds, 5–120, 20 by default). A destination that arrived in a bid rings for the campaign's dialTimeoutSeconds (5–120, 18 by default).

  3. 3

    No answer: release, record, move on

    The slots are released — exactly once, however many carrier events report the same failure — the result is written to the waterfall with the SIP answer, a call.failover event is sent naming the next target, and the next route is reserved and dialled.

  4. 4

    An answer ends the walk

    The call is connected from the moment the buyer's leg answers. Caps that count on connected calls move now, and call.connected is sent.

If the caller hangs up while a target is ringing, the walk stops, the slots are released and the waterfall says which target was ringing when they left.

The failover budget

A long plan could keep a caller listening to other people's phones ring for minutes. The failover budget is your promise to the caller about how long that can go on. It is the campaign setting failoverBudgetSeconds: 10 to 180 seconds, 45 by default, and never shorter than dialTimeoutSeconds.

  • The clock starts when the call arrives. For a call a publisher won at auction, it starts when their caller actually arrives, not when the bid was claimed.
  • The budget is never a reason not to ring the first target. It is only checked before the second attempt and each one after.
  • It is checked before a dial, not during one. A ring that has started is allowed to finish. So the longest a caller can wait is roughly the budget plus one ring time-out.
  • A route skipped at the dial costs no time and does not use up the budget.
TimeWith a 45-second budget and 20-second ring time-outs
0 sAttempt 1 is dialled. The budget is not consulted for a first attempt.
20 sAttempt 1 rings out. 20 is less than 45, so attempt 2 is dialled.
40 sAttempt 2 rings out. 40 is less than 45, so attempt 3 is dialled.
60 sAttempt 3 rings out. 60 is past the budget: no fourth attempt, even if the plan has more routes. The unrouted action runs.
To guarantee two fallbacks, make the budget longer than two ring time-outs. To protect callers on a long plan, make it shorter.

When every target fails: the unrouted action

A call that reaches no buyer ends with an unrouted reason. Three of them come from the plan:

ReasonWhen
no_targetsThe plan has no active routes.
no_eligible_targetEvery route was skipped, at the judging stage or at the dial. The waterfall lists the reason for each.
all_targets_failedAt least one target was dialled and none answered, or the failover budget ran out.

What the caller gets then is the campaign's unrouted action, unroutedAction:

ActionWhat happensNeeds
hangupThe call ends. This is the default.
messageThe call is answered, your message is read out by a text-to-speech voice in US English, and the call ends.unroutedMessage, up to 500 characters
forwardOne last transfer, to a number of yours. It is an attempt like any other in the waterfall, but it has no target, no buyer and no revenue: the call is not sold. If the forward number does not answer either, the call ends.unroutedForwardE164, in international format
Saving message without a message, or forward without a number, is refused with a 400.
  • The action applies to five reasons: campaign_closed, campaign_capped, no_targets, no_eligible_target and all_targets_failed. Those are the ones where "sorry, nobody is available" or an office line is a sensible answer.
  • It does not apply to the other six. A blocked, anonymous or repeat caller is not forwarded to your office — that is what refusing them was for — and a workspace that is lapsed or unfunded, or a campaign that is not live, does not answer the call at all. Those callers get a busy signal.
  • A call answered on the forward number still counts as unrouted, not connected, in every report. A fallback answer is not a buyer connection.
  • Every unrouted call sends a call.unrouted event carrying the reason, its sentence and the action that was taken.

Reading the plan's live indicators

The campaign's Routing plan tab and GET /campaigns/{id}/routing show the plan as it stands now, with enough on each route to tell whether it would take a call this minute.

IndicatorWhereHow to read it
activethe routefalse: the route is ignored. It is not judged and never shows as skipped.
target.statusthe routeAnything but active and every call skips it with paused.
target.buyer.statusthe routeA paused buyer takes all of its targets out of every plan at once, with buyer_paused. Nothing on the targets themselves changes.
target.healththe routehealthy, degraded, down or unknown. For a bidding target, down means its circuit breaker has tripped and calls skip it with suppressed until it is retried. degraded is a warning only: the target is still used. For a SIP target it reflects the last reachability probe and never takes the target out of a plan by itself.
sharePctthe routeweighted mode only: this route's share of first attempts among active routes. null in the other modes and on inactive routes.
effectiveRevenueCentsthe routeWhat a converted call books on this route. null for a bidding target, where the bid decides.
usage against the capsthe targetliveCalls, hour, day, month, total and dayRevenueCents, to read beside concurrencyCap, hourlyCap and the rest. A count at its cap means the target is being skipped. Windows are UTC.
suppressedUntil, consecutiveFailures, lastErrorthe targetWhy a target is degraded or down, and when it will be tried again.
counts.activeRoutes, liveCalls, todaythe campaignHow many routes are switched on, how many calls are in progress, and today's calls, connected, converted, revenue and payout on the workspace's calendar day.
checklist.warningsthe campaignSays out loud when a plan would launch and route nothing — for example when every active target belongs to a buyer that is not active.
The Overview page raises the same conditions as alerts: a campaign with no routes, a target that is down, degraded or capped, a buyer that is paused or capped.

Save a plan with the API

A plan is always replaced as a whole, in one transaction. That is deliberate: a plan is an ordering, and an ordering edited one row at a time passes through states nobody chose. Send every route you want to keep; a route you leave out is removed. The body is compared with what is stored, so an unchanged route keeps its id, and sending the same plan twice changes nothing.

An API key with the campaigns:write scope can read and replace a plan. It can re-rank, re-weight, switch a route off or drop it, but every target it names must already be one of your own: a key cannot make calls go anywhere they could not already go. See API keys for creating one.

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": "weighted",
  "routes": [
    {
      "targetId": "a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d",
      "weight": 60
    },
    {
      "targetId": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f",
      "weight": 40
    }
  ]
}'
200 — the saved plan
{
  "mode": "weighted",
  "failoverBudgetSeconds": 45,
  "dialTimeoutSeconds": 18,
  "routes": [
    {
      "id": "0f7a3c52-6d1e-4b8a-9c2f-5e4d3c2b1a09",
      "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": 60,
      "active": true,
      "revenueOverrideCents": null,
      "maxBidCents": null,
      "durationThresholdSeconds": null,
      "effectiveRevenueCents": 4200,
      "effectiveDurationThresholdSeconds": 90,
      "sharePct": 60,
      "createdAt": "2026-09-18T15:02:11.000Z",
      "updatedAt": "2026-09-20T09:41:30.000Z"
    },
    {
      "id": "1a8b4d63-7e2f-4c9b-8d3a-6f5e4d3c2b10",
      "targetId": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f",
      "target": {
        "id": "c2d3e4f5-a6b7-4c8d-9e0f-1a2b3c4d5e6f",
        "name": "BlueSky Legal — Intake",
        "kind": "static",
        "type": "number",
        "status": "active",
        "health": "unknown",
        "revenueCents": 3800,
        "durationThresholdSeconds": 60,
        "buyer": {
          "id": "e2d3c4b5-a697-4887-9bac-1d2e3f4a5b6c",
          "name": "BlueSky Legal",
          "status": "active"
        }
      },
      "priority": 2,
      "weight": 40,
      "active": true,
      "revenueOverrideCents": null,
      "maxBidCents": null,
      "durationThresholdSeconds": null,
      "effectiveRevenueCents": 3800,
      "effectiveDurationThresholdSeconds": 60,
      "sharePct": 40,
      "createdAt": "2026-09-20T09:41:30.000Z",
      "updatedAt": "2026-09-20T09:41:30.000Z"
    }
  ],
  "saved": {
    "added": 1,
    "updated": 1,
    "removed": 0,
    "unchanged": 0
  }
}
Read the plan
curl "https://api.buy3.io/api/v1/campaigns/6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d/routing" \
  -H "Authorization: Bearer $BUY3_API_KEY"
StatusCodeWhy
400bad_requestA route failed validation. details is keyed by row, for example routes[2].weight. A target listed twice, an archived target and a target that cannot take a tracking call are all reported this way, against routes[i].targetId.
404not_foundThe campaign, or a targetId, is not in your workspace. A target that belongs to somebody else and one that does not exist get the same answer.
409conflictThe campaign is archived — or it is live and the new plan would leave it with no active route to an active target. Pause the campaign first if you really mean to empty its plan.
Every change to a plan is written to the activity log with the routes before and after. Full request and response details are in the Routing plans API reference.

Things that change the order for one caller

  • A repeat caller under same_target. When the campaign's repeat-caller policy is same_target and the target that took this caller last time is eligible now, it is moved to the front of the plan, whatever the mode says. If it is not eligible, the plan stands as drawn. See Repeat and blocked callers.
  • A call won at auction. When a publisher pings the campaign, wins and claims the bid, the winning target is first and the auction's runners-up follow in the auction's own order, which already followed the campaign's routing mode. A runner-up whose bid has expired by the time it is needed is passed over. See How RTB works.

Next steps