15-minute walkthrough with a solutions engineer.
Live calls from homeowners with a job ready to book.
Qualified calls from people who need representation now.
Shoppers ready to talk coverage and pricing.
High-intent calls across lending and debt services.
Patients calling to book care and consultations.
Drivers ready for service, repair, or a new vehicle.
Travelers calling to book trips and packages.
Customers looking for the best car rental deals, vehicles, and booking options.
Ping-post is a two-step pre-call auction. It lets you know the buy-side price before you deliver a caller, so you can decide whether to spend airtime on the call.
In the ping step you send us anonymized lead metadata (ZIP, state, vertical, hashed phone). We run a real-time auction across eligible buyers and return a pingToken, the price we'll pay you, and a TTL.
In the post step — only if the price works — you send the token back with the caller's real phone number. We return a tracking number to dial, lock the buyer, and route the call when it lands on our PSTN gateway.
If you don't post within the TTL, nothing happens. No obligation, no charge, no penalty.
Publishers integrating via the ping-post API. If you send calls via DIRECT, CALL_CENTER, or RINGBA single-step flows, you don't need this — keep your existing integration.
A 30-second tour of the happy path.
sourceType: API_PING_POST and to generate API credentials./api/cx/ping. Compare the returned price to your cost./api/cx/post with the ping token.dialNumber, attaching the X-PP-Token SIP header (or ?token= URL fallback).# 1. Ping
curl -sS -X POST https://api.proaxis.ai/api/cx/ping \
-H "X-Vendor-API-Key: cx_xxx" \
-H "X-Vendor-API-Secret: xxx" \
-H "Content-Type: application/json" \
-d '{"campaignId":"cmp_xxx","zip":"90210","state":"CA","vertical":"roofing"}'
# 2. Post (using the pingToken from step 1)
curl -sS -X POST https://api.proaxis.ai/api/cx/post \
-H "X-Vendor-API-Key: cx_xxx" \
-H "X-Vendor-API-Secret: xxx" \
-H "Content-Type: application/json" \
-d '{"pingToken":"pp_xxx","callerPhone":"+14155551234","firstName":"Jane","lastName":"Doe"}'
Two static headers on every request. Both come from your admin contact when the Publisher account is created.
| Header | Status | Notes |
|---|---|---|
X-Vendor-API-Key | Required | Your API key. Begins with cx_. Safe to log. |
X-Vendor-API-Secret | Required | Your API secret. Shown once at creation; the admin can rotate it. Treat like a password — never commit it to source control or log it. |
Pings are rejected with 403 id_verification_required until the admin approves your uploaded ID document. Submit it first via the Publisher portal.
Five interactions across three actors.
POST /api/cx/ping. We auction the call across eligible buyers and return pingToken, the Publisher-facing price, and an expiry.POST /api/cx/post. Send the token plus the real caller phone. We return dialNumber and X-PP-Token instructions.dialNumber with X-PP-Token. We validate the header, look up the locked buyer, and bridge the live call. Status callbacks fire as usual.https://api.proaxis.ai/api/cx/ping
Run the auction. Returns a pingToken + price you can compare against your cost.
| Field | Required | Notes |
|---|---|---|
campaignId | Required | Must be a campaign you are attached to. |
zip | Required | 5-digit US ZIP. |
state | Optional | 2-letter state. We resolve it from zip if omitted. |
vertical | Optional | Free-form category (e.g. roofing, solar). |
callerPhoneHash | Optional | Recommended. SHA-256 of "<your-salt>:<E.164 phone>". Used for fraud dedupe — we never reverse it. |
ivrAnswers | Optional | Arbitrary JSON, stored for audit (e.g. { "homeowner": true }). |
clickId | Optional | Your own tracking identifier; echoed back if present. |
ttlSeconds | Optional | Auction TTL. Default 90, clamped to [15, 300]. |
POST /api/cx/ping HTTP/1.1
X-Vendor-API-Key: cx_xxx
X-Vendor-API-Secret: xxx
Content-Type: application/json
{
"campaignId": "cmp_xxx",
"zip": "90210",
"state": "CA",
"vertical": "roofing",
"callerPhoneHash": "sha256:9a8f...",
"ivrAnswers": { "homeowner": true },
"clickId": "your-tracking-id",
"ttlSeconds": 90
}
{
"success": true,
"matched": true,
"data": {
"pingToken": "pp_xxx",
"price": 47.50,
"currency": "USD",
"expiresAt": "2026-05-14T17:01:30.000Z",
"expiresIn": 90
}
}
p95 under 500ms. Slot the ping into your IVR's pre-routing stage; callers shouldn't hear dead air.
https://api.proaxis.ai/api/cx/post
Commit to the auction. Returns a dialNumber and SIP token to bridge with.
| Field | Required | Notes |
|---|---|---|
pingToken | Required | The token from the ping response. |
callerPhone | Required | E.164 format, e.g. +14155551234. |
firstName | Optional | Caller first name if known. |
lastName | Optional | Caller last name if known. |
{
"success": true,
"matched": true,
"data": {
"dialNumber": "+18005551111",
"dialInstructions": {
"headers": { "X-PP-Token": "pp_xxx" },
"fallbackQueryParam": "token"
},
"callLogId": "clog_xxx",
"expiresAt": "2026-05-14T17:01:30.000Z",
"reauctioned": false
}
}
/post is safe to retry. If a network blip drops your first request after we processed it, the retry returns already_posted instead of double-billing.
Once /post returns a dialNumber, bridge the inbound caller to it. The token tells our switch which auction this call belongs to.
dialNumber returned by /api/cx/post.X-PP-Token: pp_xxx with the token value.?token=<pingToken> to the dial URI as a fallback. Our switch checks both.Recordings, status callbacks, billing, and dispute flow are handled identically to non-ping-post calls.
https://api.proaxis.ai/api/cx/ping/:pingToken
Inspect an auction's sanitized state. Useful for support tickets and reconciliation.
GET /api/cx/ping/pp_xxx HTTP/1.1 X-Vendor-API-Key: cx_xxx X-Vendor-API-Secret: xxx
Returns auction metadata: status (open, posted, expired, reauctioned), price, expiry. Buyer identity is never exposed.
Drop-in templates in shell, Node, and Python. Replace the credentials and campaign id.
# Ping + post in a single shell session.
PING=$(curl -sS -X POST https://api.proaxis.ai/api/cx/ping \
-H "X-Vendor-API-Key: $CX_KEY" \
-H "X-Vendor-API-Secret: $CX_SECRET" \
-H "Content-Type: application/json" \
-d '{"campaignId":"cmp_xxx","zip":"90210","state":"CA","vertical":"roofing"}')
TOKEN=$(echo "$PING" | jq -r '.data.pingToken')
PRICE=$(echo "$PING" | jq -r '.data.price')
echo "Auction price: $PRICE"
# Decide: if your cost cap is $40 and price is $47.50, skip /post.
# Otherwise:
curl -sS -X POST https://api.proaxis.ai/api/cx/post \
-H "X-Vendor-API-Key: $CX_KEY" \
-H "X-Vendor-API-Secret: $CX_SECRET" \
-H "Content-Type: application/json" \
-d "{\"pingToken\":\"$TOKEN\",\"callerPhone\":\"+14155551234\"}"
const BASE = 'https://api.proaxis.ai';
const headers = {
'X-Vendor-API-Key': process.env.CX_KEY,
'X-Vendor-API-Secret': process.env.CX_SECRET,
'Content-Type': 'application/json'
};
const ping = await fetch(`${BASE}/api/cx/ping`, {
method: 'POST',
headers,
body: JSON.stringify({ campaignId: 'cmp_xxx', zip: '90210', state: 'CA', vertical: 'roofing' })
}).then(r => r.json());
const { pingToken, price } = ping.data;
console.log('Auction price:', price);
// Decide: if your cost cap is $40 and price is $47.50, skip /post.
if (price <= 40) {
const post = await fetch(`${BASE}/api/cx/post`, {
method: 'POST',
headers,
body: JSON.stringify({ pingToken, callerPhone: '+14155551234' })
}).then(r => r.json());
console.log(post.data.dialNumber);
}
import os, requests
BASE = "https://api.proaxis.ai"
headers = {
"X-Vendor-API-Key": os.environ["CX_KEY"],
"X-Vendor-API-Secret": os.environ["CX_SECRET"],
"Content-Type": "application/json",
}
ping = requests.post(f"{BASE}/api/cx/ping", headers=headers, json={
"campaignId": "cmp_xxx", "zip": "90210", "state": "CA", "vertical": "roofing"
}).json()
ping_token = ping["data"]["pingToken"]
price = ping["data"]["price"]
print("Auction price:", price)
# Decide: if your cost cap is $40 and price is $47.50, skip /post.
if price <= 40:
post = requests.post(f"{BASE}/api/cx/post", headers=headers, json={
"pingToken": ping_token, "callerPhone": "+14155551234"
}).json()
print(post["data"]["dialNumber"])
Ringba's RTB Target model is single-shot — one webhook in, one bid response out. We expose a dedicated endpoint that runs the full ping-post auction atomically and replies with a Ringba-shaped payload. No second call from your side.
The default /api/cx/ping + /api/cx/post flow is two-shot. Ringba's RTB is single-shot, so we expose a thin adapter at /api/cx/ringba/rtb that bundles both steps into one request/response. If you're running a custom dialer, Boberdoo, or your own code, use the two-shot endpoints — same auction underneath.
Confirm all of these with your admin contact before you wire up Ringba — if any are missing the integration won't work.
cx_a1b2c3…)403https://api.proaxis.ai/api/cx/ringba/rtb
| Field | Required | Notes |
|---|---|---|
Content-Type | Required | application/json |
X-Vendor-API-Key | Required | Your publisher API key. |
X-Vendor-API-Secret | Required | Your publisher API secret. |
| Field | Required | Notes |
|---|---|---|
campaignId | Required | Provided by the admin. |
zip | Required | 5-digit US ZIP. |
state | Optional | 2-letter state. Inferred from zip if omitted. |
vertical | Optional | Free-text industry tag (e.g. solar, roofing). |
callerPhone | Required | Real E.164 caller phone — Ringba's [Call.CallerId]. Not a hash; the bridge keys off this. |
firstName | Optional | Caller first name if your IVR captured it. |
lastName | Optional | Caller last name if available. |
clickId | Optional | Your Ringba inbound call id ([Call.InboundCallId]) — useful for reconciliation. |
POST /api/cx/ringba/rtb HTTP/1.1
X-Vendor-API-Key: cx_xxx
X-Vendor-API-Secret: xxx
Content-Type: application/json
{
"campaignId": "cmp_xxx",
"zip": "90210",
"state": "CA",
"vertical": "solar",
"callerPhone": "+14155551234",
"firstName": "Jane",
"lastName": "Doe",
"clickId": "ringba-call-id-abc123"
}
{
"accepted": true,
"bid": 60.00,
"currency": "USD",
"phoneNumber": "+18005551111",
"pingToken": "pp_abc123...",
"callLogId": "clog_xxx",
"expiresAt": "2026-05-16T15:42:11.523Z",
"reauctioned": false
}
{
"accepted": false,
"reason": "no_eligible_contractors"
}
bid is the amount we'll pay you for the call (USD). phoneNumber is the tracking DNIS Ringba should bridge to (E.164). pingToken is informational — you don't send it back; the bridge uses caller-phone reconciliation. Bridge the call before expiresAt (typically 90s out).
https://api.proaxis.ai/api/cx/ringba/rtb, Method: POST, Content-Type: application/json.X-Vendor-API-Key: cx_a1b2c3... X-Vendor-API-Secret: <your-secret>
{
"campaignId": "<paste-campaign-id-here>",
"zip": "[Call.Zip]",
"state": "[Call.State]",
"vertical": "<your-vertical-tag>",
"callerPhone": "[Call.CallerId]",
"firstName": "[Call.FirstName]",
"lastName": "[Call.LastName]",
"clickId": "[Call.InboundCallId]"
}
| What | JSON Path | Used As |
|---|---|---|
| Acceptance | accepted | If false, no-bid |
| Bid amount | bid | Your bid for the auction |
| Currency | currency | USD (always) |
| Destination | phoneNumber | Bridge target |
{response.phoneNumber} (adjust to your account's interpolation syntax), Preserve Caller ID: YES, bridge timeout ≥ 30 seconds.Ringba MUST pass the original caller's number on the bridge. Our switch identifies which auction the inbound call belongs to by matching (campaignId, callerPhone) against the auction we just locked. If Ringba overwrites the caller ID with a buyer-tracking number, the lookup fails and the call hangs up with "Your call could not be authorized." Verify "Pass Caller ID" (or your account's equivalent) is enabled on the outbound dial. If compliance requires you to overwrite caller ID, contact us — we can switch the account to a different bridge mechanism.
phoneNumber (the [PINGPOST] DNIS).From (caller) and To (DNIS).(campaignId, callerPhone) from the last 5 minutes.curl -X POST https://api.proaxis.ai/api/cx/ringba/rtb \
-H "Content-Type: application/json" \
-H "X-Vendor-API-Key: cx_..." \
-H "X-Vendor-API-Secret: ..." \
-d '{
"campaignId": "cmp_xxx",
"zip": "90210",
"state": "CA",
"callerPhone": "+14155551234"
}'
accepted: true with a bid and phoneNumber. If you get accepted: false, reason: no_eligible_contractors — auth is fine but no buyer matches right now. Confirm with the admin that a buyer is active, in coverage, and within schedule.
| Symptom | Likely Cause | Fix |
|---|---|---|
401 invalid_credentials | Header names or values wrong | Values are case-sensitive; trim trailing whitespace. |
403 id_verification_required | Identity doc not approved yet | Upload via Publisher portal, ask admin to approve. |
400 vendor_not_attached_to_campaign | Admin hasn't attached you | Ask admin to attach you to the campaign. |
accepted: false, no_eligible_contractors | No buyer matches geo / vertical / schedule | Confirm an active buyer is in coverage right now. |
| Endpoint OK but Ringba reports no bid | Response parser paths wrong | Verify the bid and phoneNumber JSON paths. |
| Bridges then immediately hangs up "could not be authorized" | Caller ID overwritten by Ringba | Enable Pass Caller ID on the outbound dial. |
| Bridges but no buyer rings | DNIS misconfig or wrong campaign | Check the auction detail — the buyer should be locked there. |
500 internal_error | Our side | Retry. If it persists, send the failing pingToken to admin. |
accepted: false is not an error. It means we don't have a buyer right now — Ringba should treat it as a no-bid and try the next target in your auction.pingToken in the response is informational. You don't send it back; the bridge uses caller-phone reconciliation. Store it in your logs for dispute handling.Boberdoo is form-driven but supports HTTP webhook delivery, which is enough to script a ping-post handshake.
/api/cx/ping; configure it to capture data.pingToken and data.price as response variables.data.price is below your payout target./api/cx/post with the captured token and the caller's phone.data.dialNumber and the token to your transfer system (Five9, NICE inContact, etc.) so the agent or IVR can complete the bridge with the SIP header.The lowest-friction setup is to send Boberdoo's webhook to your own thin middleware (50 lines of Node), do the ping-post handshake there, and return a flat { "dialNumber": "..." } response that Boberdoo forwards to your dialer.
If you control the dialplan, ping-post fits naturally as a pre-Dial hook.
; extensions.conf — answer, ping, then bridge with the SIP header
[from-pstn]
exten => _X.,1,Answer()
same => n,Set(CURL_RESULT=${CURL(https://api.proaxis.ai/api/cx/ping,\
X-Vendor-API-Key: ${CX_KEY}\
X-Vendor-API-Secret: ${CX_SECRET}\
Content-Type: application/json\
POSTFIELDS: {"campaignId":"cmp_xxx","zip":"${ZIP}","state":"${STATE}"})})
same => n,Set(PINGTOKEN=${SHELL(echo '${CURL_RESULT}' | jq -r '.data.pingToken')})
same => n,Set(PRICE=${SHELL(echo '${CURL_RESULT}' | jq -r '.data.price')})
same => n,GotoIf($[${PRICE} < 30]?hangup)
; post and capture dialNumber
same => n,Set(POST_RESULT=${CURL(https://api.proaxis.ai/api/cx/post,\
...same headers...\
POSTFIELDS: {"pingToken":"${PINGTOKEN}","callerPhone":"${CALLERID(num)}"})})
same => n,Set(DIALNUM=${SHELL(echo '${POST_RESULT}' | jq -r '.data.dialNumber')})
; bridge with the SIP header
same => n,SIPAddHeader(X-PP-Token: ${PINGTOKEN})
same => n,Dial(SIP/${DIALNUM}@trunk,30)
same => n,Hangup()
same => n(hangup),Hangup()
// Twilio webhook handler. Twilio POSTs to /voice when a call lands;
// respond with TwiML that dials our number with the token header.
import twilio from 'twilio';
const VoiceResponse = twilio.twiml.VoiceResponse;
app.post('/voice', async (req, res) => {
const { From, FromZip, FromState } = req.body;
const p = await ping({ campaignId: 'cmp_xxx', zip: FromZip, state: FromState, vertical: 'roofing' });
if (!p.matched || p.data.price < MIN_PRICE) {
const r = new VoiceResponse();
r.say('We are unable to connect you at this time.');
return res.type('text/xml').send(r.toString());
}
const r = await post({ pingToken: p.data.pingToken, callerPhone: From });
const twiml = new VoiceResponse();
const dial = twiml.dial();
// Twilio's <Sip> verb supports custom headers. Use SIP for X-PP-Token,
// falling back to ?token= for PSTN dialers.
dial.sip(`sip:${r.data.dialNumber}@your-trunk?X-PP-Token=${p.data.pingToken}`);
res.type('text/xml').send(twiml.toString());
});
PSTN trunks strip custom SIP headers. If your carrier is PSTN-only, use the URL fallback (?token=<pingToken>) when dialing the tracking number. Our switch reads both.
Quotas, idempotency, and what HTTP statuses to expect.
/ping — 10 req/s per Publisher, burst 30./post — no global cap. A token can only be posted once; retries are idempotent.| Code | Meaning |
|---|---|
200 | Always returned for valid auth + body. Check the success / matched flags inside. |
401 | Missing or invalid X-Vendor-API-Key / X-Vendor-API-Secret. |
403 | Publisher identity not yet approved (id_verification_required) or sourceType ≠ API_PING_POST. |
400 | Validation error. Body explains which field. |
429 | Rate limit. Retry with exponential backoff; respect the Retry-After header. |
5xx | Server error. Safe to retry /post (it is idempotent); for /ping, retry as a fresh auction. |
Just don't post. The auction expires harmlessly within the TTL. No obligation, no charge.
The first /post wins. The second returns already_posted. This is intentional — it makes /post safe to retry on network failure.
It isn't. The price returned at ping is the price we pay you. If the locked buyer becomes ineligible between ping and post, we re-auction to a fallback at the same Publisher-facing price; you always get exactly what the ping promised.
Billable rules are per-campaign — typically duration ≥ 90 seconds, configurable by the admin. Use GET /api/cx/calls/:callSid after the call to inspect.
Ask the admin to rotate. They'll click Regenerate credentials on your Publisher record — the old secret stops working immediately and a new pair is shown once. The admin will deliver the new pair to you via a secure channel.
Yes — point at the staging base URL your admin shares with you. Same shape, same credentials format, no live billing.