Ringba-compatible ping
A single-step ping URL, per publisher lane, that accepts the parameters a Ringba-style platform already sends and answers in the field names it already parses — so a partner can send you calls without writing new code.
After this page you can switch on a single-step ping URL for one publisher lane, hand it over, and know exactly what their platform will send, what it will get back, and the three differences from ping and post that change how a call behaves.
Many publishers already have a ping integration built for an incumbent platform, and asking them to write another is how a new partnership stalls. Each lane can therefore expose a compatible ping URL: one request that takes the parameters they already send and answers in the field names they already parse.
It is a dialect, not a second engine. Behind the URL are the same auction, the same bid rows, the same reservation, the same call record, the same rate limits, the same IP allowlist and the same ping ledger as the two-step protocol. Nothing about a call that arrives this way is different once it has arrived.
How it differs from ping and post
| Ping and post | Single-step URL | |
|---|---|---|
| Requests | Two: ping, then post. | One. The auction runs and the bid is claimed in the same request. |
| Authentication | A bearer key in a header. | A token in the URL itself, because that is what the sending platform can carry. |
| Method | POST, or GET with the same fields. | GET or POST. Both take the same parameters and answer the same shapes. |
| Response | The platform's own field names, money in cents. | bidId, bidAmount, expireInSeconds, phoneNumber and the rest. Money in dollars. |
| A no-bid | ok: false with a reject object. | bidAmount: 0 with a rejectReason string carrying the code in brackets. |
| Releasing | POST /v1/release frees the slot at once. | There is no release in the dialect. An unused claim closes itself. |
| Signed requests | Supported, and enforceable per key. | Not possible — see the warning below. |
The URL
https://api.buy3.io/api/rtb/compat/ringba/<token>.json- The
.jsonsuffix is optional and ignored. Keep it: it is what a platform expecting a JSON endpoint is usually configured with. - The token is the credential and is specific to one publisher on one campaign. Treat it as opaque — letters, digits,
_and-— and never parse it. - It is a separate credential from the routing key, so handing out the URL never exposes the bearer key, and either can be replaced without the other.
- A rotated routing key carries its URL forward, so a key rotation does not break a partner who only uses this door.
What the publisher sends
A GET with a query string, or a POST with a JSON body — and a POST may carry both, in which case the body wins. Parameter names are matched without regard to case, because CID, cid and Cid all arrive in the wild.
| Parameter | Type | Rules |
|---|---|---|
CID | string | Up to 32 characters. Ten digits, eleven digits starting with 1, or E.164. Anything else is a no-bid with code 1101; a campaign that requires a caller ID and gets none answers 1100. |
zipcode | string | Up to 12 characters. |
state | string | A two-letter code drives the campaign's and the lane's state filters. Anything longer is kept as the tag state instead of being refused — it is still something the publisher said about the caller. |
exposeCallerId | flag | Whether the caller's number may be passed to the buyer. Defaults to the campaign's setting. |
sipOk | flag | The publisher can take a SIP handoff. Without it no sipAddress comes back. |
publisherInboundCallId | string | Up to 120 characters. The publisher's own id for the call — and the idempotency key. |
| anything else | scalar | Becomes a tag with the source publisher: at most 40 tags, a key cut at 64 characters and a value at 512. Objects, arrays and empty values are dropped. |
yes, y, true, 1 or on for true and no, n, false, 0 or off for false. Anything else is treated as unset.curl -sS "https://api.buy3.io/api/rtb/compat/ringba/$BUY3_COMPAT_TOKEN.json\
?CID=2145550142\
&state=TX\
&zipcode=75201\
&exposeCallerId=yes\
&sipOk=yes\
&publisherInboundCallId=nw-88231\
&sub_id=fb-42"curl -sS -X POST "https://api.buy3.io/api/rtb/compat/ringba/$BUY3_COMPAT_TOKEN.json" \
-H "Content-Type: application/json" \
-d '{
"CID": "2145550142",
"state": "TX",
"zipcode": "75201",
"sipOk": "yes",
"publisherInboundCallId": "nw-88231",
"sub_id": "fb-42"
}'A bid
{
"bidId": "7c6b5a49-3827-4615-9048-372615049382",
"bidAmount": 27.5,
"expireInSeconds": 58,
"bidExpireDT": "2026-09-20T14:04:09.000Z",
"bidExpireEpoch": 1789913049000,
"phoneNumber": "+18885550100",
"phoneNumberNoPlus": "18885550100",
"sipAddress": "sip:7c6b5a4938274615904837261504@sip.example.com",
"bidTerms": [
{ "code": 101, "message": "Call must connect" },
{ "code": 100, "message": "Minimum connected duration 90 seconds" }
],
"warnings": [],
"callId": "0b9f6c1e-52a4-4a0e-9f0a-3d1c7e8b2a11",
"ref": "CA-482137",
"auctionId": "3f2e1d0c-9b8a-4765-8432-10fedcba9876",
"postback": {
"url": "https://api.buy3.io/api/rtb/v1/conversion",
"key": "b3p_4f1d2c3b4a5e6f70"
}
}| Field | Meaning |
|---|---|
bidId | The claimed bid. Quote it in any support conversation. |
bidAmount | What this publisher is paid, in dollars as a number. It is never what the buyer pays. |
expireInSeconds | How long is left to send the caller. |
bidExpireDT, bidExpireEpoch | The same moment as an ISO 8601 instant and as epoch milliseconds. |
phoneNumber, phoneNumberNoPlus | Where to send the caller, with and without the leading +. Present when there is a number to give. |
sipAddress | Present when the ping said sipOk and the platform has a SIP domain. Prefer it: a SIP leg always reaches the platform. |
bidTerms | What the payout depends on, as { code, message }. The codes are in Reject codes. |
warnings | Advisory only, as { code, description }. Never changes the outcome. See Warnings. |
callId, ref | Beyond the dialect, and ignored by a client that only knows it: the call this became, and the short reference both consoles show. |
auctionId | The auction's reference. It finds this exact request in either ledger. |
postback | A URL and a per-call key for the buyer to report a conversion. Pass them on with the call. See Conversion postback. |
A no-bid
{
"bidId": "3f2e1d0c-9b8a-4765-8432-10fedcba9876",
"bidAmount": 0,
"rejectReason": "That state is not being bought (Code: 1008)"
}bidAmountis0. Read that, not the status — a no-bid is a200, as the dialect expects.bidIdcarries the auction's id when there was an auction, so a refusal can still be looked up, andnullwhen the request was refused before one existed.rejectReasonis the description and the code in one string:"That state is not being bought (Code: 1008)". Branch on the number in the brackets; the words may be improved at any time.- A claim that lost its capacity between the two internal steps comes back as a no-bid too, with code
1000, and a full publisher lane as1013.
What is not a bid of zero
| Status | Body | When |
|---|---|---|
400 | { "ok": false, "error": "malformed_request" } | The body is not valid JSON. |
401 | { "ok": false, "error": "invalid_key" } | The token is unknown, malformed or revoked. |
403 | { "ok": false, "error": "compat_disabled" } | The URL is switched off for that lane. |
403 | publisher_not_live · campaign_not_live | The account or the campaign is not live. |
403 | { "ok": false, "error": "ip_not_allowed" } | The address is not on the key's allowlist. |
409 | The no-bid shape, (Code: 1025) or (Code: 1003) | An identical request is still being scored, or that caller already holds a live bid on this campaign. |
429 | The no-bid shape, (Code: 1024), with Retry-After | The key's rate limit. Honour the header. |
Retries
Send publisherInboundCallId on every request. It is the publisher's own id for the inbound call and the idempotency key: a retried ping for the same call replays the first answer byte for byte instead of selling one caller twice. Without it, an Idempotency-Key header is honoured in its place.
That matters more here than on the two-step protocol, because one request both prices and claims. A retry without an idempotency key is a second claim on the same person.
Switching it on
- 1
Check the lane has a live routing key
The URL sits on the key. A lane with none is refused with a sentence saying so; re-issue or rotate one first.
- 2
Turn signatures off on that key
A platform that pings a bare URL cannot sign a request, so the two cannot both be on. Switching the URL on against a signing key is refused with
409 compat_needs_unsigned_key, and requiring signatures on a key with the URL on is refused with409 compat_enabled. - 3
Switch the URL on
From the campaign's Publishers tab, or with the endpoint below. Switching it on mints the token and answers with the finished URL. A key that was on the older routing surface is widened so it can also answer the auction — never narrowed, so an existing integration keeps working.
curlcurl -X POST "https://api.buy3.io/api/tracking/campaigns/6a2d1f0e-8b7c-4c55-a1d3-0e9f8a7b6c5d/publishers/c4e1a2b3-9d8f-4f6e-b5a4-1c2d3e4f5a6b/compat" \ -H "Authorization: Bearer $BUY3_SESSION_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "enabled": true }' - 4
Send the URL to the publisher
It is shown in full on the lane and in their own portal, because it is their credential for their own lane.
Switching the URL off forgets the token, so turning it back on gives a new URL. A token that has been in a URL has been in places nobody can enumerate, and "off" should mean the old one is dead for good. Replacing the URL while leaving it on is its own action and takes effect immediately.
Moving a publisher across
For a partner already integrated with another platform, the change is usually one field in their configuration. Their existing parameters map straight across, and the fields they parse out of the answer have the same names.
| What they change | What stays the same |
|---|---|
| The ping URL. | The parameter names they send, including their own sub-id fields, which arrive here as tags. |
| Nothing else, in most stacks. | The response fields they read: bidId, bidAmount, expireInSeconds, phoneNumber, sipAddress. |
Three things to tell them before the first live call:
- Only ping with a caller ready. A bid is already a claim, so a speculative ping holds a buyer's slot and produces a call that never arrives.
- Transfer from the number they pinged with. When they are given a
phoneNumberrather than a SIP address, the call is matched by the caller's own number against the claim, so the caller must arrive from theCIDthey sent, inside the expiry. SendingsipOkavoids the whole question. - Send
publisherInboundCallId. It is what makes a retry safe, and it is the handle that ties their records to the platform's when something has to be looked up.
A worked example
Northwind Media have a dialler already configured against another platform and want to start sending to Acme Health inbound this week.
- Their lane already exists and has a live routing key. The key does not require signatures, so nothing blocks the URL.
- You switch the URL on from the campaign's Publishers tab and send them the result.
- They change one field in their platform's configuration and point a test caller at it. The first request answers
bidAmount: 0with"This campaign requires a caller ID (Code: 1100)"— their test harness was not sendingCID. - They fix the mapping and ping again with
CID=2145550142,state=TXandpublisherInboundCallId=nw-88231. The answer is a bid of 27.50 with 58 seconds to use it, a platform number and a SIP address. - They transfer to the SIP address. The call appears in your call log with their sub-id as a tag, and in the ping ledger as one row on the compat door — with the door named and the token nowhere in it.
- Two weeks later they move to the two-step protocol for better control over releases. They already hold the bearer key; you switch the URL off, and the old one stops working that second.
Field-by-field detail is in the Ringba-compatible ping reference.
