Hours and caps

Set when a campaign and each target take calls, and how many: the schedule shape, concurrency reserved at the dial, the hourly, daily, monthly and lifetime windows that roll in UTC, and how buyer caps sit above target caps.

After this guide you will be able to write a schedule that says what you mean — including a split shift and a night shift that crosses midnight — put caps at the right level, say exactly which call moves which counter, and read a target's usage against its caps without guessing when the count resets.

Hours say *when* calls are taken and caps say *how many*. They exist because a buyer who is sent a call at three in the morning, or a hundred more than they agreed to pay for, stops being a buyer. Both are set at more than one level, and a call has to pass every level that applies to it. When one does not, the call's waterfall says which level, which window and what the limit was.

Where limits live

LevelHoursCapsWhat happens when it stops a call
CampaignYes, on the workspace's clockCalls in progress, calls a dayThe call is not routed: campaign_closed or campaign_capped, and the caller gets the unrouted action.
BuyerConcurrency, hourly, daily, monthly, daily revenueEvery target that buyer owns is skipped with buyer_capped. The call carries on down the plan.
TargetYes, in its own zoneConcurrency, hourly, daily, monthly, lifetime, daily revenueThat target is skipped with closed, concurrency, capped or spend_capped.
Publisher laneYes, in its own zoneConcurrency, hourly, daily, monthly, daily payoutA publisher's ping is refused when the lane is full and it is set to block. A call dialled on one of their numbers is counted on the lane and never refused. See Payout terms.
A phone or SIP target also has an endpoint behind it, and the endpoint can carry hours and a concurrency limit of its own. Both are tested, and the tighter one wins.

Hours of operation

A schedule is a time zone and seven days, each day a list of open windows. No schedule at all means always open — which is why a target nobody has ever opened the setting on is still considered at any hour.

Weekdays 8 till 8, Saturday mornings, closed Sunday
{
  "tz": "America/Chicago",
  "days": {
    "mon": [
      [
        "08:00",
        "20:00"
      ]
    ],
    "tue": [
      [
        "08:00",
        "20:00"
      ]
    ],
    "wed": [
      [
        "08:00",
        "20:00"
      ]
    ],
    "thu": [
      [
        "08:00",
        "20:00"
      ]
    ],
    "fri": [
      [
        "08:00",
        "20:00"
      ]
    ],
    "sat": [
      [
        "09:00",
        "13:00"
      ]
    ],
    "sun": []
  }
}
RuleDetail
The day keysmon, tue, wed, thu, fri, sat, sun. Every day is written out in the stored schedule. An unknown key is a 400.
A window["HH:MM", "HH:MM"] on a 24-hour clock, or { "from": …, "to": … }. The start is included and the end is not, so ["08:00", "20:00"] is open at 19:59 and closed at 20:00.
Windows a dayUp to 6. An empty list closes that day.
Closing at midnight"24:00" is allowed, as a closing time only.
Crossing midnightA window whose end is before its start wraps: ["22:00", "02:00"] is open from ten at night until two the next morning.
Closed all weekRefused. A schedule that closes all seven days is a target nobody can reach — send null instead, which means always open, or pause the target.
The zonetz inside the schedule wins. Without it the row's own timezone is used, then the workspace's, then America/New_York.
An unrecognised zoneTreated as open. A mistyped zone is a configuration error to fix, not a reason to refuse a live caller.
The same shape is used by a campaign, a target, a delivery endpoint and a publisher lane.

Two schedules worth copying

A buyer who takes a lunch break has two windows on the same day. Nothing is open between them, and a call arriving at one o'clock skips that target with closed:

A split shift
{
  "tz": "America/New_York",
  "days": {
    "mon": [
      [
        "09:00",
        "12:30"
      ],
      [
        "13:30",
        "18:00"
      ]
    ],
    "tue": [
      [
        "09:00",
        "12:30"
      ],
      [
        "13:30",
        "18:00"
      ]
    ],
    "wed": [
      [
        "09:00",
        "12:30"
      ],
      [
        "13:30",
        "18:00"
      ]
    ],
    "thu": [
      [
        "09:00",
        "12:30"
      ],
      [
        "13:30",
        "18:00"
      ]
    ],
    "fri": [
      [
        "09:00",
        "12:30"
      ],
      [
        "13:30",
        "17:00"
      ]
    ],
    "sat": [],
    "sun": []
  }
}

A night desk is written on the day the shift starts. Monday's window runs to two on Tuesday morning, and Saturday's stops at midnight rather than running into an unstaffed Sunday:

A night shift
{
  "tz": "America/Los_Angeles",
  "days": {
    "mon": [
      [
        "22:00",
        "02:00"
      ]
    ],
    "tue": [
      [
        "22:00",
        "02:00"
      ]
    ],
    "wed": [
      [
        "22:00",
        "02:00"
      ]
    ],
    "thu": [
      [
        "22:00",
        "02:00"
      ]
    ],
    "fri": [
      [
        "22:00",
        "02:00"
      ]
    ],
    "sat": [
      [
        "22:00",
        "24:00"
      ]
    ],
    "sun": []
  }
}

In the console, a campaign's schedule is under Settings in the When it takes calls card, labelled Hours of operation, and a target's is in the When it is open section of its editor. A target row shows an Open or Closed pill, with the next change on hover; Always open means it has no schedule.

How caps are counted

There are three kinds of limit, and they behave differently on purpose.

KindWhat it countsHow it is enforced
ConcurrencyCalls in progress on this target, buyer or line right now.Reserved, atomically, at the moment of the dial.
Hourly, daily, monthly, lifetimeCalls inside the current window.Counted when the call reaches the moment the target names.
Daily revenueMoney booked against this target or buyer today.Counted at settlement, when there is any money to count.

Concurrency is reserved, not read

Taking a concurrent slot is a single conditional update — *add one, if there is room* — so two calls arriving in the same millisecond cannot both take the last slot. Exactly one wins it; the other sees concurrency against that target and moves down the plan. A slot is taken on the target, then its buyer, then the target's line, and it is all three or none: if the buyer is full, the target's slot is handed straight back.

  • The slot is released when that leg ends, however the call ends, and releasing twice is harmless.
  • A cap of 0 still counts live calls — it simply never refuses one — so the live figure on the console is true either way.
  • The cap you see tested while the plan is judged is a read, not a promise. A target can look free and be full a moment later; the guarantee is made at the dial.

Window caps count once, at the moment you choose

A target's A cap counts a call setting (capCountsOn) decides which moment of a call's life moves its hourly, daily, monthly and lifetime counters. It is asked once per call, and exactly one moment answers yes — so a call is never counted twice, and never by a setting that changed while it was in progress.

SettingConsole labelA call countsChoose it when
claimedEvery dialThe moment the target is dialled, answered or not.The buyer's cap is about how many calls you may send, and a rung phone costs them something.
connectedConnected callsWhen the buyer's leg answers. The default.The cap is about how many calls the buyer can handle.
convertedConverted callsAt settlement, when the call becomes billable.The cap is about how many calls the buyer will pay for.
The same setting governs the buyer's window caps for that call, and the frozen value is read — not the target's live setting — so a change made mid-call cannot double-count it.
  • Money is different. Revenue counts against a daily revenue cap at converted and nowhere else, whatever capCountsOn says, because that is the only moment there is any money.
  • The lifetime cap (totalCap) uses the same moment and never resets. It is the one cap a reset is usually meant for.
  • A call counted against a target is counted against its buyer at the same moment, for the same call.

The campaign's own caps are counted, not reserved

A campaign carries two caps — calls in progress and calls a day — and they are counted at the gate rather than reserved, because a campaign has no slot to take. Two callers landing together can therefore both pass a cap with one place left. That is deliberate: these are your own throttle on your own traffic, while the caps that are a contract with somebody else are reserved.

  • The daily figure counts this campaign's calls since midnight on the workspace's clock, and only those that reached a buyer or are still in progress. A call that rang out and ended unanswered does not use up the day.
  • The concurrency figure counts this campaign's calls that are still live.
  • Either one over its limit ends the call with campaign_capped, and the caller gets the unrouted action.

Windows roll over in UTC

A target's, a buyer's and a lane's window counters keep a count and a stamp saying which window the count belongs to. The hour is the UTC clock hour, the day is the UTC date and the month starts on the 1st, UTC — whatever time zone your workspace, your buyer or their hours are in.

WindowConsole labelResets
HourlyPer hourAt the top of the UTC hour.
DailyPer dayAt midnight UTC.
MonthlyPer monthOn the 1st, UTC.
LifetimeLifetimeNever.
Daily revenueDaily revenueWith the day, at midnight UTC.
The campaign's daily cap is the one exception: it is counted on the workspace's calendar day, not on the UTC day.

Nothing sweeps the counters at midnight. The write rolls the window: the same statement that adds a call either adds to the current count or starts a new count at 1 and moves the stamp. And a read treats a stale window as zero, so a target nobody has called today reads as 0 today without anybody having written that down. Two calls landing either side of midnight both count, and neither erases the other.

The concurrency counters are different: they are not a window, so an hourly job rebuilds them from the calls themselves. A slot that was never given back — a process that died mid-call — is corrected within the hour rather than leaving a target wedged one call under its cap for good.

Buyer caps versus target caps

A buyer's caps hold across every target that buyer owns; a target's hold for that destination alone. They are both tested, and whichever is tighter stops the call. Put a limit on the buyer when it is the company's budget, and on the target when it is one room's phone lines.

CapOn a targetOn a buyer
ConcurrencyYes — skip reason concurrencyYes — skip reason buyer_capped
Hourly, daily, monthlyYes — cappedYes — buyer_capped
LifetimeYes — capped
Daily revenueYes — spend_cappedYes — buyer_capped
A buyer's revenue cap has no code of its own: to the reader it is the buyer being full for the day, which is what buyer_capped says.

Worked example: one buyer, five lines, forty calls a day

Acme Health will take five calls at once, wants no more than forty connected calls a day and stops buying once the day's revenue reaches $1,500. Their Dallas floor is one target of theirs:

The target's limits
{
  "name": "Acme Health — Dallas floor",
  "concurrencyCap": 5,
  "hourlyCap": 0,
  "dailyCap": 40,
  "monthlyCap": 800,
  "totalCap": 0,
  "dailyRevenueCapCents": 150000,
  "capCountsOn": "connected"
}
  1. At 14:02 UTC the target has three calls in progress and 38 connected today. A caller arrives: the target passes every test, a slot is taken (four of five), and the phone rings.
  2. The buyer's side answers. Now the call counts — capCountsOn is connected — so the day goes to 39 and the hour to 8, on the target and on Acme Health together.
  3. The call converts at 90 seconds and books $42.00. The day's revenue goes to $1,470 — still under the cap.
  4. The fortieth connected call of the day takes the day count to 40. From that moment every call skips this target with capped, and the waterfall says so in full: "The target Acme Health — Dallas floor has reached its daily cap of 40 calls." The plan carries on to whoever is next.
  5. A caller two minutes later, with five calls already up, would have seen concurrency instead — a different reason, a different fix.
  6. At midnight UTC nothing happens. The first call of the next UTC day writes 1 against the new day and moves the stamp, and the count reads 1.

While all that is going on, the target's usage block is what the console draws its bars from:

usage, read beside the caps above
{
  "usage": {
    "liveCalls": 3,
    "hour": 7,
    "day": 38,
    "month": 402,
    "total": 5711,
    "dayRevenueCents": 142800
  },
  "liveCalls": 3
}
  • liveCalls against concurrencyCap — three of five.
  • day against dailyCap — 38 of 40, so the bar is red.
  • dayRevenueCents against dailyRevenueCapCents — $1,428 of $1,500.
  • total has no cap here, so it is a count and not a limit.

Clearing a target's counters

Reset caps on a target zeroes its hour, day, month and lifetime call counts and the day's revenue, and starts each window now. It is what you want after a test run has filled a daily cap with calls that were never real.

  • It does not touch liveCalls. Calls that are genuinely in progress stay counted, and a stuck slot is corrected by the hourly rebuild instead.
  • It does not touch the caps themselves, and it does not touch the buyer's counters — a buyer has no reset of its own. If a buyer's day has to be let through, raise the cap.
  • It cannot be undone, so the response and the activity log both record what the counters read before.
curl -X POST "https://api.buy3.io/api/v1/targets/a9b8c7d6-e5f4-4a3b-9c2d-1e0f9a8b7c6d/reset-caps" \
  -H "Authorization: Bearer $BUY3_API_KEY"

An API key with targets:write can reset counters and change a target's caps, hours and states — never where its calls go. The full request and response are in the Targets API reference.

When a cap is not what you wanted

What you seeWhat it usually is
A target skipped with capped all afternoonA daily cap that filled on the UTC day, not yours. Check usage.day against dailyCap.
A target skipped with concurrency while its phones are freeSlots that were taken and never released — or genuinely simultaneous calls. The hourly rebuild corrects the first within the hour.
Every target of one buyer skipped at onceA buyer cap, or a paused buyer. buyer_capped and buyer_paused are different reasons.
Calls ending as campaign_cappedThe campaign's own daily cap or concurrency, counted on the workspace's clock.
A target skipped with closed when somebody is thereThe schedule's zone. tz inside the schedule beats the target's timezone field.
Every one of these is a sentence on the call's waterfall naming the cap and the figure. Simulate a call asks the same questions without waiting for a caller.

Next steps