DEVELOPER DOCS · V1

Ping-Post Integration API

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.

Who this guide is for

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.

⚡ Sub-500ms ping response 🔒 Locked pricing ✓ Optional post — no penalty to skip

Quickstart

A 30-second tour of the happy path.

  1. Ask the admin to provision a Publisher account with sourceType: API_PING_POST and to generate API credentials.
  2. Upload an identity document via the Publisher portal and wait for approval.
  3. Send a ping to /api/cx/ping. Compare the returned price to your cost.
  4. If the price works, post the real caller phone to /api/cx/post with the ping token.
  5. Bridge the call to the returned dialNumber, attaching the X-PP-Token SIP header (or ?token= URL fallback).
shell
# 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"}'

Authentication

Two static headers on every request. Both come from your admin contact when the Publisher account is created.

HeaderStatusNotes
X-Vendor-API-KeyRequiredYour API key. Begins with cx_. Safe to log.
X-Vendor-API-SecretRequiredYour API secret. Shown once at creation; the admin can rotate it. Treat like a password — never commit it to source control or log it.
Identity verification gate

Pings are rejected with 403 id_verification_required until the admin approves your uploaded ID document. Submit it first via the Publisher portal.

The Ping-Post Flow

Five interactions across three actors.

  1. Caller dials your tracking number. Your IVR/dialer captures zip and vertical (whatever signals you have at this point).
  2. Your system → POST /api/cx/ping. We auction the call across eligible buyers and return pingToken, the Publisher-facing price, and an expiry.
  3. You decide. Is the price worth your airtime? If not, drop the call or send it elsewhere.
  4. Your system → POST /api/cx/post. Send the token plus the real caller phone. We return dialNumber and X-PP-Token instructions.
  5. Bridge to dialNumber with X-PP-Token. We validate the header, look up the locked buyer, and bridge the live call. Status callbacks fire as usual.
POST https://api.proaxis.ai/api/cx/ping

Run the auction. Returns a pingToken + price you can compare against your cost.

Request Body

FieldRequiredNotes
campaignIdRequiredMust be a campaign you are attached to.
zipRequired5-digit US ZIP.
stateOptional2-letter state. We resolve it from zip if omitted.
verticalOptionalFree-form category (e.g. roofing, solar).
callerPhoneHashOptionalRecommended. SHA-256 of "<your-salt>:<E.164 phone>". Used for fraud dedupe — we never reverse it.
ivrAnswersOptionalArbitrary JSON, stored for audit (e.g. { "homeowner": true }).
clickIdOptionalYour own tracking identifier; echoed back if present.
ttlSecondsOptionalAuction TTL. Default 90, clamped to [15, 300].

Example Request

http
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 Response

json
{
  "success": true,
  "matched": true,
  "data": {
    "pingToken": "pp_xxx",
    "price": 47.50,
    "currency": "USD",
    "expiresAt": "2026-05-14T17:01:30.000Z",
    "expiresIn": 90
  }
}
Latency target

p95 under 500ms. Slot the ping into your IVR's pre-routing stage; callers shouldn't hear dead air.

POST https://api.proaxis.ai/api/cx/post

Commit to the auction. Returns a dialNumber and SIP token to bridge with.

Request Body

FieldRequiredNotes
pingTokenRequiredThe token from the ping response.
callerPhoneRequiredE.164 format, e.g. +14155551234.
firstNameOptionalCaller first name if known.
lastNameOptionalCaller last name if known.

Success Response

json
{
  "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
  }
}
Idempotency

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

Delivering the Call

Once /post returns a dialNumber, bridge the inbound caller to it. The token tells our switch which auction this call belongs to.

  1. Bridge the inbound caller to the dialNumber returned by /api/cx/post.
  2. Attach a SIP header X-PP-Token: pp_xxx with the token value.
  3. If your dialer cannot attach SIP headers, append ?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.

GET https://api.proaxis.ai/api/cx/ping/:pingToken

Inspect an auction's sanitized state. Useful for support tickets and reconciliation.

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

Code Examples

Drop-in templates in shell, Node, and Python. Replace the credentials and campaign id.

Shell

shell
# 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\"}"

Node.js

javascript
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);
}

Python

python
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 RTB Integration

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.

Use this section only if you're a Ringba RTB vendor

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.

Prerequisites

Confirm all of these with your admin contact before you wire up Ringba — if any are missing the integration won't work.

  • Publisher API key (e.g. cx_a1b2c3…)
  • Publisher API secret — shown once, store it safely
  • Campaign ID — cuid string the admin provides
  • ID verification approved — without it every RTB call returns 403
  • [PINGPOST] tracking DNIS provisioned — the Twilio number Ringba will bridge to

The Endpoint

POST https://api.proaxis.ai/api/cx/ringba/rtb

Required Headers

FieldRequiredNotes
Content-TypeRequiredapplication/json
X-Vendor-API-KeyRequiredYour publisher API key.
X-Vendor-API-SecretRequiredYour publisher API secret.

Request Body

FieldRequiredNotes
campaignIdRequiredProvided by the admin.
zipRequired5-digit US ZIP.
stateOptional2-letter state. Inferred from zip if omitted.
verticalOptionalFree-text industry tag (e.g. solar, roofing).
callerPhoneRequiredReal E.164 caller phone — Ringba's [Call.CallerId]. Not a hash; the bridge keys off this.
firstNameOptionalCaller first name if your IVR captured it.
lastNameOptionalCaller last name if available.
clickIdOptionalYour Ringba inbound call id ([Call.InboundCallId]) — useful for reconciliation.

Example Request

http
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"
}

Response — Accepted (200)

json
{
  "accepted": true,
  "bid": 60.00,
  "currency": "USD",
  "phoneNumber": "+18005551111",
  "pingToken": "pp_abc123...",
  "callLogId": "clog_xxx",
  "expiresAt": "2026-05-16T15:42:11.523Z",
  "reauctioned": false
}

Response — Rejected (200)

json
{
  "accepted": false,
  "reason": "no_eligible_contractors"
}
Response fields

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

Ringba Configuration, Step by Step

  1. In Ringba: Campaigns → [your campaign] → Real-Time Bidding → Targets → New Target.
  2. Configure the request — Target Name: ProAxis CX, URL: https://api.proaxis.ai/api/cx/ringba/rtb, Method: POST, Content-Type: application/json.
  3. Add the auth headers:
    headers
    X-Vendor-API-Key: cx_a1b2c3...
    X-Vendor-API-Secret: <your-secret>
  4. Use Ringba's call-tag tokens in the body template. Missing tags resolve to empty strings — the optional fields tolerate that.
    json
    {
      "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]"
    }
  5. Configure the response parser as JSON with these field paths:
    WhatJSON PathUsed As
    AcceptanceacceptedIf false, no-bid
    Bid amountbidYour bid for the auction
    CurrencycurrencyUSD (always)
    DestinationphoneNumberBridge target
  6. Configure the dial: destination type Phone Number (PSTN), number {response.phoneNumber} (adjust to your account's interpolation syntax), Preserve Caller ID: YES, bridge timeout ≥ 30 seconds.
  7. Set Target timeout: 5 seconds (we typically respond in < 1s). Our auction TTL is 90s — Ringba bridges right after our response so you'll be well inside that window in practice.
  8. Add the target to your campaign's RTB auction at your preferred priority and save.
Caller ID preservation is mandatory

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.

How the Bridge Resolves

  1. Ringba bridges the inbound caller to our phoneNumber (the [PINGPOST] DNIS).
  2. Twilio fires our inbound webhook with the original From (caller) and To (DNIS).
  3. We look up the auction by (campaignId, callerPhone) from the last 5 minutes.
  4. Match found → we connect the caller to the buyer we locked at RTB time. No match → call ends with the auth message.

Testing End-to-End

  1. Smoke-test from a terminal first (no Ringba involved):
    shell
    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"
      }'
    Expect 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.
  2. Place a real call into your Ringba campaign from a phone whose ZIP matches a buyer. Watch Ringba's RTB log for the request/response and our admin auctions screen for the new row.
  3. Verify the call actually rings the buyer. If you hear "Your call could not be authorized," jump to troubleshooting below — it's almost always caller-ID preservation.

Troubleshooting

SymptomLikely CauseFix
401 invalid_credentialsHeader names or values wrongValues are case-sensitive; trim trailing whitespace.
403 id_verification_requiredIdentity doc not approved yetUpload via Publisher portal, ask admin to approve.
400 vendor_not_attached_to_campaignAdmin hasn't attached youAsk admin to attach you to the campaign.
accepted: false, no_eligible_contractorsNo buyer matches geo / vertical / scheduleConfirm an active buyer is in coverage right now.
Endpoint OK but Ringba reports no bidResponse parser paths wrongVerify the bid and phoneNumber JSON paths.
Bridges then immediately hangs up "could not be authorized"Caller ID overwritten by RingbaEnable Pass Caller ID on the outbound dial.
Bridges but no buyer ringsDNIS misconfig or wrong campaignCheck the auction detail — the buyer should be locked there.
500 internal_errorOur sideRetry. If it persists, send the failing pingToken to admin.

Constraints Worth Knowing

  • Caller ID preservation is mandatory — this is how the bridge finds the auction. Ringba does this by default; don't override.
  • 90-second TTL after the RTB response. Ringba bridges immediately in practice — well inside the window.
  • One buyer per call. If they don't answer, the call ends — no automatic overflow to a second buyer.
  • 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.
  • No rate-limit middleware in production yet — self-limit to ~10 req/s to avoid overwhelming the auction engine.
  • 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 Integration

Boberdoo is form-driven but supports HTTP webhook delivery, which is enough to script a ping-post handshake.

  1. Create an HTTP Buyer in Boberdoo with two delivery steps. The first delivery URL is /api/cx/ping; configure it to capture data.pingToken and data.price as response variables.
  2. Use Boberdoo's "Filter on Buyer Response" feature to drop the lead if data.price is below your payout target.
  3. If the price clears the filter, fire the second delivery to /api/cx/post with the captured token and the caller's phone.
  4. Pass the returned 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.
Stuck on Boberdoo's parser?

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.

Custom Dialer (Asterisk / FreePBX / Twilio)

If you control the dialplan, ping-post fits naturally as a pre-Dial hook.

Asterisk Dialplan

asterisk
; 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 Programmable Voice

javascript
// 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());
});
SIP header support varies

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.

Rate Limits & Errors

Quotas, idempotency, and what HTTP statuses to expect.

Per-Publisher Quotas

  • /ping — 10 req/s per Publisher, burst 30.
  • /post — no global cap. A token can only be posted once; retries are idempotent.

HTTP Status Reference

CodeMeaning
200Always returned for valid auth + body. Check the success / matched flags inside.
401Missing or invalid X-Vendor-API-Key / X-Vendor-API-Secret.
403Publisher identity not yet approved (id_verification_required) or sourceType ≠ API_PING_POST.
400Validation error. Body explains which field.
429Rate limit. Retry with exponential backoff; respect the Retry-After header.
5xxServer error. Safe to retry /post (it is idempotent); for /ping, retry as a fresh auction.

FAQ

What if my ping returns matched but the customer hangs up before I can post?

Just don't post. The auction expires harmlessly within the TTL. No obligation, no charge.

Can I post the same token twice?

The first /post wins. The second returns already_posted. This is intentional — it makes /post safe to retry on network failure.

Why is my price different from my ping response?

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.

How do I know if a call counts as billable?

Billable rules are per-campaign — typically duration ≥ 90 seconds, configurable by the admin. Use GET /api/cx/calls/:callSid after the call to inspect.

I lost my API secret. What now?

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.

Can I test against a sandbox?

Yes — point at the staging base URL your admin shares with you. Same shape, same credentials format, no live billing.

NEED HELP INTEGRATING? Email us and our team will help you get set up.
integrations@proaxis.ai