External RTB targets
Ping a buyer's bidding endpoint before each call and route on the price they answer with: the request you send, every response-mapping field, the presets for the common platforms, the time budget, the circuit breaker and where a floor and a ceiling apply.
After this guide you will be able to connect a buyer who prices each call themselves, map their answer onto the four facts the platform needs, keep a slow buyer from holding up a caller, and read the ping ledger when a buyer insists they bid and you can see they did not.
An external RTB target has no fixed price. When a call arrives, the platform sends the buyer's endpoint a ping describing it; the buyer answers with a bid, the terms it depends on and where to send the call. It exists because some buyers will pay far more for some calls than others, and a single fixed price either loses you the good ones or overpays for the rest.
Everything on this page happens while a caller is on the line. That one fact explains every limit below: a single time budget, no retries, and a buyer who keeps failing being taken out of the running.
The request you send
The bidding sections of the target's form are The ping we send them, How they authenticate us, Reading their answer and What you will accept.
| Field | Console label | Rules |
|---|---|---|
bidUrl | Bid URL | Required, up to 2,000 characters. Public https in production, with no credentials in it. It may contain macros. |
bidMethod | Method | GET, POST, PUT or PATCH. A GET sends no body, so its macros must be in the URL. |
bidContentType | Content type | Whatever their endpoint expects. Defaults to application/json. |
bidHeaders | Headers | A map of name to template. Values may contain macros. Authentication headers are added for you. |
bidBodyTemplate | Body template | Up to 20,000 characters. Left empty, a small flat envelope is sent. |
pingTimeoutMs | Ping timeout | 50–10,000 ms, 1,500 by default. Below 400 ms the platform still waits 400 — a TCP connection to anywhere takes longer than that, and a lower figure would only ever record its own time-out. |
{
"requestId": "{{auction.id}}",
"callerId": "{{caller.e164}}",
"callerHash": "{{caller.hash}}",
"state": "{{caller.state}}",
"zip": "{{caller.zip}}",
"vertical": "{{vertical.id}}",
"campaign": "{{campaign.name}}",
"publisher": "{{publisher.name}}",
"sipOk": true
}How they authenticate us
authType | What is sent |
|---|---|
none | Nothing. Saving this clears any credential already stored. |
basic | Authorization: Basic … from the username and the secret. |
bearer | Authorization: Bearer <secret>. |
api_key_header | The secret in the header you name in Header name. |
hmac_sha256 | x-buy3-timestamp, x-buy3-nonce and x-buy3-signature over the body, signed with their secret — the same scheme buy3 asks publishers for inbound. |
oauth2_client_credentials | A token minted at their Token URL and cached until a minute before it expires, then Authorization: Bearer <token>. |
ip_acl | Nothing. The buyer allows our addresses on their side. |
digest | A digest challenge response. |
«redacted». Responses carry hasAuthSecret and the last four characters only.Minting an OAuth token is a second request to a second URL of the buyer's, and it spends the same budget as the ping it is for — so an OAuth buyer whose token endpoint is slow can time out before their bid endpoint is ever reached.
The response you read
Buyers' platforms do not agree on a response format, so you tell the platform where to look. Each setting is a dot path into the JSON they return — bidAmount, bidTerms.0.callMinDuration, buyers.0.payout. Letters, digits, dots, dashes and underscores; it is not JSONPath, and there are no filters or wildcards.
| Setting | Console label | What it finds |
|---|---|---|
acceptMode | What counts as a bid | http_2xx — any successful status is a bid; truthy — the field at acceptPath must be present and not false, 0 or empty; equals — the field at acceptPath must equal acceptEquals, compared as text. |
bidPath | Price path | Required. Where the amount they will pay sits. A value that is not a positive number is no bid, recorded as no_price_in_response. |
bidUnit | …in | dollars or cents. Read 4.5 as four dollars fifty, or as four and a half cents — getting this wrong is a hundredfold error in your books. |
destinationPath | Destination path | Required. Where they say to send the caller. |
destinationKind | …is | did, sip or either. A value starting sip: or sips: is read as SIP; otherwise ten digits or more become a phone number. |
durationPath | Duration path | Where they state the talk time this bid converts at. Left empty, the target's own threshold applies. |
durationOffsetSeconds | Duration offset | −600 to 600, added to whatever they said — a few seconds either way when their clock and yours disagree. |
rejectReasonPath | Reject reason path | Where they explain a no-bid. Whatever is there is stored on the ping, so your ledger says why they passed. |
A bid that names a price but no usable destination is skipped with no_destination, unless the target also has an endpoint of its own to fall back on. A destination that arrives in the bid is dialled for the campaign's ring time-out rather than the target's, because it is not the address the target was configured with.
{
"bidId": "b-88213",
"bidAmount": 41,
"expireInSeconds": 60,
"phoneNumber": "+18325550144",
"bidTerms": [
{
"code": "min_duration",
"callMinDuration": 75
}
]
}{
"bidAmount": 0,
"rejectReason": "state not purchased"
}With acceptMode: "http_2xx", bidPath: "bidAmount" and bidUnit: "dollars", the first answer is a bid of 4100 cents converting at 75 seconds, to be dialled on +1 832 555 0144. The second is a 200 as well — but bidAmount is 0, which is not a positive number, so it is a no bid, and rejectReason is kept beside it.
Presets
A preset fills the response paths you have left blank and never overwrites something you have typed. Picking one is usually the whole integration: paste the buyer's URL, choose their platform, save.
| Preset | Fills |
|---|---|
| Ringba | acceptMode http_2xx, bidPath bidAmount, bidUnit dollars, destinationKind did, destinationPath phoneNumber — or sipAddress when the kind is sip — durationPath bidTerms.0.callMinDuration, rejectReasonPath rejectReason. |
| Retreaver | acceptMode http_2xx, bidPath retreaver_payout, bidUnit dollars, destinationKind did, destinationPath inbound_number, durationPath retreaver_seconds. |
| TrackDrive | acceptMode http_2xx and bidUnit dollars only. TrackDrive has no standard paths, so you type the price and destination paths yourself. |
| Custom | Nothing. |
Price limits
Three settings sit between what a buyer says and what your books record. They are in What you will accept on the target, and one of them can also be set per campaign on the routing plan.
| Setting | Console label | Where it lives | What it does |
|---|---|---|---|
bidMultiplier | Multiplier | The target | Multiplies every bid as it is read, before anything else. 1 leaves it alone; 0.9 keeps a tenth back. |
minBidCents | Floor | The target | The least you will accept. A campaign can carry a floor too, and the higher of the two applies. |
maxBidCents | Ceiling | The target, the route or the campaign | The most you will pay a buyer. The tightest of the three that is set applies. |
Where they take effect depends on how the call reached the campaign, and the two doors are not the same:
| A call dialled on your number | A publisher's ping that wins | |
|---|---|---|
| The multiplier | Applied as the bid is read. | Applied as the bid is read. |
| Tag filters | Applied to the bid. | Applied to the bid. |
| The floor | A bid under it is treated as no bid: the target is skipped with no_bid, and the ledger records "bid of 22.00 is under the minimum of 25.00". | The bid is raised to the floor. |
| The ceiling | Not applied by the router. | The bid is cut to the tightest ceiling. |
The price the plan orders on, and the price a converted call books, is the figure after the multiplier, the filters and whichever limits applied. In highest_bid mode a multiplier can therefore change who is dialled first.
The ping round
Every bidding target still standing after the plan's other tests is pinged at the same moment, under one budget, so a slow buyer cannot hold up a fast one.
- The round has one budget, 2,000 ms unless the campaign carries a budget of its own. A single timer fires at it however many buyers are still outstanding, and whatever has answered by then is what the plan gets.
- Each target also has its own Ping timeout, and whichever fires first wins. So the slowest buyer on a campaign sets the caller's wait, up to the budget.
- At most 12 buyers are asked at once. Beyond that they go in batches, because an uncapped fan-out starves the server that is also routing everybody else's calls.
- A ping is never retried. A retry is a second auction: the buyer has already decided, may already have reserved capacity, and would be asked to hold two slots for one caller.
- A buyer who does not answer in time is skipped with
timeout; one whose endpoint errors is skipped witherror. Both are written to the ledger with the request, the response and the latency.
A redirect is not followed. A 3xx is scored as an error with the Location in the reason, so you can save the final URL on the target — and so that a vetted public URL cannot answer 302 to somewhere the platform refused to go.
The circuit breaker
One endpoint that swallows connections would otherwise spend the whole budget on every single call, and every other buyer would lose the auction to a time-out. So failures are counted.
| Consecutive failures | health | Effect |
|---|---|---|
| 1–2 | unchanged | None. Buyers have bad minutes. |
| 3 | degraded | A warning only — the target is still pinged. The console shows Degraded with the last error. |
| 6 | down | The target is suppressed for one minute. Every plan skips it with suppressed until a background pass puts it back in the running. |
5xx are failures. A clean no-bid is a success — the buyer answered, they simply said no — so a buyer who declines all day never trips the breaker.- One successful ping clears the count, the error and the suppression, and sets
healthback tohealthy. - Suppression lapses on its own. A background pass runs every ten seconds, and the first one after the minute clears the window, the failure count and
healthback tounknown— so the target is bidding again without anybody doing anything. Until that pass,health: downis enough on its own to keep it skipped. - There is no event for this.
target.downis emitted for a diallednumberorsiptarget only; a bidding target that has stopped answering is visible in itshealth, in the ping ledger and on the console's target list. - Activating a target — even one that is already active, via a status change — clears the breaker at once, which is the way to retry a buyer you have just fixed.
- The counters are shared with the reachability probes on a SIP endpoint, so a target's health is one number whatever kind it is.
Safe URLs
A bid URL and a token URL are typed into a form by a customer, and this server then calls them from inside its own network and stores the response where that customer can read it. Without a rule, a target pointed at a cloud metadata address would be server-side request forgery with a results page. Every customer-supplied URL therefore goes through the same check when it is saved and again at every send, because DNS changes after a form is saved.
httpsonly in production.- No credentials in the URL — no
user:pass@. - Not
localhost, and not a.local,.internalor.localhostname. - The name is resolved and every address behind it must be public: loopback, private, link-local and cloud metadata ranges are refused.
- Redirects are never followed.
Worked example: connecting a buyer who bids
- 1
Create the target
Under Targets, New target, type RTB buyer, owned by the buyer BlueSky Legal. Paste their bid URL, pick their platform as the preset and set your floor and ceiling:
The target{ "name": "BlueSky Legal — RTB", "buyerId": "e2d3c4b5-a697-4887-9bac-1d2e3f4a5b6c", "type": "rtb", "durationThresholdSeconds": 60, "rtb": { "preset": "ringba", "bidUrl": "https://rtb.example.com/v2/bid.json", "bidMethod": "POST", "bidContentType": "application/json", "bidHeaders": { "x-partner": "northwind" }, "pingTimeoutMs": 1500, "authType": "bearer", "authSecret": "not-a-real-token", "acceptMode": "http_2xx", "bidPath": "bidAmount", "bidUnit": "dollars", "durationPath": "bidTerms.0.callMinDuration", "destinationPath": "phoneNumber", "destinationKind": "did", "rejectReasonPath": "rejectReason", "minBidCents": 2500, "maxBidCents": 9000, "bidMultiplier": 1 } }revenueCentsis refused on an RTB target — the bid is the price. ThedurationThresholdSecondsyou set is the fallback for a buyer who sends no duration with their bid. - 2
Test it before a caller does
Test on the target sends one real ping and shows you the exact request, their raw answer, the latency and what your paths made of it. See Testing a target.
- 3
Put it on a plan
Add it to a campaign's routing plan. In
highest_bidmode it competes on price with your fixed-price targets; inprioritymode it sits where you put it and its bid is simply what the call books. - 4
Watch the ledger
Every ping is a row in the ping ledger with the request, the response, the status, the latency and the reject reason. When a buyer says they never got a ping, you can show them the one they answered — and when they say they bid $50, you can show them the $41 they sent.
