# Store Payment Methods

Securely store cards with the Elements SDK. Card numbers live only inside a BillerAPI-hosted iframe — they never reach your servers, your bundle, or any `postMessage`. Bill-payment execution is not available; attempts return `501 PAYMENT_EXECUTION_NOT_AVAILABLE`.

## Available and reserved flows

| Flow | What it does | `onSuccess` first arg |
| --- | --- | --- |
| .addPaymentMethod() | Collect + tokenize a card. No money moves. | payment_method_id |
| .pay() | Reserved bill-payment flow. It currently returns 501 PAYMENT_EXECUTION_NOT_AVAILABLE; no payment is initiated. | No success value |

## Step 1 — Mint a pay_token (server-side)

Mirror the link-token flow. Call `POST /v1/pay-tokens` from your server with your client credentials. The token is **short-lived (15 minutes)** and scoped to a single `client_user_id` with an optional `bill_id`, amount, or pre-bound payment method.

**cURL**

```bash
curl -X POST https://sandbox.api.billerapi.com/v1/pay-tokens \
  -H "Authorization: Bearer $BILLERAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "client_user_id": "user_42",
    "bill_id": "bill_123",
    "amount": { "value": 13160, "currency": "USD" }
  }'

# => {
#   "success": true,
#   "pay_token": "payt_...",
#   "pay_token_id": "...",
#   "expires_at": "2026-06-28T12:15:00Z"
# }
```

**Node**

```javascript
// Your server endpoint — never expose the client secret to the browser
const res = await fetch('https://sandbox.api.billerapi.com/v1/pay-tokens', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer $BILLERAPI_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ client_user_id: 'user_42', bill_id: 'bill_123' }),
});
const { pay_token } = await res.json();
// hand pay_token to the browser
```

> **Warning — client_id is authoritative from auth context**
>
> `client_id` is resolved from your authenticated request, never trusted from the request body. Amount and bill scope on the token are part of the reserved payment contract. Payment initiation currently terminates with `501 PAYMENT_EXECUTION_NOT_AVAILABLE` before money movement.

## Step 2 — Launch card storage (browser)

Pass the `pay_token` to `addPaymentMethod`. The hosted iframe validates the token, collects the card, and tokenizes it. Your code never sees the PAN. The `pay` example below documents the reserved contract only; it is not an executable payment path.

**Add Payment Method**

```javascript
import { BillerApiElements } from 'billerapi-js';

const elements = new BillerApiElements({ clientId: 'your_client_id', environment: 'sandbox' });

elements.addPaymentMethod({
  payToken: 'payt_xxx',
  onSuccess: (paymentMethodId, metadata) => {
    // Stored card — last_4 + card_brand are safe to display
    console.log('Saved', paymentMethodId, metadata.card_brand, metadata.last_4);
  },
  onExit: (error) => {
    if (error) console.error(error.code, error.message);
  },
}).open();
```

**Bill payment (unavailable)**

```javascript
// Reference only: execution returns 501 PAYMENT_EXECUTION_NOT_AVAILABLE.
elements.pay({
  payToken: 'payt_xxx',
  billId: 'bill_123', // optional if the pay_token is already bill-scoped
  onSuccess: (paymentAttemptId, metadata) => {
    console.log('Payment submitted', paymentAttemptId, metadata.bill_id);
    // Wait for the pay.succeeded webhook before marking the bill paid
  },
  onExit: (error) => {
    if (error) console.error(error.code, error.message);
  },
}).open();
```

**React**

```react
import { ElementsProvider, useAddPaymentMethod } from 'billerapi-react';

function AddCardButton({ payToken }) {
  const { open, ready } = useAddPaymentMethod({
    payToken,
    onSuccess: (paymentMethodId) => console.log('Saved', paymentMethodId),
  });
  return <button onClick={open} disabled={!ready}>Add a card</button>;
}
```

## The PCI boundary

> **Warning — Card data never crosses postMessage**
>
> The card number and CVV live **only** in the BillerAPI-origin hosted iframe's local state. They are never sent over `postMessage`, never reach your backend, and never touch BillEBox. The SDK only ever receives a tokenized `payment_method_id` plus display-safe `last_4` and `card_brand`.

The hosted card form collects exactly three fields — card number, expiry, and CVV. No cardholder name or ZIP is collected, and the card is tokenized through Vault before any identifier leaves the iframe.

## Payment execution is unavailable

> **Note — Money movement is gated**
>
> The `.pay()` flow does not execute payments in sandbox or production. It returns `501 PAYMENT_EXECUTION_NOT_AVAILABLE`, and the hosted page renders an unavailable state instead of charging a card. Launch requires compliance approval, isolated operations, verified biller support, staged evidence, and explicit sign-off; it is not a configuration-only change. `.addPaymentMethod()` (no money movement) remains available end-to-end.

## Future payment outcomes

> **Warning — Confirm payments server-side**
>
> If payment execution becomes available, `onSuccess` will mean the attempt was submitted, not that it settled. A `payment_attempt_id` will not by itself prove a paid bill. Integrations must wait for an authoritative `pay.succeeded` or `pay.failed` webhook. SDK callbacks run in the browser and can be lost.

See the [webhook confirmations guide](/docs/guides/webhook-confirmations) for the exact `pay.*` event states, and the [Webhooks guide](/docs/guides/webhooks) for registering an endpoint and verifying signatures.

## Related

- [Elements SDK](/docs/guides/elements-sdk) — Install, the three flows, theming, errors
- [Webhooks Guide](/docs/guides/webhooks) — Reserved pay.* outcome contract
- [Authentication](/docs/guides/authentication) — Client credentials for minting tokens
- [Getting Started](/docs/guides/getting-started) — End-to-end integration walkthrough
