Skip to main content

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:

  1. Your server gets the hosted form URL from Rev.io.
  2. Your page shows it in an iframe, waits for Paya's message, and relays it to your server.
  3. 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.

Call Rev.io from your server, not the browser

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=...&timestamp=...&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 fieldExampleSend 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.