Add a Card with TRX
Card entry happens on a page hosted by TRX Services, shown in an iframe on your page. Your code never sees the card number. Three steps:
- Your server gets the hosted page URL and a tracking GUID from Rev.io.
- Your page shows it in an iframe and tells your server when the customer is done.
- Your server hands the tracking GUID to Rev.io, which builds the payment account.
Not sure your tenant is on TRX? See Which gateway is this tenant on? For Paya, use Add a Card with Paya.
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 signals completion.
Step 1: Get the hosted page URL
curl "https://api.psarev.io/billing/api/v1/payment-accounts/account-form?CustomerId=123&Style=PsaLight" \
-H "Authorization: Bearer $TOKEN"
{
"url": "https://pay.trxservices.com/?clientId=...&paypage=...&session=...",
"paymentProcessor": "Trx",
"trackingGuid": "7f3c2a1e-5b7d-4c8e-9a0f-1b2c3d4e5f60"
}
| Field | Notes |
|---|---|
url | Set this as the iframe src. It is a single-use session. Fetch a fresh one every time you render the page. |
trackingGuid | Keep it server side for step 3. |
Style (query) | PsaLight, PsaDark, or Billing. Sets the page colors. |
The page accepts cards only (Visa, MasterCard, Amex, Discover) and asks for the cardholder's first and last name, plus ZIP for USD. For ACH see Add a Bank Account.
Render the page as soon as you fetch the URL. If the customer submits more than 5 minutes after the account-form call, Rev.io rejects the result and step 3 fails with Guid not found.
Reference: Get Account Form
Step 2: Show the iframe and signal completion
Render the url into your page. When the customer submits, TRX sends the card details to Rev.io server-to-server and posts a message to the parent window. The message content is not needed; it only means the customer is done.
<iframe id="card-form" title="Payment Account Form" style="border:0;width:100%;height:700px"></iframe>
<script>
const formUrl = "https://pay.trxservices.com/?clientId=..."; // step 1 url, rendered by your server
const TRX_ORIGINS = ["https://pay.trxservices.com", "https://pay.trxservices.net"];
let handled = false;
document.getElementById("card-form").src = formUrl;
window.addEventListener("message", async (event) => {
if (handled || !TRX_ORIGINS.includes(event.origin)) return;
let data;
try { data = typeof event.data === "string" ? JSON.parse(event.data) : event.data; } catch { return; }
if (data?.action === "reload") return location.reload(); // customer chose to start over
handled = true; // each page completes once
const res = await fetch("/cards/complete", { method: "POST" }); // your server, step 3
const { paymentAccountId } = await res.json();
console.log("Stored payment account", paymentAccountId);
});
</script>
The Rev.io web app sizes this page at 100% × 700px.
Step 3: Store the card by tracking GUID
Your server already holds the trackingGuid from step 1. Hand it to Rev.io.
curl -X POST https://api.psarev.io/billing/api/v1/payment-accounts/cards/trx \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "customerId": 123, "trackingGuid": "7f3c2a1e-5b7d-4c8e-9a0f-1b2c3d4e5f60" }'
{ "paymentAccountId": 4570 }
TRX's callback can land a moment after the iframe signals. If you get 400 Guid not found., wait 500 ms and try again, up to three attempts. That is what the Rev.io web app does. Each miss takes about two seconds, because Rev.io waits for the callback before answering.
Server side, Node shown:
const BASE = "https://api.psarev.io";
const HEADERS = { Authorization: `Bearer ${process.env.REVIO_TOKEN}`, "Content-Type": "application/json" };
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
async function storeTrxCard(customerId, trackingGuid, attempt = 1) {
await new Promise((r) => setTimeout(r, 500)); // give the TRX callback time to land
try {
return await revio("POST", "/billing/api/v1/payment-accounts/cards/trx", { customerId, trackingGuid });
} catch (err) {
const notYet = err.status === 400 && JSON.stringify(err.body ?? "").toLowerCase().includes("guid not found");
if (notYet && attempt < 3) return storeTrxCard(customerId, trackingGuid, attempt + 1);
throw err;
}
}
400 This card has already been processed. means the GUID was already used. Render a fresh page.
Reference: Create TRX Card. TRX Services does not publish developer documentation; the Rev.io API reference is the source of truth for this flow.
Verify the stored card
curl "https://api.psarev.io/billing/api/v1/payment-accounts/123" \
-H "Authorization: Bearer $TOKEN"
{
"data": [
{
"id": 4570,
"processor": "Trx",
"method": "CreditCard",
"isAutopay": false,
"lastProcessResult": "Success",
"card": { "nameFirst": "Jane", "nameLast": "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.