# PayGate.to — Reloadable Virtual Cards API

> Machine-readable reference for AI agents and LLMs. Source of truth: https://paygate.to/docs/virtual-credit-cards-api/reloadable/
> Generated from the hosted documentation on 2026-09-07. Endpoints: 18.

## How to use this file

Give this whole file to an AI assistant and ask it to write the integration. It contains every
endpoint, every parameter, the exact example values from the documentation, and real response
payloads. Nothing here is summarised or paraphrased away.

## Facts an agent needs before writing code

- **Authentication: none.** No sign up, no API key, no token, no auth header. Do not invent one.
- **Transport:** plain HTTPS. Parameters go in the query string unless a request body is shown.
- **Placeholders:** anything in `{curly braces}` is a value the integrator supplies.
- **URL-encoding:** values that are themselves URLs (callbacks) must be percent-encoded.
  Values already shown encoded in an example are passed through exactly as-is — re-encoding them breaks the call.
- **Base host:** `api.paygate.to`. A white-label deployment may serve the same paths from its own domain.

## Overview

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 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.

**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 cents**: `balanceCents`, `amountCents`, `tierPriceCents`, `priceCents`, and the `amount` parameter of `wallet.php`, `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`.

## Endpoints

### Account & Session

- [GET Sign Up](#sign-up)
- [GET Sign In](#sign-in)
- [GET Session & Balance](#session-balance)

### Funding the Account

- [GET Create Deposit](#create-deposit)
- [GET Affiliate Deposit](#affiliate-deposit)
- [GET Sub-affiliate Deposit](#sub-affiliate-deposit)
- [GET List Active Deposits](#list-active-deposits)
- [GET Cancel Deposit](#cancel-deposit)
- [GET Account Activity](#account-activity)

### Cards

- [GET List Cards & Pricing](#list-cards)
- [GET Issue Card](#issue-card)
- [GET Load Card](#load-card)
- [GET Unload Card](#unload-card)
- [GET Card Activity](#card-activity)

### Security & Reveal

- [GET TOTP Status](#totp-status)
- [GET TOTP Setup](#totp-setup)
- [GET TOTP Submit](#totp-submit)
- [GET Reveal Card Number](#reveal-card-number)

### Apple Pay & Google Pay

- [GET Arm Wallet Code](#arm-wallet-code)
- [GET Check Wallet Code](#check-wallet-code)

---

## Group: 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.

---

## Sign Up

`GET` — anchor `#sign-up` — group: Account & Session

```http
GET https://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.

**OK — Account created — HTTP 200**

```json
{
    "token": "NBcvfznMlfzL4ey%2BJmhdcdKVJpQBBqIxuOfZ2GFpbD0Orblz7zfywxXN4Te0Jheo%2F5vGoUuXwfrfxOd9zVOcTw%3D%3D",
    "session": "JSgPHmS2RHUFoR5bsBDw2Gklw_NghKvZs0D9nn9AaiU"
}
```

**Example Request — browser side, one retry**

Copyasync 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.

---

## Sign In

`GET` — anchor `#sign-in` — group: Account & Session

```http
GET https://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.

 Copyconst 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.

| Parameter | Example value | Description |
|---|---|---|
| `token` | `NBcvfznMlfzL4ey%2BJmhdcdKVJpQBBqIxuOfZ2GFpbD0Orblz7zfywxXN4Te0Jheo%2F5vGoUuXwfrfxOd9zVOcTw%3D%3D` | Recovery token, URL-encoded |
| `session` | `JSgPHmS2RHUFoR5bsBDw2Gklw_NghKvZs0D9nn9AaiU` | Last known session |

**OK — Signed in — session still valid — HTTP 200**

```json
{
    "token": "NBcvfznMlfzL4ey%2BJmhdcdKVJpQBBqIxuOfZ2GFpbD0Orblz7zfywxXN4Te0Jheo%2F5vGoUuXwfrfxOd9zVOcTw%3D%3D",
    "session": "JSgPHmS2RHUFoR5bsBDw2Gklw_NghKvZs0D9nn9AaiU"
}
```

**Example Request**

GEThttps://api.paygate.to/crypto/cards/reloadable/signin.php?token=NBcvfznMlfzL4ey%2BJmhdcdKVJpQBBqIxuOfZ2GFpbD0Orblz7zfywxXN4Te0Jheo%2F5vGoUuXwfrfxOd9zVOcTw%3D%3D&session=JSgPHmS2RHUFoR5bsBDw2Gklw_NghKvZs0D9nn9AaiUCopy

---

## Session & Balance

`GET` — anchor `#session-balance` — group: Account & Session

```http
GET https://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.

| Parameter | Example value | Description |
|---|---|---|
| `session` | `JSgPHmS2RHUFoR5bsBDw2Gklw_NghKvZs0D9nn9AaiU` | Active session |

**OK — Signed in · balance $6.01 — HTTP 200**

```json
{
    "balanceCents": 601
}
```

**Example Request**

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

---

## Group: Funding the Account

**One deposit at a time.** An account may hold only one deposit in `awaiting` at any moment. While one is outstanding, wallet.php, custom-affiliate.php and custom-sub-affiliate.php will not issue another address — for the same coin or any other — until the outstanding one expires or is cancelled. Call list-active-deposits.php before offering a deposit form, and give the customer cancel-deposit.php as the way out.

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`

| Ticker | Coin | Network |
|---|---|---|
| `btc` | Bitcoin | Bitcoin |
| `xmr` | Monero | Monero |
| `ltc` | Litecoin | Litecoin |
| `eth` | Ethereum | Ethereum |
| `erc20/usdc` | USD Coin | Ethereum (ERC-20) |
| `erc20/usdt` | Tether | Ethereum (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.

---

## Create Deposit

`GET` — anchor `#create-deposit` — group: Funding the Account

```http
GET https://api.paygate.to/crypto/{ticker}/cards/reloadable/wallet.php?session={session}&amount={cents}
```

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.

**`amount` is required on every funding call, in integer US cents, and the minimum is `3000` — $30.00.** Send `3000` for $30.00, `3056` for $30.56, `5000` for $50.00. A decimal such as `30.56` is rejected, and so is anything below the floor, such as `2999` or `2500`. Multiply the dollar figure your customer sees by 100 and round to a whole number before sending it.

 Query Parameters

 - session: active session.

 - amount: the amount to fund, in integer US cents. Required, minimum 3000 ($30.00).

 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.

| Parameter | Example value | Description |
|---|---|---|
| `session` | `3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYc` | Active session |
| `amount` | `5000` | $50.00, in integer US cents. Required, minimum `3000` |

**OK — Deposit intent created — HTTP 200**

```json
{
    "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"
}
```

**Example Request**

GEThttps://api.paygate.to/crypto/xmr/cards/reloadable/wallet.php?session=3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYc&amount=5000Copy

**Rendering the QR**

Copy<img src="data:image/png;base64,{qr_code}" alt="Payment QR Code"/>

**Driving the expiry countdown**

Copy// 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();
}

---

## Affiliate Deposit

`GET` — anchor `#affiliate-deposit` — group: Funding the Account

```http
GET https://api.paygate.to/crypto/{ticker}/cards/reloadable/custom-affiliate.php?session={session}&amount={cents}&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.

 - amount: the amount to fund, in integer US cents. Required, minimum 3000 ($30.00).

 - 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.

| Parameter | Example value | Description |
|---|---|---|
| `session` | `3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYc` | Active session |
| `amount` | `5000` | $50.00, in integer US cents. Required, minimum `3000` |
| `affiliate` | `1FTpXfNvv1obYpLLFHHh8eEK2vaLwFTHQ2` | Your wallet, matching the ticker's network |
| `affiliate_fee` | `0.075` | 7.5% commission |

**OK — Deposit intent created with affiliate split — HTTP 200**

```json
{
    "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"
}
```

**Example Request**

GEThttps://api.paygate.to/crypto/btc/cards/reloadable/custom-affiliate.php?session=3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYc&amount=5000&affiliate=1FTpXfNvv1obYpLLFHHh8eEK2vaLwFTHQ2&affiliate_fee=0.075Copy

---

## Sub-affiliate Deposit

`GET` — anchor `#sub-affiliate-deposit` — group: Funding the Account

```http
GET https://api.paygate.to/crypto/{ticker}/cards/reloadable/custom-sub-affiliate.php?session={session}&amount={cents}&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.

 - amount: the amount to fund, in integer US cents. Required, minimum 3000 ($30.00).

 - 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.

| Parameter | Example value | Description |
|---|---|---|
| `session` | `3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYc` | Active session |
| `amount` | `5000` | $50.00, in integer US cents. Required, minimum `3000` |
| `affiliate` | `0xBEf747A2FB84b6e5D4006B10a3f93cbd45B6E504` | Your wallet |
| `affiliate_fee` | `0.075` | 7.5% |
| `sub_affiliate` | `0x260c3d5a94894b489EBC7912cB70DC5c89d31d97` | Partner wallet |
| `sub_affiliate_fee` | `0.02` | 2% |

**OK — Deposit intent created with two-level split — HTTP 200**

```json
{
    "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"
}
```

**Example Request**

GEThttps://api.paygate.to/crypto/eth/cards/reloadable/custom-sub-affiliate.php?session=3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYc&amount=5000&affiliate=0xBEf747A2FB84b6e5D4006B10a3f93cbd45B6E504&affiliate_fee=0.075&sub_affiliate_fee=0.02&sub_affiliate=0x260c3d5a94894b489EBC7912cB70DC5c89d31d97Copy

---

## List Active Deposits

`GET` — anchor `#list-active-deposits` — group: Funding the Account

```http
GET https://api.paygate.to/crypto/cards/reloadable/list-active-deposits.php?session={session}
```

Returns the deposits this account currently holds in awaiting. Call it before you show a deposit form: an account may hold only one awaiting deposit, so if this returns a non-empty array the funding endpoints will refuse to issue another address until that one expires or is cancelled.

 No address and no QR code are returned here. Those exist only in the response that created the deposit — deliberately, so a stale address can never be shown as if it were live. A customer who leaves the page and comes back can be told which coin is expected and when the window closes, but must cancel and start again to get an address.

 Query Parameters

 - session: active session.

 Response

 A JSON array. It is empty when the session is valid and nothing is awaiting.

 - id: the deposit identifier — pass it to cancel-deposit.php.

 - status: awaiting for as long as the deposit is outstanding.

 - expiresAt: ISO-8601 UTC instant after which the deposit lapses. Render it in the customer's own time zone.

 - payCurrency: the coin the deposit expects, in the flattened form usdterc20 rather than the path ticker erc20/usdt.

| Parameter | Example value | Description |
|---|---|---|
| `session` | `RRpJdT9p74zDjsaux6UC_PT93inc1Ai6fVSajgqYL_0` | Active session |

**OK — Nothing awaiting — HTTP 200**

```json
[]
```

**OK — One deposit awaiting — HTTP 200**

```json
[
  {
    "id": "cmtshqrb0000fqm07uv3p3d78",
    "status": "awaiting",
    "expiresAt": "2026-09-08T10:52:33.128Z",
    "payCurrency": "usdterc20"
  }
]
```

**Example Request**

GEThttps://api.paygate.to/crypto/cards/reloadable/list-active-deposits.php?session=RRpJdT9p74zDjsaux6UC_PT93inc1Ai6fVSajgqYL_0Copy

---

## Cancel Deposit

`GET` — anchor `#cancel-deposit` — group: Funding the Account

```http
GET https://api.paygate.to/crypto/cards/reloadable/cancel-deposit.php?session={session}&id={depositId}
```

Cancels an awaiting deposit and frees the account to create a new one. Pass the id reported by list-active-deposits.php, or the id returned when the deposit was created. Cancelling is the only way to change coin before the window closes on its own.

 Cancel closes the address for good. Anything sent to a cancelled deposit address afterwards is not credited and is not recoverable. Only cancel once the customer has confirmed they have not sent, and are not about to send, to that address.

 Query Parameters

 - session: active session.

 - id: the awaiting deposit's identifier.

 Response

 - ok: true when the deposit was cancelled. A following call to list-active-deposits.php returns [].

| Parameter | Example value | Description |
|---|---|---|
| `session` | `RRpJdT9p74zDjsaux6UC_PT93inc1Ai6fVSajgqYL_0` | Active session |
| `id` | `cmtshqrb0000fqm07uv3p3d78` | The awaiting deposit to cancel |

**OK — Deposit cancelled — HTTP 200**

```json
{"ok":true}
```

**Example Request**

GEThttps://api.paygate.to/crypto/cards/reloadable/cancel-deposit.php?session=RRpJdT9p74zDjsaux6UC_PT93inc1Ai6fVSajgqYL_0&id=cmtshqrb0000fqm07uv3p3d78Copy

---

## Account Activity

`GET` — anchor `#account-activity` — group: Funding the Account

```http
GET https://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.

| Parameter | Example value | Description |
|---|---|---|
| `session` | `3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYc` | Active session |

**OK — Account ledger — HTTP 200**

```json
{
    "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"
        }
    ]
}
```

**Example Request**

GEThttps://api.paygate.to/crypto/cards/reloadable/account-activity.php?session=3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYcCopy

---

## Group: 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.

---

## List Cards & Pricing

`GET` — anchor `#list-cards` — group: Cards

```http
GET https://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.

| Parameter | Example value | Description |
|---|---|---|
| `session` | `3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYc` | Active session |

**OK — Two cards — a funded Aurora and a fresh Eclipse — HTTP 200**

```json
{
  "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 }
}
```

**Example Request**

GEThttps://api.paygate.to/crypto/cards/reloadable/list-cards.php?session=3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYcCopy

---

## Issue Card

`GET` — anchor `#issue-card` — group: Cards

```http
GET https://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.

| Parameter | Example value | Description |
|---|---|---|
| `session` | `3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYc` | Active session |
| `type` | `aurora` | Optional. aurora · shadow · eclipse. Defaults to aurora |

**OK — Card minted — status issuing — HTTP 200**

```json
{
  "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 — Balance below the tier price — HTTP 200**

```json
{
    "error": "insufficient_funds",
    "priceCents": 4500
}
```

**Example Request**

GEThttps://api.paygate.to/crypto/cards/reloadable/issue-card.php?session=3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYc&type=auroraCopy

**Error Response**

Error
 200
 Balance below the tier price

 Copy{
 "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.

---

## Load Card

`GET` — anchor `#load-card` — group: Cards

```http
GET https://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.

| Parameter | Example value | Description |
|---|---|---|
| `amount` | `1500` | Cents debited from the account balance. Minimum 1500 |
| `id` | `cmtnchwyd00aqp207mbotyouw` | Card id |
| `session` | `3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYc` | Active session |

**OK — Loaded — HTTP 200**

```json
{
    "ok": true
}
```

**Error — Balance below the requested amount — HTTP 200**

```json
{
    "error": "insufficient_funds"
}
```

**Example Request**

GEThttps://api.paygate.to/crypto/cards/reloadable/load-card.php?amount=1500&id=cmtnchwyd00aqp207mbotyouw&session=3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYcCopy

**Error Response**

Error
 200
 Balance below the requested amount

 Copy{
 "error": "insufficient_funds"
}

---

## Unload Card

`GET` — anchor `#unload-card` — group: Cards

```http
GET https://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.

| Parameter | Example value | Description |
|---|---|---|
| `id` | `cmtkm8nk600j7lp07gv3lis6a` | Card id |
| `session` | `Z3EkzT4Y1LRdfWgdmKCsOdRAtQ_10K57mf32bSenAxE` | Active session |
| `amount` | `1500` | Cents returned to the account balance. Minimum 1500, capped at unloadableCents |

**OK — Unloaded back to balance — HTTP 200**

```json
{
    "ok": true
}
```

**Example Request**

GEThttps://api.paygate.to/crypto/cards/reloadable/unload-card.php?id=cmtkm8nk600j7lp07gv3lis6a&session=Z3EkzT4Y1LRdfWgdmKCsOdRAtQ_10K57mf32bSenAxE&amount=1500Copy

---

## Card Activity

`GET` — anchor `#card-activity` — group: Cards

```http
GET https://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

 - id: the card id from list-cards.php.

 - session: active session.

 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.

| Parameter | Example value | Description |
|---|---|---|
| `id` | `cmtkm8nk600j7lp07gv3lis6a` | Card id |
| `session` | `Z3EkzT4Y1LRdfWgdmKCsOdRAtQ_10K57mf32bSenAxE` | Active session |

**OK — A reversed foreign refund and a card load — HTTP 200**

```json
{
    "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"
        }
    ]
}
```

**Example Request**

GEThttps://api.paygate.to/crypto/cards/reloadable/card-activity.php?id=cmtkm8nk600j7lp07gv3lis6a&session=Z3EkzT4Y1LRdfWgdmKCsOdRAtQ_10K57mf32bSenAxECopy

---

## Group: 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, and you are 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 with `&setup=1`.

**4. Submit and retry** — send the current code to totp-submit.php without `setup`, then repeat the original call, which now answers.

---

## TOTP Status

`GET` — anchor `#totp-status` — group: Security & Reveal

```http
GET https://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.

| Parameter | Example value | Description |
|---|---|---|
| `session` | `3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYc` | Active session |

**OK — Authenticator already enrolled — HTTP 200**

```json
{
    "ok": true,
    "method": "totp"
}
```

**Setup needed No authenticator enrolled yet — HTTP 200**

```json
{
    "ok": true,
    "method": "totp_setup_required"
}
```

**Example Request**

GEThttps://api.paygate.to/crypto/cards/reloadable/totp-status.php?session=3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYcCopy

---

## TOTP Setup

`GET` — anchor `#totp-setup` — group: Security & Reveal

```http
GET https://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:

 Copyotpauth://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.

| Parameter | Example value | Description |
|---|---|---|
| `session` | `tRUJ_J_OOp34gDVUZywkroKrkokU1TqmvX5TZWp4HC0` | Active session |

**OK — Enrolment secret issued — HTTP 200**

```json
{
    "totp_secret": "BM5BVFLJBWN2XOMM4HKZHI7DRHAHJXH7"
}
```

**Example Request**

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

---

## TOTP Submit

`GET` — anchor `#totp-submit` — group: Security & Reveal

```http
GET https://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.

| Parameter | Example value | Description |
|---|---|---|
| `session` | `Z3EkzT4Y1LRdfWgdmKCsOdRAtQ_10K57mf32bSenAxE` | Active session |
| `code` | `651957` | Current six-digit TOTP code |
| `setup` | `1` | Optional. First-time enrolment confirmation only |

**OK — Enrolled and trusted — HTTP 200**

```json
{
    "ok": true,
    "rotated": false,
    "trusted": true
}
```

**OK — Session stepped up — HTTP 200**

```json
{
    "ok": true,
    "trusted": true
}
```

**Error — setup=1 sent against an enrolled account — HTTP 200**

```json
{
    "error": "already_enabled"
}
```

**Example Request — confirming a new enrolment**

GEThttps://api.paygate.to/crypto/cards/reloadable/totp-submit.php?session=3rzSe-1JuvDX5WdYIXBX7BbSaii8hHQiNSzit9mMGYc&code=988657&setup=1Copy

 OK
 200
 Enrolled and trusted

 Copy{
 "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=651957Copy

 OK
 200
 Session stepped up

 Copy{
 "ok": true,
 "trusted": true
}

**Error Response**

Error
 200
 setup=1 sent against an enrolled account

 Copy{
 "error": "already_enabled"
}

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

---

## Reveal Card Number

`GET` — anchor `#reveal-card-number` — group: Security & Reveal

```http
GET https://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.

| Parameter | Example value | Description |
|---|---|---|
| `id` | `cmtkm8nk600j7lp07gv3lis6a` | Card id |
| `session` | `Z3EkzT4Y1LRdfWgdmKCsOdRAtQ_10K57mf32bSenAxE` | Stepped-up session |

**Step-up Session is not trusted yet — HTTP 200**

```json
{
    "error": "step_up_required",
    "method": "totp"
}
```

**OK — Card credentials — HTTP 200**

```json
{
    "ok": true,
    "pan": "4466148010102479",
    "cvv": "300",
    "expMonth": 1,
    "expYear": 2031,
    "pin": "089136"
}
```

**Example Request**

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

**Error Response — step-up required**

Step-up
 200
 Session is not trusted yet

 Copy{
 "error": "step_up_required",
 "method": "totp"
}

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

**Example — the full optimistic reveal flow**

Copyconst 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
}

---

## Group: 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.

---

## Arm Wallet Code

`GET` — anchor `#arm-wallet-code` — group: Apple Pay & Google Pay

```http
GET https://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.

| Parameter | Example value | Description |
|---|---|---|
| `id` | `cmtkm8nk600j7lp07gv3lis6a` | Card id |
| `session` | `Z3EkzT4Y1LRdfWgdmKCsOdRAtQ_10K57mf32bSenAxE` | Stepped-up session |
| `type` | `apple` | apple or google |

**Step-up Session is not trusted yet — HTTP 200**

```json
{
    "error": "step_up_required",
    "method": "totp"
}
```

**OK — Listener armed — HTTP 200**

```json
{
    "ok": true,
    "status": "arming"
}
```

**Example Request**

GEThttps://api.paygate.to/crypto/cards/reloadable/arm-wallet-code.php?id=cmtkm8nk600j7lp07gv3lis6a&session=Z3EkzT4Y1LRdfWgdmKCsOdRAtQ_10K57mf32bSenAxE&type=appleCopy

**Error Response — step-up required**

Step-up
 200
 Session is not trusted yet

 Copy{
 "error": "step_up_required",
 "method": "totp"
}

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

---

## Check Wallet Code

`GET` — anchor `#check-wallet-code` — group: Apple Pay & Google Pay

```http
GET https://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.

| Parameter | Example value | Description |
|---|---|---|
| `id` | `cmtkm8nk600j7lp07gv3lis6a` | Card id |
| `session` | `Z3EkzT4Y1LRdfWgdmKCsOdRAtQ_10K57mf32bSenAxE` | Active session |
| `type` | `apple` | Must match the armed type |

**Waiting Window open — keep polling — HTTP 200**

```json
{
    "ok": true,
    "status": "arming",
    "armedAt": "2026-09-04T21:09:13.214Z"
}
```

**Timeout Minute elapsed, no code captured — HTTP 200**

```json
{
    "ok": true,
    "status": "timeout",
    "armedAt": "2026-09-04T21:09:21.749Z"
}
```

**Ready Code captured — show it and stop polling — HTTP 200**

```json
{
    "ok": true,
    "status": "ready",
    "code": "641614",
    "expiresAt": "2026-09-04T15:13:40.631Z"
}
```

**Example Request**

GEThttps://api.paygate.to/crypto/cards/reloadable/check-wallet-code.php?id=cmtkm8nk600j7lp07gv3lis6a&session=Z3EkzT4Y1LRdfWgdmKCsOdRAtQ_10K57mf32bSenAxE&type=appleCopy

**Example — arm, then poll every 10 seconds**

Copyconst 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

 - Open Google Wallet: Add to Wallet, then Payment card.

 - Enter the number from Reveal; Google will ask to verify the card.

 - Pick Email — never phone or SMS — then press Get code on your page and wait there.

 - Six digits land on the page: type them into Wallet and tap through.

 Apple Wallet

 - Open Wallet on your iPhone: tap + and Debit or Credit Card.

 - Enter the number from Reveal; Apple will ask to verify the card.

 - Pick Email — never phone or SMS — then press Get code on your page and wait there.

 - Six digits land on the page: type them into Wallet and tap through.

---

# Reference

## Card Object

REFCard 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.

---

## Card Tiers

REFCard 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 doesApple 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 cardPrepaid — the whole balance is spendable, up to $150,000 riding the card.
 LoadsFrom $15 · 5% now on your volume · the ladder eases 5% → 3.5% over your last 30 days.
 CurrencyAny — a non-USD charge converts at the network's lowest regular rate; we add $0.
 Arrives emptyMints 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 & validityA declined attempt costs nothing · valid 2+ years, an email before expiry.

 Shadow

 The essential card

 What it doesEvery 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 cardPrepaid — the whole balance is spendable, up to $150,000 riding the card.
 LoadsFrom $15 · 5% now on your volume · the ladder eases 5% → 3.5% over your last 30 days.
 CurrencyAny — a non-USD charge converts at the network's lowest regular rate; we add $0.
 Arrives emptyMints 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 & validityA declined attempt costs nothing · valid 2+ years, an email before expiry.

 Eclipse

 The ad card

 What it doesBuilt 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 cardPrepaid — the whole balance is spendable, up to $150,000 riding the card.
 LoadsFrom $15 · 3.5%, easing to 2.5% as your last-30-days Eclipse volume grows.
 CurrencyAny — a non-USD charge converts at the network's lowest regular rate; we add $0.
 Arrives emptyMints 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 & validityA 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.

---

## Closed Merchant Categories (MCC)

REFClosed 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

---

## Errors

REFErrors

 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.

---

_End of PayGate.to — Reloadable Virtual Cards API. Hosted documentation: https://paygate.to/docs/virtual-credit-cards-api/reloadable/_
