Add a Card with Paya
Card entry happens in a form hosted by Paya, shown in an iframe on your page. Your code never sees the card number. Three steps:
- Your server gets the hosted form URL from Rev.io.
- Your page shows it in an iframe, waits for Paya's message, and relays it to your server.
- Your server stores the vault token on the customer.
Not sure your tenant is on Paya? See Which gateway is this tenant on? For TRX, use Add a Card with TRX.
Rev.io only accepts browser requests from its own domains. A fetch to api.psarev.io from your site's origin is rejected. Keep the JWT and every Rev.io call on your server; the browser only hosts the iframe and relays its result.
Step 1: Get the hosted form URL
curl "https://api.psarev.io/billing/api/v1/payment-accounts/account-form?CustomerId=123" \
-H "Authorization: Bearer $TOKEN"
{
"url": "https://api.payaconnect.com/v2/accountform?developer-id=...&user-id=...×tamp=...&hash-key=...&data=...",
"paymentProcessor": "Paya",
"trackingGuid": ""
}
url is a signed, single-use link to Paya's AccountForm. Fetch a fresh one every time you render the form. The form accepts cards only (Visa, MasterCard, Amex, Discover). For ACH see Add a Bank Account.
Reference: Get Account Form
Step 2: Show the iframe and relay the result
Render the url into your page. When the customer submits, Paya posts the new Account Vault record to the parent window as a JSON string. The page sends it to your server.
<iframe id="card-form" title="Payment Account Form" style="border:0;width:22em;height:400px"></iframe>
<script>
const formUrl = "https://api.payaconnect.com/v2/accountform?..."; // step 1 url, rendered by your server
const PAYA_ORIGINS = ["https://api.payaconnect.com", "https://api.sandbox.payaconnect.com"];
let handled = false;
document.getElementById("card-form").src = formUrl;
window.addEventListener("message", async (event) => {
if (handled || !PAYA_ORIGINS.includes(event.origin)) return;
let vault;
try { vault = typeof event.data === "string" ? JSON.parse(event.data) : event.data; } catch { return; }
if (vault?.action === "reload") return location.reload(); // customer chose to start over
if (!(vault?.id && vault?.last_four)) return; // lifecycle event, not a card
handled = true; // each form completes once
const res = await fetch("/cards/complete", { // your server, step 3
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(vault),
});
const { paymentAccountId } = await res.json();
console.log("Stored payment account", paymentAccountId);
});
</script>
The Rev.io web app sizes this form at 22em × 400px. Paya docs: Using postMessage.
Step 3: Store the vault token
The relayed message is Paya's Account Vault record. You need four fields from it and must convert two of them:
| Paya field | Example | Send to Rev.io as |
|---|---|---|
id | "11e8c4a0d2f1b2c3a4b5c6d7" | cardToken.payaAccountVaultId |
last_four | "1111" | last4 |
exp_date (MMYY) | "1228" | expirationDate as a full date, e.g. "2028-12-31" |
account_type | "visa" | brand: visa → Visa, mc → MasterCard, disc → Discover, amex → Amex, diners → Diners, anything else → Card |
account_holder_name | "Jane Doe" | nameFirst (Paya returns one name field; leave nameLast null) |
Server side, Node shown:
const BASE = "https://api.psarev.io";
const HEADERS = { Authorization: `Bearer ${process.env.REVIO_TOKEN}`, "Content-Type": "application/json" };
const BRANDS = { visa: "Visa", mc: "MasterCard", disc: "Discover", amex: "Amex", diners: "Diners" };
async function revio(method, path, body) {
const res = await fetch(BASE + path, { method, headers: HEADERS, body: body && JSON.stringify(body) });
if (!res.ok) throw Object.assign(new Error(res.statusText), { status: res.status, body: await res.json().catch(() => null) });
return res.status === 204 ? null : res.json();
}
// POST /cards/complete, called by the page in step 2 with the vault record
function storePayaCard(customerId, vault) {
const mm = vault.exp_date.slice(0, 2);
const yy = vault.exp_date.slice(2, 4);
const lastDay = new Date(2000 + Number(yy), Number(mm), 0).getDate(); // last day of that month
return revio("POST", "/billing/api/v1/payment-accounts/cards", {
customerId,
nameFirst: vault.account_holder_name ?? null,
nameLast: null,
cardToken: { payaAccountVaultId: vault.id },
last4: vault.last_four,
expirationDate: `20${yy}-${mm}-${String(lastDay).padStart(2, "0")}`,
brand: BRANDS[vault.account_type] ?? "Card",
});
}
The same call with curl:
curl -X POST https://api.psarev.io/billing/api/v1/payment-accounts/cards \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"customerId": 123,
"nameFirst": "Jane Doe",
"nameLast": null,
"cardToken": { "payaAccountVaultId": "11e8c4a0d2f1b2c3a4b5c6d7" },
"last4": "1111",
"expirationDate": "2028-12-31",
"brand": "Visa"
}'
{ "paymentAccountId": 4570 }
Validation: expirationDate must be today or later, and cardToken must carry a token (otherwise 400 Unable to parse a valid card token.). brand defaults to Card.
Reference: Create Card
Verify the stored card
curl "https://api.psarev.io/billing/api/v1/payment-accounts/123" \
-H "Authorization: Bearer $TOKEN"
{
"data": [
{
"id": 4570,
"processor": "Paya",
"method": "CreditCard",
"isAutopay": false,
"lastProcessResult": "Success",
"card": { "nameFirst": "Jane Doe", "last4": "1111", "expirationDate": "2028-12-31", "brand": "Visa", "isDebit": false }
}
]
}
Update or remove
- Change the name or expiration date with Update Card. The card number cannot be changed. Add a new card instead.
- Remove with Delete Card.
Next step
Make a Payment with the paymentAccountId and the payment method where isCc is true, or enroll the card in autopay.