Pay Sessions

Pay Sessions

Create short-lived pay_tokens to launch the Elements add-payment-method and pay flows. Your backend mints a pay token (client-authenticated), hands it to the hosted Elements iframe, and the iframe validates it to read its scope before rendering.

Related guides: Accept Payments · Elements SDK

The Pay Token object

A pay token is a short-lived, single-purpose credential your backend mints (client-authenticated) and hands to the hosted Elements iframe. It is the browser's only credential — your client secret never reaches the frontend, and card PAN/CVV never leave the BillerAPI-hosted page.

Attributes

pay_token*stringThe pay token used to initialize the Elements add-payment-method / pay flows
pay_token_id*stringUnique identifier for the pay token
client_id*stringYour client ID (authoritative — taken from the credential, never the body)
client_user_id*stringYour unique identifier for the end user
statusstringAWAITING_PAYMENT_METHOD | AWAITING_PAYMENT | COMPLETED | EXPIRED
bill_idstringBill the token is scoped to (required to run a payment)
amount_valueintegerAmount in minor units (cents); 0 when unscoped
amount_currencystringISO 4217 currency code, e.g. "USD"
expires_atstringISO 8601 timestamp of when the token expires (15-minute TTL)
The Pay Token object
{
  "pay_token": "payt_5f3c…",
  "pay_token_id": "ptid_789xyz",
  "client_id": "your_client_id",
  "client_user_id": "user_12345",
  "status": "AWAITING_PAYMENT",
  "bill_id": "bill_abc123",
  "amount_value": 1299,
  "amount_currency": "USD",
  "expires_at": "2026-06-29T12:15:00Z"
}
POST/v1/pay/token/create

Server-to-server. Mint a pay token for the hosted Elements flows. Requires client credentials (API key / API key); the client_id is taken from the credential — a mismatched body value is rejected with 403. Tokens are short-lived (15 minutes).

Request body

client_user_id*stringYour unique identifier for the end user
bill_idstringBill to pay (required for the pay flow; omit for add-payment-method only)
amountobject{ value: integer (minor units), currency: string } — the amount to authorize
payment_method_idstringPre-bind a saved payment method
metadata_jsonstringOpaque JSON string echoed back on the token
Sample uses your_client_id · sign in to auto-fill your sandbox key
curl -X POST https://sandbox.api.billerapi.com/v1/pay/token/create \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $BILLERAPI_API_KEY" \
  -d '{
    "client_user_id": "user_12345",
    "bill_id": "bill_abc123",
    "amount": { "value": 1299, "currency": "USD" }
  }'
Response201 Created
{
  "success": true,
  "pay_token": "payt_5f3c…",
  "pay_token_id": "ptid_789xyz",
  "expires_at": "2026-06-29T12:15:00Z"
}
POST/v1/pay/token/validate

Unauthenticated — the token itself is the credential (capability URL). The hosted Elements page calls this on load to confirm the token and read its scope (bill, amount, status) before rendering. You normally don't call this directly; the SDK does.

Request body

pay_token*stringThe pay token to validate
Sample uses your_client_id · sign in to auto-fill your sandbox key
curl -X POST https://sandbox.api.billerapi.com/v1/pay/token/validate \
  -H "Content-Type: application/json" \
  -d '{ "pay_token": "payt_5f3c…" }'
Response200 OK
{
  "valid": true,
  "pay_token_id": "ptid_789xyz",
  "client_id": "your_client_id",
  "client_user_id": "user_12345",
  "status": "AWAITING_PAYMENT",
  "bill_id": "bill_abc123",
  "amount_value": 1299,
  "amount_currency": "USD",
  "expires_at": "2026-06-29T12:15:00Z"
}

Next: launch the flow

Pass the pay_token to the Elements SDK on your frontend. Outcomes are confirmed by webhooks (pay.succeeded / pay.failed) — the SDK callbacks are UX hints.

Elements
import { BillerApiElements } from 'billerapi-js';

const elements = new BillerApiElements({ clientId: 'your_client_id', environment: 'sandbox' });
elements.pay({
  payToken,                       // from POST /v1/pay/token/create
  onSuccess: (paymentAttemptId) => console.log('paying', paymentAttemptId),
  onExit: (err) => console.log('closed', err),
}).open();
Was this page helpful?