← All API docs
API Collection · Virtual Credit Cards

Reloadable Virtual Credit Cards without KYC

This is a different product from the one-time Virtual Credit Cards API. One-time cards are bought per order and redeemed once with a redeem_id. Reloadable cards live inside a lightweight anonymous account: you top the account up in crypto, mint one or more cards against that balance, then load, unload and reload those same cards for as long as you keep them. The two collections share nothing — different endpoints, different identifiers, different lifecycle.

This public API issues reloadable crypto-funded Visa and Mastercard virtual cards without KYC. There is no email, no password and no personal data: an account is a session string plus a recovery token, both generated in one call and both held only by you.

1

Create an account

One browser-side call to signup.php returns a token and a session. Save both — they are the only way back in.

2

Top up in crypto

wallet.php returns a deposit address, QR code, minimum amount and an expiry. The balance lands in USD cents.

3

Mint a card

issue-card.php charges the tier price from the balance and returns a new card. Prices come live from list-cards.php.

4

Load, reveal, spend

load-card.php moves balance onto the card, reveal-card-number.php shows the PAN behind TOTP.

Authentication

There are no API keys. Every endpoint below except Sign Up takes a single session query parameter. A session is a bearer credential: anyone holding it controls the account and its cards, so treat it exactly as you would a password.

  • session — the working credential. Passed with every request. Store it in a cookie (or your own server-side store) and reuse it; it does not need to be re-issued for each call.
  • token — the recovery credential. It is returned once, at sign up, and is only used with signin.php to recover or rotate a session. It arrives already percent-encoded: store it and send it back byte-for-byte, without encoding it a second time. See Sign In.

There is no password reset. Nothing about the account is recoverable from our side — no email, no support recovery, no identity check. If the customer loses both the token and the session, the balance and every card on the account are gone permanently. Any integration must force the customer to store the pair before letting them continue.

Rate limits and where to call from

signup.php and signin.php are heavily rate limited and are intended to be called client side, from the end customer's browser, so the limit applies per customer rather than per server. Calling them from your backend puts every customer behind one shared limit and will start returning 400 quickly.

Sign up performs real key generation and provisioning work, so it is slow by design — commonly 20 to 60 seconds. Show a spinner with reassuring copy for the whole wait, and on failure retry once only. Every other endpoint in this collection responds normally and may be called from either side.

Money is always in cents

Every monetary field in this collection is an integer number of US centsbalanceCents, amountCents, tierPriceCents, priceCents, and the amount parameter of load-card.php and unload-card.php. 601 is $6.01. Never send or render a decimal.

Base URL

https://api.paygate.to/crypto/cards/reloadable/

Deposit endpoints are the exception: they carry the coin ticker before the card path, matching the Crypto Payment Processor convention — https://api.paygate.to/crypto/{ticker}/cards/reloadable/wallet.php.

Account & Session

An account is created once and then addressed only by its session. These three endpoints cover the whole identity surface of the product: create the pair, recover the pair, and check that a session is still alive.

GET

Sign Up

GEThttps://api.paygate.to/crypto/cards/reloadable/signup.php

Creates a new anonymous reloadable card account. Takes no parameters. Returns the only two credentials that will ever exist for this account.

Call this from the browser

This endpoint is heavily rate limited and is designed to be requested client side, directly from the end customer's browser, so that the limit is scoped to that customer. A server-side proxy funnels every one of your customers through a single limit and will be throttled almost immediately.

Expect it to be slow

The account and its cryptographic material are provisioned during the request, so the response commonly takes 20 to 60 seconds. Do not set a short client timeout — allow at least 90 seconds. Render a spinner for the entire wait with copy that sets the expectation, for example: “Securely creating your account — this can take up to a minute. Please keep this page open.”

Retry policy

If the request fails or returns 400 Bad Request, retry exactly once, keeping the spinner on screen. Do not loop: repeated attempts will trip the rate limit and lock the customer out of signing up for a while. If the single retry also fails, tell the customer to try again in a few minutes.

Response

  • token: the permanent recovery credential, URL-encoded. Used only with signin.php. Store it verbatim, including the %2B, %2F and %3D escapes.
  • session: the working credential. Pass it as the session parameter on every other endpoint in this collection.
Query Parameters

None.

Example Response
OK 200 Account created
{
    "token": "NBcvfznMlfzL4ey%2BJmhdcdKVJpQBBqIxuOfZ2GFpbD0Orblz7zfywxXN4Te0Jheo%2F5vGoUuXwfrfxOd9zVOcTw%3D%3D",
    "session": "JSgPHmS2RHUFoR5bsBDw2Gklw_NghKvZs0D9nn9AaiU"
}
Example Request — browser side, one retry
async function signUp() {
  const URL = "https://api.paygate.to/crypto/cards/reloadable/signup.php";

  // The call is slow by design — keep the spinner up for the whole wait.
  showSpinner("Securely creating your account — this can take up to a minute.");

  async function attempt() {
    const r = await fetch(URL, { headers: { Accept: "application/json" } });
    if (!r.ok) throw new Error("HTTP " + r.status);   // 400 lands here
    const d = await r.json();
    if (!d || !d.token || !d.session) throw new Error("Malformed response");
    return d;
  }

  let data;
  try {
    data = await attempt();
  } catch (e) {
    // Retry ONCE only. Never loop — the endpoint is rate limited.
    data = await attempt();
  } finally {
    hideSpinner();
  }

  // The customer MUST store both values before continuing.
  return data;   // { token, session }
}

Block the flow until the customer has saved the pair. Show the token and session in copyable fields, offer a download, and require an explicit acknowledgement before moving on. There is no second chance to display them.

GET

Sign In

GEThttps://api.paygate.to/crypto/cards/reloadable/signin.php?token={token}&session={session}

Recovers access when the working session has been lost, expired or invalidated. Both saved values must be supplied together — the token alone is not enough, and the session alone is not enough.

If the pair is valid the account is returned. When the supplied session is still alive it comes back unchanged; when it has expired, a new session is issued in its place. Always overwrite your stored session with the value from this response rather than assuming it matched what you sent.

The token is unchanged by this call and stays valid for the life of the account.

Query Parameters

  • token: the recovery token exactly as returned by signup.php.
  • session: the last session the customer holds, valid or not.

Do not URL-encode the token — it already is. The value returned by signup.php contains percent escapes such as %2B, %2F and %3D. Passing it through a generic query-string builder that calls encodeURIComponent (or PHP's urlencode, or requests' params=) encodes the % itself, so %3D becomes %253D and the sign-in is rejected. Concatenate the token into the URL verbatim.

const token = "…hSTupRqLCQfsJU70pDvFEgoeS5FcSFSVDMbE3ljTGCvjN3A%3D%3D";

// WRONG — the token is encoded a second time: %3D%3D becomes %253D%253D
`${API}signin.php?${new URLSearchParams({ token, session })}`

// RIGHT — the token goes in exactly as it was issued
`${API}signin.php?token=${token}&session=${encodeURIComponent(session)}`

The same applies in reverse: do not decode it before storing. Whatever you show the customer to save, and whatever they paste back, should be the same string the API issued.

Rate limited like sign up. Call it from the browser, and only when a session has actually failed — never as a pre-flight check before ordinary requests. Use session.php for that.

Query Parameters
ParameterExample valueDescription
tokenNBcvfznMlfzL4ey%2BJmhdcdKVJpQBBqIxuOfZ2GFpbD0Orblz7zfywxXN4Te0Jheo%2F5vGoUuXwfrfxOd9zVOcTw%3D%3DRecovery token, URL-encoded
sessionJSgPHmS2RHUFoR5bsBDw2Gklw_NghKvZs0D9nn9AaiULast known session
Example Request
GEThttps://api.paygate.to/crypto/cards/reloadable/signin.php?token=NBcvfznMlfzL4ey%2BJmhdcdKVJpQBBqIxuOfZ2GFpbD0Orblz7zfywxXN4Te0Jheo%2F5vGoUuXwfrfxOd9zVOcTw%3D%3D&session=JSgPHmS2RHUFoR5bsBDw2Gklw_NghKvZs0D9nn9AaiU
Example Response
OK 200 Signed in — session still valid
{
    "token": "NBcvfznMlfzL4ey%2BJmhdcdKVJpQBBqIxuOfZ2GFpbD0Orblz7zfywxXN4Te0Jheo%2F5vGoUuXwfrfxOd9zVOcTw%3D%3D",
    "session": "JSgPHmS2RHUFoR5bsBDw2Gklw_NghKvZs0D9nn9AaiU"
}

When the supplied session had expired, the session field carries a new value. Persist it and discard the old one.

GET

Session & Balance

GEThttps://api.paygate.to/crypto/cards/reloadable/session.php?session={session}

The cheap, unlimited way to answer two questions at once: is this session still signed in, and what is the account balance. Use it on page load, after a deposit confirms, and after any operation that moves money.

A successful response means the session is live. A failure means the session is dead and the customer must recover it with signin.php using their saved token.

Response

  • balanceCents: the spendable account balance in US cents. This is the pool that pays card issuing fees and card loads — it is not money on any card.
Query Parameters
ParameterExample valueDescription
sessionJSgPHmS2RHUFoR5bsBDw2Gklw_NghKvZs0D9nn9AaiUActive session
Example Request
GEThttps://api.paygate.to/crypto/cards/reloadable/session.php?session=JSgPHmS2RHUFoR5bsBDw2Gklw_NghKvZs0D9nn9AaiU
Example Response
OK 200 Signed in · balance $6.01
{
    "balanceCents": 601
}

Funding the Account

Money enters the account as a crypto deposit. Each call creates a fresh, single-use deposit address with its own expiry — addresses are not reusable and must not be cached or shown again after they lapse.

Supported tickers

The ticker sits in the path, before the card segment: https://api.paygate.to/crypto/{ticker}/cards/reloadable/wallet.php

TickerCoinNetwork
btcBitcoinBitcoin
xmrMoneroMonero
ltcLitecoinLitecoin
ethEthereumEthereum
erc20/usdcUSD CoinEthereum (ERC-20)
erc20/usdtTetherEthereum (ERC-20)

For coin logos, display names and live metadata, reuse the shared List all Coins & Minimums endpoint from the Crypto Payment Processor collection — https://api.paygate.to/crypto/info.php returns a logo URL per coin, keyed by network and symbol. That is the same source the PayGate.to deposit selectors use, so a ticker such as erc20/usdc resolves as info["erc20"]["usdc"].logo, and a bare ticker such as btc as info["btc"].logo.

Coin and network must match the ticker exactly, and the amount must be at or above minimum_amount. A deposit of the wrong asset, on the wrong chain, or below the stated minimum is not recoverable and is not refundable. Surface the network name and the minimum prominently next to the address, not in fine print.

Deposit addresses expire. expiresAt is an ISO-8601 UTC timestamp — in the example below the window is 60 minutes from createdAt. Drive a countdown from expiresAt rather than from a decrementing counter, so it stays honest when the tab is backgrounded. When it reaches zero, clear the QR and address from the page and offer to generate a new deposit.

GET

Create Deposit

GEThttps://api.paygate.to/crypto/{ticker}/cards/reloadable/wallet.php?session={session}

Creates a deposit intent for the signed-in account and returns the address, QR code, minimum and expiry. Once the deposit confirms on chain the account balance rises automatically — poll session.php or account-activity.php to detect it.

Query Parameters

  • session: active session.

Response

  • ok: true when the deposit intent was created.
  • id: the deposit intent identifier.
  • asset: uppercase asset symbol, e.g. BTC.
  • status: awaiting until the transaction is seen on chain.
  • confirmations: confirmations counted so far.
  • expiresAt: ISO-8601 UTC instant after which this address must not be used.
  • createdAt: ISO-8601 UTC creation instant.
  • minimum_amount: the smallest accepted deposit, denominated in the coin. Anything below this is lost.
  • network: human-readable chain name to display to the customer.
  • payCurrency: lowercase coin identifier.
  • address_in: the deposit address for this intent only.
  • qr_code: base64 PNG of the payment QR. Render it as <img src="data:image/png;base64,{qr_code}" alt="Payment QR Code"/>.
  • ipn_token: tracking token — quote it to support when asking about a deposit.
Query Parameters
ParameterExample valueDescription
session3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYcActive session
Example Request
GEThttps://api.paygate.to/crypto/xmr/cards/reloadable/wallet.php?session=3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYc
Example Response
OK 200 Deposit intent created
{
    "ok": true,
    "id": "cmtnbkye5009jp207myrkr06t",
    "asset": "BTC",
    "status": "awaiting",
    "confirmations": 0,
    "expiresAt": "2026-09-04T20:01:13.669Z",
    "payCurrency": "btc",
    "createdAt": "2026-09-04T19:01:13.373Z",
    "minimum_amount": 0.00029,
    "network": "Bitcoin",
    "address_in": "3AtsETkcLm3rbM3fL8bWYvF8SCP4tCUybu",
    "qr_code": "iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAIAAAB7GkOtAAAyP0lEQVR4nO3deZQkR30n8O8vIuvsue8ZtUbn6BihE6EDCSQwSCADNseC8fLwsjZid23Ms/2wOWyvbfbhhefnNcb4eWVpfaxt1lw25jJCiAFJaHSgW2h0jDS3RnNf3XVkRvz2j6ye7pnRTFdOV1ZndXw/j380VGdGVWbltzIi4xeiqiAiovCY6W4AERFNDwYAEVGgGABERIFiABARBYoBQEQUKAYAEVGgGABERIFiABARBYoBQEQUKAYAEVGgGABERIFiABARBYoBQEQUKAYAEVGgokyvFpGc2jFTZaq23f3Hm18R72lvw8CdY4NVUD2/j5fnQ0FkOhC8AyAiChQDgIgoUAwAIqJAMQCIiALFACAiChQDgIgoUAwAIqJAMQCIiALFACAiChQDgIgoUAwAIqJAMQCIiALFACAiClS2aqDdG6wSiZkUoTzhwNV0LILu31p+H29OW57BRy2TGfw55HTm8A6AiChQDAAiokAxAIiIAsUAICIKFAOAiChQDAAiokAxAIiIAsUAICIKFAOAiChQDAAiokAxAIiIAsUAICIKFAOAiChQDAAiokDlVQ46kyIUWC5CIdmc2pDp4+3+xZlaO+2bLUgbupfTyVCEt1YERXhrRbjm8A6AiChQDAAiokAxAIiIAsUAICIKFAOAiChQDAAiokAxAIiIAsUAICIKFAOAiChQDAAiokAxAIiIAsUAICIKFAOAiChQDAAiokDlVQ56BteKzsnAfWKDVa84vza0P2Wi1CKuQglqXNShDYUAe8AiIgCxQAgIgoUA4CIKFAMACKiQDEAiIgCxQAgIgoUA4CIKFAMACKiQDEAiIgCxQAgIgoUA4CIKFAMACKiQDEAiIgClVc10CLUMqRUEcpVTvtms255sBTh4y2CGXyIc8I7ACKiQDEAiIgCxQAgIgoUA4CIKFAMACKiQDEAiIgCxQAgIgoUA4CIKFAMACKiQDEAiIgCxQAgIgoUA4CIKFAMACKiQDEAiIgCJQNX8ZUwgEV9B6tOb36fWPefw8BVui7CaUZZ8Q6AiChQDAAiokAxAIiIAsUAICIKFAOAiChQDAAiokAxAIiIAsUAICIKFAOAiChQDAAiokAxAIiIAsUAICIKFAOAiChQDAAiokANXjnonArq5tGA/NqQSRFqBRdB98di4D6xIpxmRTDtB64IRbwztYF3AEREgWIAEBEFigFARBQoBgARUaAYAEREgWIAEBEFigFARBQoBgARUaAYAEREgWIAEBEFigFARBQoBgARUaAYAEREgcqrGmh+JTOLUEIvJ4P11qa98iJyO3MG7oQsQhtyUoTiu0U41bvHaqBERDQ5BgARUaAYAEREgWIAEBEFigFARBQoBgARUaAYAEREgWIAEBEFigFARBQoBgARUaAYAEREgWIAEBEFigFARBQoBgARUaCi6W4AMGjVVovQ2vzq9A5WYeGcWpvfIe7+cxisA4FifC9yktNRK0LFb94BEBEFigFARBQoBgARUaAYAEREgWIAEBEFigFARBQoBgARUaAYAEREgWIAEBEFigFARBQoBgARUaAYAEREgWIAEBEFigFARBQoya88bE6mversPTQAAAAASUVORK5CYII=",
    "ipn_token": "ZEE2cW8zb1N0N2o1WW1DNHd3RXpEaUlSbWxOWmJJby1VcU5KclplQmRGWGJWUnVqaUwtNFdmSmoxcUdJUVN3ZTZ1WjVkUWZTZHZPM1RjTERabnBiVGh2SGczN3M4ZlVXeWc%3D"
}

The qr_code value above is abbreviated for readability; the live response returns the complete base64 PNG.

Rendering the QR
<img src="data:image/png;base64,{qr_code}" alt="Payment QR Code"/>
Driving the expiry countdown
// Derive the countdown from expiresAt, never from a decrementing counter —
// background tabs get throttled and a counter silently drifts.
function startDepositTimer(expiresAt, onExpire) {
  const endAt = new Date(expiresAt).getTime();
  const el = document.getElementById("depositTimer");

  function render() {
    const left = Math.max(0, Math.round((endAt - Date.now()) / 1000));
    el.textContent = String(Math.floor(left / 60)).padStart(2, "0") + ":" +
                     String(left % 60).padStart(2, "0");
    if (left <= 0) { clearInterval(id); onExpire(); }
  }

  const id = setInterval(render, 1000);
  document.addEventListener("visibilitychange", render);   // re-sync on return
  render();
}
GET

Affiliate Deposit

GEThttps://api.paygate.to/crypto/{ticker}/cards/reloadable/custom-affiliate.php?session={session}&affiliate={wallet}&affiliate_fee=0.075

Identical to Create Deposit, plus an instant percentage commission paid to your own wallet on every top-up made through your integration. Use this when you are rebranding or white-labelling the reloadable card product.

Query Parameters

  • session: active session.
  • affiliate: your payout wallet address.
  • affiliate_fee: your commission as a decimal fraction. 0.075 is 7.5%.

The affiliate wallet must be on the same network as the selected ticker. A Bitcoin deposit pays a Bitcoin address; an eth or erc20/* deposit pays an EVM address. A mismatch loses the commission payout.

Response

Same shape as Create Deposit.

Query Parameters
ParameterExample valueDescription
session3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYcActive session
affiliate1FTpXfNvv1obYpLLFHHh8eEK2vaLwFTHQ2Your wallet, matching the ticker's network
affiliate_fee0.0757.5% commission
Example Request
GEThttps://api.paygate.to/crypto/btc/cards/reloadable/custom-affiliate.php?session=3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYc&affiliate=1FTpXfNvv1obYpLLFHHh8eEK2vaLwFTHQ2&affiliate_fee=0.075
Example Response
OK 200 Deposit intent created with affiliate split
{
    "ok": true,
    "id": "cmtnbkye5009jp207myrkr06t",
    "asset": "BTC",
    "status": "awaiting",
    "confirmations": 0,
    "expiresAt": "2026-09-04T20:01:13.669Z",
    "payCurrency": "btc",
    "createdAt": "2026-09-04T19:01:13.373Z",
    "minimum_amount": 0.00029,
    "network": "Bitcoin",
    "address_in": "3AtsETkcLm3rbM3fL8bWYvF8SCP4tCUybu",
    "qr_code": "iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAIAAAB7GkOtAAAyP0lEQVR4nO3d…AAAASUVORK5CYII=",
    "ipn_token": "ZEE2cW8zb1N0N2o1WW1DNHd3RXpEaUlSbWxOWmJJby1VcU5KclplQmRGWGJWUnVqaUwtNFdmSmoxcUdJUVN3ZTZ1WjVkUWZTZHZPM1RjTERabnBiVGh2SGczN3M4ZlVXeWc%3D"
}
GET

Sub-affiliate Deposit

GEThttps://api.paygate.to/crypto/{ticker}/cards/reloadable/custom-sub-affiliate.php?session={session}&affiliate={wallet}&affiliate_fee=0.075&sub_affiliate={wallet}&sub_affiliate_fee=0.02

Two-level commission on a single deposit: you take your cut, and your own downstream partner takes theirs, both paid instantly on confirmation. Use this to run an affiliate programme of your own on top of the reloadable card product.

Query Parameters

  • session: active session.
  • affiliate: your payout wallet.
  • affiliate_fee: your commission as a decimal fraction — 0.075 is 7.5%.
  • sub_affiliate: your partner's payout wallet.
  • sub_affiliate_fee: their commission as a decimal fraction — 0.02 is 2%.

Both wallets must match the network of the selected ticker. In the example below the ticker is eth, so both addresses are EVM addresses.

Response

Same shape as Create Deposit.

Query Parameters
ParameterExample valueDescription
session3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYcActive session
affiliate0xBEf747A2FB84b6e5D4006B10a3f93cbd45B6E504Your wallet
affiliate_fee0.0757.5%
sub_affiliate0x260c3d5a94894b489EBC7912cB70DC5c89d31d97Partner wallet
sub_affiliate_fee0.022%
Example Request
GEThttps://api.paygate.to/crypto/eth/cards/reloadable/custom-sub-affiliate.php?session=3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYc&affiliate=0xBEf747A2FB84b6e5D4006B10a3f93cbd45B6E504&affiliate_fee=0.075&sub_affiliate_fee=0.02&sub_affiliate=0x260c3d5a94894b489EBC7912cB70DC5c89d31d97
Example Response
OK 200 Deposit intent created with two-level split
{
    "ok": true,
    "id": "cmtnbkye5009jp207myrkr06t",
    "asset": "ETH",
    "status": "awaiting",
    "confirmations": 0,
    "expiresAt": "2026-09-04T20:01:13.669Z",
    "payCurrency": "eth",
    "createdAt": "2026-09-04T19:01:13.373Z",
    "minimum_amount": 0.0042,
    "network": "Ethereum",
    "address_in": "0x2801651C19E3d55eB52145001e70c27B2Bbe1E22",
    "qr_code": "iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAIAAAB7GkOtAAAyP0lEQVR4nO3d…AAAASUVORK5CYII=",
    "ipn_token": "ZEE2cW8zb1N0N2o1WW1DNHd3RXpEaUlSbWxOWmJJby1VcU5KclplQmRGWGJWUnVqaUwtNFdmSmoxcUdJUVN3ZTZ1WjVkUWZTZHZPM1RjTERabnBiVGh2SGczN3M4ZlVXeWc%3D"
}
GET

Account Activity

GEThttps://api.paygate.to/crypto/cards/reloadable/account-activity.php?session={session}

The full ledger for the account balance: deposits in, card purchases and card loads out. This is the statement view — for movements on an individual card use card-activity.php instead.

Response

  • ok: true on success.
  • pending: array of movements not yet settled — typically deposits seen on chain but still confirming. Render these separately, greyed, and do not count them in the spendable balance.
  • entries: array of settled ledger entries, newest first.

Entry fields

  • id: ledger entry identifier.
  • kind: deposit (crypto top-up in), card_purchase (a tier fee charged by issue-card.php), or card_load (balance moved onto a card by load-card.php).
  • amountCents: signed. Positive credits the account, negative debits it.
  • balanceAfter: account balance in cents immediately after this entry — useful for reconciling a statement without re-summing.
  • ref: related object id. For card_purchase and card_load this is the card id; for deposit it is the deposit intent id.
  • note: human-readable description already formatted for display, including the fee actually charged and the exchange rate used.
  • createdAt: ISO-8601 UTC timestamp.

Reading the example below. A 1.016607 LTC deposit at $49.43 credited 7101 ($71.01). An Aurora card was then minted for 5000 ($50.00), leaving 2101. Finally 1500 ($15.00) was taken from the balance to load a card: the 5% load fee took $0.75 and $14.25 reached the card — the amount parameter of load-card.php is what leaves the account, not what arrives on the card.

Query Parameters
ParameterExample valueDescription
session3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYcActive session
Example Request
GEThttps://api.paygate.to/crypto/cards/reloadable/account-activity.php?session=3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYc
Example Response
OK 200 Account ledger
{
    "ok": true,
    "pending": [],
    "entries": [
        {
            "id": "cmtkmoz7m00kdlp07oma95yja",
            "kind": "card_load",
            "amountCents": -1500,
            "balanceAfter": 601,
            "ref": "cmtkm8nk600j7lp07gv3lis6a",
            "note": "Loaded $14.25 onto Primary ·· 2479 · 5% load fee $0.75",
            "createdAt": "2026-09-02T21:48:58.307Z"
        },
        {
            "id": "cmtkm8nkt00j9lp07kp918b7o",
            "kind": "card_purchase",
            "amountCents": -5000,
            "balanceAfter": 2101,
            "ref": "cmtkm8nk600j7lp07gv3lis6a",
            "note": "Aurora creation — one time, per card",
            "createdAt": "2026-09-02T21:36:16.733Z"
        },
        {
            "id": "cmtk9lgti000kmy07nw5rxmaz",
            "kind": "deposit",
            "amountCents": 7101,
            "balanceAfter": 7101,
            "ref": "cmtk91jgc00hbp907i0d5hlb3",
            "note": "LTC top-up · 1.016607 LTC @ $49.43",
            "createdAt": "2026-09-02T15:42:19.494Z"
        }
    ]
}

Cards

Cards are minted against the account balance and stay on the account until they are retired. list-cards.php is the centre of gravity for the whole product: it returns every card, the live tier prices, the per-account caps and what may currently be minted. Treat it as the source of truth and re-fetch it after every mutating call — never hardcode a price, a cap or a capability.

GET

List Cards & Pricing

GEThttps://api.paygate.to/crypto/cards/reloadable/list-cards.php?session={session}

Returns every card on the account plus the live catalogue: tier prices, discounts, per-tier caps, the account-wide card limit and current availability. Almost every other endpoint in this collection needs a value that comes from here — above all the card id, which addresses a card in load, unload, reveal, activity and wallet provisioning.

Prices are dynamic. tierPriceCents, discounts and eclipseDoor.priceCents move — Eclipse in particular is a repricing door that can close or ease. Always render the price from this response at the moment of display, and re-read it immediately before charging. A hardcoded price will eventually be wrong and the mint will fail with insufficient_funds.

Top-level fields

  • ok: true on success.
  • cards: array of card objects — see the Card Object reference for every field.
  • tierPriceCents: the one-time minting fee per tier, in cents, before discounts.
  • discounts: per-tier discount in cents currently applied to that tier.
  • discountCents: account-wide discount in cents.
  • eclipseDoor: Eclipse availability window — baseCents (list price), priceCents (price right now), repriced (whether the current price differs from base), closes30d and easesAt (when the door next moves; null when it is not scheduled).
  • eclipseUkEdition: enabled, listed, and refundFeeBps — the refund fee in basis points (400 = 4%).
  • walletEdition.enabled: whether the wallet-provisioning edition is live on this account at all. Gate the Apple Pay and Google Pay UI on this and on the individual card's walletEligible.
  • maxLiveCards: total live cards allowed on the account across all tiers.
  • tierCaps: maximum live cards allowed per tier.
  • availability.canMint: whether any card may be minted right now.
  • availability.byTier: per-tier mintability. A tier is offerable only when this is true.
  • eclipseInterest: onList / dismissed — whether this account has registered interest in Eclipse while the door is closed.
  • eclipseRelaunch.pending: an Eclipse relaunch is scheduled.

Deriving the UI from this response

The correct way to build a tier selector:

  • Offer a tier only when availability.canMint and availability.byTier[tier] are both true.
  • Price it as tierPriceCents[tier] − discounts[tier] − discountCents, floored at zero, and label Eclipse from eclipseDoor.priceCents when that differs from tierPriceCents.eclipse.
  • Disable it when the count of live cards of that tier has reached tierCaps[tier], or when cards.length has reached maxLiveCards.
  • Disable it with a "top up" prompt when balanceCents is below the computed price.
Query Parameters
ParameterExample valueDescription
session3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYcActive session
Example Request
GEThttps://api.paygate.to/crypto/cards/reloadable/list-cards.php?session=3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYc
Example Response
OK 200 Two cards — a funded Aurora and a fresh Eclipse
{
  "ok": true,
  "cards": [
    {
      "id": "cmtkm8nk600j7lp07gv3lis6a",
      "status": "active",
      "network": "visa",
      "tier": "aurora",
      "balanceCents": 897,
      "spendableCents": 897,
      "pendingCents": 0,
      "clearingCents": 0,
      "pendingSpendCents": 0,
      "reservedCents": 0,
      "unloadableCents": 897,
      "holderName": "Max Berg",
      "nameChosenByHolder": false,
      "checkoutFree": false,
      "aiSubsNote": false,
      "edition": null,
      "feeLadder": "standard",
      "billing": {
        "address": "Flat E, 3/F, Manning House, 52 Argyle Street, North Point",
        "city": "Hong Kong",
        "zip": "999077",
        "country": "Hong Kong"
      },
      "billingCountryOnly": null,
      "billingEditable": true,
      "unloadSupported": true,
      "billingSource": "holder",
      "last4": "2479",
      "expMonth": 1,
      "expYear": 2031,
      "label": "Primary",
      "createdAt": "2026-09-02T21:36:16.709Z",
      "activatedAt": "2026-09-02T21:36:25.757Z",
      "funded": true,
      "provider": "managed",
      "otpPush": false,
      "bin": "446614",
      "walletEligible": true,
      "consecutiveDeclines": 0,
      "nsfLockState": "none",
      "mccLockState": "none",
      "tapLockState": "none",
      "abuseLockState": "none"
    },
    {
      "id": "cmtnchwyd00aqp207mbotyouw",
      "status": "active",
      "network": "mastercard",
      "tier": "eclipse",
      "balanceCents": 0,
      "spendableCents": 0,
      "pendingCents": 0,
      "clearingCents": 0,
      "pendingSpendCents": 0,
      "reservedCents": 0,
      "unloadableCents": 0,
      "nameChosenByHolder": true,
      "checkoutFree": true,
      "aiSubsNote": true,
      "edition": null,
      "feeLadder": "eclipse",
      "mccMode": "deny",
      "mccCount": 12,
      "billingCountryOnly": "United Kingdom (GB)",
      "billingEditable": false,
      "unloadSupported": true,
      "billingSource": "house",
      "last4": "3340",
      "expMonth": 8,
      "expYear": 2028,
      "label": "Card 2",
      "createdAt": "2026-09-04T19:26:51.156Z",
      "activatedAt": "2026-09-04T19:26:59.661Z",
      "funded": false,
      "provider": "managed",
      "otpPush": true,
      "bin": "525797",
      "walletEligible": false,
      "consecutiveDeclines": 0,
      "nsfLockState": "none",
      "mccLockState": "none",
      "tapLockState": "none",
      "abuseLockState": "none"
    }
  ],
  "tierPriceCents": { "shadow": 2500, "aurora": 4500, "eclipse": 500 },
  "eclipseDoor": {
    "baseCents": 500,
    "priceCents": 500,
    "repriced": false,
    "closes30d": 0,
    "easesAt": null
  },
  "eclipseUkEdition": { "enabled": false, "listed": false, "refundFeeBps": 400 },
  "walletEdition": { "enabled": true },
  "discounts": { "shadow": 0, "aurora": 0, "eclipse": 0 },
  "discountCents": 0,
  "maxLiveCards": 11,
  "tierCaps": { "shadow": 3, "aurora": 3, "eclipse": 5 },
  "availability": {
    "canMint": true,
    "byTier": { "aurora": true, "shadow": true, "eclipse": true }
  },
  "eclipseInterest": { "onList": false, "dismissed": false },
  "eclipseRelaunch": { "pending": false }
}
GET

Issue Card

GEThttps://api.paygate.to/crypto/cards/reloadable/issue-card.php?session={session}&type=aurora

Mints a new card of the requested tier and charges the tier fee to the account balance. The only requirement is balance — there is nothing to submit, verify or approve. The holder name and billing address are assigned by the issuer.

Query Parameters

  • session: active session.
  • type: aurora, shadow or eclipse. Optional — omitting it mints an aurora, which is the most expensive tier, so always send it explicitly.

Response

  • ok: true when the card was minted.
  • card: the new card object. See the Card Object reference.
  • priceCents: what was actually charged, in cents.
  • discountCents: discount applied to this mint, in cents.

A new card arrives as status: "issuing" with no last4, bin, expMonth or expYear. Those appear once the issuer activates it, usually within seconds. Poll list-cards.php until status becomes active and activatedAt is set, rather than expecting the mint response to be complete.

The card mints empty and funded is false. An unfunded card cannot be revealed, cannot be added to a wallet, and declines every charge. It must first be loaded with at least $15.00 — see Load Card.

Query Parameters
ParameterExample valueDescription
session3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYcActive session
typeauroraOptional. aurora · shadow · eclipse. Defaults to aurora
Example Request
GEThttps://api.paygate.to/crypto/cards/reloadable/issue-card.php?session=3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYc&type=aurora
Example Response
OK 200 Card minted — status issuing
{
  "ok": true,
  "card": {
    "id": "cmtnchwyd00aqp207mbotyouw",
    "status": "issuing",
    "network": "visa",
    "tier": "eclipse",
    "balanceCents": 0,
    "spendableCents": 0,
    "pendingCents": 0,
    "clearingCents": 0,
    "pendingSpendCents": 0,
    "reservedCents": 0,
    "unloadableCents": 0,
    "nameChosenByHolder": true,
    "checkoutFree": true,
    "aiSubsNote": true,
    "edition": null,
    "feeLadder": "eclipse",
    "mccMode": "deny",
    "mccCount": 12,
    "billingCountryOnly": "United Kingdom (GB)",
    "billingEditable": false,
    "unloadSupported": true,
    "billingSource": "house",
    "label": "Card 2",
    "createdAt": "2026-09-04T19:26:51.156Z",
    "funded": false,
    "provider": "managed",
    "otpPush": false,
    "walletEligible": false,
    "consecutiveDeclines": 0,
    "nsfLockState": "none",
    "mccLockState": "none",
    "tapLockState": "none",
    "abuseLockState": "none"
  },
  "priceCents": 500,
  "discountCents": 0
}
Error Response
Error 200 Balance below the tier price
{
    "error": "insufficient_funds",
    "priceCents": 4500
}

priceCents is echoed back so you can tell the customer exactly how much they are short: priceCents − balanceCents. Send them to the top-up flow with that figure pre-filled.

GET

Load Card

GEThttps://api.paygate.to/crypto/cards/reloadable/load-card.php?amount=1500&id={cardId}&session={session}

Moves money from the account balance onto a card. This is what activates a freshly minted card and what tops one up afterwards.

Query Parameters

  • amount: cents taken from the account balance. Minimum 1500 ($15.00).
  • id: the card id from list-cards.php.
  • session: active session.

amount is debited from the account, not credited to the card. The load fee comes out of that amount: at the standard 5% rate, amount=1500 debits $15.00 from the balance and puts $14.25 on the card. Show the customer both figures before they confirm. The rate eases from 5% toward 3.5% (Eclipse: 3.5% easing to 2.5%) as their trailing 30-day volume grows — the note on the resulting account activity entry states the rate that was actually charged.

Response

A bare success acknowledgement. Re-fetch list-cards.php and session.php afterwards to pick up the new card balance, the new account balance and the flip of funded to true.

Query Parameters
ParameterExample valueDescription
amount1500Cents debited from the account balance. Minimum 1500
idcmtnchwyd00aqp207mbotyouwCard id
session3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYcActive session
Example Request
GEThttps://api.paygate.to/crypto/cards/reloadable/load-card.php?amount=1500&id=cmtnchwyd00aqp207mbotyouw&session=3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYc
Example Response
OK 200 Loaded
{
    "ok": true
}
Error Response
Error 200 Balance below the requested amount
{
    "error": "insufficient_funds"
}
GET

Unload Card

GEThttps://api.paygate.to/crypto/cards/reloadable/unload-card.php?id={cardId}&session={session}&amount=1500

The reverse of Load Card: pulls money off a card and returns it to the account balance, where it can be used to load another card or mint a new one.

Query Parameters

  • id: the card id.
  • session: active session.
  • amount: cents to pull off the card. Minimum 1500 ($15.00), maximum the card's unloadableCents.

The minimum unload is 1500 ($15.00) — the same floor as Load Card. A card holding less than that in unloadableCents cannot be unloaded at all yet, so do not offer the action for it; tell the customer how much is actually free to return instead of showing a control that can only fail.

Bound the amount by unloadableCents, not by balanceCents. A card's balance can include money that is spoken for — authorisations still clearing (pendingSpendCents, clearingCents) or held (reservedCents). Only unloadableCents is actually free to leave. So the amount a customer may enter is bounded on both sides: at least 1500, at most unloadableCents. Offer the action only when unloadSupported is true and unloadableCents >= 1500.

Response

Same shape as Load Card. Re-fetch list-cards.php and session.php afterwards.

Query Parameters
ParameterExample valueDescription
idcmtkm8nk600j7lp07gv3lis6aCard id
sessionZ3EkzT4Y1LRdfWgdmKCsOdRAtQ_10K57mf32bSenAxEActive session
amount1500Cents returned to the account balance. Minimum 1500, capped at unloadableCents
Example Request
GEThttps://api.paygate.to/crypto/cards/reloadable/unload-card.php?id=cmtkm8nk600j7lp07gv3lis6a&session=Z3EkzT4Y1LRdfWgdmKCsOdRAtQ_10K57mf32bSenAxE&amount=1500
Example Response
OK 200 Unloaded back to balance
{
    "ok": true
}
GET

Card Activity

GEThttps://api.paygate.to/crypto/cards/reloadable/card-activity.php?id={cardId}&session={session}

Transactions on a single card: authorisations, settlements, refunds, loads and unloads. This is the card statement, as opposed to the account ledger returned by account-activity.php.

This response has a different shape from Account Activity. It returns a single transactions array — there is no entries and no pending. Rows are richer, carrying a merchant, a merchant category code, a status and fees. Do not reuse the account-ledger parser here.

Query Parameters

Response

  • ok: true on success.
  • transactions: array of transactions, newest first.

Transaction fields

  • id: transaction identifier.
  • kind: what the movement is — purchase, refund, load, unload, reversal, fee.
  • source: where it came from — api for card-network activity, ledger for movements you initiated such as a load.
  • amountCents: the amount in US cents, as a magnitude. Direction comes from kind, not from the sign — see below.
  • feeCents: fee charged on this transaction, in US cents. On a load this is the load fee.
  • intlFeeCents: international fee, in US cents, present when the charge was in a foreign currency.
  • status: settled, pending, authorized, reversed, declined.
  • merchant: merchant name, when the network supplied one. Absent on ledger movements such as a load.
  • mcc: the merchant's category code as a string. Check it against the closed categories to explain a decline.
  • currency: the currency the merchant charged in.
  • fx: present on a non-USD charge — { amountCents, currency }, the original amount in the merchant's own currency and its minor units. In the example below the customer was charged 386 GBP pence (£3.86), which settled as 522 US cents ($5.22).
  • authorizedAt: ISO-8601 UTC instant the authorisation was taken.
  • createdAt: ISO-8601 UTC instant the transaction was recorded.
  • settledAt: ISO-8601 UTC settlement instant, once settled.

Amounts are unsigned, so derive the direction from kind. load, refund and reversal put money on the card; purchase, unload and fee take it off. Rendering amountCents without that mapping shows a refund and a purchase identically.

Query Parameters
ParameterExample valueDescription
idcmtkm8nk600j7lp07gv3lis6aCard id
sessionZ3EkzT4Y1LRdfWgdmKCsOdRAtQ_10K57mf32bSenAxEActive session
Example Request
GEThttps://api.paygate.to/crypto/cards/reloadable/card-activity.php?id=cmtkm8nk600j7lp07gv3lis6a&session=Z3EkzT4Y1LRdfWgdmKCsOdRAtQ_10K57mf32bSenAxE
Example Response
OK 200 A reversed foreign refund and a card load
{
    "ok": true,
    "transactions": [
        {
            "id": "cmtkoorngw0tas307i8roock4",
            "kind": "refund",
            "source": "api",
            "amountCents": 522,
            "feeCents": 0,
            "intlFeeCents": 7,
            "status": "reversed",
            "merchant": "SMSPOOL",
            "mcc": "5734",
            "currency": "GBP",
            "fx": {
                "amountCents": 386,
                "currency": "GBP"
            },
            "authorizedAt": "2026-09-02T22:42:52.000Z",
            "createdAt": "2026-09-02T22:44:47.740Z"
        },
        {
            "id": "cmtkmoz7m00kdlp07oma95yja",
            "kind": "load",
            "source": "ledger",
            "amountCents": 1425,
            "feeCents": 75,
            "status": "settled",
            "createdAt": "2026-09-02T21:48:58.307Z",
            "settledAt": "2026-09-02T21:48:58.307Z"
        }
    ]
}

Reading the second row against account activity: the customer sent amount=1500 to load-card.php, feeCents took 75, and amountCents 1425 is what reached the card. The card statement records what arrived; the account ledger records what left.

Security & Reveal

Reading a card number, and provisioning a card into Apple Pay or Google Pay, are the two sensitive operations in this API. Both sit behind a TOTP step-up: a six-digit code from an authenticator app, submitted against the session before the sensitive call will answer.

The step-up is per session, not per request. Once a session has been stepped up it is trusted and further sensitive calls succeed without another code, until that trust lapses. The correct integration pattern is therefore optimistic: call the sensitive endpoint first, and only prompt for a code if it answers step_up_required.

1

Try the sensitive call

reveal-card-number.php or arm-wallet-code.php. If it answers, the session was already trusted — done.

2

Handle step_up_required

On {"error":"step_up_required","method":"totp"}, check totp-status.php to learn whether the account has TOTP yet.

3

Enrol, if needed

totp_setup_required means first use: get a secret from totp-setup.php, show it as a QR and as text, confirm with totp-submit.php&setup=1.

4

Submit and retry

Send the current code to totp-submit.php without setup, then repeat the original call — it now answers.

GET

TOTP Status

GEThttps://api.paygate.to/crypto/cards/reloadable/totp-status.php?session={session}

Tells you which branch of the step-up flow to run: prompt for a code, or walk the customer through first-time enrolment.

Response

  • ok: true.
  • method: totp — an authenticator is already enrolled, so ask for a code. totp_setup_required — nothing is enrolled yet, so run totp-setup.php first.
Query Parameters
ParameterExample valueDescription
session3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYcActive session
Example Request
GEThttps://api.paygate.to/crypto/cards/reloadable/totp-status.php?session=3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYc
Example Responses
OK 200 Authenticator already enrolled
{
    "ok": true,
    "method": "totp"
}
Setup needed 200 No authenticator enrolled yet
{
    "ok": true,
    "method": "totp_setup_required"
}
GET

TOTP Setup

GEThttps://api.paygate.to/crypto/cards/reloadable/totp-setup.php?session={session}

Issues the shared secret for a new authenticator enrolment. Call it only when totp-status.php returned totp_setup_required. The enrolment is not live until it is confirmed with totp-submit.php and setup=1.

Response

  • totp_secret: a base32 secret for any standard TOTP app — Google Authenticator, Aegis, 1Password, Bitwarden and so on. Standard parameters: SHA-1, 6 digits, 30-second period.

Presenting the secret

Show it both ways — as a scannable QR built from an otpauth:// URI, and as the plain base32 string for manual entry. Build the URI client side:

otpauth://totp/PayGate.to:Reloadable%20Card?secret={totp_secret}&issuer=PayGate.to&algorithm=SHA1&digits=6&period=30

Warn the customer to back the secret up before confirming. Like the account token, it is shown once. Losing the authenticator locks reveal and wallet provisioning for that account.

Query Parameters
ParameterExample valueDescription
sessiontRUJ_J_OOp34gDVUZywkroKrkokU1TqmvX5TZWp4HC0Active session
Example Request
GEThttps://api.paygate.to/crypto/cards/reloadable/totp-setup.php?session=tRUJ_J_OOp34gDVUZywkroKrkokU1TqmvX5TZWp4HC0
Example Response
OK 200 Enrolment secret issued
{
    "totp_secret": "BM5BVFLJBWN2XOMM4HKZHI7DRHAHJXH7"
}
GET

TOTP Submit

GEThttps://api.paygate.to/crypto/cards/reloadable/totp-submit.php?session={session}&code=651957

Does double duty. With setup=1 it confirms a new enrolment; without it, it steps up an existing session so sensitive calls will answer.

Query Parameters

  • session: active session.
  • code: the current six-digit code from the authenticator.
  • setup: pass 1 only on the confirmation step of a first-time enrolment. Passing it against an account that already has TOTP fails with already_enabled.

Response

  • ok: true when the code was accepted.
  • trusted: true — the session is now stepped up. Retry the sensitive call immediately.
  • rotated: present on enrolment; indicates whether an existing secret was replaced.
Query Parameters
ParameterExample valueDescription
sessionZ3EkzT4Y1LRdfWgdmKCsOdRAtQ_10K57mf32bSenAxEActive session
code651957Current six-digit TOTP code
setup1Optional. First-time enrolment confirmation only
Example Request — confirming a new enrolment
GEThttps://api.paygate.to/crypto/cards/reloadable/totp-submit.php?session=3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYc&code=988657&setup=1
OK 200 Enrolled and trusted
{
    "ok": true,
    "rotated": false,
    "trusted": true
}
Example Request — stepping up an existing session
GEThttps://api.paygate.to/crypto/cards/reloadable/totp-submit.php?session=Z3EkzT4Y1LRdfWgdmKCsOdRAtQ_10K57mf32bSenAxE&code=651957
OK 200 Session stepped up
{
    "ok": true,
    "trusted": true
}
Error Response
Error 200 setup=1 sent against an enrolled account
{
    "error": "already_enabled"
}

Drop the setup parameter and resubmit the current code as an ordinary step-up.

GET

Reveal Card Number

GEThttps://api.paygate.to/crypto/cards/reloadable/reveal-card-number.php?id={cardId}&session={session}

Returns the full card credentials: PAN, CVV, expiry and PIN. This is the most sensitive call in the collection and is gated twice — by TOTP step-up, and by the card being funded.

Query Parameters

  • id: the card id.
  • session: active session, stepped up.

An unfunded card will not reveal. A card whose funded is false has never been loaded; it holds nothing, declines everything and has no credentials to show. Load it with at least $15.00 first — see Load Card.

Response

  • ok: true.
  • pan: the full 16-digit card number.
  • cvv: the three-digit security code.
  • expMonth / expYear: expiry, as integers.
  • pin: the card PIN, where the issuer provides one.

Handling rules for the response. Never log it, never persist it, never send it to your own backend or analytics, and never place it in the URL, the page title or any element that a browser extension or screenshot tool would sweep up. Render it into the DOM only, mask it until the customer explicitly clicks to show, auto-hide after a short interval, and clear the variable afterwards. Card credentials belong on screen and nowhere else.

Query Parameters
ParameterExample valueDescription
idcmtkm8nk600j7lp07gv3lis6aCard id
sessionZ3EkzT4Y1LRdfWgdmKCsOdRAtQ_10K57mf32bSenAxEStepped-up session
Example Request
GEThttps://api.paygate.to/crypto/cards/reloadable/reveal-card-number.php?id=cmtkm8nk600j7lp07gv3lis6a&session=Z3EkzT4Y1LRdfWgdmKCsOdRAtQ_10K57mf32bSenAxE
Error Response — step-up required
Step-up 200 Session is not trusted yet
{
    "error": "step_up_required",
    "method": "totp"
}

Prompt for a code, send it to totp-submit.php, then repeat this request unchanged.

Example Response
OK 200 Card credentials
{
    "ok": true,
    "pan": "4466148010102479",
    "cvv": "300",
    "expMonth": 1,
    "expYear": 2031,
    "pin": "089136"
}
Example — the full optimistic reveal flow
const API = "https://api.paygate.to/crypto/cards/reloadable/";
const j = (u) => fetch(u, { headers: { Accept: "application/json" } }).then(r => r.json());

async function revealCard(cardId, session, ui) {
  // 1. Try first. A trusted session answers straight away.
  let d = await j(`${API}reveal-card-number.php?id=${cardId}&session=${session}`);

  // 2. Only step up if the API asks for it.
  if (d.error === "step_up_required") {
    const status = await j(`${API}totp-status.php?session=${session}`);

    if (status.method === "totp_setup_required") {
      // 3. First-time enrolment: show the secret, then confirm it.
      const { totp_secret } = await j(`${API}totp-setup.php?session=${session}`);
      const code = await ui.enrolAuthenticator(totp_secret);   // QR + manual entry
      const done = await j(`${API}totp-submit.php?session=${session}&code=${code}&setup=1`);
      if (!done.ok) throw new Error(done.error || "enrolment_failed");
    } else {
      // 4. Already enrolled: just ask for the current code.
      const code = await ui.askForCode();
      const up = await j(`${API}totp-submit.php?session=${session}&code=${code}`);
      if (!up.ok) throw new Error(up.error || "bad_code");
    }

    // 5. Repeat the original call — the session is trusted now.
    d = await j(`${API}reveal-card-number.php?id=${cardId}&session=${session}`);
  }

  if (!d.ok) throw new Error(d.error || "reveal_failed");
  return d;   // { pan, cvv, expMonth, expYear, pin } — render only, never store
}

Apple Pay & Google Pay

Adding a card to Apple Wallet or Google Wallet needs a verification code. The wallet app offers to send that code by SMS, phone call or email; the customer must choose email, because that is the channel this API listens on. The code is then relayed back to your page.

The mechanic is a short arming window: you arm a listener on the card, the customer triggers "Get code" inside their wallet app, and you poll for about a minute until the code arrives or the window times out.

Only offer this on cards that support it. Show the Apple Pay and Google Pay controls when the card's walletEligible is true and walletEdition.enabled is true in the list-cards.php response. In the reference response, the Aurora card is walletEligible: true while the Eclipse card is false — never assume by tier, always read the field.

Order of operations matters. Arm the listener before the customer presses "Get code" in their wallet app, not after. A code that arrives while nothing is armed is discarded, and the customer has to start the wallet flow again.

GET

Arm Wallet Code

GEThttps://api.paygate.to/crypto/cards/reloadable/arm-wallet-code.php?id={cardId}&session={session}&type=apple

Opens a one-minute window during which a wallet verification code for this card will be captured and made available to check-wallet-code.php.

Query Parameters

  • id: the card id.
  • session: active session — this call is step-up protected, exactly like reveal.
  • type: apple or google.

Response

  • ok: true.
  • status: arming — the window is open. Begin polling.
Query Parameters
ParameterExample valueDescription
idcmtkm8nk600j7lp07gv3lis6aCard id
sessionZ3EkzT4Y1LRdfWgdmKCsOdRAtQ_10K57mf32bSenAxEStepped-up session
typeappleapple or google
Example Request
GEThttps://api.paygate.to/crypto/cards/reloadable/arm-wallet-code.php?id=cmtkm8nk600j7lp07gv3lis6a&session=Z3EkzT4Y1LRdfWgdmKCsOdRAtQ_10K57mf32bSenAxE&type=apple
Error Response — step-up required
Step-up 200 Session is not trusted yet
{
    "error": "step_up_required",
    "method": "totp"
}

Run the same step-up as for revealtotp-submit.php with the current code — then repeat this request unchanged.

Example Response
OK 200 Listener armed
{
    "ok": true,
    "status": "arming"
}
GET

Check Wallet Code

GEThttps://api.paygate.to/crypto/cards/reloadable/check-wallet-code.php?id={cardId}&session={session}&type=apple

Polls the armed window. Call it every 10 seconds after arming, for as long as the status stays arming.

Query Parameters

  • id: the card id.
  • session: active session.
  • type: apple or google — must match what was armed.

Response

  • ok: true.
  • status: arming while the window is open and no code has arrived; ready once a code has been captured; timeout if the minute elapses with nothing captured.
  • code: the six digits, present when status is ready. Stop polling and show them.
  • expiresAt: the ISO-8601 UTC instant the arming window closes. It describes the window, not the life of the code — a captured code is not invalidated when this passes. Use it to anchor your countdown.
  • armedAt: ISO-8601 UTC instant the window opened, returned while arming. With the one-minute window this gives the same deadline as expiresAt; anchor the countdown to whichever you receive rather than to a local counter, so a throttled background tab cannot make it drift.

There is one deadline, and it is the arming window — not the code. expiresAt tells you when the window to capture a code closes. Once ready has handed you the six digits, that countdown has done its job.

Never hide or expire a captured code. Leave it on screen for the rest of the session. The customer has to switch to their wallet app to type it in, and a code that disappears while they are doing so cannot be recovered — they would have to arm and capture all over again. Offer a "request another code" control instead, so a new one is always a click away.

timeout is recoverable and common. It usually means the customer had not yet pressed "Get code", or picked SMS instead of email. Show a plain "no code arrived — try again" state with a single button that re-arms, and repeat the reminder to choose Email in the wallet app.

Query Parameters
ParameterExample valueDescription
idcmtkm8nk600j7lp07gv3lis6aCard id
sessionZ3EkzT4Y1LRdfWgdmKCsOdRAtQ_10K57mf32bSenAxEActive session
typeappleMust match the armed type
Example Request
GEThttps://api.paygate.to/crypto/cards/reloadable/check-wallet-code.php?id=cmtkm8nk600j7lp07gv3lis6a&session=Z3EkzT4Y1LRdfWgdmKCsOdRAtQ_10K57mf32bSenAxE&type=apple
Example Responses
Waiting 200 Window open — keep polling
{
    "ok": true,
    "status": "arming",
    "armedAt": "2026-09-04T21:09:13.214Z"
}
Timeout 200 Minute elapsed, no code captured
{
    "ok": true,
    "status": "timeout",
    "armedAt": "2026-09-04T21:09:21.749Z"
}
Ready 200 Code captured — show it and stop polling
{
    "ok": true,
    "status": "ready",
    "code": "641614",
    "expiresAt": "2026-09-04T15:13:40.631Z"
}

Stop the poll the moment this arrives and render the six digits. Leave them on screen — expiresAt here is the arming window's close, not a life for the code, so there is nothing to count down and nothing to hide. Give the customer a way to request another code if they need one later.

Example — arm, then poll every 10 seconds
const API = "https://api.paygate.to/crypto/cards/reloadable/";
const j = (u) => fetch(u, { headers: { Accept: "application/json" } }).then(r => r.json());
const sleep = (ms) => new Promise(r => setTimeout(r, ms));

// `type` is "apple" or "google". Handle step_up_required exactly as for reveal.
async function getWalletCode(cardId, session, type, ui) {
  const armed = await j(`${API}arm-wallet-code.php?id=${cardId}&session=${session}&type=${type}`);
  if (armed.error === "step_up_required") return { needsStepUp: true };
  if (!armed.ok) throw new Error(armed.error || "arm_failed");

  ui.waiting();   // "Waiting for the code — press Get code in your wallet app"

  // Poll every 10s until a code lands or the window times out.
  for (let i = 0; i < 8; i++) {
    await sleep(10000);
    const d = await j(`${API}check-wallet-code.php?id=${cardId}&session=${session}&type=${type}`);

    if (d.status === "timeout") return { timeout: true };

    // Anchor the on-screen countdown to the API's own timing for the window.
    // expiresAt is when the WINDOW closes; armedAt is when it opened.
    if (d.expiresAt) ui.countdownTo(new Date(d.expiresAt).getTime());
    else if (d.armedAt) ui.countdownTo(new Date(d.armedAt).getTime() + 60000);

    // "ready" hands over the six digits. Stop polling and show them — then
    // leave them alone: they are not on a timer, and hiding them would cost
    // the customer the whole arming round trip.
    if (d.status === "ready" && d.code) return { code: String(d.code) };
  }
  return { timeout: true };
}
Customer-facing instructions

Present these steps beside the button, so the customer knows to pick email before pressing it.

Google Wallet

  1. Open Google Wallet: Add to Wallet, then Payment card.
  2. Enter the number from Reveal; Google will ask to verify the card.
  3. Pick Email — never phone or SMS — then press Get code on your page and wait there.
  4. Six digits land on the page: type them into Wallet and tap through.

Apple Wallet

  1. Open Wallet on your iPhone: tap + and Debit or Credit Card.
  2. Enter the number from Reveal; Apple will ask to verify the card.
  3. Pick Email — never phone or SMS — then press Get code on your page and wait there.
  4. Six digits land on the page: type them into Wallet and tap through.

Reference

REF

Card Object

The card object is returned by list-cards.php and by issue-card.php. Fields absent on a freshly minted card appear once the issuer activates it.

Identity & state
FieldTypeDescription
idstringThe card identifier. Required by load, unload, reveal, card-activity and wallet provisioning.
statusstringissuing immediately after minting, active once the issuer has provisioned it.
networkstringvisa or mastercard. Assigned by the issuer — it is not selectable, and can differ from the network of an earlier card on the same account.
tierstringaurora, shadow or eclipse. See Card Tiers.
fundedbooleanfalse until the card has been loaded. An unfunded card cannot be revealed, cannot be added to a wallet, and declines every charge.
labelstringDisplay name, e.g. Primary, Card 2.
last4stringLast four digits. Absent while issuing.
binstringIssuer BIN. Absent while issuing.
expMonthintegerExpiry month, 1–12. Absent while issuing.
expYearintegerExpiry year, four digits. Absent while issuing.
createdAtstringISO-8601 UTC mint time.
activatedAtstringISO-8601 UTC activation time. Absent while issuing.
providerstringIssuing programme identifier, e.g. managed.
editionstring · nullSpecial edition marker, or null.
Money — all in US cents
FieldTypeDescription
balanceCentsintegerEverything currently on the card, including amounts already committed.
spendableCentsintegerWhat a new charge can actually draw on. Show this as the card balance.
pendingSpendCentsintegerAuthorised at a merchant but not yet settled.
clearingCentsintegerSettling — leaving the card but not yet gone.
pendingCentsintegerIncoming and not yet available.
reservedCentsintegerHeld back and unavailable to spend or unload.
unloadableCentsintegerWhat unload-card.php may return to the account. Cap the unload input at this figure, not at balanceCents — and note the $15.00 minimum: below 1500 the card cannot be unloaded at all yet.
feeLadderstringWhich load-fee ladder applies: standard (5% easing to 3.5%) or eclipse (3.5% easing to 2.5%).
Holder & billing
FieldTypeDescription
holderNamestringName on the card. Use it verbatim at checkout — a mismatch is a common decline cause.
nameChosenByHolderbooleanWhether the customer picked the name rather than the issuer assigning one.
billingobjectFull billing address — address, city, zip, country. Present when billingSource is holder.
billingCountryOnlystring · nullSet instead of billing when the programme supplies only a country, e.g. United Kingdom (GB).
billingSourcestringholder — the address belongs to the card holder; house — a programme address is used.
billingEditablebooleanWhether the billing address may be changed.
Capabilities
FieldTypeDescription
walletEligiblebooleanThe gate for Apple Pay and Google Pay. Show wallet controls only when this is true and walletEdition.enabled is true.
unloadSupportedbooleanWhether unload-card.php may be offered for this card. Offer it only when this is true and unloadableCents >= 1500.
otpPushbooleanThe card receives issuer one-time-password pushes.
checkoutFreebooleanThe card carries no per-checkout charge.
aiSubsNotebooleanThe card is flagged as suited to AI and software subscriptions.
mccModestringHow the card's own merchant-category list is applied, e.g. deny. Present on tiers with a bespoke list.
mccCountintegerHow many categories are on that card-specific list. See Closed Categories.
Health & locks

Four independent lock states, each none on a healthy card. Anything else means the card has tripped a protection and needs the customer's attention — surface it on the card, do not bury it.

FieldTypeDescription
consecutiveDeclinesintegerDeclines in a row. A climbing figure usually means an empty card or a closed category.
nsfLockStatestringRepeated insufficient-funds attempts.
mccLockStatestringAttempts at a closed merchant category — see Closed Categories.
tapLockStatestringContactless / wallet usage.
abuseLockStatestringProgramme abuse protection.
REF

Card Tiers

Every price below is illustrative. The minting fee for each tier comes from tierPriceCents in the list-cards.php response, adjusted by discounts and discountCents, with Eclipse additionally governed by eclipseDoor. Read them live and render them live.

All three tiers share one requirement: balance. The card mints against it and nothing else is asked for — no identity, no document, no approval.

Aurora

The complete card
What it does
Apple Pay, Google Pay and tap to pay from the first minute — everywhere cards are taken.
Spend power
$50,000 a day · $1,000,000 a month · no per-charge cap — the network's ceilings, not ours.
On the card
Prepaid — the whole balance is spendable, up to $150,000 riding the card.
Loads
From $15 · 5% now on your volume · the ladder eases 5% → 3.5% over your last 30 days.
Currency
Any — a non-USD charge converts at the network's lowest regular rate; we add $0.
Arrives empty
Mints with nothing on it — load it right after.
Keep-alive
$29 of spend a month keeps it yours; three idle months retire it — the balance comes home, the card fee doesn't.
Declines & validity
A declined attempt costs nothing · valid 2+ years, an email before expiry.

Shadow

The essential card
What it does
Every online checkout, Google Pay from the card page, 3-D Secure approved for you — works everywhere cards are taken.
Spend power
$50,000 a day · $1,000,000 a month · no per-charge cap — the network's ceilings, not ours.
On the card
Prepaid — the whole balance is spendable, up to $150,000 riding the card.
Loads
From $15 · 5% now on your volume · the ladder eases 5% → 3.5% over your last 30 days.
Currency
Any — a non-USD charge converts at the network's lowest regular rate; we add $0.
Arrives empty
Mints with nothing on it — load it right after.
Keep-alive
$29 of spend a month keeps it yours; three idle months retire it — the balance comes home, the card fee doesn't.
Declines & validity
A declined attempt costs nothing · valid 2+ years, an email before expiry.

Eclipse

The ad card
What it does
Built for media spend — Meta, Google Ads, TikTok, X — and the software, cloud and digital tools a campaign leans on clear too. Only gambling, cash and money transfer stay closed.
Spend power
$25,000 a day · $500,000 a month · $25,000 per charge — the network's ceilings, not ours.
On the card
Prepaid — the whole balance is spendable, up to $150,000 riding the card.
Loads
From $15 · 3.5%, easing to 2.5% as your last-30-days Eclipse volume grows.
Currency
Any — a non-USD charge converts at the network's lowest regular rate; we add $0.
Arrives empty
Mints with nothing on it — load it right after.
Keep-alive
$29 of spend a month keeps it yours; three idle months retire it — the balance comes home, the card fee doesn't.
Declines & validity
A declined attempt costs $0.25, the network's fee passed through at cost · valid 2+ years, an email before expiry.

Wallet support is per card, not per tier

Aurora is the tier built around Apple Pay, Google Pay and tap to pay, but the authoritative answer for any individual card is its walletEligible flag from list-cards.php, combined with walletEdition.enabled. Render the wallet marks and the provisioning flow from those fields only.

3-D Secure is approved automatically

When a checkout opens a 3-D Secure page, there is nothing for the customer to fetch or type. Leave the page open and let its timer run: approval is automatic and usually lands within about two minutes, well inside the five-minute window. If the timer does run out unapproved, retry the payment once.

REF

Closed Merchant Categories (MCC)

The issuer closes a set of merchant types at the network level: every cut, any balance. A charge at one of these fails no matter what the card holds, and repeated attempts get the card itself flagged — so the card shields itself with a warning first, then a lock.

Blocking runs on the merchant's registered category code, not on what is in the basket. The label on the door decides. A supermarket inside a casino resort is still a casino MCC; a legitimate purchase at a pawn shop is still a pawn shop MCC.

What happens on an attempt

  • The first charge at any closed category declines and draws a warning.
  • Another within a month locks the card. This shows up as mccLockState on the card object.
  • Unlock is self-serve, once a month.
  • A closed-category charge after an unlock retires the card for good. The balance returns to the account; the card fee is not refunded.

Eclipse cards carry their own, much narrower list. Where a card object includes mccMode and mccCount, that card is governed by a bespoke list of mccCount categories rather than the full programme list below — for Eclipse, only gambling, cash and money transfer stay closed. Read those two fields rather than assuming.

The seven closed groups — 57 codes
GroupCodesWhat it covers
Airlines (legacy codes)10A block of legacy carrier merchant codes the issuer closes wholesale. Book flights through a travel site or agent instead.
Casinos & gaming venues34Named casino and casino-hotel properties: floor charges, chips and cages decline, online or in person.
Gambling & betting5Lotteries, licensed online casinos, sportsbooks and race betting, online or at the counter.
Cash withdrawal2ATMs and bank cash-out desks: manual and automated cash disbursements.
Restricted retail3Pawn shops, antique reproductions, inbound teleservice sellers.
Political & religious2Political organisations and religious organisations.
Automated fuel pumps1Pay-at-the-pump fuel dispensers, where the pre-authorisation and the final amount differ. Pay inside at the kiosk instead.
Every code on the list — 57 codes

Airlines (legacy codes)

  • 3011 Aeroflot
  • 3013 Alitalia
  • 3016 SAS
  • 3018 Varig (Brazil)
  • 3023 Mexicana
  • 3027 UTA/Interair
  • 3031 Olympic Airways
  • 3033 Ansett Airlines
  • 3036 VASP (Brazil)
  • 3041 Balkan-Bulgarian Airlines

Casinos & gaming venues

  • 3551 Mirage Hotel and Casino
  • 3555 Treasure Island Hotel and Casino
  • 3560 Aladdin Resort and Casino
  • 3564 Sam's Town Hotel and Casino
  • 3582 California Hotel and Casino
  • 3597 Riverside Resort Hotel and Casino
  • 3620 Binion's Horseshoe Club
  • 3624 Lady Luck Hotel and Casino
  • 3628 Excalibur Hotel and Casino
  • 3662 Circus Circus Hotel and Casino
  • 3667 Luxor Hotel and Casino
  • 3669 Eldorado Hotel and Casino
  • 3676 Monte Carlo Hotel and Casino
  • 3679 Silver Legacy Hotel and Casino
  • 3682 Sahara Hotel and Casino
  • 3708 Virgin River Hotel and Casino
  • 3712 Buffalo Bill's Hotel and Casino
  • 3728 Bally's Hotel and Casino
  • 3731 Harrah's Hotels and Casinos
  • 3737 Riviera Hotel and Casino
  • 3738 Tropicana Resort & Casino
  • 3761 Primadonna Hotel and Casino
  • 3762 Whiskey Pete's Hotel and Casino
  • 3764 Beau Rivage Hotel and Casino
  • 3766 Fremont Hotel and Casino
  • 3767 Main Street Station Hotel and Casino
  • 3768 Silver Star Hotel and Casino
  • 3769 Stratosphere Hotel and Casino
  • 3771 Caesar's Hotel and Casino
  • 3773 The Venetian Resort Hotel Casino
  • 3774 New York-New York Hotel and Casino
  • 3792 Claridge Casino Hotel
  • 3794 Grand Casino Hotels
  • 3796 Peppermill Hotel Casino

Gambling & betting

  • 7800 Government-Owned Lotteries (US region only)
  • 7801 Government-Licensed On-Line Casinos (US region only)
  • 7802 Government-Licensed Horse/Dog Racing (US region only)
  • 7995 Betting — lottery tickets, casino chips, off-track betting, wagers
  • 9406 Government-Owned Lotteries (non-US region)

Cash withdrawal

  • 6010 Financial Institutions — Manual Cash Disbursements
  • 6011 Financial Institutions — Automated Cash Disbursements

Restricted retail

  • 5933 Pawn Shops
  • 5937 Antique Reproductions
  • 5967 Direct Marketing — Inbound Teleservices Merchant

Political & religious

  • 8651 Political Organizations
  • 8661 Religious Organizations

Automated fuel pumps

  • 5542 Gas stations — pay-at-the-pump dispensers
REF

Errors

Errors are returned in the response body as {"error": "..."} rather than signalled only by the HTTP status, so always parse the body and branch on the error key. Treat the absence of ok: true as a failure even when the transport succeeded.

ErrorRaised byWhat to do
insufficient_fundsissue-card, load-cardThe account balance is below what the call needs. issue-card echoes priceCents — show the shortfall and open the top-up flow with it pre-filled.
step_up_requiredreveal-card-number, arm-wallet-codeThe session is not trusted. Read method (totp), collect a code, submit it to totp-submit.php, then repeat the original call unchanged.
already_enabledtotp-submitsetup=1 was sent against an account that already has TOTP. Resubmit the same code without setup.
HTTP 400signup, signinUsually the rate limit. Retry once with the spinner still on screen, then ask the customer to wait a few minutes. Never loop.
Session rejectedAny endpoint taking sessionThe session has expired or been invalidated. Recover with signin.php using the saved token, persist the returned session, and replay the request.

No endpoints match your search.