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.

FieldConsole labelRules
bidUrlBid URLRequired, up to 2,000 characters. Public https in production, with no credentials in it. It may contain macros.
bidMethodMethodGET, POST, PUT or PATCH. A GET sends no body, so its macros must be in the URL.
bidContentTypeContent typeWhatever their endpoint expects. Defaults to application/json.
bidHeadersHeadersA map of name to template. Values may contain macros. Authentication headers are added for you.
bidBodyTemplateBody templateUp to 20,000 characters. Left empty, a small flat envelope is sent.
pingTimeoutMsPing timeout50–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.
The default body, when you write no template
{
  "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

authTypeWhat is sent
noneNothing. Saving this clears any credential already stored.
basicAuthorization: Basic … from the username and the secret.
bearerAuthorization: Bearer <secret>.
api_key_headerThe secret in the header you name in Header name.
hmac_sha256x-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_credentialsA token minted at their Token URL and cached until a minute before it expires, then Authorization: Bearer <token>.
ip_aclNothing. The buyer allows our addresses on their side.
digestA digest challenge response.
The secret is write-only: it never appears in a response, in the activity log or in a test's request preview, where its header shows as «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.

SettingConsole labelWhat it finds
acceptModeWhat counts as a bidhttp_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.
bidPathPrice pathRequired. 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…indollars 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.
destinationPathDestination pathRequired. Where they say to send the caller.
destinationKind…isdid, sip or either. A value starting sip: or sips: is read as SIP; otherwise ten digits or more become a phone number.
durationPathDuration pathWhere they state the talk time this bid converts at. Left empty, the target's own threshold applies.
durationOffsetSecondsDuration offset−600 to 600, added to whatever they said — a few seconds either way when their clock and yours disagree.
rejectReasonPathReject reason pathWhere 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.

A bid, in one platform's dialect
{
  "bidId": "b-88213",
  "bidAmount": 41,
  "expireInSeconds": 60,
  "phoneNumber": "+18325550144",
  "bidTerms": [
    {
      "code": "min_duration",
      "callMinDuration": 75
    }
  ]
}
A no-bid from the same platform
{
  "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.

PresetFills
RingbaacceptMode http_2xx, bidPath bidAmount, bidUnit dollars, destinationKind did, destinationPath phoneNumber — or sipAddress when the kind is sipdurationPath bidTerms.0.callMinDuration, rejectReasonPath rejectReason.
RetreaveracceptMode http_2xx, bidPath retreaver_payout, bidUnit dollars, destinationKind did, destinationPath inbound_number, durationPath retreaver_seconds.
TrackDriveacceptMode http_2xx and bidUnit dollars only. TrackDrive has no standard paths, so you type the price and destination paths yourself.
CustomNothing.
What is stored is the paths, not the choice — so a target saved with the TrackDrive preset reads back as Custom, and one whose paths match Ringba's reads back as Ringba.

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.

SettingConsole labelWhere it livesWhat it does
bidMultiplierMultiplierThe targetMultiplies every bid as it is read, before anything else. 1 leaves it alone; 0.9 keeps a tenth back.
minBidCentsFloorThe targetThe least you will accept. A campaign can carry a floor too, and the higher of the two applies.
maxBidCentsCeilingThe target, the route or the campaignThe 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 numberA publisher's ping that wins
The multiplierApplied as the bid is read.Applied as the bid is read.
Tag filtersApplied to the bid.Applied to the bid.
The floorA 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 ceilingNot applied by the router.The bid is cut to the tightest ceiling.
The two behaviours of the floor are deliberate: on a dialled call nobody has promised the buyer anything, so a bid that is too low simply loses. In an auction the bid is already the basis of a quote, and clamping keeps the quote inside your own terms.

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 with error. 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 failureshealthEffect
1–2unchangedNone. Buyers have bad minutes.
3degradedA warning only — the target is still pinged. The console shows Degraded with the last error.
6downThe target is suppressed for one minute. Every plan skips it with suppressed until a background pass puts it back in the running.
A timeout, a transport error, a redirect and a 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 health back to healthy.
  • 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 health back to unknown — so the target is bidding again without anybody doing anything. Until that pass, health: down is enough on its own to keep it skipped.
  • There is no event for this. target.down is emitted for a dialled number or sip target only; a bidding target that has stopped answering is visible in its health, 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.

  • https only in production.
  • No credentials in the URL — no user:pass@.
  • Not localhost, and not a .local, .internal or .localhost name.
  • 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. 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
      }
    }

    revenueCents is refused on an RTB target — the bid is the price. The durationThresholdSeconds you set is the fallback for a buyer who sends no duration with their bid.

  2. 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. 3

    Put it on a plan

    Add it to a campaign's routing plan. In highest_bid mode it competes on price with your fixed-price targets; in priority mode it sits where you put it and its bid is simply what the call books.

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

Next steps