# Funding via Bank Transfer
Source: https://docs.reap.global/accounts/bank-transfers
How to top up the master collateral account by wire.
Projects enabled for bank transfers can fund the master collateral account with a wire instead of an on-chain deposit. The money reaches Reap through the banking system, and the balance is credited once the bank has settled it.
Applies to [Program-Funded](/program-configuration/funding-models#program-funded) projects only. Talk to Reap to have it enabled on yours.
## Getting the bank details
Call [`GET /fiat-deposits/bank-details`](/api-reference/fiat-deposits/get-bank-details). The response carries the beneficiary name and address, the account number, the bank name and address, the SWIFT code, and the domestic bank and branch codes.
Always quote the returned `reference` on the transfer. Unless your business holds a dedicated account, the one you are given is shared with other Reap customers, and the reference is the only thing that identifies the transfer as yours. A transfer without it may be delayed or returned.
The reference belongs to your business rather than to a single transfer, so it is the same every time.
Send the transfer in the billing currency of your card program. That is the currency the master account is credited in.
Read the bank details fresh before each transfer rather than storing them. A bank can close an account, and a transfer sent to a closed account has to be recalled.
`FIAT_DEPOSITS_NOT_ENABLED` means the capability is off for the project; talk to Reap to have it turned on.
## What happens after you send
The bank settles the transfer, and Reap credits the master account balance. You then receive a [FIAT\_DEPOSIT\_CREATED](/api-reference/fiat-deposit-created) webhook, and the [balance endpoint](/api-reference/accounts/get-account-balance) reflects the new amount.
A bank transfer carries no deposit fee.
Timing depends on the rail and the sending bank. A domestic transfer usually settles the same day. An international one can take two or three business days.
Unlike a crypto deposit, a bank transfer becomes visible to you only once it is settled and credited. There is no earlier status to poll for, so a transfer you have sent but not yet seen a webhook for is still with the banks.
## Reading a deposit
Look one up with [`GET /fiat-deposits/:id`](/api-reference/fiat-deposits/get-fiat-deposit), or list them alongside your other activity through the [activities endpoint](/api-reference/activities/list-activities) with `type=FIAT_DEPOSIT`.
Each deposit reports the transfer twice. `amount` and `currency` are what reached your balance. `originalAmount` and `originalCurrency` are what the bank recorded receiving.
`transactionId` is the bank's own identifier for the transfer. Use it to reconcile a deposit against a bank statement.
## Testing in sandbox
Bank transfers cannot be sent end to end in sandbox, so use [`POST /simulation/fiat-deposits`](/api-reference/simulation/simulate-a-fiat-deposit) to create one. It returns the deposit it recorded, credits the balance, and fires the same webhook a real transfer would.
Sandbox serves placeholder bank details. The account is not real and no wire can reach it, so the endpoint is there to build your integration against, not to send money to. Only the details returned in production identify a live account.
Pass the `amount` to credit and the `currency` you are sending. A currency your project does not accept is rejected with `SIMULATION_UNSUPPORTED_CURRENCY`, listing the ones it does. A sender name is generated unless you pass one, and the reference is left off unless you pass it.
# Funding via Deposits
Source: https://docs.reap.global/accounts/deposits
How to fund an account with on-chain token transfers.
To fund an account, send a supported token to the account's deposit address for the relevant chain. Send only the tokens listed under [Supported Assets](/accounts/supported-assets). Anything else stays in the account without counting toward the spending balance, and [withdrawals](/accounts/withdrawals) cannot move it back out.
In [Program-Funded](/program-configuration/funding-models#program-funded) projects, user accounts do not have deposit addresses. Deposits go to the project's [master collateral account](#master-collateral-funding), which has its own addresses and follows the same flow described below. User-level spending power is set by [posting](/virtual-assets/postings) against each account, not by depositing.
## Deposit flow
When you send a supported token to a deposit address:
1. The system detects the on-chain transfer and creates a deposit in `PENDING` status.
2. Once the transaction is finalized on-chain, the deposit moves to `CONFIRMED`.
3. The deposit is validated.
4. If validation passes, the deposit is `APPROVED` and the account balance is updated. If validation fails, the deposit is `REJECTED`.
You receive a [CRYPTO\_DEPOSIT\_CREATED](/api-reference/crypto-deposit-created) webhook when the deposit is first detected, and a [CRYPTO\_DEPOSIT\_STATUS\_UPDATED](/api-reference/crypto-deposit-status-updated) webhook at each subsequent status change.
`CONFIRMED` means only that the transaction is final on-chain - it does **not** mean the funds have been credited to the account. Crediting happens at `APPROVED`, after validation passes. Do not treat `CONFIRMED` as available balance; always rely on the [balance endpoint](/api-reference/accounts/get-account-balance) or wait for `APPROVED`.
### Minimum deposit amount
Deposits below **USD 0.50** are rejected with a `REJECTED` status. This threshold exists to prevent dust deposits. We recommend filtering out rejected deposits before displaying deposit history to your users, as low-value rejections add noise to the UI.
## Master collateral funding
In [Program-Funded](/program-configuration/funding-models#program-funded) projects, the master collateral account is provisioned at project setup. It is the only crypto-holding account in the project, and every authorized card transaction across the program is settled against it. Keep it funded above expected outstanding spend.
To find the master account and its deposit addresses, call [`GET /accounts?ownerType=PROJECT`](/api-reference/accounts/list-accounts). The response includes deposit addresses per enabled chain, the same as for user accounts in User-Funded projects. The deposit flow above applies unchanged: send a supported stablecoin, wait for the [CRYPTO\_DEPOSIT\_STATUS\_UPDATED](/api-reference/crypto-deposit-status-updated) webhook, and the balance is available once approved.
A master deposit is `APPROVED` once the funds are credited and available to back card spend. That lands shortly after on-chain finality, so expect a short gap between the `CONFIRMED` and `APPROVED` webhooks.
The master account can also be funded by wire. See [Funding via Bank Transfer](/accounts/bank-transfers).
### Deposit fee
Master deposits can carry a deposit fee, set per project in basis points and agreed with Reap. The fee is charged on the deposited amount, and the master account balance is credited net of it.
Each deposit reports what it was charged in `feeAmount`, in the same token as `amount`. A project with no deposit fee reports `0`. Deposits into user accounts are never charged a deposit fee and report `null`.
The fee Reap has charged on this account's deposits appears under `liabilities` on the [balance endpoint](/api-reference/accounts/get-account-balance), so the available balance already accounts for it.
### Per-asset minimum for master deposits
Master deposits enforce a higher floor than user deposits:
| Asset | Minimum per deposit |
| - | - |
| `USDC` | 2 USD |
Deposits below the floor are `REJECTED`. The floor is per deposit, not per balance, so a series of smaller deposits is rejected even if their total is above the threshold.
## Checking deposit status
Look up a deposit directly using the [deposit endpoint](/api-reference/crypto-deposits/get-crypto-deposit). Once a deposit is approved, the [balance endpoint](/api-reference/accounts/get-account-balance) will reflect the updated balance.
## Testing in sandbox
Deposit testing requires real on-chain transactions. There is no simulation shortcut for the deposit flow. You send actual tokens on testnet, and the system processes them end to end.
To get sandbox tokens funded to your test wallet, reach out to your Reap contact. See [Supported Assets](/accounts/supported-assets) for the token contract addresses on the supported chains.
### Scenario 1: Successful deposit
1. [Create an account](/api-reference/accounts/create-account) and retrieve its deposit address for all supported chains.
2. Send RUSDC from your funded test wallet to that address.
3. Confirm you receive a [CRYPTO\_DEPOSIT\_CREATED](/api-reference/crypto-deposit-created) webhook.
4. Wait for [CRYPTO\_DEPOSIT\_STATUS\_UPDATED](/api-reference/crypto-deposit-status-updated) webhooks as the deposit progresses. The first will show `status: CONFIRMED` (the transaction has finalized on-chain). The second will show `status: APPROVED` (it has cleared validation and the balance has updated).
5. Check the [balance endpoint](/api-reference/accounts/get-account-balance). It should reflect the deposited amount.
### Scenario 2: Unsupported token
1. Send native ETH or SOL to the same deposit address.
2. Confirm you receive a [CRYPTO\_DEPOSIT\_CREATED](/api-reference/crypto-deposit-created) webhook confirming the transfer was detected.
3. Confirm a [CRYPTO\_DEPOSIT\_STATUS\_UPDATED](/api-reference/crypto-deposit-status-updated) webhook with `status: REJECTED`. Unsupported tokens are rejected immediately on detection, without going through on-chain finalization.
4. Check the [assets endpoint](/api-reference/accounts/get-account-assets) to confirm the token appears there, and check the [balance endpoint](/api-reference/accounts/get-account-balance) to confirm it is unchanged.
# Overview
Source: https://docs.reap.global/accounts/overview
How accounts work and how their balance is computed.
An account is the unit of spending in Reap. Every card is issued against an account, every authorization is evaluated against an account's balance, and every settlement clears debt on an account.
## Ownership
Each account belongs to a single owner. You choose the owner type when you [create the account](/api-reference/accounts/create-account). Your project's [program mode](/program-configuration/modes) constrains which types are allowed.
* **USER ownership.** Each account belongs to a verified individual user with their own isolated balance. The default, and the only option for consumer programs. Also available in corporate programs for per-user spending.
* **COMPANY ownership.** Each account belongs to a verified company with a single pooled balance shared across all cardholders under that company. Only available in corporate programs.
Individual cardholders always require an approved KYC application before a card can be issued to them, even when the balance lives at the company level.
## The signer key
In [User-Funded](/program-configuration/funding-models#user-funded) projects, [creating an account](/api-reference/accounts/create-account) requires a `signers` entry for each chain family the project has enabled. You prove ownership of each key by signing a message from [Generate signer message](/api-reference/accounts/generate-signer-message).
That key is what authorizes money leaving the account later. Reap holds one key to the account's wallets and the signer you register is the other, so [withdrawals](/accounts/withdrawals) need a signature from both. Nothing about deposits or card spend depends on it.
You decide whose key it is. A key held in your backend lets you withdraw on the user's behalf, while a key in the user's own wallet means no withdrawal can happen without them. [Who holds the signing key](/accounts/withdrawals#who-holds-the-signing-key) covers the tradeoff.
The curve differs per chain family, so one key cannot cover both. An account on EVM chains and Solana needs two, and Reap cannot replace a key the owner loses.
## Account status
An account is `ACTIVE` or `RESTRICTED`. Reap restricts an account when its owner's compliance state requires it, for example a rejected application or a case under compliance review. The [`ACCOUNT_STATUS_UPDATED`](/api-reference/account-status-updated) webhook fires on every change, and Reap lifts the restriction once the underlying issue is resolved.
A restricted account declines every card authorization. Nothing else about it changes. The cards on it keep their own statuses, and you can still freeze a card, set a PIN, or display card details. A card can therefore report `status: ACTIVE` while its account is restricted, and declines carry the `ACCOUNT_NOT_ACTIVE` [decline reason](/transactions/decline-reasons). If you show cardholders whether a card can be used right now, read the account status alongside the [card status](/cards/overview#card-statuses).
## How balance is computed
Every account has a balance made up of three numbers:
```
availableBalance = assets - liabilities
```
* **Assets** is what backs spending. The shape depends on your [funding model](/program-configuration/funding-models). In User-Funded projects it is stablecoin deposits held in the account's wallets. In Program-Funded projects it is the Virtual Asset balances you allocate (Managed mode); under [External authorization](/transactions/external-authorization/overview) no per-user asset balance is held in Reap.
* **Liabilities** is outstanding card spend: authorizations on hold and cleared amounts that have not yet been settled. On a master collateral account it also includes any [deposit fees](/accounts/deposits#deposit-fee) charged on deposits into that account.
* **Available balance** is what the cardholder can actually use at the moment of a card swipe.
Call [Get account balance](/api-reference/accounts/get-account-balance) at any time to read these values. `availableBalance` is the number you surface in your product when showing a user how much they have loaded on their card.
## Typical funding flow
1. [Create an account](/api-reference/accounts/create-account) with the owner type, owner ID, and a [signer key](#the-signer-key) per enabled chain family. You get back a deposit address for each enabled chain.
2. Share the deposit addresses with your user. They send supported stablecoins. See [Funding via Deposits](/accounts/deposits).
3. Track deposits via the [CRYPTO\_DEPOSIT\_CREATED](/api-reference/crypto-deposit-created) and [CRYPTO\_DEPOSIT\_STATUS\_UPDATED](/api-reference/crypto-deposit-status-updated) webhooks.
4. Once a deposit is approved, the balance updates automatically. Check it via [Get account balance](/api-reference/accounts/get-account-balance).
5. To move funds back out to an external address, see [Withdrawing Funds](/accounts/withdrawals).
1. Your project has a project-level master collateral account, provisioned at setup. Fund it with stablecoin deposits. See [Funding via Deposits](/accounts/deposits).
2. [Create an account](/api-reference/accounts/create-account) for each verified user or company. Accounts in Program-Funded do not have their own deposit addresses.
3. [Post deposits, withdrawals, and settlements](/virtual-assets/postings) against each account to keep its Virtual Asset balances in sync with what the user has on your side.
4. Call [Get account balance](/api-reference/accounts/get-account-balance) at any time. Balance updates as you allocate and as the user spends.
## Assets breakdown
The [assets endpoint](/api-reference/accounts/get-account-assets) shows what is held in the account, broken down per chain and per token (or per Virtual Asset symbol in Program-Funded projects).
Only approved balances are included. Deposits that are still being confirmed on chain do not count toward `assets` or `availableBalance`. They surface in the deposit and activity feeds until approval, at which point both values update.
## Activity
The [activity feed](/api-reference/activities/list-activities) gives a chronological record of all fund movements on an account: deposits, withdrawals, card transactions, virtual asset postings, and more. Filter by account or activity type to scope it to a specific user, or use it to power a transaction history view in your product.
# Supported Assets
Source: https://docs.reap.global/accounts/supported-assets
Which tokens and chains are accepted for deposits.
Only supported tokens are accepted for deposits. Sending any other token, including native chain tokens like ETH or SOL, will not count toward the spending balance.
The tables below cover every asset the platform supports. Your project accepts a subset of them. [`GET /accounts/:id`](/api-reference/accounts/get-account) names the assets each of your deposit addresses takes, so read the list from there rather than hardcoding one.
Which chains apply depends on your [funding model](/program-configuration/funding-models). Each table below marks the model it serves.
| Funding model | Where deposits land | Chains |
| - | - | - |
| [User-Funded](/program-configuration/funding-models#user-funded) | Each user's own deposit addresses | Base, Polygon, Solana |
| [Program-Funded](/program-configuration/funding-models#program-funded) | Your master collateral account | Ethereum, Polygon, Solana |
Ethereum serves master collateral deposits only. Base serves user deposits only.
A token address is only meaningful together with its chain. The same address can be a different token on another chain, which is the case for the sandbox tokens below. Always match on chain and address together, and never route by address alone.
Production uses standard USDC issued by Circle and USDT issued by Tether.
**Base** (User-Funded)
| Token | Symbol | Standard | Contract Address |
| - | - | - | - |
| USD Coin | USDC | ERC-20 | `0x833589fcd6edb6e08f4c7c32d4f71b54bda02913` |
**Ethereum** (Program-Funded)
| Token | Symbol | Standard | Contract Address |
| - | - | - | - |
| USD Coin | USDC | ERC-20 | `0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48` |
| Tether USD | USDT | ERC-20 | `0xdac17f958d2ee523a2206206994597c13d831ec7` |
**Polygon** (both models)
| Token | Symbol | Standard | Contract Address |
| - | - | - | - |
| USD Coin | USDC | ERC-20 | `0x3c499c542cef5e3811e1192ce70d8cc03d5c3359` |
| Tether USD | USDT | ERC-20 | `0xc2132d05d31c914a87c6611c10748aeb04b58e8f` |
**Solana** (both models)
| Token | Symbol | Standard | Mint Address |
| - | - | - | - |
| USD Coin | USDC | Token | `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` |
| Tether USD | USDT | Token | `Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB` |
The sandbox uses Reap-issued test tokens on public testnets. RUSDC and RUSDT are functionally equivalent to USDC and USDT but exist solely for integration testing. Contact the Reap team to receive test tokens for your wallet.
**Base Sepolia** (User-Funded)
| Token | Symbol | Standard | Contract Address |
| - | - | - | - |
| Reap USD Coin | RUSDC | ERC-20 | `0xeB9Cb47c97ecff4875a1f29A8345818BC0eDc30E` |
**Ethereum Sepolia** (Program-Funded)
| Token | Symbol | Standard | Contract Address |
| - | - | - | - |
| Reap USD Coin | RUSDC | ERC-20 | `0x8868B2867e744Be555d3456238330467343652e1` |
| Reap Tether USD | RUSDT | ERC-20 | `0xeB9Cb47c97ecff4875a1f29A8345818BC0eDc30E` |
`0xeB9Cb47c97ecff4875a1f29A8345818BC0eDc30E` is RUSDC on Base Sepolia and RUSDT on Ethereum Sepolia. The two testnet deployments reuse each other's addresses across the two symbols. This is intentional and not a documentation error, so treat chain and address as one key when you resolve a token.
**Polygon Amoy** (both models)
| Token | Symbol | Standard | Contract Address |
| - | - | - | - |
| Reap USD Coin | RUSDC | ERC-20 | `0x00D4e14024d8AbC6D6D35708b5bEC65B2f28e6E3` |
| Reap Tether USD | RUSDT | ERC-20 | `0x0aAACf028519E30A88683222B00f4c7A8B5913CB` |
**Solana Devnet** (both models)
| Token | Symbol | Standard | Mint Address |
| - | - | - | - |
| Reap USD Coin | RUSDC | Token-2022 | `BAkGvYZjgR3u2zcphn2LLtXaWq7aDRBH27ipbV7uDZeU` |
| Reap Tether USD | RUSDT | Token-2022 | `B2DPuMqUAXvzbsBXsWkVyLH1exXqp8aRojRL3b7zVXrp` |
We are actively expanding our supported chains and tokens. If a chain or asset you need is not listed here, reach out to your Reap contact.
# Withdrawing Funds
Source: https://docs.reap.global/accounts/withdrawals
How to move tokens out of an account wallet to an external address.
A withdrawal sends tokens from an account's wallet to any address you name. Reap covers the network fee and charges a flat fee per chain, so you do not have to hold gas on any chain or manage a signing pipeline of your own.
Withdrawals apply to [User-Funded](/program-configuration/funding-models#user-funded) projects, where each account holds its funds in its own wallets. In [Program-Funded](/program-configuration/funding-models#program-funded) projects the user's funds sit on your side, so you record a withdrawal by [posting](/virtual-assets/postings) against the account instead.
## How a withdrawal works
Every withdrawal needs a signature from the wallet owner. Reap holds one key and you or your user holds the other. Nothing moves on chain without both, so neither side can withdraw alone.
The key is the [signer you registered](/accounts/overview#the-signer-key) when you created the account. Each chain family has its own curve, so an account on both EVM chains and Solana carries two signer keys and each one signs only its own withdrawals.
Call the [assets endpoint](/api-reference/accounts/get-account-assets). Each chain reports a `withdrawable` cap and the `withdrawalFee` for that chain.
[Initiate crypto withdrawal](/api-reference/crypto-withdrawals/initiate-crypto-withdrawal) with the account, chain, asset, gross amount, and destination address. Reap reserves the funds and returns the withdrawal in `PENDING_SIGNATURE`. An [idempotency key](/api-reference/idempotency) is required.
[Mint signing payload](/api-reference/crypto-withdrawals/mint-signing-payload) returns `payloadToSign`, the `executionId` that identifies it, and `validUntil`.
The wallet owner signs `payloadToSign` with their registered key.
[Submit withdrawal signature](/api-reference/crypto-withdrawals/submit-withdrawal-signature) with the signature and the `executionId` you were given. The withdrawal moves to `PROCESSING` and Reap broadcasts the transfer.
`transactionId` appears once the transfer reaches the network. The withdrawal reaches `COMPLETED` when the transfer is irreversible on chain.
```mermaid theme={null}
sequenceDiagram
participant You as Your backend
participant Reap
participant Owner as Wallet owner
You->>Reap: Initiate withdrawal
Reap-->>You: PENDING_SIGNATURE, funds reserved
You->>Reap: Fetch signing payload
Reap-->>You: payloadToSign, validUntil
You->>Owner: Relay payload
Owner-->>You: Signature
You->>Reap: Submit signature
Reap-->>You: PROCESSING
Reap-->>You: COMPLETED
```
## Signing
You do not need to parse `payloadToSign`. Forward it to the wallet exactly as it arrives and sign that string. Do not re-encode, hash, or trim it.
**EVM chains**
```typescript theme={null}
const signature = await walletClient.signMessage({ account, message: payloadToSign });
```
**Solana**
```typescript theme={null}
const signature = bs58.encode(
nacl.sign.detached(Buffer.from(payloadToSign), keypair.secretKey)
);
```
### Who holds the signing key
You choose whether your backend signs or the end user does. The API, the payload, and the code above are the same either way, so this is a product decision rather than an integration one. Reap holds the other key in both cases.
You register a key you control and sign every withdrawal server-side. Your own rules decide what leaves an account. That is what you want when withdrawals run on a schedule, clear an internal approval step, or have to work while the user is offline. The user never sees a prompt, so a withdrawal finishes in one call from your product.
The tradeoff is concentration. One key authorizes every account, so whoever holds it can withdraw from all of them. Keep it in a KMS or HSM and treat access to it as access to client funds.
The key lives in the user's wallet and you relay `payloadToSign` to their device. Nothing leaves an account without the person. A breach of your systems cannot move funds on its own, and the user holds a real veto over each withdrawal. This is the closer fit when you present the wallet as the user's own.
The tradeoff is that the user has to be there. Scheduled and automated withdrawals stop being possible. Every prompt costs you some drop-off, and you own the recovery story for a user who loses their key.
### Payloads are short-lived
A payload commits chain resources that other traffic can take, so `validUntil` is a few minutes out. Fetch a payload immediately before you sign it rather than at the start of a session.
Each fetch retires the previous payload. Only the newest one can be submitted, which is why you pass back the `executionId` you were handed: a signature over a retired payload is rejected and you fetch a fresh one. Fetching a payload never affects the withdrawal itself, so you can do it as often as you need inside the signature window.
### Verify before you sign
`payloadToSign` is readable JSON, not a hash. Your backend can decode it and check the destination, asset, and amount match the withdrawal you requested before relaying it to a user. On EVM the transfer sits in the typed data under `parameters.payload`; on Solana it is a hex-serialized transaction under `parameters.unsignedTransaction`, which decodes with `@solana/web3.js`.
The signature covers those exact bytes. Change one byte and it no longer verifies, so what you inspect is what executes.
## Fees and amounts
The amount you request is the gross. The fee comes out of it rather than on top, so `netAmount` is what the destination receives and the gross is what leaves the account.
| Chain | Fee | Minimum withdrawal |
| - | - | - |
| Base | 0.30 USD | 5 USD |
| Polygon | 0.30 USD | 5 USD |
| Solana | 0.75 USD | 5 USD |
The fee is flat because the cost behind it is flat. Sending a transfer costs the same whatever it carries, so charging a percentage would bill a large withdrawal for work nobody did.
Solana costs more despite its cheap network fees. A first transfer to a destination has to open a token account for it, and that account needs a rent deposit in SOL that Reap funds and does not get back.
Each withdrawal reports the fee it was charged in `fee.amount`, fixed when you initiated it. The [assets endpoint](/api-reference/accounts/get-account-assets) reports the current fee per chain in the asset you hold, so you can show a user what a withdrawal will cost before they commit to one.
### The withdrawable cap
`withdrawable` on the assets endpoint is the largest gross amount you can request on that chain. It nets out outstanding card spend, including authorizations still on hold. A withdrawal therefore cannot take the [available balance](/accounts/overview#how-balance-is-computed) below zero. It reads zero while the account is not `ACTIVE`.
Two things to keep in mind:
* It is a per-chain figure that assumes nothing else is withdrawn, so the caps across chains cannot all be taken at once.
* It is advisory and recomputed on every request. The authoritative check happens when you initiate, and a card authorization in between can move it.
## Statuses
| Status | What it means |
| - | - |
| `PENDING_SIGNATURE` | Funds are reserved and Reap is waiting for the owner signature |
| `PROCESSING` | Signed and executing on chain |
| `COMPLETED` | The transfer is irreversible on chain |
| `EXPIRED` | The signature window closed and Reap released the funds |
| `CANCELLED` | You cancelled it and Reap released the funds |
| `FAILED` | The withdrawal did not go through, see `failureReason` |
You receive a [CRYPTO\_WITHDRAWAL\_CREATED](/api-reference/crypto-withdrawal-created) webhook when the withdrawal is created, and a [CRYPTO\_WITHDRAWAL\_STATUS\_UPDATED](/api-reference/crypto-withdrawal-status-updated) webhook at every status change after that. You can also read a withdrawal at any time with [Get crypto withdrawal](/api-reference/crypto-withdrawals/get-crypto-withdrawal), and withdrawals appear in the [activity feed](/api-reference/activities/list-activities) alongside deposits and card transactions.
Reserved funds stop backing card spend for as long as the withdrawal is open. They return to the balance on `EXPIRED`, `CANCELLED`, and `FAILED`.
`PROCESSING` with a `transactionId` means the transfer is on the network, not that it has landed. Treat only `COMPLETED` as final.
### Failures
`failureReason` says what went wrong:
* `TRANSACTION_FAILED`: the transfer did not go through on chain. The funds never left the account.
* `ACCOUNT_RESTRICTED`: Reap restricted the account while the withdrawal was open. See [account status](/accounts/overview#account-status).
* `REJECTED_BY_COMPLIANCE`: the destination did not clear screening. Reap pauses withdrawals on the account and reviews it. Further withdrawals are refused until that review finishes.
## The signature window
A withdrawal stays signable for **15 minutes** from the moment you initiate it, reported as `expiresAt`. After that it moves to `EXPIRED` and Reap releases the funds.
The window is short because an unsigned withdrawal costs the account something for nothing. The reserved funds no longer back card spend, and one open withdrawal per wallet and chain blocks the next. Signing normally follows initiation within the same session. If a user walks away, let the withdrawal expire or [cancel](/api-reference/crypto-withdrawals/cancel-crypto-withdrawal) it. Initiating another one costs nothing.
Cancelling works only while the withdrawal is awaiting a signature. Once it is signed and broadcasting there is nothing to cancel.
Withdrawals are irreversible. Reap cannot recover funds sent to a wrong address, and the destination must be a valid address on the chain you named. Check the address with your user before you initiate.
## Testing in sandbox
The flow works end to end on testnets with the test tokens listed under [Supported Assets](/accounts/supported-assets). Fund an account, then withdraw to any address you control on the same chain and watch for the status webhooks. Reap pays the testnet gas, the same as in production.
# FAQ
Source: https://docs.reap.global/agentic-payments/faq
Short answers to the questions that come up about the agentic payments roadmap
Roadmap priorities can change, so contact us before committing to a build that depends on anything below.
Yes. If your agent uses its own catalog or search, use the custom checkout URL flow and contact us to enable it with the merchant integrations you need.
It is planned shortly after the current webhook work and will be enabled per account, so contact us once it is live.
Merchant coverage changes as integrations launch, so contact us with the merchants you need and we will confirm what is live or in progress.
Yes, but both require production approval from Visa and our internal teams, so start the review early.
Not yet. MCP client builds remain in sandbox for now, so share your timeline and we will tell you how it aligns with ours.
## Next steps
Store a card and confirm it is ready to charge.
Run a purchase end to end.
# How It Works
Source: https://docs.reap.global/agentic-payments/how-it-works
The actors and resources behind a single agentic purchase
An agentic purchase runs in two phases. Setup happens once per card. The purchase flow then repeats for every order against that stored card.
## Who does what
* The user is the cardholder who stores a card and approves each charge on a hosted page
* The client is you, and your agent chooses what to buy and drives the API calls
* Reap stores the card and prices the order with the merchant, then completes the checkout and reports the outcome
* The merchant is the seller that supplies the catalog and the current price, including the shipping options and the tax
## The two phases
```mermaid theme={null}
flowchart LR
subgraph setup ["Setup, once per card"]
A[Create enrollment] --> B[User enters the card]
end
subgraph purchase ["Purchase, once per order"]
C[Search products] --> D[Price the order] --> E[Create checkout] --> F[User approves] --> G[Order confirmed]
end
B -- enrollment ready to charge --> C
G -. next order .-> C
```
Setup runs once and the enrollment stays reusable. The purchase phase then repeats for every later order.
## One purchase in detail
```mermaid theme={null}
sequenceDiagram
participant User
participant Agent as Your agent
participant Reap
participant Merchant
User->>Agent: Ask for a purchase
Agent->>Reap: Search products and pick one
Reap-->>Agent: Products with prices
Agent->>Reap: Create a quote and select shipping
Reap->>Merchant: Open the merchant checkout
Merchant-->>Reap: Shipping options and totals
Reap-->>Agent: Quote with the final amount
Agent->>Reap: Create a checkout for the quote and the enrollment
Reap-->>Agent: Approval URL
Agent->>User: Send the user to the approval page
User->>Reap: Approve the charge
Reap->>Merchant: Place the order
Reap-->>User: Return the user to your return URL
Agent->>Reap: Read the checkout
Reap-->>Agent: Final amount
```
The search line and the quote line each stand for more than one call. [One-Time Purchases](/agentic-payments/one-time-purchases) shows every request and response in order.
## Enrollment
An enrollment stores one card and returns an ID. Create one with [`POST /agentic/enrollments`](/api-reference/agentic/create-enrollment) and pass that ID to checkout whenever you charge the card. You can charge only an enrollment with status `ACTIVE`.
Three sources store a card, and every source takes an owner and a return URL. The user then finishes the enrollment on a page hosted by Reap.
* A Reap card uses `source` set to `REAP_CARD` with the card ID
* A BIN sponsor card uses `source` set to `BIN_SPONSOR` with the public token ID
* An external card uses `source` set to `EXTERNAL`, and the user enters the card on the hosted page
Every enrollment belongs to an owner. Use `REAP_USER` for a Reap cardholder and `CLIENT_REFERENCE` for a customer record in your own system. Every call to [`GET /agentic/enrollments`](/api-reference/agentic/list-enrollments) covers a single owner, so keep the owner reference you sent.
[Setup](/agentic-payments/setup) walks through each source with a request and a response.
## Mandate
A mandate records the terms the user approved for one enrollment. It holds the approved amount, the charge frequency, the maximum number of charges, and the merchant scope. Read it with [`GET /agentic/mandates/:id`](/api-reference/agentic/get-mandate).
The approved amount acts as a ceiling. The final quote total has to stay at or below that amount for the charge to run under the same approval. You can pause, resume, and cancel a mandate. [Lifecycle and Statuses](/agentic-payments/lifecycle) covers each operation.
## Product search
Product search reads merchant catalogs and turns what the user chose into the variant a quote needs.
* [`POST /agentic/products/search`](/api-reference/agentic/search-products) returns matching products with a price range and one preview variant
* [`POST /agentic/products/details`](/api-reference/agentic/get-product-details) expands a product into its description, media, and option groups
* [`POST /agentic/products/variant`](/api-reference/agentic/resolve-variant) turns the selected options into one purchasable variant
Search returns a price range rather than one price, because shipping and tax arrive with the quote. [One-Time Purchases](/agentic-payments/one-time-purchases) shows each call with a request and a response.
## Quote
[`POST /agentic/quotes`](/api-reference/agentic/create-quote) opens the merchant checkout for the selected items and returns pricing from the merchant. It gives you the available shipping options. It also returns an itemized total with subtotal, shipping, tax, discounts, and the final amount.
A quote accepts a variant rather than a product, so resolve the chosen options to a variant first. Every quote carries an `expiresAt` value. Open the checkout before that time, and create a new quote once it passes.
## Checkout
[`POST /agentic/checkouts`](/api-reference/agentic/create-checkout) charges an `ACTIVE` enrollment for an unexpired quote. The response tells you what to do next.
* When `nextAction` is present, send the user to `nextAction.url` to approve the charge
* When the user returns to your return URL, read the checkout with [`GET /agentic/checkouts/:id`](/api-reference/agentic/get-checkout) to get `orderId` and `finalAmount`
A checkout cannot open against an enrollment outside `ACTIVE`. Read the enrollment with [`GET /agentic/enrollments/:id`](/api-reference/agentic/get-enrollment) before you charge it. Create a new enrollment when the stored card is revoked or expired.
## Idempotency
Send an [`Idempotency-Key`](/api-reference/idempotency) header on every call that creates something. [`POST /agentic/enrollments`](/api-reference/agentic/create-enrollment), [`POST /agentic/quotes`](/api-reference/agentic/create-quote), and [`POST /agentic/checkouts`](/api-reference/agentic/create-checkout) all require it, and a call that omits the header is rejected. A retry with the same key replays the first response instead of storing a second card, creating a second quote, or running a second charge.
Send `Reap-Version` on every request. See [versioning](/api-reference/versioning) for how versions change.
## Next steps
Store a card and confirm it is ready to charge.
Run the purchase phase end to end.
# Lifecycle and Statuses
Source: https://docs.reap.global/agentic-payments/lifecycle
What each status means and which operations change it
Every agentic resource reports a `status`. This page maps each value to the action it calls for, and covers the operations that move a resource from one value to the next.
## Enrollment
```mermaid theme={null}
stateDiagram-v2
[*] --> REQUIRES_ACTION: create enrollment for a new card
[*] --> ACTIVE: create enrollment for an already issued card
REQUIRES_ACTION --> ACTIVE: user enters the card
ACTIVE --> REVOKED: revoke enrollment
REVOKED --> [*]
```
| Status | What it means | What to do |
| - | - | - |
| `REQUIRES_ACTION` | A hosted step is outstanding | Send the user to `nextAction.url` before `expiresAt`, then read the enrollment again |
| `ACTIVE` | The card is stored and ready | Use the enrollment ID at checkout |
| `REVOKED` | The stored card no longer accepts charges | Create a new enrollment for a replacement card |
| `FAILED` | Reap could not store the card | Create a new enrollment and send the user through the hosted step again |
| `EXPIRED` | The user did not finish the hosted step before `expiresAt` | Create a new enrollment and send the user to the new `nextAction.url` |
An enrollment created from a Reap card or a BIN sponsor card can reach `ACTIVE` without a hosted step. An external card always passes through the hosted card entry page.
### Revoke an enrollment
Revoke a stored card with [`POST /agentic/enrollments/:id/revoke`](/api-reference/agentic/revoke-enrollment) when the user removes it or when the card should stop working.
```bash Request theme={null}
curl --request POST \
--url https://sandbox.api.reap.global/agentic/enrollments/:id/revoke \
--header 'Authorization: Bearer ' \
--header 'Reap-Version: '
```
```json Response theme={null}
{
"id": "",
"status": "REVOKED",
"owner": {
"type": "CLIENT_REFERENCE",
"id": "",
"email": "jsmith@example.com"
},
"paymentMethod": {
"type": "CARD",
"network": "",
"last4": "4242",
"expiryMonth": 11,
"expiryYear": 2029
},
"nextAction": null,
"createdAt": "2026-01-01T00:00:00Z",
"updatedAt": "2026-01-01T00:00:00Z"
}
```
Revoking is final. You cannot charge or restore a revoked enrollment. You will need to create a new enrollment to use the same card.
## Mandate
```mermaid theme={null}
stateDiagram-v2
[*] --> REQUIRES_ACTION: create mandate
REQUIRES_ACTION --> ACTIVE: user approves the mandate
ACTIVE --> PAUSED: pause
PAUSED --> ACTIVE: resume
ACTIVE --> CANCELLED: cancel
PAUSED --> CANCELLED: cancel
ACTIVE --> REVOKED: revoke the enrollment
PAUSED --> REVOKED: revoke the enrollment
CANCELLED --> [*]
REVOKED --> [*]
```
| Status | What it means | What to do |
| - | - | - |
| `REQUIRES_ACTION` | The mandate is not approved yet | Complete the step in `nextAction`, then read the mandate again |
| `ACTIVE` | Charges can run under the approved mandate | Charge inside `amount`, `maxCharges`, and `merchantScope` |
| `PAUSED` | Charges are on hold and the mandate is intact | Resume the mandate to continue |
| `CANCELLED` | You ended the mandate and the stored card stays usable | Collect a new mandate from the user |
| `REVOKED` | The enrollment behind the mandate was revoked | Create a new enrollment, then collect a new mandate |
`CANCELLED` and `REVOKED` are both end states, and neither returns to `ACTIVE`. The difference is scope. Canceling ends one mandate and leaves the stored card ready for a new one. Revoking the enrollment stops every mandate that points at that card.
### Pause a mandate
[`POST /agentic/mandates/:id/pause`](/api-reference/agentic/pause-mandate) holds charges and keeps the approved mandate.
```bash Request theme={null}
curl --request POST \
--url https://sandbox.api.reap.global/agentic/mandates/:id/pause \
--header 'Authorization: Bearer ' \
--header 'Reap-Version: '
```
```json Response theme={null}
{
"id": "",
"status": "PAUSED",
"enrollmentId": "",
"frequency": "WEEKLY",
"maxCharges": 12,
"merchantScope": "LISTED",
"amount": { "amount": 150, "currency": "USD" }
}
```
### Resume a mandate
```bash Request theme={null}
curl --request POST \
--url https://sandbox.api.reap.global/agentic/mandates/:id/resume \
--header 'Authorization: Bearer ' \
--header 'Reap-Version: '
```
[`POST /agentic/mandates/:id/resume`](/api-reference/agentic/resume-mandate) returns `status` as `ACTIVE` with the same mandate. Resuming needs no further approval from the user, because nothing about the mandate changed.
### Cancel a mandate
```bash Request theme={null}
curl --request POST \
--url https://sandbox.api.reap.global/agentic/mandates/:id/cancel \
--header 'Authorization: Bearer ' \
--header 'Reap-Version: '
```
[`POST /agentic/mandates/:id/cancel`](/api-reference/agentic/cancel-mandate) returns `status` as `CANCELLED`. The enrollment stays untouched and remains available for a new purchase.
Pause and resume suit a temporary hold. Cancel ends the mandate for good. Revoking the enrollment is the wider action, because it stops charges across every mandate that points at the same stored card, and those mandates report `REVOKED` rather than `CANCELLED`.
## Quote
A quote has no status field. It has an expiry instead.
* Read `expiresAt` on every quote and open the checkout before that time
* Create a new quote with [`POST /agentic/quotes`](/api-reference/agentic/create-quote) once the expiry has passed, because merchant prices can move in between
* Read the quote at any time with [`GET /agentic/quotes/:id`](/api-reference/agentic/get-quote) to get the current total
## Checkout
```mermaid theme={null}
stateDiagram-v2
[*] --> REQUIRES_ACTION: create checkout
[*] --> PROCESSING: create checkout under an approved mandate
REQUIRES_ACTION --> PROCESSING: user approves the charge
REQUIRES_ACTION --> EXPIRED: no approval before expiresAt
PROCESSING --> COMPLETED: the merchant places the order
PROCESSING --> FAILED: the charge or the order does not go through
COMPLETED --> [*]
FAILED --> [*]
EXPIRED --> [*]
```
| Status | What it means | What to do |
| - | - | - |
| `REQUIRES_ACTION` | The charge is waiting on user approval | Send the user to `nextAction.url` before `expiresAt` |
| `PROCESSING` | The charge is approved and the order is being placed | Read the checkout again until it reports a final status |
| `COMPLETED` | The merchant order is placed | Store `orderId` and reconcile against `finalAmount` |
| `FAILED` | The charge or the merchant order did not go through | Tell the user the purchase did not happen, then create a new quote and a new checkout |
| `EXPIRED` | The user did not approve before `expiresAt` | Create a new quote and a new checkout, because the old quote is stale |
`COMPLETED`, `FAILED`, and `EXPIRED` are final. A checkout never leaves one of them, so branch on the status value rather than treating anything other than `COMPLETED` as still in flight. Neither `FAILED` nor `EXPIRED` can be retried in place, and both need a fresh quote and a fresh checkout.
[`POST /agentic/checkouts`](/api-reference/agentic/create-checkout) returns a `nextAction` when the charge needs approval. A checkout created under a mandate the user approved earlier skips that step and moves straight into `PROCESSING`.
Read the checkout with [`GET /agentic/checkouts/:id`](/api-reference/agentic/get-checkout) after the user lands back on your return URL. Returning to that URL does not by itself mean the order is placed, so read the status. A `nextAction` of `null` on a completed checkout means you have nothing left to do.
## Next steps
The full purchase sequence with requests and responses.
Short answers to common integration questions.
# One-Time Purchases
Source: https://docs.reap.global/agentic-payments/one-time-purchases
Run a single agentic purchase from product search to a confirmed order
This page runs one purchase end to end. You start with a stored card and finish with a merchant order reference and the amount charged.
## Before you start
You need an enrollment with status `ACTIVE`. [Setup](/agentic-payments/setup) covers how to create one, and [`GET /agentic/enrollments/:id`](/api-reference/agentic/get-enrollment) reads its status.
```bash Confirm the enrollment theme={null}
curl --request GET \
--url https://sandbox.api.reap.global/agentic/enrollments/:id \
--header 'Authorization: Bearer ' \
--header 'Reap-Version: '
```
Continue when `status` reads `ACTIVE`. Anything else means you cannot charge the card yet.
## Check the mandate
A single charge runs under a mandate with `frequency` set to `ONE_TIME` and `maxCharges` set to 1. Read the mandate with [`GET /agentic/mandates/:id`](/api-reference/agentic/get-mandate) to confirm the terms before you price anything.
```bash Request theme={null}
curl --request GET \
--url https://sandbox.api.reap.global/agentic/mandates/:id \
--header 'Authorization: Bearer ' \
--header 'Reap-Version: '
```
```json Response theme={null}
{
"id": "",
"status": "ACTIVE",
"enrollmentId": "",
"frequency": "ONE_TIME",
"maxCharges": 1,
"merchantScope": "LISTED",
"merchant": {
"name": "",
"url": "https://example.com",
"country": "US"
},
"amount": {
"amount": 150,
"currency": "USD"
}
}
```
The `amount` is a ceiling rather than a target. Compare it against the final quote total once you select a shipping option. A one-time mandate authorizes a single charge and cannot authorize another once that charge completes.
## Run the purchase
Send the words the user gave you to [`POST /agentic/products/search`](/api-reference/agentic/search-products). Filters narrow the result set, and `context` sets the country and currency for pricing.
If your application handles discovery, skip the search and variant steps. Construct a merchant checkout URL with the item quantity and any attribution parameters. Then continue at **Create the quote**.
```bash Request theme={null}
curl --request POST \
--url https://sandbox.api.reap.global/agentic/products/search \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--header 'Reap-Version: ' \
--data '{
"query": "Sony WH 1000XM5 headphones",
"context": {
"country": "US",
"currency": "USD"
},
"filters": {
"price": {
"min": "100",
"max": "200"
},
"availability": "AVAILABLE_ONLY"
},
"pagination": {
"limit": 20
}
}'
```
```json Response theme={null}
{
"id": "",
"products": [
{
"id": "",
"merchant": {
"name": ""
},
"name": "Sony WH 1000XM5 Wireless Headphone",
"imageUrl": "https://example.com/image.jpg",
"priceRange": {
"min": { "amount": 129, "currency": "USD" },
"max": { "amount": 149, "currency": "USD" }
},
"available": true,
"previewVariant": {
"id": "",
"name": "",
"price": { "amount": 129, "currency": "USD" },
"available": true
}
}
],
"pagination": {
"nextCursor": "",
"hasNextPage": true,
"returnedCount": 20
},
"warnings": []
}
```
Shortlist a product by `id`. Add `merchantPreference` to the request when the user names a merchant or when you want Reap to prefer one.
Search results carry a price range and one preview variant. [`POST /agentic/products/details`](/api-reference/agentic/get-product-details) expands a product into its description, media, and option groups.
```bash Request theme={null}
curl --request POST \
--url https://sandbox.api.reap.global/agentic/products/details \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--header 'Reap-Version: ' \
--data '{
"productIds": [""]
}'
```
```json Response theme={null}
{
"products": [
{
"id": "",
"name": "Sony WH 1000XM5 Wireless Headphones",
"options": [
{
"name": "Color",
"values": [
{ "optionId": "", "label": "Black", "available": true },
{ "optionId": "", "label": "Silver", "available": false }
]
}
],
"defaultVariant": {
"id": "",
"price": { "amount": 129, "currency": "USD" },
"available": true,
"requiresShipping": true
}
}
],
"errors": []
}
```
Carry the `optionId` of each chosen value into the next step. Check `available` on every option value, because an unavailable value cannot resolve to a purchasable variant. Any product ID that fails to resolve comes back in `errors` rather than stopping the call.
Skip the next step when the user accepts the `defaultVariant`, which is already a purchasable variant.
A quote accepts a variant rather than a product. Turn the selected options into one variant with [`POST /agentic/products/variant`](/api-reference/agentic/resolve-variant).
```bash Request theme={null}
curl --request POST \
--url https://sandbox.api.reap.global/agentic/products/variant \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--header 'Reap-Version: ' \
--data '{
"productId": "",
"optionIds": [""]
}'
```
```json Response theme={null}
{
"id": "",
"name": "",
"options": [
{ "name": "Color", "value": "Black" }
],
"price": { "amount": 129, "currency": "USD" },
"available": true,
"requiresShipping": true
}
```
A `requiresShipping` value of `true` means the quote needs a shipping address. Stop here when `available` reads `false` and offer the user another option.
[`POST /agentic/quotes`](/api-reference/agentic/create-quote) opens the merchant checkout and returns current pricing from the merchant. Send exactly one of `items` and `externalCheckout`. `offerCode` is optional for both quote sources.
Use `items` with the variant IDs returned by Reap discovery. Send the shipping address whenever a variant requires shipping.
```bash Request theme={null}
curl --request POST \
--url https://sandbox.api.reap.global/agentic/quotes \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: ' \
--header 'Reap-Version: ' \
--data '{
"items": [{ "variantId": "var_123", "quantity": 1 }],
"email": "avery.tan@reap.hk",
"offerCode": "SAVE10"
}'
```
Use `externalCheckout` with the checkout URL your application constructed. Reap sends the URL to the merchant without rebuilding it, so attribution parameters remain intact. External checkout quotes require a shipping address.
External checkout URLs are available only for merchant domains that Reap has allowlisted for your integration. Contact Reap to request access before you send checkout URLs for a new merchant.
```bash Request theme={null}
curl --request POST \
--url https://sandbox.api.reap.global/agentic/quotes \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: ' \
--header 'Reap-Version: ' \
--data '{
"externalCheckout": {
"merchantDomain": "merchant.example",
"checkoutUrl": "https://merchant.example/cart/variant-1:1?attributes[partner_click_id]=example-123"
},
"email": "avery.tan@reap.hk",
"shippingAddress": {
"firstName": "Avery",
"lastName": "Tan",
"phone": "+85200000000",
"addressLine1": "123 Example Street",
"city": "Example City",
"postalCode": "000000",
"country": "HK"
},
"offerCode": "SAVE10"
}'
```
```json Response theme={null}
{
"id": "",
"shippingOptions": [
{
"id": "",
"name": "Standard",
"selected": false,
"price": { "amount": 5, "currency": "USD" }
},
{
"id": "",
"name": "Express",
"selected": true,
"price": { "amount": 13, "currency": "USD" }
}
],
"amountBreakdown": {
"itemsSubtotal": { "amount": 129, "currency": "USD" },
"shipping": { "amount": 13, "currency": "USD" },
"tax": {
"amount": { "amount": 5, "currency": "USD" },
"includedInPrices": false
},
"discounts": [],
"additionalCharges": [],
"finalAmount": { "amount": 142, "currency": "USD" }
},
"expiresAt": "2026-01-01T00:00:00Z"
}
```
Keep the quote ID and note `expiresAt`. Prices come from the merchant, so the total can differ from the variant price once shipping and tax land.
Reap makes one merchant request for each quote attempt. It does not retry a failed request.
Correct `CHECKOUT_URL_INVALID`, `OFFER_CODE_INVALID`, `OFFER_CODE_EXPIRED`, and `QUOTE_UNFULFILLABLE` errors before you create another quote. `CARD_PAYMENT_UNAVAILABLE` means card payment is unavailable for this checkout. Do not retry the same request unchanged; choose another checkout or contact Reap.
One option arrives preselected. Send a different one to [`POST /agentic/quotes/:id/shipping-option`](/api-reference/agentic/select-shipping-option), which reprices the quote and returns the updated breakdown.
```bash Request theme={null}
curl --request POST \
--url https://sandbox.api.reap.global/agentic/quotes/:id/shipping-option \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--header 'Reap-Version: ' \
--data '{
"shippingOptionId": ""
}'
```
```json Response theme={null}
{
"id": "",
"shippingOptions": [
{
"id": "",
"name": "Standard",
"selected": true,
"price": { "amount": 5, "currency": "USD" }
},
{
"id": "",
"name": "Express",
"selected": false,
"price": { "amount": 13, "currency": "USD" }
}
],
"amountBreakdown": {
"itemsSubtotal": { "amount": 129, "currency": "USD" },
"shipping": { "amount": 5, "currency": "USD" },
"tax": {
"amount": { "amount": 5, "currency": "USD" },
"includedInPrices": false
},
"finalAmount": { "amount": 134, "currency": "USD" }
},
"expiresAt": "2026-01-01T00:00:00Z"
}
```
Show `amountBreakdown.finalAmount` to the user as the amount you will charge. Read the quote again with [`GET /agentic/quotes/:id`](/api-reference/agentic/get-quote) at any point to get the current total without changing anything.
[`POST /agentic/checkouts`](/api-reference/agentic/create-checkout) charges the enrollment for the quote. Send the URL the user returns to after approving. In the sandbox, send `X-Simulate-Checkout: COMPLETED` to simulate a completed checkout. This header is rejected in production.
```bash Request theme={null}
curl --request POST \
--url https://sandbox.api.reap.global/agentic/checkouts \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: ' \
--header 'Reap-Version: ' \
--header 'X-Simulate-Checkout: COMPLETED' \
--data '{
"quoteId": "",
"enrollmentId": "",
"presentation": {
"type": "REDIRECT",
"returnUrl": "https://example.com/orders/done"
}
}'
```
```json Response theme={null}
{
"id": "",
"status": "REQUIRES_ACTION",
"quoteId": "",
"enrollmentId": "",
"amount": { "amount": 134, "currency": "USD" },
"nextAction": {
"type": "REDIRECT",
"url": "",
"expiresAt": "2026-01-01T00:00:00Z"
}
}
```
Branch on `nextAction`. When it holds a value, send the user to `nextAction.url` to review and approve the charge. When it is empty, the charge already ran under terms the user approved earlier.
Read the checkout with [`GET /agentic/checkouts/:id`](/api-reference/agentic/get-checkout) once the user lands back on your return URL. A completed checkout carries the merchant order reference and the amount charged.
```bash Request theme={null}
curl --request GET \
--url https://sandbox.api.reap.global/agentic/checkouts/:id \
--header 'Authorization: Bearer ' \
--header 'Reap-Version: '
```
```json Response theme={null}
{
"id": "",
"status": "COMPLETED",
"quoteId": "",
"enrollmentId": "",
"orderId": "",
"finalAmount": { "amount": 134, "currency": "USD" },
"nextAction": null,
"createdAt": "2026-01-01T00:00:00Z",
"updatedAt": "2026-01-01T00:00:00Z"
}
```
Store `orderId` against your own record of the purchase. It is the reference the user and the merchant both recognize. Reconcile against `finalAmount` rather than the quote total, because it is the amount actually charged.
## Next steps
Charge the same card again on a schedule.
Every status value and what to do about it.
Field level detail for every field, header, and error sits in the [API Reference](/api-reference/overview) tab. This page carries the sequence rather than repeating that detail.
# Overview
Source: https://docs.reap.global/agentic-payments/overview
Let an agent buy on behalf of a user while every charge stays under user approval
Agentic Payments lets an AI agent complete a purchase for a user without handling the card number of that user.
The user stores a card once. Your agent then searches merchant catalogs and prices an order. It opens a checkout against the stored card, and the user approves the charge on a page hosted by Reap. Reap completes the purchase with the merchant and returns the order reference.
## What Reap handles
You do not need to build any of the following.
* Card storage, so the card number never reaches your agent or your servers
* Merchant catalog search, product detail lookup, and live pricing
* The hosted page where the user enters a new card
* The hosted page where the user approves each charge
* Checkout with the merchant, including the order reference that comes back
## What you build
* The agent logic that decides what to buy
* The call sequence described in this section
## Core resources
Five resources carry the whole flow. [How It Works](/agentic-payments/how-it-works) explains each one.
* An enrollment stores a card. [`POST /agentic/enrollments`](/api-reference/agentic/create-enrollment) returns the enrollment ID, which is the payment handle for every later purchase.
* A mandate records the charge terms the user approved for one enrollment. Read it with [`GET /agentic/mandates/:id`](/api-reference/agentic/get-mandate).
* Product search finds a product in merchant catalogs and resolves it to a purchasable variant. Start with [`POST /agentic/products/search`](/api-reference/agentic/search-products).
* A quote prices an order with the merchant and expires after a short window. Create it with [`POST /agentic/quotes`](/api-reference/agentic/create-quote).
* A checkout charges the enrollment and completes the order with the merchant. Create it with [`POST /agentic/checkouts`](/api-reference/agentic/create-checkout).
## Use cases
The user asks the agent to buy Sony WH-1000XM5 headphones for about USD 130. The agent finds them at audiohub.com and quotes USD 138.42 including shipping and tax. The user opens the approval link, checks the order, and pays with their card. The agent reports order 12345 with Friday delivery.
## Next steps
The actors, the resources, and the two phases of a purchase.
Store a card and confirm it is ready to charge.
Run a single purchase from search to a confirmed order.
Charge the same card on a schedule under approved terms.
[Lifecycle and Statuses](/agentic-payments/lifecycle) covers every status value and the operations that change it. The [FAQ](/agentic-payments/faq) answers the questions that come up most during an integration.
# Recurring Purchases
Source: https://docs.reap.global/agentic-payments/recurring-purchases
Charge the same stored card on a schedule under terms the user approved once
A recurring purchase reuses one enrollment and one mandate across many charges. The user approves the terms once, and each later charge runs inside those terms instead of prompting the user again.
## What the mandate controls
Read the mandate with [`GET /agentic/mandates/:id`](/api-reference/agentic/get-mandate) and check four things.
| Term | What it does |
| - | - |
| `amount` | Sets the ceiling for a charge. The final quote total has to stay at or below it. |
| `frequency` | Sets the cadence as `WEEKLY`, `MONTHLY`, or `YEARLY`. |
| `maxCharges` | Caps how many charges the mandate authorizes in total. |
| `merchantScope` | Limits where the mandate can be used. A `LISTED` scope names the merchant in the `merchant` block. |
```json Response theme={null}
{
"id": "",
"status": "ACTIVE",
"enrollmentId": "",
"frequency": "MONTHLY",
"maxCharges": 12,
"merchantScope": "LISTED",
"merchant": {
"name": "",
"url": "https://example.com",
"country": "US"
},
"amount": {
"amount": 150,
"currency": "USD"
}
}
```
You can charge only while `status` reads `ACTIVE`. [Lifecycle and Statuses](/agentic-payments/lifecycle) lists every value.
## The charge cycle
```mermaid theme={null}
flowchart LR
A[Read the mandate] --> B[Price the order again]
B --> C{"Total at or below the ceiling?"}
C -- Yes --> D[Charge and confirm]
C -- No --> E[Collect new terms from the user]
D -. next cycle .-> A
```
Each cycle repeats the purchase phase and reuses only the enrollment and the mandate. The ceiling check is the one point where a cycle can stop, so branch on it before you charge.
Confirm the status is `ACTIVE` and that charges remain under `maxCharges`. A paused or canceled mandate cannot authorize the cycle.
Create a fresh quote for the cycle with [`POST /agentic/quotes`](/api-reference/agentic/create-quote) and select a shipping option. Merchant prices move, so never reuse a quote or an old total across cycles. The call sequence matches [One-Time Purchases](/agentic-payments/one-time-purchases).
Check `amountBreakdown.finalAmount` against the mandate `amount` before you charge. Hold the cycle and ask the user for new terms when the total sits above the ceiling.
Create the checkout with [`POST /agentic/checkouts`](/api-reference/agentic/create-checkout) against the same enrollment, then read it with [`GET /agentic/checkouts/:id`](/api-reference/agentic/get-checkout) to capture `orderId` and `finalAmount` for that cycle. Branch on `nextAction` in the checkout response, and send the user to the approval page whenever it holds a value.
## Control the schedule
A mandate stays under your control between cycles.
* [`POST /agentic/mandates/:id/pause`](/api-reference/agentic/pause-mandate) stops charges and keeps the approved terms
* [`POST /agentic/mandates/:id/resume`](/api-reference/agentic/resume-mandate) continues a paused mandate under the same terms
* [`POST /agentic/mandates/:id/cancel`](/api-reference/agentic/cancel-mandate) ends the mandate
Resuming needs no further approval from the user, because the terms have not changed. [Lifecycle and Statuses](/agentic-payments/lifecycle) covers each operation with a request and a response.
Revoking an enrollment stops charges across every mandate that points at the same stored card. Pause or cancel the mandate instead when the card should stay usable.
## Next steps
Pause, resume, cancel, and every status value.
Short answers to common integration questions.
# Setup
Source: https://docs.reap.global/agentic-payments/setup
Store a card once and confirm that it is ready to charge
Agentic Payments enrolls only cards that support Cloud Token Framework and Visa Token Service tokenization. Check with your card issuer or BIN sponsor before you create an enrollment. The [Visa fact sheet](https://usa.visa.com/content/dam/VCOM/global/products/documents/visa-vts-cloud-token-framework-fact-sheet.pdf) covers what the framework provides.
## Before you start
* A Reap API key for the sandbox or production environment
* Agentic Payments enabled on your project
* An HTTPS return URL that receives the user after the hosted card entry page and the hosted approval page
Every request takes an [`Authorization`](/api-reference/authentication) header and a [`Reap-Version`](/api-reference/versioning) header. The examples below use the sandbox host. Switch to the production host when you go live.
## Store a card
Pick the source that matches the card you are storing. [`POST /agentic/enrollments`](/api-reference/agentic/create-enrollment) requires an [`Idempotency-Key`](/api-reference/idempotency). A retry with the same key replays the first response instead of storing the card twice.
Use this source for a card Reap already issued.
```bash Request theme={null}
curl --request POST \
--url https://sandbox.api.reap.global/agentic/enrollments \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: ' \
--header 'Reap-Version: ' \
--data '{
"source": "REAP_CARD",
"cardId": ""
}'
```
```json Response theme={null}
{
"id": "",
"status": "REQUIRES_ACTION",
"source": "REAP_CARD",
"owner": {
"type": "REAP_USER",
"id": ""
}
}
```
The `owner` block names the cardholder that Reap resolved from the card.
Use this source for a card issued by a BIN sponsor.
```bash Request theme={null}
curl --request POST \
--url https://sandbox.api.reap.global/agentic/enrollments \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: ' \
--header 'Reap-Version: ' \
--data '{
"source": "BIN_SPONSOR",
"cardId": ""
}'
```
```json Response theme={null}
{
"id": "",
"status": "REQUIRES_ACTION",
"source": "BIN_SPONSOR"
}
```
This response carries no `owner` block. The sponsor card ID already resolves the cardholder on the sponsor side.
Use this source when the user enters a new card. Send the owner reference from your own system and the URL the user returns to.
```bash Request theme={null}
curl --request POST \
--url https://sandbox.api.reap.global/agentic/enrollments \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: ' \
--header 'Reap-Version: ' \
--data '{
"source": "EXTERNAL",
"owner": {
"type": "CLIENT_REFERENCE",
"id": "",
"email": "jsmith@example.com"
},
"presentation": {
"type": "REDIRECT",
"returnUrl": "https://example.com/cards/added"
}
}'
```
```json Response theme={null}
{
"id": "",
"status": "REQUIRES_ACTION",
"source": "EXTERNAL",
"owner": {
"type": "CLIENT_REFERENCE",
"id": "",
"email": "jsmith@example.com"
},
"nextAction": {
"type": "REDIRECT",
"url": "",
"expiresAt": "2026-01-01T00:00:00Z"
}
}
```
Keep the enrollment ID. You need it in the next two steps and at every checkout.
This step applies to all sources. Redirect the user to `nextAction.url` before `expiresAt` passes, and the user enters the card on a page hosted by Reap. Card details never reach your servers.
Reap sends the user back to the `returnUrl` you supplied. Returning to that URL means the hosted page is finished. It does not mean the card is stored, so confirm the status next.
Read the enrollment with [`GET /agentic/enrollments/:id`](/api-reference/agentic/get-enrollment) after any hosted step.
```bash Request theme={null}
curl --request GET \
--url https://sandbox.api.reap.global/agentic/enrollments/:id \
--header 'Authorization: Bearer ' \
--header 'Reap-Version: '
```
```json Response theme={null}
{
"id": "",
"status": "ACTIVE",
"owner": {
"type": "REAP_USER",
"id": "",
"email": "jsmith@example.com"
},
"paymentMethod": {
"type": "CARD",
"network": "",
"last4": "4242",
"expiryMonth": 11,
"expiryYear": 2029
},
"nextAction": null,
"createdAt": "2026-01-01T00:00:00Z",
"updatedAt": "2026-01-01T00:00:00Z"
}
```
A status of `ACTIVE` means the card is ready to charge. A status of `REQUIRES_ACTION` means a hosted step is still outstanding, and `nextAction` tells you where to send the user. The `paymentMethod` block gives you the network and the last four digits to display in your own interface.
You can charge only an `ACTIVE` enrollment. Read the enrollment before you open a checkout rather than assuming the hosted page succeeded.
## Present the available enrollment cards to the user
List the enrollments of one owner with [`GET /agentic/enrollments`](/api-reference/agentic/list-enrollments) and show each stored card so the user can pick one. The `paymentMethod` block carries the network, the last four digits, and the expiry to display. Scope every list call to a single owner.
```bash Request theme={null}
curl --request GET \
--url 'https://sandbox.api.reap.global/agentic/enrollments?ownerType=REAP_USER&ownerId=:id&limit=20' \
--header 'Authorization: Bearer ' \
--header 'Reap-Version: '
```
```json Response theme={null}
{
"items": [
{
"id": "",
"status": "ACTIVE",
"owner": {
"type": "REAP_USER",
"id": "",
"email": "jsmith@example.com"
},
"paymentMethod": {
"type": "CARD",
"network": "",
"last4": "4242",
"expiryMonth": 11,
"expiryYear": 2029
},
"nextAction": null,
"createdAt": "2026-01-01T00:00:00Z",
"updatedAt": "2026-01-01T00:00:00Z"
}
],
"nextCursor": null
}
```
Follow `nextCursor` when it holds a value. A `null` cursor means the last page.
## Test cards
Use these test cards on the hosted card entry page to run the full payment flow in the sandbox. They behave like real cards but never move money. Reap adds more cards and networks as it supports them.
These cards only work through the Agentic Payments API and are declined everywhere else.
| Card number | CVC | Expiry |
| - | - | - |
| 4622 9431 2313 7797 | 640 | 12/27 |
| 4622 9431 2313 7805 | 304 | 12/27 |
| 4622 9431 2313 7847 | 698 | 12/27 |
## One-time passwords
The card verification step may ask for a one-time password. Enter `456789`. The same code works whether you choose email or SMS delivery.
## Next steps
Run a single purchase against the enrollment you created.
Revoke a stored card and read every status value.
Field level detail for every field, header, and error sits in the [API Reference](/api-reference/overview) tab. This page carries the sequence rather than repeating that detail.
# FAQ
Source: https://docs.reap.global/airport-lounge-access/faq
Coverage, the plan, spending control and the questions prospects raise in evaluations
Airport Lounge Access is coming soon and is not currently available through the Reap API.
Clients must generate the QR code using the provided raw QR payload and render it in their app. The QR code or barcode is then scanned at the lounge reception.
Passes admit cardholders to airport lounges in the Priority Pass network. Your Reap team shares the lounge directory and plan coverage during onboarding.
One plan is available today. LOUNGE\_PP\_1PA is a single use pass in the form of QR code that covers one lounge visit and stays valid for 365 days from issuance.
The lounge pass can only be used at participating lounges within the Priority Pass network. The name on the pass must match the boarding pass name exactly for access.
Yes. A lounge pass can be created for another traveller. The name provided when creating the pass must exactly match the name shown on the boarding pass.
The prepaid pool is the cap. Every purchase passes an explicit approval before stock is loaded, every issuance draws the pool down by exactly one, counters never go negative and a failed creation never consumes stock.
The lounge records the visit and the pass turns redeemed. A redeemed pass is complete, so issue a new pass from your inventory when the cardholder needs another visit.
Block and freeze disable the passes on that card. Unfreezing restores the passes when the cardholder or client unfreezes the card, while unblocking a blocked card requires Reap support. Delete and replace expire the passes permanently. Only the target card is affected, and nothing changes if the card operation fails. The [lifecycle page](/airport-lounge-access/lifecycle-and-events) maps every transition.
Reap retrieves the lounge network report once a day and updates pass usage and visit records from it.
Airport Lounge Access is switched on per business and requests made before enablement are rejected. Ask your Reap contact to enable the capability during onboarding.
The flow is built around a general reward model, with a reward type and a plan on every request. Airport lounge access is the first benefit available through it.
## Next steps
Start with inventory, then issue and track
# Lifecycle and Events
Source: https://docs.reap.global/airport-lounge-access/lifecycle-and-events
The four pass statuses, the card operations that move them and the webhook events that announce every change
A pass holds one of four statuses and stays valid for 365 days from issuance. Card operations move those statuses automatically. Webhook events report each purchase decision and each new pass, so nothing in the flow needs polling.
## The four statuses
| Status | Meaning |
| - | - |
| Active | Ready for lounge entry, with the visit still available |
| Redeemed | The visit is used and the pass is complete |
| Disabled | Suspended while the card is blocked or frozen, and restored when the card returns |
| Expired | Past the validity window or retired with a deleted or replaced card. Final |
```mermaid theme={null}
stateDiagram-v2
direction LR
[*] --> Active : Creation completes
Active --> Redeemed : The visit is used
Active --> Disabled : Card blocked or frozen
Disabled --> Active : Card unblocked or unfrozen
Active --> Expired : 365 days elapse, card deleted or replaced
Disabled --> Expired : 365 days elapse, card deleted or replaced
Expired --> [*]
```
Expiry runs automatically each night. It needs no call from you and applies to any pass past its expiry date regardless of its current status.
## Card events that update passes
| Card operation | Effect on the passes on that card |
| - | - |
| Delete or replace | Passes become expired |
| Block or freeze | Passes become disabled |
| Unblock or unfreeze | Passes return to active |
Only the passes on the target card are affected, and no pass changes if the card operation itself fails, so pass status can never drift from card status.
Delete and replace retire the passes on the card permanently, and an expired pass cannot be reactivated. Check for unused passes before replacing a card.
## Webhook events
Register one endpoint and handle the three events below. Delivery and [signature verification](/webhooks/signature-verification) work the same as every other [Reap event](/webhooks/overview).
| Event | Fires when | What to do |
| - | - | - |
| `PURCHASE_REQUEST_APPROVED` | A purchase is approved and the passes land in your pool | Refresh inventory and begin issuing passes |
| `PURCHASE_REQUEST_REJECTED` | A purchase is rejected with a reason | Surface the reason and raise a corrected request if needed |
| `PASS_CREATION_COMPLETED` | Creation finishes and the pass becomes active | Fetch the pass and present it to the cardholder |
Lounge visits do not raise an event today. Reap updates every pass from the lounge network report once a day, and a webhook that reports each visit as it happens is coming soon.
## Next steps
Purchase inventory, issue passes and track lounge visits
Coverage, the plan, spending control and the questions prospects raise
# Overview
Source: https://docs.reap.global/airport-lounge-access/overview
Offer airport lounge visits as a built in benefit on the cards you issue
Airport Lounge Access is coming soon and is not currently available through the Reap API.
## Four parties, one flow
Purchase pass inventory, issue passes to your cardholders, monitor stock and usage and receive webhook notifications
Reviews every purchase, holds the inventory pool, creates each pass on the lounge network and keeps pass status in step with the card
Admits the cardholder at lounges in the Priority Pass network and reports every visit back to Reap
Receives the pass inside your app and scans the QR code or barcode at the lounge door
## The pass journey
Inventory is purchased in batches and passes are issued one card at a time. Redemption and expiry then run for as long as each pass lives. The flow is the same for every client, whether you operate through the Reap dashboard or a direct API integration.
```mermaid theme={null}
flowchart LR
A["Purchase
request"] --> B["Reap
review"]
B -->|"Approved"| C["Inventory
pool"]
B -->|"Rejected"| R["Reason via
webhook"]
C --> D["Pass issued
to a card"]
D --> E["Cardholder scans
at the lounge"]
E --> F["Visit recorded
on the pass"]
```
The [working with passes](/airport-lounge-access/working-with-passes) guide walks each stage of the journey in order.
## The spending control model
The prepaid inventory pool is the spending cap. A pass can only be issued while stock remains, every successful issuance draws the pool down by exactly one, and a failed creation never draws it down at all.
Passes enter the pool only through an approved purchase, so exposure is agreed before a single pass reaches a cardholder. Counters never fall below zero. When one pass remains, only one issuance can succeed. Approved purchases are invoiced to your business and every pass carries its recorded cost.
## The plan
One plan is available today. `LOUNGE_PP_1PA` is a single use pass that covers one lounge visit and stays valid for 365 days from issuance. Pricing is configured for your business during onboarding. A redeemed pass is complete, so issue a new pass from your inventory when a cardholder needs another visit.
## Who does what
Reap operates the whole flow behind the Reap API. It reviews and approves every inventory purchase, holds the pool and creates each pass on the lounge network at the moment of issuance. It records lounge visits against the right pass, keeps pass status synchronised with the card lifecycle and delivers webhook notifications and audit records throughout.
You call the rewards endpoints from your existing integration and register a [webhook endpoint](/webhooks/overview) for the purchase decision and pass creation events. The pass returns a raw QR payload and a barcode string rather than an image, so your app generates the scannable QR code from the payload and renders both to the cardholder. You decide when to purchase stock and when to issue a pass. The same flow also runs through the Reap dashboard, so a benefit can launch before any integration work begins.
You request a batch of passes and Reap reviews and decides the purchase.
Approved passes land in your inventory pool and appear in your counters.
You issue a pass to a card and Reap creates it on the lounge network within seconds.
The cardholder scans the pass at the lounge and the network reports the visit to Reap.
Reap records the visit, updates the pass and expires or disables passes as the card and the calendar require.
## Before you start
1. Airport Lounge Access enabled for your business by your Reap team, since requests made before enablement are rejected
2. The lounge plan configured for your business
3. Cards issued through Reap, because every pass attaches to an [existing card](/cards/overview)
4. A registered [webhook endpoint](/webhooks/overview) for the purchase decision and pass creation events
## Next steps
Purchase inventory, issue passes and track lounge visits
The four pass statuses, the card operations that move them and the webhook events
# Working with Passes
Source: https://docs.reap.global/airport-lounge-access/working-with-passes
Purchase inventory, issue passes to cards and track lounge visits
Three actions cover the whole operational journey. You purchase stock into the pool, issue passes to individual cards and watch visits land as cardholders redeem at the lounge. Each action below opens with what it does, then walks the steps involved.
## Purchase inventory
Passes are purchased before they are issued. You submit a purchase request for a plan and a quantity, your Reap team reviews it, and approval loads the passes straight into your inventory pool. Nothing reaches the pool from a request that is still pending or rejected.
The request captures what you want to buy and nothing moves until it is decided.
* Your application submits the reward type, plan name and quantity with `POST /cards/rewards/request-purchase`
* Reap records the request as pending review and routes it to your Reap team
* No passes are added at this stage
Your Reap team approves or rejects the request and every request is decided exactly once.
* Approval records the invoice detail and loads the purchased passes into your pool
* Rejection records a reason and loads nothing
The outcome lands on your webhook endpoint either way.
* Reap delivers the decision through the `PURCHASE_REQUEST_APPROVED` or `PURCHASE_REQUEST_REJECTED` webhook
* On approval, refresh your inventory and begin issuing
* On rejection, surface the reason and raise a corrected request if needed
### Reading your inventory
`GET /cards/rewards/inventory` returns your stock scoped to your business, with filters for reward type and plan name. Each row shows the purchased, consumed and available totals for the plan, with audit detail alongside.
The Reap dashboard shows the same counters read only and flags low stock once available passes fall below your threshold, which defaults to ten. Raise the next purchase request before the pool runs dry so issuance never has to wait on a review.
## Issue a pass
One call issues a pass to an existing card. The call returns immediately, creation runs in the background on the lounge network, and the pass becomes active shortly after. A webhook announces completion so you can present the pass without polling.
One call starts the whole journey and returns straight away.
* Your application calls `POST /cards/:id/rewards` with the reward type and plan name
* Reap validates the feature, the card ownership and the available stock, then queues creation and acknowledges immediately
Reap creates the pass on the lounge network, and the step typically completes in around ten seconds.
* Reap records the pass against the card and draws the pool down by one
* A failed creation leaves no pass record and no net change to your inventory, so issue again
A webhook announces the pass so you never have to poll.
* The `PASS_CREATION_COMPLETED` webhook fires once the pass is active
* Your application fetches it with `GET /cards/:id/rewards`
* The pass carries a raw QR payload and a barcode string rather than a ready made image, so generate the QR code from the payload and render both to the cardholder
When a single pass remains in the pool and two issuances race, exactly one succeeds. Counters never fall below zero, so a burst of issuance activity can never spend more stock than you hold.
Every pass returned on the card includes what your app needs to display the benefit:
* The status, one of active, redeemed, disabled or expired
* The visit allowance and the visits already recorded
* The raw QR payload and the barcode string for lounge entry
* The recorded cost, the currency and the expiry date
## Track redemptions
Entry needs only the QR code and barcode rendered in your app. The cardholder carries no separate membership card and completes no enrolment with the lounge network. Reap retrieves the lounge network report once a day and updates pass usage and visit records from it, so your app can always show whether a pass has been used. The pass turns redeemed once its visit is recorded. No client action is required.
## The machinery behind the flow
* `POST /cards/rewards/request-purchase` submits a purchase request for a plan and a quantity
* `GET /cards/rewards/inventory` returns the purchased, consumed and available totals for your business
* `POST /cards/:id/rewards` issues a pass to a card and queues creation on the lounge network
* `GET /cards/:id/rewards` lists the passes on a card with status, usage and the QR and barcode payloads
These guides stay at the level of names and purpose. Field level detail belongs to the API Reference.
## Next steps
The four pass statuses, the card operations that move them and the webhook events
Coverage, the plan, spending control and the questions prospects raise
# 3ds challenge created
Source: https://docs.reap.global/api-reference/3ds-challenge-created
/webhooks/openapi.json webhook CARD_3DS_CHALLENGE_CREATED
Fired when a new 3DS authentication challenge is received for a card. The partner must respond within the expiry window.
# Account status updated
Source: https://docs.reap.global/api-reference/account-status-updated
/webhooks/openapi.json webhook ACCOUNT_STATUS_UPDATED
Fired when an account status changes (e.g., restricted, reactivated).
# Create account
Source: https://docs.reap.global/api-reference/accounts/create-account
/api-reference/openapi.json post /accounts/
Creates a new account for the given owner. Set `ownerType` to `USER` (default) or `COMPANY` (corporate programs only) to choose the owner type. The owner must have an approved application (KYC for users, KYB for companies). For projects that provision user wallets (`USER_FUNDED` funding model), deposit addresses are created on all enabled chains and signer data is required (address + signed auth message from `GET /accounts/auth-message`). For projects without user wallets (`PROGRAM_FUNDED` funding model), no deposit addresses are provisioned and signers are not required.
# Generate signer message
Source: https://docs.reap.global/api-reference/accounts/generate-signer-message
/api-reference/openapi.json get /accounts/auth-message
Generates a time-limited authentication message that must be signed by the end-user's wallet to prove key ownership. The signed message is required when creating an account (`POST /accounts`). Messages expire after 15 minutes.
# Get account
Source: https://docs.reap.global/api-reference/accounts/get-account
/api-reference/openapi.json get /accounts/{id}
Retrieves account details and chain addresses, each listing the assets that address accepts - send only those, as a deposit of anything else fails with `ASSET_NOT_ACCEPTED`. List responses omit the accepted assets, so read them from here. Use `GET /accounts/:id/balance` to retrieve balance information.
# Get account assets
Source: https://docs.reap.global/api-reference/accounts/get-account-assets
/api-reference/openapi.json get /accounts/{id}/assets
Retrieves per-asset balance breakdown with valuations, aggregated by symbol across all chains. Crypto assets also carry a `byChain` breakdown with the amount held, the withdrawable cap, and the withdrawal fee for each chain. All valuations are in the project's billing currency.
# Get account balance
Source: https://docs.reap.global/api-reference/accounts/get-account-balance
/api-reference/openapi.json get /accounts/{id}/balance
Retrieves the account financial position including total asset value, liabilities, and net available balance. Use `GET /accounts/:id/assets` for per-asset breakdown. All values are in the project's billing currency (see the `currency` field).
# List accounts
Source: https://docs.reap.global/api-reference/accounts/list-accounts
/api-reference/openapi.json get /accounts/
Retrieves a paginated list of accounts. Use `GET /accounts/:id/balance` to retrieve balance details for a specific account.
# List activities
Source: https://docs.reap.global/api-reference/activities/list-activities
/api-reference/openapi.json get /activities/
Retrieves a paginated, chronologically ordered list of fund movements. Supports filtering by resource identifiers, activity type, time window, and status.
# Cancel mandate
Source: https://docs.reap.global/api-reference/agentic/cancel-mandate
/api-reference/openapi.json post /agentic/mandates/{id}/cancel
Agentic Payments - enrollments, mandates, products, quotes, and checkouts.
# Create checkout
Source: https://docs.reap.global/api-reference/agentic/create-checkout
/api-reference/openapi.json post /agentic/checkouts
Opens the payment for an unexpired quote against an `ACTIVE` enrollment, whose stored card and owner are used for the charge. The response carries a redirect to a Reap-hosted approval page where the user reviews the order and approves the payment. After approval the user returns to `presentation.returnUrl` and Reap completes the merchant checkout.
# Create enrollment
Source: https://docs.reap.global/api-reference/agentic/create-enrollment
/api-reference/openapi.json post /agentic/enrollments
Stores a card for later agentic purchases. `EXTERNAL` returns a redirect to a hosted card-entry page and becomes `ACTIVE` once the customer submits their card. `REAP_CARD` and `BIN_SPONSOR` are coming soon. Pass the enrollment id to `POST /agentic/checkouts` to charge it.
# Create quote
Source: https://docs.reap.global/api-reference/agentic/create-quote
/api-reference/openapi.json post /agentic/quotes
Opens and prices a merchant checkout from product variants or a checkout URL assembled by the client. Send exactly one of `items` and `externalCheckout`. Prices always come from the merchant. The response itemizes the total and lists the available shipping options. Quotes are short-lived: create the checkout before `expiresAt` passes, or open a fresh quote. `shippingAddress` is required for external checkout quotes and when any item requires shipping. Use `offerCode` to price an offer before approval. Reap makes one quote attempt and does not retry a failed merchant request. The error code states whether to correct the request or create a new quote.
# Get checkout
Source: https://docs.reap.global/api-reference/agentic/get-checkout
/api-reference/openapi.json get /agentic/checkouts/{id}
Returns the checkout and its lifecycle status. Poll after sending the user to the approval page until the checkout reaches a terminal status; a completed checkout carries the merchant order reference and the final charged amount.
# Get enrollment
Source: https://docs.reap.global/api-reference/agentic/get-enrollment
/api-reference/openapi.json get /agentic/enrollments/{id}
Agentic Payments - enrollments, mandates, products, quotes, and checkouts.
# Get mandate
Source: https://docs.reap.global/api-reference/agentic/get-mandate
/api-reference/openapi.json get /agentic/mandates/{id}
Agentic Payments - enrollments, mandates, products, quotes, and checkouts.
# Get product details
Source: https://docs.reap.global/api-reference/agentic/get-product-details
/api-reference/openapi.json post /agentic/products/details
Returns full details for a product, including its purchasable variants. When a product has options, use `POST /agentic/products/variant` to resolve a variant from the selected option ids. Quote line items reference the returned variant ids.
# Get quote
Source: https://docs.reap.global/api-reference/agentic/get-quote
/api-reference/openapi.json get /agentic/quotes/{id}
Returns a previously created quote, including its shipping options and amount breakdown. Create the checkout before `expiresAt` passes.
# List enrollments
Source: https://docs.reap.global/api-reference/agentic/list-enrollments
/api-reference/openapi.json get /agentic/enrollments
Lists the enrollments of a single owner. `ownerId` is required, and a list is always scoped to exactly one owner.
# Pause mandate
Source: https://docs.reap.global/api-reference/agentic/pause-mandate
/api-reference/openapi.json post /agentic/mandates/{id}/pause
Agentic Payments - enrollments, mandates, products, quotes, and checkouts.
# Resolve variant
Source: https://docs.reap.global/api-reference/agentic/resolve-variant
/api-reference/openapi.json post /agentic/products/variant
Resolves a purchasable variant from selected option values. The returned variant id is the only id accepted by `POST /agentic/quotes`.
# Resume mandate
Source: https://docs.reap.global/api-reference/agentic/resume-mandate
/api-reference/openapi.json post /agentic/mandates/{id}/resume
Agentic Payments - enrollments, mandates, products, quotes, and checkouts.
# Revoke enrollment
Source: https://docs.reap.global/api-reference/agentic/revoke-enrollment
/api-reference/openapi.json post /agentic/enrollments/{id}/revoke
Agentic Payments - enrollments, mandates, products, quotes, and checkouts.
# Search products
Source: https://docs.reap.global/api-reference/agentic/search-products
/api-reference/openapi.json post /agentic/products/search
Searches merchant catalogs for products matching a free-text query, with optional merchant preference and result filters. Use `POST /agentic/products/details` on a result to resolve its purchasable variants.
# Select shipping option
Source: https://docs.reap.global/api-reference/agentic/select-shipping-option
/api-reference/openapi.json post /agentic/quotes/{id}/shipping-option
Selects a shipping option on an existing quote and re-prices it. The response is the updated quote with a fresh amount breakdown. `QUOTE_REPLACEMENT_REQUIRED` means the caller must create a new quote.
# Authentication
Source: https://docs.reap.global/api-reference/authentication
How to authenticate API requests to Reap.
All API requests require authentication using an API key.
## API Key
Include your API key as a Bearer token in the `Authorization` header:
```
Authorization: Bearer YOUR_API_KEY
```
API keys are project-scoped. Each key grants access to resources within a single project.
## API Versioning
Include the `Reap-Version` header in every request to specify which API version to use:
```
Reap-Version: 2025-02-14
```
The version uses a date-based format (YYYY-MM-DD). Requests without this header will be rejected.
## Example
```bash theme={null}
curl https://sg.sandbox.api.reap.global/users \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Reap-Version: 2025-02-14"
```
## Environments
| Environment | Base URL |
| - | - |
| Singapore sandbox | `https://sg.sandbox.api.reap.global` |
| Singapore production | `https://sg.prod.api.reap.global` |
| Mexico sandbox | `https://mx.sandbox.api.reap.global` |
| Mexico production | `https://mx.prod.api.reap.global` |
`https://sandbox.api.reap.global` and `https://prod.api.reap.global` remain valid aliases for Singapore.
API keys are scoped to a single environment. A sandbox key cannot be used against production and vice versa.
## IP allowlist
Each key can optionally be restricted to a list of IPv4/IPv6 addresses or CIDR ranges. An empty list means the key works from any IP.
When a list is set, requests from any other address are rejected with `403` and code `API_KEY_IP_NOT_ALLOWED`. Reap uses the last `X-Forwarded-For` hop (the address the load balancer saw). If that hop is missing, the request is also rejected.
Contact the Reap team to obtain API keys for your project.
# Card authorization request
Source: https://docs.reap.global/api-reference/card-authorization-request
/webhooks/openapi.json webhook CARD_AUTHORIZATION_REQUEST
Sent synchronously to an External-authorization project. Reap POSTs the proposed authorization and blocks on your approve/decline decision, which you return in the HTTP response body within the request timeout. This is the only webhook whose response Reap consumes: a non-2xx, timeout, or unparseable body is treated as a decline (fail closed).
# Get card design
Source: https://docs.reap.global/api-reference/card-designs/get-card-design
/api-reference/openapi.json get /card-designs/{id}
Card designs available to the project. Designs are assigned to projects out of band; clients pick a design when issuing or shipping cards.
# List card designs
Source: https://docs.reap.global/api-reference/card-designs/list-card-designs
/api-reference/openapi.json get /card-designs/
Returns the card designs. By default only active designs (usable for new cards) are returned; pass the `status` query parameter to widen or change the filter.
# Set default card design
Source: https://docs.reap.global/api-reference/card-designs/set-default-card-design
/api-reference/openapi.json put /card-designs/default
Sets the card design used by default when issuing cards. The design should be active.
# Card dispute status updated
Source: https://docs.reap.global/api-reference/card-dispute-status-updated
/webhooks/openapi.json webhook CARD_DISPUTE_STATUS_UPDATED
Fired when a card dispute status changes.
# Card fraud alert created
Source: https://docs.reap.global/api-reference/card-fraud-alert-created
/webhooks/openapi.json webhook CARD_FRAUD_ALERT_CREATED
Fired when a fraud alert is created for a card transaction (detected by fraud monitoring, or reported by the partner). Detected alerts must be confirmed or declined within the response window. Card blocks are delivered separately via CARD_STATUS_UPDATED.
# Card fraud alert status updated
Source: https://docs.reap.global/api-reference/card-fraud-alert-status-updated
/webhooks/openapi.json webhook CARD_FRAUD_ALERT_STATUS_UPDATED
Fired when a fraud alert status changes. Confirmed fraud also blocks the card; subscribe to CARD_STATUS_UPDATED for that transition.
# Card shipment status updated
Source: https://docs.reap.global/api-reference/card-shipment-status-updated
/webhooks/openapi.json webhook CARD_SHIPMENT_STATUS_UPDATED
Fired when a card shipment status changes (e.g., in production, ready to ship, in transit, delivered). The payload matches GET /card-shipments/:id, including the cards in the shipment.
# Add card to draft
Source: https://docs.reap.global/api-reference/card-shipments/add-card-to-draft
/api-reference/openapi.json post /card-shipments/{id}/cards
Append a card to a shipment in `DRAFT` status. Rejects if the card is already in another active shipment, or if the shipment is already at the 200-card limit.
# Create shipment draft
Source: https://docs.reap.global/api-reference/card-shipments/create-shipment-draft
/api-reference/openapi.json post /card-shipments/
Creates a new card shipment in `DRAFT` status. No manufacturer call is made until the draft is explicitly submitted.
# Delete shipment draft
Source: https://docs.reap.global/api-reference/card-shipments/delete-shipment-draft
/api-reference/openapi.json delete /card-shipments/{id}
Only shipments in `DRAFT` status can be deleted.
# Get shipment
Source: https://docs.reap.global/api-reference/card-shipments/get-shipment
/api-reference/openapi.json get /card-shipments/{id}
Unified card shipments API. Ship one card or up to 200 cards in a single draft-first shipment.
# List shipments
Source: https://docs.reap.global/api-reference/card-shipments/list-shipments
/api-reference/openapi.json get /card-shipments/
Returns a paginated list of card shipments, most recent first. Pass `cardId` to filter to shipments that include a given card (or any of several cards). Pass `include=cards` to include every card in each matching shipment.
# Remove card from draft
Source: https://docs.reap.global/api-reference/card-shipments/remove-card-from-draft
/api-reference/openapi.json delete /card-shipments/{id}/cards/{memberId}
Remove a card from a shipment in `DRAFT` status. Removing the last remaining card is not allowed - delete the shipment instead.
# Submit shipment
Source: https://docs.reap.global/api-reference/card-shipments/submit-shipment
/api-reference/openapi.json post /card-shipments/{id}/submit
Transitions the shipment from `DRAFT` to `PLACED`. Single-card shipments hit the single-ship endpoint; multi-card shipments use the bulk-ship endpoint. On failure the shipment remains in `DRAFT` so the client can retry.
Returns the full shipment detail (including `cards[]`). When the manufacturer rejects a subset of cards on a multi-card submit, the response uses HTTP **207 Multi-Status**: the surviving cards transition to `PLACED` upstream while each rejected card is returned with `productionStatus = "BULK_SHIP_REJECTED"` and a `rejectedAt` timestamp. A 200 indicates every card was accepted.
# Update shipping details
Source: https://docs.reap.global/api-reference/card-shipments/update-shipping-details
/api-reference/openapi.json patch /card-shipments/{id}
Update the box-level destination, recipient, or courier on a `DRAFT` shipment. Use `POST /:id/cards` and `DELETE /:id/cards/:memberId` to change the card set.
# Card status updated
Source: https://docs.reap.global/api-reference/card-status-updated
/webhooks/openapi.json webhook CARD_STATUS_UPDATED
Fired when a card status changes (e.g., activated, frozen, unfrozen, cancelled).
# Card tokenization requested
Source: https://docs.reap.global/api-reference/card-tokenization-requested
/webhooks/openapi.json webhook CARD_TOKENIZATION_REQUESTED
Fired when a cardholder adds a card to a digital wallet and chooses SMS verification. Contains the OTP so it can be delivered through an alternative channel if the SMS does not arrive. The OTP is valid for a short window (around 2 minutes).
# Card transaction created
Source: https://docs.reap.global/api-reference/card-transaction-created
/webhooks/openapi.json webhook CARD_TRANSACTION_CREATED
Fired when a new card transaction is created (e.g., authorization, purchase).
# Card transaction updated
Source: https://docs.reap.global/api-reference/card-transaction-updated
/webhooks/openapi.json webhook CARD_TRANSACTION_UPDATED
Fired when a card transaction receives a new lifecycle event (e.g., clearing, reversal, refund).
# Get card transaction
Source: https://docs.reap.global/api-reference/card-transactions/get-card-transaction
/api-reference/openapi.json get /card-transactions/{id}
Retrieves card transaction details including merchant information, amounts, and lifecycle events. The response shape varies by transaction status.
# Activate physical card
Source: https://docs.reap.global/api-reference/cards/activate-physical-card
/api-reference/openapi.json post /cards/{id}/activate
Activates a physical card using the 6-digit activation code from the card mailer. Use `GET /cards/:id/activation-code` only when the cardholder has lost the code that came with the card packaging.
# Block card
Source: https://docs.reap.global/api-reference/cards/block-card
/api-reference/openapi.json post /cards/{id}/block
Blocks a card. A block is the hard stop - a compromised card, a dispute, an account on hold - while freeze is the soft toggle a cardholder flips. A blocked card declines everything: online, in-store, and ATM. Blocking a frozen card keeps the freeze, so the card returns to `FROZEN` when the block lifts. Idempotent, and a block placed by Reap is never replaced: if the card already carries a block with `blockLiftable: false`, that reason stays and the card remains one only Reap support can unblock.
# Create card
Source: https://docs.reap.global/api-reference/cards/create-card
/api-reference/openapi.json post /cards/
Creates a new virtual or physical card for a user. User must have approved application; account must be active. The phone number used for OTP verification is taken from the user record; the card is issued against the user's verified identity.
# Delete card
Source: https://docs.reap.global/api-reference/cards/delete-card
/api-reference/openapi.json delete /cards/{id}
Deletes a card. Returns `404 CARD_NOT_FOUND` if the card does not exist or has already been deleted.
# Freeze card
Source: https://docs.reap.global/api-reference/cards/freeze-card
/api-reference/openapi.json post /cards/{id}/freeze
Freezes a card. Frozen cards cannot be used for transactions.
# Get 3DS challenge
Source: https://docs.reap.global/api-reference/cards/get-3ds-challenge
/api-reference/openapi.json get /card-3ds-challenges/{challengeId}
Retrieves a 3DS challenge by ID.
# Get activation code
Source: https://docs.reap.global/api-reference/cards/get-activation-code
/api-reference/openapi.json get /cards/{id}/activation-code
Retrieves the 6-digit activation code for a physical card. Use only when the cardholder has lost the code that came with the card packaging. Verify the cardholder's identity through your own secure channel before sharing the code.
# Get card
Source: https://docs.reap.global/api-reference/cards/get-card
/api-reference/openapi.json get /cards/{id}
Retrieves a single card by its unique identifier.
# List cards
Source: https://docs.reap.global/api-reference/cards/list-cards
/api-reference/openapi.json get /cards/
Retrieves a paginated list of cards. Use cursor-based pagination. Filter by `accountId` to get cards for a specific account.
# Push-provision card
Source: https://docs.reap.global/api-reference/cards/push-provision-card
/api-reference/openapi.json post /cards/{id}/push-provisioning
Generates the credentials needed to add a card to a digital wallet directly from your app, without the cardholder entering card details. Pass the result to the wallet SDK to complete provisioning. Each call returns fresh, single-use credentials.
# Respond to 3DS challenge
Source: https://docs.reap.global/api-reference/cards/respond-to-3ds-challenge
/api-reference/openapi.json post /card-3ds-challenges/{challengeId}/respond
Approves or rejects a 3DS authentication challenge. Must respond within 5 minutes of the challenge being created.
# Reveal card details
Source: https://docs.reap.global/api-reference/cards/reveal-card-details
/api-reference/openapi.json post /cards/{id}/reveal
Generates a short-lived URL to securely display card details (PAN, CVV, expiry) in an iframe or WebView. The URL is valid for 5 minutes and is single-use. Do not store, log, or expose this URL beyond the immediate display context.
# Reveal PAN
Source: https://docs.reap.global/api-reference/cards/reveal-pan
/api-reference/openapi.json post /cards/{id}/reveal-pan
Returns the card details (PAN, CVV, expiry) as an encrypted payload. Available only to PCI-scoped partners approved by Reap; all other projects receive `403 PAN_REVEAL_NOT_ENABLED`. Decrypt with the private half of the RSA key pair registered with Reap, using the OAEP hash named by `encryption`. Never store or log the decrypted card details.
# Unblock card
Source: https://docs.reap.global/api-reference/cards/unblock-card
/api-reference/openapi.json post /cards/{id}/unblock
Lifts a block. Only blocks reported with `blockLiftable: true` can be lifted this way; the rest return `409 CARD_BLOCK_NOT_LIFTABLE` and need Reap support. A card frozen before it was blocked returns to `FROZEN`, not `ACTIVE`, and a card unfrozen while blocked returns to `ACTIVE`. Idempotent; unblocking a card that holds no block returns it unchanged.
# Unfreeze card
Source: https://docs.reap.global/api-reference/cards/unfreeze-card
/api-reference/openapi.json post /cards/{id}/unfreeze
Unfreezes a previously frozen card.
# Update 3DS challenge method
Source: https://docs.reap.global/api-reference/cards/update-3ds-challenge-method
/api-reference/openapi.json put /cards/{id}/3ds-challenge-method
Updates the 3DS challenge verification method for a card.
# Update card PIN
Source: https://docs.reap.global/api-reference/cards/update-card-pin
/api-reference/openapi.json put /cards/{id}/pin
Updates the PIN for a card. Only supported for physical cards; virtual cards have no PIN. PIN must be 4-12 digits and cannot contain sequences of 3+ consecutive or repeated numbers.
# Advance company application
Source: https://docs.reap.global/api-reference/companies/advance-company-application
/api-reference/openapi.json post /companies/{id}/applications/
Advances the company application. For Managed KYB, send `redirectUri` (the default when `method` is omitted) to get a verification URL for the corporate client. Returns an error if an active Managed application already exists. For Universal KYB, send `method: UNIVERSAL_KYB` and the verified business pack. The first call creates the application; later calls amend it and return the same application until it is submitted. `nextAction` is `null` for Managed KYB.
# Create company
Source: https://docs.reap.global/api-reference/companies/create-company
/api-reference/openapi.json post /companies/
Creates a new company with the provided name. Only available in `CORPORATE` program mode.
# Get company
Source: https://docs.reap.global/api-reference/companies/get-company
/api-reference/openapi.json get /companies/{id}
Retrieves a single company by its unique identifier, including deactivated companies (`isDeactivated` is true).
# Get company application
Source: https://docs.reap.global/api-reference/companies/get-company-application
/api-reference/openapi.json get /companies/{id}/applications/{applicationId}
Retrieves a specific company application by its ID.
# Get related person
Source: https://docs.reap.global/api-reference/companies/get-related-person
/api-reference/openapi.json get /companies/{id}/related-persons/{personId}
Retrieves a related person by ID, including archived persons (`isActive` is false).
# Get related person application
Source: https://docs.reap.global/api-reference/companies/get-related-person-application
/api-reference/openapi.json get /companies/{id}/related-person-applications/{applicationId}
Retrieves a specific KYC verification application for a related person.
# List companies
Source: https://docs.reap.global/api-reference/companies/list-companies
/api-reference/openapi.json get /companies/
Retrieves a paginated list of companies. Use cursor-based pagination by passing the `nextCursor` value from the response to get subsequent pages.
# List company applications
Source: https://docs.reap.global/api-reference/companies/list-company-applications
/api-reference/openapi.json get /companies/{id}/applications/
Returns a paginated list of all company applications, ordered by creation date descending. Use the `status` query parameter to filter by application status. For example, to retrieve only active applications: `?status=INITIATED,IN_PROGRESS,REVIEW_REQUIRED`.
# List related person applications
Source: https://docs.reap.global/api-reference/companies/list-related-person-applications
/api-reference/openapi.json get /companies/{id}/related-person-applications/
Returns a paginated list of related person KYC applications for the company, ordered by creation date descending. Use the `status` query parameter to filter by application status (comma-separated), or `relatedPersonId` to scope to a single related person.
# List related persons
Source: https://docs.reap.global/api-reference/companies/list-related-persons
/api-reference/openapi.json get /companies/{id}/related-persons/
Returns a paginated list of related persons (officers and UBOs) for the company, ordered by creation date descending. Archived persons are omitted unless `includeArchived` is true. Use the `status` query parameter to filter by verification status (comma-separated), or `id` to fetch a single person by ID. Related persons are automatically discovered during company verification.
# Submit company application
Source: https://docs.reap.global/api-reference/companies/submit-company-application
/api-reference/openapi.json post /companies/{id}/applications/submit
Submits a complete Universal KYB application for review. Required company documents and every declared beneficial-owner identity document must already be uploaded. On success the company moves to `IN_REVIEW` and `nextAction` is `null`. An incomplete submission returns `COMPANY_APPLICATION_INCOMPLETE` with `missing` and `invalid` so every remaining gap can be fixed in one pass.
# Upload application documents
Source: https://docs.reap.global/api-reference/companies/upload-application-documents
/api-reference/openapi.json post /companies/{id}/applications/documents
Uploads one company supporting document type for a Universal KYB application. Set `documentType` to a pending company document from `nextAction` and attach its file(s) as `files`. Upload one type per request. Beneficial-owner identity documents use `POST /companies/:id/related-persons/:personId/documents`.
# Upload related person documents
Source: https://docs.reap.global/api-reference/companies/upload-related-person-documents
/api-reference/openapi.json post /companies/{id}/related-persons/{personId}/documents
Uploads one declared beneficial-owner identity document for a Universal KYB application. `uboDocumentType` must match a type declared for this person. Send the front image first, then the back if the document has two sides.
# Company status updated
Source: https://docs.reap.global/api-reference/company-status-updated
/webhooks/openapi.json webhook COMPANY_STATUS_UPDATED
Fired when a company status changes (e.g., KYB approved, rejected, restricted).
# Crypto deposit created
Source: https://docs.reap.global/api-reference/crypto-deposit-created
/webhooks/openapi.json webhook CRYPTO_DEPOSIT_CREATED
Fired when a new on-chain deposit is detected on an account deposit address.
# Crypto deposit status updated
Source: https://docs.reap.global/api-reference/crypto-deposit-status-updated
/webhooks/openapi.json webhook CRYPTO_DEPOSIT_STATUS_UPDATED
Fired when a crypto deposit status changes (e.g., approved, rejected).
# Get crypto deposit
Source: https://docs.reap.global/api-reference/crypto-deposits/get-crypto-deposit
/api-reference/openapi.json get /crypto-deposits/{id}
Retrieves crypto deposit details including asset metadata.
# Cancel crypto withdrawal
Source: https://docs.reap.global/api-reference/crypto-withdrawals/cancel-crypto-withdrawal
/api-reference/openapi.json post /crypto-withdrawals/{id}/cancel
Cancels a withdrawal that is still awaiting a signature and releases the reserved funds.
# Get crypto withdrawal
Source: https://docs.reap.global/api-reference/crypto-withdrawals/get-crypto-withdrawal
/api-reference/openapi.json get /crypto-withdrawals/{id}
Retrieves a crypto withdrawal by id. Pure read - does not mint a signing payload.
# Initiate crypto withdrawal
Source: https://docs.reap.global/api-reference/crypto-withdrawals/initiate-crypto-withdrawal
/api-reference/openapi.json post /crypto-withdrawals/
Reserves funds against an in-flight withdrawal and opens the signature window. Fetch a signing payload with `GET /crypto-withdrawals/:id/payload`, then submit the owner signature with `POST /crypto-withdrawals/:id/sign`.
# Mint signing payload
Source: https://docs.reap.global/api-reference/crypto-withdrawals/mint-signing-payload
/api-reference/openapi.json get /crypto-withdrawals/{id}/payload
Mints a fresh payload for the wallet owner to sign. Call this immediately before signing; each call retires any earlier unsigned payload. Only valid while the withdrawal awaits a signature.
# Submit withdrawal signature
Source: https://docs.reap.global/api-reference/crypto-withdrawals/submit-withdrawal-signature
/api-reference/openapi.json post /crypto-withdrawals/{id}/sign
Submits the wallet owner signature over the current signing payload. The withdrawal then proceeds to broadcast.
# File dispute
Source: https://docs.reap.global/api-reference/disputes/file-dispute
/api-reference/openapi.json post /disputes/
Files a dispute against a cleared card transaction. Card, cardholder, and currency are derived from the transaction. Status changes arrive as `CARD_DISPUTE_STATUS_UPDATED`.
# Get dispute
Source: https://docs.reap.global/api-reference/disputes/get-dispute
/api-reference/openapi.json get /disputes/{id}
Retrieves a single card dispute by its unique identifier.
# List disputes
Source: https://docs.reap.global/api-reference/disputes/list-disputes
/api-reference/openapi.json get /disputes/
Returns a paginated list of card disputes, newest first. Filter by status, reason, category, card, transaction, or filed time range. No status default is applied.
# Errors
Source: https://docs.reap.global/api-reference/errors
Error response format and status codes.
All error responses follow a consistent format.
## Error Response Format
```json theme={null}
{
"error": {
"code": "USER_NOT_FOUND",
"message": "User not found",
"detail": null
}
}
```
| Field | Type | Description |
| - | - | - |
| `code` | `string` | Machine-readable error code for programmatic handling |
| `message` | `string` | Human-readable error description |
| `detail` | `object \| null` | Additional context when available |
## HTTP Status Codes
| Status | Meaning |
| - | - |
| `400` | Bad Request. The request is malformed or the endpoint rejected it. |
| `401` | Unauthorized. Invalid or missing API key. |
| `403` | Forbidden. Valid API key but insufficient permissions, or the client IP is outside the key's allowlist (`API_KEY_IP_NOT_ALLOWED`). |
| `404` | Not Found. Resource does not exist. |
| `409` | Conflict. Resource already exists or state conflict. |
| `422` | Unprocessable Entity. The request failed validation. See [Validation errors](#validation-errors). |
| `429` | Too Many Requests. Rate limit exceeded. See [Rate Limiting](/api-reference/rate-limiting). |
| `500` | Internal Server Error. Contact support if persistent. |
## Errors any endpoint can return
Each endpoint page in the API reference lists the errors specific to that endpoint. The errors below can come from any endpoint, so they are documented only here.
| Status | Code | Cause |
| - | - | - |
| `400` | `API_VERSION_HEADER_MISSING` | The `Reap-Version` header is missing. See [Versioning](/api-reference/versioning). |
| `400` | `API_VERSION_INVALID` | The `Reap-Version` header is not a supported version. |
| `400` | `PARSE_ERROR` | The request body could not be parsed. |
| `400` | `IDEMPOTENT_PARAMETER_MISMATCH` | A POST reused an `Idempotency-Key` with a different body. See [Idempotency](/api-reference/idempotency). |
| `401` | `API_KEY_REQUIRED` | The `Authorization` header is missing. See [Authentication](/api-reference/authentication). |
| `401` | `INVALID_AUTH_HEADER` | The `Authorization` header is not a Bearer token. |
| `401` | `INVALID_API_KEY` | The API key is unknown or revoked, or it belongs to another environment or region. |
| `403` | `API_KEY_IP_NOT_ALLOWED` | The request came from an IP address outside the key's allowlist. |
| `404` | `ROUTE_NOT_FOUND` | No endpoint matches the method and path. |
| `409` | `IDEMPOTENCY_REQUEST_IN_PROGRESS` | Another POST with the same `Idempotency-Key` is still running. |
| `422` | `VALIDATION_FAILED` | The request does not match the endpoint's schema. See [Validation errors](#validation-errors). |
| `429` | `RATE_LIMIT_EXCEEDED` | The project exceeded its rate limit. See [Rate Limiting](/api-reference/rate-limiting). |
| `500` | `INTERNAL_SERVER_ERROR` | The request failed on our side. |
## Validation errors
A request that does not match the endpoint's schema returns `422` with code `VALIDATION_FAILED`. The `detail` object says what failed:
* `on` names the part of the request that failed: `body`, `query`, `params` or `headers`.
* `errors` lists each problem. Every entry has a `path` to the field and a `message`. Most entries also carry a `code` for the rule that failed.
For example, sending `cardType` instead of `type` to `POST /cards` returns:
```json theme={null}
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Validation failed",
"detail": {
"on": "body",
"errors": [
{
"path": "type",
"message": "Invalid option: expected one of \"VIRTUAL\"|\"PHYSICAL\"",
"code": "invalid_value"
}
]
}
}
}
```
The API ignores fields it does not recognize. A misspelled field therefore shows up as an error on the required field it was meant to fill. Nested fields use dot-separated paths, such as `config.maxAmount`.
A few checks run after schema validation, such as decoding a pagination `cursor`. These also return `VALIDATION_FAILED`, with a shorter `detail` in place of `errors`.
## Handling Errors
Use the `code` field for programmatic error handling. Branch on `code` rather than the HTTP status, because several errors can share a status. For example, a few endpoints use `422` for their own business-rule errors alongside `VALIDATION_FAILED`.
```javascript theme={null}
if (response.error.code === 'USER_NOT_FOUND') {
// Handle missing user
} else if (response.error.code === 'PHONE_NUMBER_ALREADY_EXISTS') {
// Handle duplicate phone
}
```
## Retries
For `5xx` errors, retry with exponential backoff. For `429`, wait for the `Retry-After` header. For other `4xx` errors, fix the request before retrying.
# Create fee override
Source: https://docs.reap.global/api-reference/fees/create-fee-override
/api-reference/openapi.json post /fees/
Creates an account-level override for one fee type. An account has at most one override per fee type; creating a second returns a conflict. Project defaults cannot be created - every project already has one row per fee type.
# Delete fee override
Source: https://docs.reap.global/api-reference/fees/delete-fee-override
/api-reference/openapi.json delete /fees/{id}
Removes an account-level override so the account falls back to the project default. Project defaults cannot be deleted.
# Get fee
Source: https://docs.reap.global/api-reference/fees/get-fee
/api-reference/openapi.json get /fees/{id}
Retrieves a fee row by ID.
# List effective fees
Source: https://docs.reap.global/api-reference/fees/list-effective-fees
/api-reference/openapi.json get /fees/effective
Returns the fee an account is charged for every fee type: its own override where one exists, otherwise the project default. Each entry names the row it comes from.
# List fees
Source: https://docs.reap.global/api-reference/fees/list-fees
/api-reference/openapi.json get /fees/
Returns a paginated list of your fee rows - project defaults and account overrides - optionally filtered by type or scope.
# Update fee value
Source: https://docs.reap.global/api-reference/fees/update-fee-value
/api-reference/openapi.json put /fees/{id}
Replaces the value of a fee row and bumps its version. Transactions authorized from then on are priced with the new value; settled transactions keep the version that priced them. Project defaults can only be updated from the dashboard.
# Fiat deposit created
Source: https://docs.reap.global/api-reference/fiat-deposit-created
/webhooks/openapi.json webhook FIAT_DEPOSIT_CREATED
Fired when an inbound bank transfer is credited to an account. The deposit is already settled when the event is sent - a fiat transfer is reported to Reap only once the receiving bank has reconciled it.
# Get bank details
Source: https://docs.reap.global/api-reference/fiat-deposits/get-bank-details
/api-reference/openapi.json get /fiat-deposits/bank-details
Retrieves where to send a bank transfer to fund the master collateral account. The project must be enabled for bank transfer funding, or the response is `FIAT_DEPOSITS_NOT_ENABLED`. Always quote the returned `reference` on the transfer, and read these details fresh before each transfer rather than storing them - an account can be closed by the bank, and a transfer to a closed account has to be recalled. Sandbox returns placeholder details for an account no wire can reach; simulate a deposit instead of sending one.
# Get fiat deposit
Source: https://docs.reap.global/api-reference/fiat-deposits/get-fiat-deposit
/api-reference/openapi.json get /fiat-deposits/{id}
Retrieves a fiat deposit credited to an account, including the amount as received by the bank and the amount credited.
# Get alert
Source: https://docs.reap.global/api-reference/fraud-alerts/get-alert
/api-reference/openapi.json get /fraud-alerts/{id}
Retrieves a single fraud alert by its unique identifier.
# List alerts
Source: https://docs.reap.global/api-reference/fraud-alerts/list-alerts
/api-reference/openapi.json get /fraud-alerts/
Returns a paginated list of fraud alerts, newest transaction first. Filter by status, card, transaction, origin, or transaction time range. No status default is applied.
# Report fraud
Source: https://docs.reap.global/api-reference/fraud-alerts/report-fraud
/api-reference/openapi.json post /fraud-alerts/
Files a fraud report against a past card transaction. Always confirms the transaction as fraud and blocks the card. If a pending detected alert already exists for the same transaction and is still inside the response window, this delegates to confirm. Partners learn of the block via `CARD_STATUS_UPDATED` with `blockReason: FRAUD_ALERT_CONFIRMED`.
# Respond to alert
Source: https://docs.reap.global/api-reference/fraud-alerts/respond-to-alert
/api-reference/openapi.json post /fraud-alerts/{id}/respond
Confirms or declines a pending detected fraud alert. Confirming blocks the card (`CARD_STATUS_UPDATED` with `blockReason: FRAUD_ALERT_CONFIRMED`); declining leaves the card alone. `type` is required when confirming.
# Idempotency
Source: https://docs.reap.global/api-reference/idempotency
Safely retry POST requests with the Idempotency-Key header.
Send a unique `Idempotency-Key` header on any POST request and you can safely retry on failure. The server caches the first response (status code and body) and replays it on retries. Keys expire after 24 hours.
The header is **required** on the endpoints that create money movement or a chargeable resource - creating an account, a card, a posting, a dispute, or an API key, and submitting a card shipment. Omitting it there returns `422`. Every other POST accepts it and honors it when present. The API reference marks the header as required or optional on each endpoint.
```bash theme={null}
curl -X POST https://api.reap.global/cards \
-H "Authorization: Bearer $REAP_API_KEY" \
-H "Reap-Version: 2026-01-01" \
-H "Idempotency-Key: 7e7f1a90-3e0e-4a7e-bd2c-9b3a3c2d8e1f" \
-H "Content-Type: application/json" \
-d '{ "userId": "...", "accountId": "...", "type": "VIRTUAL" }'
```
Generate one key per logical operation. Use a UUIDv4 or any high-entropy string up to 255 characters. Keep the same key across every retry - never generate a new key on each attempt.
***
## Behavior
| Situation | Response |
| - | - |
| First request | Executes and caches the response. |
| Retry, same key, same body | Returns the cached response. Includes `Idempotent-Replayed: true` response header. |
| Retry, same key, different body | `400 IDEMPOTENT_PARAMETER_MISMATCH` - use a new key for a different request. |
| Two concurrent requests, same key | `409 IDEMPOTENCY_REQUEST_IN_PROGRESS` - retry with backoff. |
| Key reused after 24 hours | Treated as a fresh request. |
All outcomes are cached, including `4xx` and `5xx`. If the first attempt returned a business error, retries replay that error. Fix the input and use a new key to try again.
`401`, `422`, and `429` responses are not cached and are safe to retry with the same key.
***
## Retry pattern
```ts theme={null}
const idempotencyKey = crypto.randomUUID(); // generate once, reuse on retries
async function createCardWithRetry(body) {
for (let attempt = 0; attempt < 5; attempt++) {
try {
const res = await fetch('https://prod.api.reap.global/cards', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Reap-Version': '2026-01-01',
'Idempotency-Key': idempotencyKey,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (res.status === 429 || res.status >= 500) {
await sleep(2 ** attempt * 1000);
continue;
}
return await res.json();
} catch {
await sleep(2 ** attempt * 1000); // network error: safe to retry same key
}
}
}
```
***
## Recommendations
* **Retry on network errors and `429`** with the same key. These are the only cases where the original request may not have reached the server.
* **Use a new key after a `4xx` business error.** The cached error replays on retries; only a new key triggers a fresh attempt.
* **Persist the key before sending** if you need guaranteed retry safety after a client crash.
# Overview
Source: https://docs.reap.global/api-reference/overview
Reap API reference.
REST API with JSON request and response bodies.
## Base URLs
| Environment | Base URL |
| - | - |
| Singapore sandbox | `https://sg.sandbox.api.reap.global` |
| Singapore production | `https://sg.prod.api.reap.global` |
| Mexico sandbox | `https://mx.sandbox.api.reap.global` |
| Mexico production | `https://mx.prod.api.reap.global` |
`https://sandbox.api.reap.global` and `https://prod.api.reap.global` remain valid aliases for Singapore.
Sandbox and production are fully isolated. API keys, webhooks, and data live in one environment and never cross.
## Required Headers
Every request must include:
| Header | Description |
| - | - |
| `Authorization` | Your API key as a Bearer token: `Bearer YOUR_API_KEY` |
| `Reap-Version` | API version in YYYY-MM-DD format. Current version: `2025-02-14` |
| `Content-Type` | `application/json` for requests with a body |
## Example Request
```bash theme={null}
curl https://sg.sandbox.api.reap.global/users \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Reap-Version: 2025-02-14"
```
## Numerical Values
Monetary and rate fields use different JSON types depending on what they represent, chosen so partners never silently lose precision:
| Field shape | JSON type | Notes |
| - | - | - |
| Fiat amounts (USD, EUR, ...) | `number` | At the currency's native precision (2 decimal places for most fiat). Safe to read as a JS `number`. |
| Crypto and virtual asset amounts | `string` | Decimal string padded to the asset's `decimals` (e.g. `"1.000000"` for 1 USDC). Lossless across all values; compare and arithmetic via a big-decimal library. |
| Exchange and conversion rates | `number` | 6 decimal places. |
A field's individual description always wins when it differs. The same conventions are followed on input bodies.
# Create policy
Source: https://docs.reap.global/api-reference/policies/create-policy
/api-reference/openapi.json post /policies/
Creates a policy. The accepted configuration depends on the policy type.
# Disable policy
Source: https://docs.reap.global/api-reference/policies/disable-policy
/api-reference/openapi.json post /policies/{id}/disable
Disables a policy so it stops applying to new transactions. Recorded usage is retained. Disabling an already-disabled policy returns it unchanged.
# Enable policy
Source: https://docs.reap.global/api-reference/policies/enable-policy
/api-reference/openapi.json post /policies/{id}/enable
Enables a policy so it applies to transactions again. Enabling an already-enabled policy returns it unchanged.
# Get policy
Source: https://docs.reap.global/api-reference/policies/get-policy
/api-reference/openapi.json get /policies/{id}
Retrieves a policy by ID.
# List effective policies
Source: https://docs.reap.global/api-reference/policies/list-effective-policies
/api-reference/openapi.json get /policies/effective
Returns the policies in force for a card, cardholder, or your program, resolved up the attachment chain (a card inherits its cardholder and program policies). Limits that track a running total over a window include their current usage and remaining headroom, so you can show how much is left. Only active policies are included.
# List policies
Source: https://docs.reap.global/api-reference/policies/list-policies
/api-reference/openapi.json get /policies/
Returns a paginated list of your policies, optionally filtered by type, status, or scope. By default only ACTIVE policies are returned; pass an explicit status filter to include disabled ones.
# Rename policy
Source: https://docs.reap.global/api-reference/policies/rename-policy
/api-reference/openapi.json patch /policies/{id}
Updates a policy label.
# Replace policy configuration
Source: https://docs.reap.global/api-reference/policies/replace-policy-configuration
/api-reference/openapi.json put /policies/{id}/config
Replaces the configuration of a policy. The `type` must match the existing policy. Properties that are fixed at creation cannot be changed.
# Rate Limiting
Source: https://docs.reap.global/api-reference/rate-limiting
Per-project request limits, response headers, and 429 handling.
The Reap API is shared infrastructure. To keep it reliable for everyone, requests are rate-limited per project on rolling per-second, per-minute, and per-day windows. Limits apply to the project as a whole, not per API key.
***
## Limits
Most endpoints share the default limit. A small set of write endpoints have tighter limits based on the downstream operations they trigger.
| Tier | Per second | Per minute | Per day |
| - | - | - | - |
| Default | 20 | 600 | 500,000 |
| Card issuance | 10 | 100 | 5,000 |
| Card operations | 15 | 250 | 10,000 |
| Identity verification | 5 | 150 | 2,000 |
| Account creation | 3 | 100 | 10,000 |
| Withdrawals | 6 | 60 | 5,000 |
| Tier | Per second | Per minute | Per day |
| - | - | - | - |
| Default | 10 | 150 | 10,000 |
| Card issuance | 5 | 50 | 500 |
| Card operations | 5 | 100 | 1,000 |
| Identity verification | 2 | 30 | 300 |
| Account creation | 2 | 50 | 2,000 |
| Withdrawals | 6 | 30 | 500 |
**Card issuance** covers:
* `POST /cards`
* `POST /card-shipments/:id/submit`
**Card operations** covers:
* `POST /cards/:id/freeze`
* `POST /cards/:id/unfreeze`
* `POST /cards/:id/block`
* `POST /cards/:id/unblock`
* `DELETE /cards/:id`
* `POST /cards/:id/reveal`
* `POST /cards/:id/reveal-pan`
* `PUT /cards/:id/pin`
* `POST /cards/:id/activate`
* `GET /cards/:id/activation-code`
* `POST /cards/:id/push-provisioning`
* `PUT /cards/:id/3ds-challenge-method`
* `POST /card-shipments`
* `POST /disputes`
**Identity verification** covers:
* `POST /companies`
* `POST /companies/:id/applications`
* `POST /users/:id/application`
* `POST /users/:id/application/documents`
**Account creation** covers:
* `POST /accounts`
Provisioning a wallet-backed account creates keys and registers deposit addresses with our chain infrastructure, so it is paced more tightly than plain reads and writes.
**Withdrawals** covers:
* `POST /crypto-withdrawals`
* `GET /crypto-withdrawals/:id/payload`
* `POST /crypto-withdrawals/:id/sign`
These limits are lower than the rest because each call reserves scarce chain and custody resources. The per-second allowance leaves room for one withdrawal's three calls back to back. Reading a withdrawal or cancelling one is not affected.
All other endpoints fall under the default limit.
***
## Response headers
Every response includes the following headers so you can track usage without making extra requests.
| Header | Description |
| - | - |
| `RateLimit-Limit` | Maximum requests allowed in the current window |
| `RateLimit-Remaining` | Requests remaining in the current window |
| `RateLimit-Reset` | Unix timestamp (seconds) when the current window resets |
| `RateLimit-Policy` | Identifier for the limit applied to this request |
Legacy `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers are also returned for compatibility with older HTTP clients.
***
## Exceeding the limit
When a request exceeds your limit, the API responds with `429 Too Many Requests`:
* The error body uses `code: RATE_LIMIT_EXCEEDED`.
* A `Retry-After` header is set to the number of seconds to wait before retrying.
```json theme={null}
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded. See the Retry-After header for when to retry."
}
}
```
Wait at least the number of seconds in `Retry-After` before retrying. Earlier retries will simply receive another `429` until the window resets.
***
## Best practices
* **Read the headers.** Track `RateLimit-Remaining` to back off proactively before the limit hits zero.
* **Honor `Retry-After`.** When you receive a `429`, wait at least the indicated number of seconds before retrying.
* **Use exponential backoff with jitter.** If you must retry beyond the first attempt, increase the wait between retries and add randomness to avoid thundering herds.
* **Spread out bulk operations.** When onboarding many users or issuing many cards, pace requests against the per-minute and per-day limits rather than firing them as fast as possible.
* **Need higher limits?** Contact the Reap team if your use case genuinely requires more headroom. We tune limits per project where it makes sense.
# Reset company application
Source: https://docs.reap.global/api-reference/simulation/reset-company-application
/api-reference/openapi.json post /simulation/companies/{companyId}/reset
Rewinds a Universal KYB sandbox company application to pending submission, keeping the pack and uploaded documents. A submitted company returns to `PENDING` and can be submitted again. An `ACTIVE` or `RESTRICTED` company returns `SIMULATION_INVALID_STATE`. Returns `204`.
# Simulate 3DS authorization
Source: https://docs.reap.global/api-reference/simulation/simulate-3ds-authorization
/api-reference/openapi.json post /simulation/card-transactions/authorization-with-3ds
Simulates a WEBHOOK 3DS challenge for a card authorization. Returns a pending `ThreeDsChallenge` and delivers `CARD_3DS_CHALLENGE_CREATED` without creating a transaction. After `POST /card-3ds-challenges/{id}/respond` with `approve: true`, authorization completes and `CARD_TRANSACTION_CREATED` is delivered. With `approve: false`, the challenge is rejected and no transaction is created (mirrors production). The card must use `3dsChallengeMethod: WEBHOOK`.
# Simulate a fiat deposit
Source: https://docs.reap.global/api-reference/simulation/simulate-a-fiat-deposit
/api-reference/openapi.json post /simulation/fiat-deposits
Simulates a bank transfer credited to the project's master collateral account. The deposit is recorded as already settled, the master balance is credited, and `FIAT_DEPOSIT_CREATED` is delivered, matching what a reconciled transfer does in production. Only available to Program-Funded projects, which are the ones that have a master collateral account. `currency` must be one the project accepts top-ups in, which is the project's billing currency.
# Simulate account status
Source: https://docs.reap.global/api-reference/simulation/simulate-account-status
/api-reference/openapi.json post /simulation/accounts/{id}/status
Simulates an account status change between `ACTIVE` and `RESTRICTED`. `RESTRICTED` restricts the account and its deposit addresses, and declines every authorization on its cards; the cards themselves keep the status they held. `ACTIVE` lifts the restriction and leaves card statuses untouched.
# Simulate authorization
Source: https://docs.reap.global/api-reference/simulation/simulate-authorization
/api-reference/openapi.json post /simulation/card-transactions/authorization
Simulates a card authorization event. Creates a new `PENDING` transaction, or performs incremental authorization on an existing one. Checks the account balance and returns 400 if insufficient funds. Pass `triggerFraudAlert: true` to also raise a detected fraud alert against the charge.
# Simulate card status
Source: https://docs.reap.global/api-reference/simulation/simulate-card-status
/api-reference/openapi.json post /simulation/cards/{id}/status
Simulates a card status change. `ACTIVE` unfreezes a frozen or blocked card. `FROZEN` freezes an active card. `BLOCKED` blocks an active or frozen card. `EXPIRED` expires a card.
# Simulate clearing
Source: https://docs.reap.global/api-reference/simulation/simulate-clearing
/api-reference/openapi.json post /simulation/card-transactions/clearing
Simulates a clearing (settlement) event. Pass `transactionId` to clear an existing authorization, or omit to simulate offline/direct clearing.
# Simulate company status
Source: https://docs.reap.global/api-reference/simulation/simulate-company-status
/api-reference/openapi.json post /simulation/companies/{companyId}/status
Simulates a company KYB outcome. Returns `204`. Observe the change via `GET /companies/{companyId}` and `COMPANY_STATUS_UPDATED`. On Universal KYB projects the company application must be submitted first; otherwise the request returns `SIMULATION_INVALID_STATE`. An incomplete application returns `COMPANY_APPLICATION_INCOMPLETE`.
# Simulate decline
Source: https://docs.reap.global/api-reference/simulation/simulate-decline
/api-reference/openapi.json post /simulation/card-transactions/decline
Simulates a declined card transaction. Creates a new `DECLINED` transaction, or declines an existing `PENDING` one.
# Simulate refund
Source: https://docs.reap.global/api-reference/simulation/simulate-refund
/api-reference/openapi.json post /simulation/card-transactions/refund
Simulates a refund event. Pass `transactionId` to refund an existing `CLEARED` transaction, or omit to simulate a standalone refund with no matching purchase (e.g., tax refund).
# Simulate reversal
Source: https://docs.reap.global/api-reference/simulation/simulate-reversal
/api-reference/openapi.json post /simulation/card-transactions/reversal
Simulates a reversal (partial or full) of an existing `PENDING` authorization. A full reversal transitions the transaction to `VOID`.
# Simulate shipment status
Source: https://docs.reap.global/api-reference/simulation/simulate-shipment-status
/api-reference/openapi.json post /simulation/card-shipments/{id}/status
Simulates a card shipment status change in sandbox. Drives production and shipping progress for a submitted shipment. Returns `204`; observe the change via `CARD_SHIPMENT_STATUS_UPDATED`.
# Simulate user application status
Source: https://docs.reap.global/api-reference/simulation/simulate-user-application-status
/api-reference/openapi.json post /simulation/users/{userId}/application
Simulates a user application (KYC) status change. The status update is processed asynchronously via an internal webhook pipeline, so the user status may not reflect the change immediately after this call returns. Clients should poll `GET /users/{userId}` until the desired status is observed.
# Statement status updated
Source: https://docs.reap.global/api-reference/statement-status-updated
/webhooks/openapi.json webhook STATEMENT_STATUS_UPDATED
Fired when a requested statement finishes: `GENERATED` when its file is ready to download via `GET /statements/{id}/download`, or `FAILED` when it could not be produced.
# Download statement
Source: https://docs.reap.global/api-reference/statements/download-statement
/api-reference/openapi.json get /statements/{id}/download
Serves a `GENERATED` statement file. Responds `302` to a short-lived URL that serves the file directly, so a plain HTTP client that follows redirects downloads it in one call; fetch that URL without your API credentials. Send `Accept: application/json` to receive the URL and its expiry as a body instead. A statement that is `PENDING` or `FAILED` has no file and responds `STATEMENT_NOT_READY`.
# Get statement
Source: https://docs.reap.global/api-reference/statements/get-statement
/api-reference/openapi.json get /statements/{id}
Retrieves a statement by id.
# List statements
Source: https://docs.reap.global/api-reference/statements/list-statements
/api-reference/openapi.json get /statements/
Lists statements requested for the project, most recently created first, including those Reap produces on a schedule.
# Request statement
Source: https://docs.reap.global/api-reference/statements/request-statement
/api-reference/openapi.json post /statements/
Requests a statement over a calendar-day period. The statement is returned `PENDING` and its file is produced asynchronously; a `STATEMENT_STATUS_UPDATED` webhook follows once it is `GENERATED` or `FAILED`. The period may span at most 31 days, must start on or after 2026-06-01, and must end on a day whose rows are final: yesterday from 10:00 UTC, otherwise the day before. At most 5 statements may be pending per project at once.
# User application status updated
Source: https://docs.reap.global/api-reference/user-application-status-updated
/webhooks/openapi.json webhook USER_APPLICATION_STATUS_UPDATED
Fired when a user application status changes (e.g., KYC approved, rejected, retry required).
# Advance the user application
Source: https://docs.reap.global/api-reference/users/advance-the-user-application
/api-reference/openapi.json post /users/{id}/application
Advances the user KYC application. Only valid while the application is in a pending state. Which verification method applies is fixed by your project configuration rather than chosen per request, and determines the request you send. With no body (Reap-run verification), Reap returns a provider SDK token in `nextAction` for the client to complete and approval arrives later via webhook. For Sumsub Token Sharing, a share token is ingested and the user moves to `IN_REVIEW`; the final decision arrives on the same compliance webhook as Managed KYC. For a full verified data pack, the response `nextAction` lists the supporting documents to upload via `POST /users/:id/application/documents`. Send an `Idempotency-Key` to safely retry without re-consuming a single-use share token.
# Create user
Source: https://docs.reap.global/api-reference/users/create-user
/api-reference/openapi.json post /users/
Creates a new user with the provided details. Phone number must be unique within the project, as is `externalId` when supplied. In `CORPORATE` mode, `companyId` is required.
# Delete user
Source: https://docs.reap.global/api-reference/users/delete-user
/api-reference/openapi.json delete /users/{id}
Deletes a user, deletes every card they hold, and restricts their accounts. Card deletion is permanent. The accounts stay open because they can still hold value and card debt to settle, but a restricted account authorizes nothing.
# Get user
Source: https://docs.reap.global/api-reference/users/get-user
/api-reference/openapi.json get /users/{id}
Retrieves a single user by their unique identifier.
# List users
Source: https://docs.reap.global/api-reference/users/list-users
/api-reference/openapi.json get /users/
Retrieves a paginated list of users. Use cursor-based pagination by passing the `nextCursor` value from the response to get subsequent pages. Use `externalId` to resolve your own identifier back to a Reap user. In `CORPORATE` mode, use `companyId` to filter users by company.
# Update user email
Source: https://docs.reap.global/api-reference/users/update-user-email
/api-reference/openapi.json put /users/{id}/email
Updates the email address for a user. Reserved or non-routable top-level domains are rejected.
# Update user phone
Source: https://docs.reap.global/api-reference/users/update-user-phone
/api-reference/openapi.json put /users/{id}/phone
Updates the phone number for a user. Phone number must be in E.164 format and unique within the project.
# Upload application documents
Source: https://docs.reap.global/api-reference/users/upload-application-documents
/api-reference/openapi.json post /users/{id}/application/documents
Uploads one supporting document type for a user whose application is awaiting documents. Set `documentType` to one of the pending documents from the advance response's `nextAction`, and attach its file(s) as `files` (repeat for multi-page or front/back). Upload one type per request, across as many requests as there are pending documents; each response reports the remaining documents in `nextAction`. When the last required document is received the application advances to review.
# Versioning & Compatibility
Source: https://docs.reap.global/api-reference/versioning
How the API evolves, what counts as a backwards-compatible change, and how to build a resilient integration.
Every request carries a `Reap-Version` header (see [Overview](/api-reference/overview)). Backwards-incompatible changes ship under a new version, so your integration keeps working until you choose to upgrade. Within a single version we continuously ship backwards-compatible improvements, and your integration should be built to absorb them without changes.
## Backwards-compatible changes
The following can happen at any time, within your current version, and are **not** considered breaking. Build your integration to tolerate them:
* Adding new API endpoints and resources.
* Adding new optional request parameters and request body fields.
* Adding new fields to existing responses and webhook payloads.
* Adding new values to an existing enum (for example a new policy `type` or decline reason `code`).
* Adding a new variant to a discriminated union (for example a new policy `type` with its own `config` shape).
* Adding new webhook event types.
* Adding new, more specific error codes, as long as the condition could not previously be resolved by your code at runtime.
* Changing the length or format of opaque identifiers (treat all IDs as variable-length strings).
## Breaking changes
These ship only under a new `Reap-Version`:
* Removing or renaming a field, parameter, or endpoint.
* Changing the type of an existing field, or the meaning of an existing value.
* Removing a value from an enum, or changing what an existing value means.
* Making a previously optional field or parameter required.
# Create posting
Source: https://docs.reap.global/api-reference/virtual-asset-postings/create-posting
/api-reference/openapi.json post /postings/
Atomically applies a posting against the target account's virtual asset balances. See the `type` field for variant semantics. For `SETTLEMENT`, the value applied to the outstanding card debt is returned as `settledAmount`, with the per-entry rate used reflected in `entries[].rateSnapshot`.
# Get posting
Source: https://docs.reap.global/api-reference/virtual-asset-postings/get-posting
/api-reference/openapi.json get /postings/{postingId}
Client-driven postings (deposit, withdrawal, settlement) against an account's virtual asset balances.
# Create virtual asset
Source: https://docs.reap.global/api-reference/virtual-assets/create-virtual-asset
/api-reference/openapi.json post /virtual-assets/
Defines a new virtual asset for the project. Only available when virtual assets are enabled for the project. For `FIXED` rate source, an initial rate must be provided. For `HTTP` rate source, an endpoint URL is required.
# Disable virtual asset
Source: https://docs.reap.global/api-reference/virtual-assets/disable-virtual-asset
/api-reference/openapi.json post /virtual-assets/{id}/disable
Permanently disable a virtual asset. One-way operation: disabled assets cannot be reactivated. Existing ledger balances remain but are excluded from balance computation and no new allocations are allowed. Idempotent: re-disabling an already-disabled asset is a no-op.
# Get virtual asset
Source: https://docs.reap.global/api-reference/virtual-assets/get-virtual-asset
/api-reference/openapi.json get /virtual-assets/{id}
Retrieves a virtual asset by ID, including the latest known rate.
# List virtual assets
Source: https://docs.reap.global/api-reference/virtual-assets/list-virtual-assets
/api-reference/openapi.json get /virtual-assets/
Returns all virtual assets defined for the project.
# Set rate for virtual asset
Source: https://docs.reap.global/api-reference/virtual-assets/set-rate-for-virtual-asset
/api-reference/openapi.json put /virtual-assets/{id}/rate
Sets a new exchange rate (in the project billing currency) for a virtual asset with `FIXED` rate source. Not available for `HTTP` rate source assets.
# Update virtual asset
Source: https://docs.reap.global/api-reference/virtual-assets/update-virtual-asset
/api-reference/openapi.json patch /virtual-assets/{id}
Update mutable fields (`name`, `httpEndpoint`). To disable the asset, use the dedicated `POST /:id/disable` endpoint.
# Create webhook endpoint
Source: https://docs.reap.global/api-reference/webhooks/create-webhook-endpoint
/api-reference/openapi.json post /webhooks/
Registers a webhook endpoint for the project. The response includes a one-time `signingSecret` that the partner must store to verify incoming webhook signatures.
`mode` defaults to `NOTIFICATION` (async event delivery), of which a maximum of 5 ACTIVE per project is enforced - disable an existing one to make room. Pass `REQUEST` to register the project's single synchronous authorization endpoint (exactly one per project, rotate-only, available only on projects with `EXTERNAL` authorization mode).
# Disable webhook endpoint
Source: https://docs.reap.global/api-reference/webhooks/disable-webhook-endpoint
/api-reference/openapi.json post /webhooks/{id}/disable
Permanently disable an async webhook endpoint. One-way operation: disabled endpoints cannot be reactivated. Create a new webhook to resume delivery to the same URL. Idempotent: re-disabling an already-disabled webhook is a no-op. `REQUEST`-mode endpoints are synchronous and rotate-only; they cannot be disabled.
# Get webhook endpoint
Source: https://docs.reap.global/api-reference/webhooks/get-webhook-endpoint
/api-reference/openapi.json get /webhooks/{id}
Webhook endpoint management. Configure HTTPS URLs that receive signed POST notifications when events occur in your project. Signing secrets are returned exactly once at create and at rotate-secret time - store them securely.
# List webhook endpoints
Source: https://docs.reap.global/api-reference/webhooks/list-webhook-endpoints
/api-reference/openapi.json get /webhooks/
Returns the project's webhook endpoints. By default only `ACTIVE` endpoints are returned; pass the `status` query parameter to widen or change the filter (e.g. `ACTIVE,DISABLED`).
# Rotate signing secret
Source: https://docs.reap.global/api-reference/webhooks/rotate-signing-secret
/api-reference/openapi.json post /webhooks/{id}/rotate-secret
Mints a new HMAC signing secret and returns it exactly once. The previous secret is invalidated immediately; in-flight deliveries signed with the old secret will fail verification on your endpoint and be retried automatically by the platform.
# Update webhook endpoint
Source: https://docs.reap.global/api-reference/webhooks/update-webhook-endpoint
/api-reference/openapi.json patch /webhooks/{id}
Update mutable fields (`name`, `url`). To rotate the signing secret, use `POST /:id/rotate-secret`. To disable, use `POST /:id/disable`.
# 3D Secure
Source: https://docs.reap.global/cards/3d-secure
Authenticate cardholders during online card payments.
3D Secure (3DS) is an authentication protocol used by Visa, Mastercard, and other networks to verify the cardholder during online card payments. When a merchant requests a 3DS check, the issuer must confirm the person making the purchase is really the cardholder. This protects against card-not-present fraud and is required by regulators like PSD2 in many regions.
Reap handles 3DS for you. The only choice you need to make is how the cardholder proves their identity when a challenge is raised.
***
## Verification methods
Each card carries a `3dsChallengeMethod` setting that determines how challenges are resolved. You can switch the method per card at any time.
| Method | Who verifies the cardholder | Integration work |
| - | - | - |
| `SMS` (default) | Reap sends a one-time code to the cardholder's phone and verifies the response. | None. |
| `WEBHOOK` | Reap forwards the challenge to your backend. You authenticate the cardholder in your app (biometric, in-app PIN, push approval) and call back. | Subscribe to a webhook and respond within 5 minutes. |
Use `SMS` if you want zero integration. Use `WEBHOOK` if you want to drive the verification step yourself, typically because your app already has a strong, in-app authentication flow you would rather reuse.
***
## Configuring the method
Set `3dsChallengeMethod` when [creating a card](/api-reference/cards/create-card). If omitted, the card defaults to `SMS`.
Switch later by calling [Update 3DS challenge method](/api-reference/cards/update-3ds-challenge-method). The change takes effect for subsequent transactions.
The current setting is exposed on the card resource as `3dsChallengeMethod`.
***
## SMS flow
When a merchant triggers a 3DS check:
1. The card network requests authentication from Reap.
2. Reap sends a one-time code to the cardholder's phone number on file.
3. The cardholder enters the code on the merchant's 3DS page.
4. Reap verifies the code and confirms (or denies) the authentication with the network.
You receive a [`CARD_TRANSACTION_CREATED`](/api-reference/card-transaction-created) webhook for the resulting authorization, with `channel: ECOMMERCE`. There are no extra integration steps.
The phone number used for SMS comes from the user record. Keep it up to date so challenges reach the cardholder. A wrong or stale number means the cardholder cannot complete the OTP step and the transaction is declined.
***
## Webhook flow
Use the `WEBHOOK` method when you want to authenticate the cardholder in your own app instead of relying on SMS OTP.
Listen for [`CARD_3DS_CHALLENGE_CREATED`](/api-reference/3ds-challenge-created). The payload includes the challenge ID, card ID, merchant name and country, transaction amount and currency, and an `expiresAt` timestamp.
Show the merchant name and amount in your app and verify the cardholder using your preferred method (biometric, PIN, push approval, etc.). Surface the time remaining so the cardholder knows the window is finite.
Call [Respond to 3DS challenge](/api-reference/cards/respond-to-3ds-challenge) with `approve: true` to approve, or `approve: false` to reject. You must respond within 5 minutes of the challenge being created. Missing the window expires the challenge and the purchase fails at the merchant.
On **approve**, the card network proceeds with authorization and you receive a [`CARD_TRANSACTION_CREATED`](/api-reference/card-transaction-created) webhook. On **reject** (or expiry), no authorization is sent, so you will **not** receive a transaction webhook.
```mermaid theme={null}
sequenceDiagram
participant Cardholder
participant Merchant
participant Reap
participant Backend as Your backend
Cardholder->>Merchant: Pay with card
Merchant->>Reap: 3DS authentication request
Reap->>Backend: CARD_3DS_CHALLENGE_CREATED
Backend->>Cardholder: Authenticate (biometric / PIN / push)
Cardholder->>Backend: Approves
Backend->>Reap: POST /card-3ds-challenges/:id/respond
Reap->>Merchant: Authentication result
Merchant->>Cardholder: Payment outcome
```
### Challenge status
A challenge moves through one of four states. Use [Get 3DS challenge](/api-reference/cards/get-3ds-challenge) to fetch the latest state by ID at any time.
| Status | Meaning |
| - | - |
| `PENDING` | Awaiting your response |
| `APPROVED` | You approved the challenge |
| `REJECTED` | You rejected the challenge |
| `EXPIRED` | No response was received within the 5-minute window. The transaction was declined |
***
## Best practices
* **Show context to the cardholder.** Surface the merchant name, country, and amount from the challenge payload. This helps the cardholder spot fraudulent purchases before approving.
* **Respect the 5-minute window.** Treat `expiresAt` as authoritative. If your authentication step might block (push notification, deep link, app cold start), make sure the cardholder can complete it well before the timeout.
* **Start with SMS.** Cards default to `SMS`, so 3DS works out of the box. Switch individual cards to `WEBHOOK` once your in-app authentication flow is ready.
* **Handle rejects.** A rejected or expired challenge means the purchase fails at the merchant and you will **not** receive a `CARD_TRANSACTION_CREATED` webhook. Tell the cardholder so they can retry if needed.
# Encrypted Retrieval
Source: https://docs.reap.global/cards/card-details/encrypted-retrieval
Retrieve card PAN, CVV, and expiry as an encrypted payload for PCI-compliant partners.
Retrieve the full card details (PAN, CVV, expiry date) as an encrypted payload that only you can decrypt. You generate the RSA key pair and register the public half with Reap; the details are encrypted with it and you decrypt them with the private half on your backend.
**Approval required.** This capability is enabled per program by Reap. Your system handles raw card data after decryption, so **your environment must be PCI DSS compliant**, and Reap enables it only after reviewing your compliance status. Projects without it receive `403 PAN_REVEAL_NOT_ENABLED`.
You are responsible for the decrypted values: never store, log, cache, or transmit them beyond the immediate use.
Most integrations do not need this. To show card details to the cardholder, use [Secure Display](/cards/card-details/secure-display) instead - see [Choosing an approach](/cards/card-details/overview#choosing-an-approach).
***
## Prerequisites
* The capability is enabled for your project by Reap. Talk to your account manager to request it; Reap reviews your PCI DSS compliance status before enabling. Calls from projects without it receive `403 PAN_REVEAL_NOT_ENABLED`.
* You hold the RSA private key:
* **Production**: you generate the key pair (RSA, 2048 bits or larger) and send **only the public key** to your implementation manager, who registers it against your program. The private key never leaves your systems and Reap never holds a copy.
* **Sandbox**: a single, publicly documented key pair is shared by all sandbox programs (see [Sandbox keys](#sandbox-keys)). It provides no confidentiality, sandbox cards are test cards.
***
## Request
See [Reveal PAN](/api-reference/cards/reveal-pan) for the full endpoint reference.
```bash theme={null}
curl -X POST https://sandbox.api.reap.global/cards/{cardId}/reveal-pan \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Reap-Version: 2025-02-14"
```
No request body. The card must exist, must not be in a terminal or inactive status, and its account must be active.
***
## Response
```json theme={null}
{
"encryptedData": "hEJ2X8QeZ...base64...",
"encryption": "RSA_OAEP_SHA256"
}
```
| Field | Description |
| - | - |
| `encryptedData` | Base64-encoded ciphertext of the card details JSON. |
| `encryption` | Encryption scheme of `encryptedData`. Dispatch your decryption on this value; new schemes may be added later. |
`encryption` is one of:
| Value | Meaning |
| - | - |
| `RSA_OAEP_SHA256` | RSA-OAEP with SHA-256. Used for new keys. |
| `RSA_OAEP_SHA1` | RSA-OAEP with SHA-1. |
The scheme is a property of the public key registered for your program, so it stays fixed until that key is replaced. Read it from every response rather than hardcoding it, so a key rotation cannot break your decryption.
Decrypting `encryptedData` yields a JSON document:
```json theme={null}
{
"pan": "5200000000001005",
"cvv": "123",
"expiryDate": "12/28"
}
```
***
## Decryption
Base64-decode `encryptedData`, then decrypt with your RSA private key using OAEP padding and the hash named by `encryption`: SHA-256 for `RSA_OAEP_SHA256`, SHA-1 for `RSA_OAEP_SHA1`.
```bash theme={null}
# Use rsa_oaep_md:sha1 when encryption is RSA_OAEP_SHA1
echo "$ENCRYPTED_DATA" | base64 -d > /tmp/blob.bin
openssl pkeyutl -decrypt -inkey private-key.pem -in /tmp/blob.bin \
-pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256
```
```javascript theme={null}
import { privateDecrypt, constants } from 'node:crypto';
// `response` is the parsed JSON body of POST /cards/{cardId}/reveal-pan,
// `privateKeyPem` the private half of the key registered with Reap.
const { encryptedData, encryption } = response;
const OAEP_HASH_BY_SCHEME = {
RSA_OAEP_SHA256: 'sha256',
RSA_OAEP_SHA1: 'sha1',
};
const cardDetails = JSON.parse(
privateDecrypt(
{
key: privateKeyPem,
padding: constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: OAEP_HASH_BY_SCHEME[encryption],
},
Buffer.from(encryptedData, 'base64')
).toString('utf8')
);
```
Always decrypt on your backend. Never ship the private key to a browser or mobile client, and never proxy the decrypted details through systems outside your PCI scope.
***
## Errors
| Status | Code | Meaning |
| - | - | - |
| 403 | `PAN_REVEAL_NOT_ENABLED` | The capability is not enabled for your project. |
| 404 | `CARD_NOT_FOUND` | The card does not exist or has been deleted. |
| 400 | `CARD_OPERATION_NOT_ALLOWED` | The card status does not permit revealing its details. |
| 400 | `ACCOUNT_NOT_ACTIVE` | The account the card belongs to is not active. |
***
## Sandbox keys
All sandbox programs share one RSA key pair. Use this private key to decrypt sandbox responses:
```
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAzn88MgWItkxYeUt5jWeRBYIiQEUFxstgYJx8tYblZ55psnYd
HwhsyoPDwG6eq0r60WFSEeP2QFqV1qceYuDNM2RziIaMMGYgYlCPjgYaiMda3wlX
0w6pMv/5VaJA6N0jcb2m5xBC3I7XqKkP3l9AfL5xCtWN71wmVA/WXVZQ755pC3YE
A4IMoOcXs/+143HWZarX7wULhK55c8uAeA1q3ZmL7boLkzBZdMQM6Qlv9DDVcgUH
dahk4MiibmOubXrOk12KAyEXZlLH8Sc3qTDQnYP60ZOz95Ffly2c5/6/vLEBP99y
J9baXwgoBpQ7amA2o+X7v+eomV5CUItJITR0FwIDAQABAoIBAQDJvT529DcjOqU5
I0aGoobpJcEGnyhMlkb7PAcOZdLbj8Vdp809k0KN+3sUFj6HxMIUKws+FUNwjc9T
nYOvQEbZjard1+1AH5ZF1sTZERd9R558xij6NpmRDMlwIwtdNSa6qXlUxus7hYYl
7b1mGBLtjDTuS8LV9WYwvCRGndyGq6489o5OHzkHrCSabPXhBWai9LLgYkjrF1v4
mo4LCCqJEjaLAdqOlUXLiM1DrDWYlGUqJhdqYmcdl85BBQjisfOrQoET5yKb+afP
r8ZNyY3AFKhwmSl5wHNcu1aY7E8UCzLYBboXd1vaWsXzo28UrUEk6jAQFW3otd3r
bUM+RhqBAoGBAP1r8fhSTh/DBaCU3CGPX6p3tRNhfWbpMt6nYVvuDVVWb46BvSmJ
W2BHK4Me8W+Bx6TKFGFI9eBsa6mevAT1R4283c1ji2pMe6W9uuojAe2ei3nRIYIi
eHrmMGLv/3EVO8ctn9OyevQuBXjEIcaNoxkTNwgln4mKonVwJctkDIlXAoGBANCZ
EkO840ZRQV8rYjEYJrE7W6hP+7FdTc/O62Gx5g6Kv11M7WY0dGJ0V/deio5sgmmV
+Hi4SYQEzG30VZS8QIzaXFOO3etuIbobf7HW7wP3sCsY0kYZpxGiu30luO3OOnvs
MLZ6wkdaArxAn1CPpmC3VngPy09cUVjAmun4mPNBAoGADP0eXsrhJDGbtTpL+hBe
J9+Q0Vh60+554/2SOOL/nfMbLqOprVpT66BaM5M/bqapM+IDn2Tc7U7BSeVP37Uu
oK4gdCl4+M7Iu5r43I1EyQgKQAke/YuEv0/TBQ8l+YbMFI5bFb+dG6zMLP9od8PE
q0FX3bwH7EXFw90mosES/2sCgYBKlcDTGZHGDb2rEq8MUM9c5ZVzTA35sBvnZtwA
1YQlxPSBnsTvm2fV4DWnzFUMcb37lNUADRy0KKcT1Z5YDSujcVw7wESXLoy+p6Bz
7R8PMxv5fOmKiReUlbQmHjvOQ8PjlWMhjCk1K/j4aL7OJl7eDA/N5A/U83ReoJeD
exDtQQKBgQD51uzxVgwUZNLkJbXCp64TNqUXdvy8+PlhqD9zU9QSpdsf2F0xM4gX
eF2r3FU8Ps9WD6SijD715A4o4/xVV7vUXKn11nVqeEkaEkn3KIZpFQiEdWQR2J9e
vErFndT9x8gfBgnKj4o6XvqHHPOPoqAC2piaXkLMhQGupqF922bj+Q==
-----END RSA PRIVATE KEY-----
```
***
## Security obligations
* Decrypt only on PCI DSS compliant backend systems.
* Never log, persist, or cache the decrypted PAN, CVV, or expiry date.
* Never expose the private key or decrypted details to client-side code.
* Every retrieval is audited by Reap.
# Overview
Source: https://docs.reap.global/cards/card-details/overview
Two ways to reveal a card's PAN, CVV, and expiry, and how to choose between them.
A card's full details (PAN, CVV, expiry date) are sensitive data that Reap holds on your behalf. There are two ways to get at them, and the right one depends on whether the values need to reach your own systems.
***
## Choosing an approach
| | [Secure Display](/cards/card-details/secure-display) | [Encrypted Retrieval](/cards/card-details/encrypted-retrieval) |
| - | - | - |
| **What you get** | A short-lived URL you render in an iframe | An RSA-encrypted payload you decrypt |
| **Where details land** | Only in the cardholder's browser or app | On your backend, in plaintext after decryption |
| **PCI DSS scope** | Stays with Reap | Moves to you |
| **Availability** | Every program | Off by default, enabled after a compliance review |
**Start with Secure Display.** It covers the common case: showing a cardholder their own card details in your app. Raw card data never enters your systems, so your PCI DSS obligations do not change.
**Use Encrypted Retrieval only when the raw values must reach your backend** and your environment is already PCI DSS compliant. Reap reviews your compliance status before enabling it per program. Projects without it receive `403 PAN_REVEAL_NOT_ENABLED`.
The two are not exclusive. A program approved for Encrypted Retrieval can still use Secure Display for its in-app reveal, and most do.
***
## What both have in common
* **The card must be usable.** It has to exist, not be in a terminal or inactive status, and belong to an active account.
* **Authenticate the cardholder first.** Neither approach checks who is asking beyond your API key, so verify the user's identity in your application before revealing anything.
* **Treat the output as short-lived.** Do not cache, log, or persist reveal URLs or decrypted values.
* **Every retrieval is audited** by Reap.
# Secure Display
Source: https://docs.reap.global/cards/card-details/secure-display
Show card PAN, CVV, and expiry to cardholders in an iframe, without handling raw card data.
Display sensitive card details (PAN, CVV, expiry date) to your users without handling raw card data. Reap API provides an iframe-based reveal flow that keeps your integration PCI-compliant.
***
## How it works
Call [Create reveal session](/api-reference/cards/reveal-card-details) from your server. Reap returns a short-lived, single-use `revealUrl`.
Pass the `revealUrl` to your client and render it as the `src` of an `
The cardholder sees PAN, CVV, and expiry date inside the iframe. No sensitive data touches your servers or client code.
***
## Security
| Property | Detail |
| - | - |
| **Single-use** | Each `revealUrl` can only be loaded once. A second load returns an error page. |
| **Short-lived** | URLs expire after 5 minutes. Generate a fresh URL each time the user wants to view card details. |
| **No raw card data** | Card details are rendered inside the iframe. Your application never handles the PAN, CVV, or expiry values. |
***
## Integration
### 1. Request a reveal URL
Call [Create reveal session](/api-reference/cards/reveal-card-details) from your backend. The response includes a `revealUrl` and an `expiresAt` timestamp.
### 2. Display in an iframe
Pass `revealUrl` from your backend to your client and load it as the `src` of an iframe or WebView.
**Client-side only.** The `revealUrl` must be loaded directly in a browser iframe or mobile WebView. Do not fetch, parse, or proxy the URL on your backend. Doing so exposes your servers to raw card data (PAN, CVV, expiry) and shifts PCI DSS compliance responsibilities onto your system.
```html theme={null}
```
```tsx theme={null}
function CardReveal({ revealUrl }: { revealUrl: string }) {
return (
);
}
```
```tsx theme={null}
import { WebView } from 'react-native-webview';
function CardReveal({ revealUrl }: { revealUrl: string }) {
return (
);
}
```
***
## Customization
The reveal endpoint accepts two optional parameters:
| Parameter | Type | Default | Description |
| - | - | - | - |
| `stylesheetUrl` | string (URL) | `null` | URL to a custom stylesheet for the card details iframe. Must be served over HTTPS. |
| `showCopyPanButton` | boolean | `false` | Display a copy button that lets the cardholder copy the PAN to clipboard. |
Pass these as optional fields in the [Create reveal session](/api-reference/cards/reveal-card-details) request body.
***
## Best practices
* **Generate on demand.** Request a new `revealUrl` each time the user taps "Show card details". Do not cache or store URLs.
* **Authenticate the cardholder first.** Only request a reveal URL after your application has verified the user's identity. The `revealUrl` does not require authentication to load, so treat it as sensitive.
* **Handle expiration.** If the iframe shows an error page, the URL has expired or was already used. Prompt the user to try again and request a fresh URL.
* **Use HTTPS for stylesheets.** If you provide a custom `stylesheetUrl`, serve it over HTTPS.
If you need the raw values on your own backend rather than in an iframe, see [Encrypted Retrieval](/cards/card-details/encrypted-retrieval).
# Manual Provisioning
Source: https://docs.reap.global/cards/digital-wallets/manual-provisioning
Let cardholders add a card to a wallet by entering its details.
Manual provisioning is when the cardholder adds a card to their wallet by typing in the card details. It works out of the box for every card program with no integration work. If you do nothing, your cardholders can still add their cards this way.
***
## How it works
The cardholder opens Apple Pay or Google Pay and chooses to add a payment card.
The cardholder enters the card number, expiry date, and CVV. They can read these from a physical card, or from your app if you show them through a [secure display](/cards/card-details/secure-display).
The wallet app shows the card program terms, which the cardholder accepts.
The wallet provider asks the cardholder to confirm the card with a passcode. Reap delivers it automatically. See [Identity verification](/cards/digital-wallets/overview#identity-verification).
The exact steps vary by device, operating system version, and wallet app. The flow above is the common path.
***
## What you need to do
Nothing is required to enable manual provisioning. Two things make it reliable:
* **Keep the cardholder's contact details accurate.** The verification passcode goes to the `email` or `phoneNumber` on the user record, whichever the cardholder picks. See [Identity verification](/cards/digital-wallets/overview#identity-verification).
* **Make card details easy to find.** For virtual cards, cardholders cannot copy a number off a physical card, so give them a [secure display](/cards/card-details/secure-display) in your app to read the details from.
For a one-tap flow that skips manual entry entirely, build [push provisioning](/cards/digital-wallets/push-provisioning).
# Overview
Source: https://docs.reap.global/cards/digital-wallets/overview
Add cards to Apple Pay and Google Pay through tokenization.
Cardholders can add their cards to a mobile wallet like Apple Pay or Google Pay and pay with their phone or watch. This page explains how that works and the two ways a card gets into a wallet.
***
## Tokenization
When a card is added to a wallet, the wallet does not store the real card number. Instead the card details (number, expiry, CVV) are replaced with a unique token that is specific to that card on that device. Payments made from the wallet use the token, not the card number.
A few consequences worth knowing:
* Each token represents one card on one device. The same card added to a phone and a watch produces two separate tokens.
* The real card number is never exposed to the merchant or stored on the device.
Tokens are managed by the wallet and the card networks, not through the Reap API.
***
## Provisioning
Provisioning is the act of adding a card to a wallet. There are two ways to do it.
| Method | How the card gets in | Integration work |
| - | - | - |
| [Manual provisioning](/cards/digital-wallets/manual-provisioning) | The cardholder opens their wallet app and types in the card details. | None. Works for every program. |
| [Push provisioning](/cards/digital-wallets/push-provisioning) | The cardholder taps an "Add to Wallet" button inside your app. | Build the button and a backend call. Requires onboarding. |
The two methods are not exclusive. Manual provisioning always works, so most programs offer it even when they also build push provisioning for the smoother in-app flow.
***
## Identity verification
During provisioning the wallet provider usually asks the cardholder to verify the card with a one-time passcode (OTP). This is the same regardless of which provisioning method is used.
The cardholder chooses whether to receive the passcode by email or SMS. Reap sends it automatically to the `email` or `phoneNumber` on the [user record](/api-reference/users/create-user). Keep both accurate, since you cannot know in advance which one the cardholder will pick. Update them with [Update user email address](/api-reference/users/update-user-email) or [Update user phone number](/api-reference/users/update-user-phone) if they change.
If the cardholder chooses to receive the passcode by SMS and you want a fallback channel (for example, to show the code in your app when the SMS is slow to arrive), subscribe to the [`CARD_TOKENIZATION_REQUESTED`](/api-reference/card-tokenization-requested) webhook. It delivers the SMS passcode as soon as verification is requested. The code is short-lived, so pass it to the cardholder right away.
Email is more reliable than SMS for OTP delivery. Encourage cardholders to choose email when the wallet app offers a choice.
# Push Provisioning
Source: https://docs.reap.global/cards/digital-wallets/push-provisioning
Add a card to a wallet with one tap from your app.
Push provisioning lets a cardholder add their card to a wallet by tapping an "Add to Wallet" button inside your app. There is no manual entry of card details. Your app requests a provisioning credential from Reap, hands it to the wallet SDK, and the card is tokenized.
Push provisioning currently supports **Google Pay**. Apple Pay push provisioning is not supported. Manual entry remains available for both wallets through [manual provisioning](/cards/digital-wallets/manual-provisioning).
***
## Before you start
Push provisioning has to be registered and approved before you can offer it. Reap requires you to:
* Tell your Reap account manager you intend to offer push provisioning, and complete Reap's registration to obtain approval.
* Request access to Google's device tokenization documentation and sign Google's non-disclosure agreement.
Your account manager guides you through these steps. They involve Google, so start early.
Manual provisioning has no such requirement, so you can ship that first and add the in-app button once approval is in place.
***
## How it works
```mermaid theme={null}
sequenceDiagram
participant Cardholder
participant App as Your app
participant Backend as Your backend
participant Reap
participant Wallet as Google Pay
Cardholder->>App: Taps "Add to Google Pay"
App->>Backend: Card ID + device context
Backend->>Reap: POST /cards/:id/push-provisioning
Reap->>Backend: Provisioning credential
Backend->>App: Provisioning credential
App->>Wallet: Hand credential to the wallet SDK
Wallet->>Cardholder: Card added (verify if prompted)
```
On the device, use the Google Pay SDK to read the active wallet ID (`getActiveWalletId()`) and the stable hardware ID (`getStableHardwareId()`). Send these to your backend along with the card ID.
From your backend, call [Generate push provisioning credentials](/api-reference/cards/push-provision-card) with `provider: GOOGLE_PAY` and the two device values as `walletAccountId` and `deviceId`.
The response returns `opc`, the credential to pass to the wallet SDK, and `last4`, the last four digits of the card number. Each call returns a fresh credential, so request one per provisioning attempt rather than caching it.
Pass the `opc` and `last4` to the Google Pay push tokenization request in your app. The wallet SDK takes over from here.
The wallet provider may ask the cardholder to confirm the card with a one-time passcode. Reap delivers it automatically. See [Identity verification](/cards/digital-wallets/overview#identity-verification).
Generate the provisioning credential on your backend, never on the device, and do not store it. Request a fresh one for each provisioning attempt.
***
## Google provisioning flow support
Reap currently supports Google's standard push provisioning flow, where the credential is used to create a device token. Google's newer Unified Android Push Provisioning (UAPP) is not yet supported. Build your Android integration against the standard flow for now.
Support for UAPP is planned. When it is added, the existing flow keeps working.
***
## Next steps
* Offer [manual provisioning](/cards/digital-wallets/manual-provisioning) as well, so cardholders can always add their card even outside your app.
* Keep each cardholder's contact details accurate so the verification passcode reaches them. See [Identity verification](/cards/digital-wallets/overview#identity-verification).
# Overview
Source: https://docs.reap.global/cards/disputes/overview
Challenge a cleared card transaction for chargeback.
When a cardholder does not recognise a charge, never received what they paid for, or was billed the wrong amount, you file a dispute and Reap takes the case to the card network. You send the transaction ID and a reason; Reap derives the card, the cardholder identity, and the currency from the transaction itself.
Subscribe to [`CARD_DISPUTE_STATUS_UPDATED`](/api-reference/card-dispute-status-updated) for the outcome. Cases can take weeks to resolve, so treat the webhook as the source of truth rather than polling.
A dispute is not a [fraud alert](/cards/fraud-alerts). Confirming a fraud alert blocks the card but does not recover the money. A dispute is the path for recovering the money. The two are independent: neither requires the other, and you can file both against the same transaction.
***
## Before you file
| Requirement | Detail |
| - | - |
| The transaction is `CLEARED` | Money has to have actually moved. `PENDING` and `VOID` transactions cannot be disputed. See the [transaction lifecycle](/transactions/lifecycle). Otherwise returns `CARD_DISPUTE_TRANSACTION_NOT_CLEARED`. |
| Within 180 days of the transaction | Measured from the transaction date. Otherwise returns `CARD_DISPUTE_FILING_WINDOW_EXPIRED`. |
| Amount is at most the cleared cardholder amount, net of refunds | Defaults to that full amount when omitted. The cardholder amount includes FX and ATM fees. A partly refunded transaction can still be disputed for the remainder. Otherwise returns `CARD_DISPUTE_AMOUNT_INVALID`. |
| No live dispute on the transaction | A second dispute returns `CARD_DISPUTE_ALREADY_EXISTS`. You may refile only if the earlier case was `CANCELED` or `DECLINED`, meaning it never reached the network. |
An `Idempotency-Key` is required on [File dispute](/api-reference/disputes/file-dispute). A retry carrying the same key replays the original response, so a network timeout on your side does not turn into an error on the retry. Without it a retry of a request that already succeeded returns `CARD_DISPUTE_ALREADY_EXISTS`.
***
## File a dispute
Pick the [dispute reason](/cards/disputes/reasons) that matches what the cardholder is telling you. The reason drives how the network handles the case, so it is worth surfacing as a clear choice in your UI rather than guessing.
Call [File dispute](/api-reference/disputes/file-dispute) with `transactionId` and `reason`.
Optionally send `amount` to dispute part of the charge, and `contactEmail` to route the outcome somewhere other than the cardholder's email on file. Everything else is derived from the transaction. The case is created as `RECEIVED` and Reap takes it from there.
You receive [`CARD_DISPUTE_STATUS_UPDATED`](/api-reference/card-dispute-status-updated) on every status change through to resolution.
***
## Dispute lifecycle
| Status | Meaning |
| - | - |
| `RECEIVED` | Recorded and queued for review. |
| `REVIEWING` | Under review before it goes to the network. |
| `SUBMITTED` | Filed with the card network. Now waiting on their ruling. |
| `WON` | The network ruled for the cardholder. |
| `LOST` | The case was filed and the network ruled for the merchant. |
| `REFUNDED` | The cardholder was credited. `refundedAmount` is that credit; it may be less than `amount`. The disputed transaction carries the credit as a `DISPUTE_REFUND` event. |
| `DECLINED` | Reap did not file the case with the network. |
| `CANCELED` | The case was withdrawn before resolution. |
```mermaid theme={null}
flowchart LR
Received["RECEIVED"] --> Reviewing["REVIEWING"]
Reviewing --> Submitted["SUBMITTED"]
Received --> Submitted
Submitted --> Won["WON"]
Submitted --> Lost["LOST"]
Submitted --> Refunded["REFUNDED"]
Won --> Refunded
```
`DECLINED` and `LOST` are easy to confuse. `DECLINED` means the case never reached the card network. `LOST` means it was filed and the network ruled for the merchant.
A dispute can be canceled or declined at any point before resolution. `resolvedAt` is set once the case reaches a final status.
***
## How a refund is calculated
File the cardholder amount: the charge plus [FX markup and ATM fees](/transactions/fees). Omit `amount` to dispute the full cleared total. The cardholder is credited `refundedAmount` when the case reaches `REFUNDED`, not the disputed `amount`.
`refundedAmount` stays `null` until then. Read it from the dispute resource or from [`CARD_DISPUTE_STATUS_UPDATED`](/api-reference/card-dispute-status-updated). The same credit is appended to the disputed transaction as a [`DISPUTE_REFUND` event](/transactions/lifecycle#dispute-refund) and delivered in `CARD_TRANSACTION_UPDATED`. The transaction's `amount.refunded` and `amount.current` reflect it.
The credit includes a proportional share of FX markup and ATM variable fee. The per-withdrawal ATM fee stays charged, the same way a merchant refund leaves it charged.
Take a $105 ATM withdrawal: $100 purchase, $1 FX, $1 ATM variable, \$3 per-withdrawal fee.
* A full dispute files $105. A full refund credits $102. The \$3 stays charged.
* A half dispute files $52.50. A matching refund credits $51.
If you keep the cardholder's balance, as in [External authorization](/transactions/external-authorization/overview), apply the credit from the `DISPUTE_REFUND` event on the transaction, the same way you apply a merchant `REFUND`. Do not also credit from the dispute webhook, or the refund is booked twice. See [Keeping your ledger in sync](/transactions/external-authorization/reconciliation#credit-a-dispute-refund).
***
## Next
The full reason vocabulary, grouped by category.
# Dispute Reasons
Source: https://docs.reap.global/cards/disputes/reasons
Pick the reason that matches what the cardholder is telling you.
Every dispute is filed under one reason, sent as `reason` on [File dispute](/cards/disputes/overview#file-a-dispute). The reason drives how the card network handles the case, so picking the closest match matters more than picking quickly.
`category` is derived from the reason and returned on the dispute, so you never send it. It is the group the reason belongs to, one of the four below, useful for organising a reason picker, and available as a filter on [List disputes](/api-reference/disputes/list-disputes).
***
## Fraud
The cardholder did not authorise the charge.
| Reason | Use when |
| - | - |
| `FRAUD_EMV_LIABILITY_SHIFT_COUNTERFEIT` | A counterfeit copy of the card was used in person. |
| `FRAUD_EMV_LIABILITY_SHIFT_NON_COUNTERFEIT` | The genuine card was used in person by someone else after being lost or stolen. |
| `FRAUD_OTHER_CARD_PRESENT` | Fraudulent use of the card in person that no other fraud reason covers. |
| `FRAUD_OTHER_CARD_ABSENT` | The card details were used online, by phone, or by mail. |
| `FRAUD_VISA_MONITORING_PROGRAM` | Fraud at a merchant Visa has placed under its fraud monitoring program. |
Reach for `FRAUD_OTHER_CARD_PRESENT` when you know the transaction was fraudulent and the card was used in person, but not how it was compromised. Use `FRAUD_OTHER_CARD_ABSENT` for the equivalent case online - it is where a confirmed [fraud alert](/cards/fraud-alerts) of type `ACCOUNT_NUMBER_MISUSE` lands when the cardholder still holds the card.
The two `FRAUD_EMV_LIABILITY_SHIFT_*` reasons carry a condition beyond the kind of fraud: they apply only where the merchant's terminal lacked the chip or PIN capability that would have prevented the transaction, which is what shifts liability to the merchant. Where the terminal was fully capable, the same fraud is filed as `FRAUD_OTHER_CARD_PRESENT` instead.
***
## Authorization
The merchant took the charge without valid authorization.
| Reason | Use when |
| - | - |
| `AUTHORIZATION_CARD_RECOVERY_BULLETIN` | The merchant charged the card without authorization while it was blocked. |
| `AUTHORIZATION_DECLINED` | The merchant charged the card after the authorization was declined. |
| `AUTHORIZATION_NO_AUTH_LATE_PRESENTMENT` | The charge was never authorized, or was settled too long after authorization. |
`AUTHORIZATION_NO_AUTH_LATE_PRESENTMENT` covers both situations under one reason, so use it whether the authorization was missing entirely or the merchant simply settled too late.
***
## Processing error
The charge reached the cardholder in the wrong shape.
| Reason | Use when |
| - | - |
| `PROCESSING_ERROR_INCORRECT_TRANSACTION_CODE` | Processed as the wrong type, such as a charge instead of a refund. |
| `PROCESSING_ERROR_INCORRECT_CURRENCY` | Processed in the wrong currency. |
| `PROCESSING_ERROR_INCORRECT_ACCOUNT_NUMBER` | Posted to the wrong card number. |
| `PROCESSING_ERROR_INCORRECT_AMOUNT` | Processed for the wrong amount. |
| `PROCESSING_ERROR_DUPLICATE_PROCESSING` | The same purchase was charged more than once, or was already paid by other means. |
| `PROCESSING_ERROR_INVALID_DATA` | Processed with invalid or incorrect data. |
***
## Consumer
The charge was authorised but the merchant did not hold up their end.
| Reason | Use when |
| - | - |
| `CONSUMER_MERCHANDISE_NOT_RECEIVED` | The goods or services were never received. |
| `CONSUMER_NOT_AS_DESCRIBED` | The goods or services were not as described, or arrived defective. |
| `CONSUMER_COUNTERFEIT_MERCHANDISE` | The goods received were counterfeit. |
| `CONSUMER_MISREPRESENTATION` | The merchant misrepresented what was being sold. |
| `CONSUMER_CANCELLED_RECURRING` | A recurring charge was taken after the cardholder cancelled. |
| `CONSUMER_CREDIT_NOT_PROCESSED` | The merchant agreed to a refund but never processed it. |
| `CONSUMER_CANCELLED_MERCHANDISE` | The cardholder cancelled the order or returned the goods and was not refunded. |
| `CONSUMER_ORIGINAL_CREDIT_NOT_ACCEPTED` | A payment pushed to the card was not accepted by the cardholder. |
| `CONSUMER_NON_RECEIPT_CASH_ATM` | An ATM debited the card without dispensing the cash. |
`CONSUMER_CREDIT_NOT_PROCESSED` and `CONSUMER_CANCELLED_MERCHANDISE` are the pair most often mixed up. Use the first when the merchant agreed to refund and never did. Use the second when the cardholder cancelled or returned per the merchant's policy and no refund followed.
# Fraud Alerts
Source: https://docs.reap.global/cards/fraud-alerts
Respond to detected fraud and report fraudulent card transactions.
Fraud monitoring flags suspicious card transactions and gives you a short window to confirm or decline with the cardholder. You can also report fraud against a past transaction yourself. Confirming or reporting always blocks the card; declining or letting a detection expire leaves it alone.
Subscribe to fraud alert webhooks for the alert lifecycle, and to [`CARD_STATUS_UPDATED`](/api-reference/card-status-updated) for the block. Card state lives on that stream, not on the fraud alert resource.
Confirming fraud blocks the card. It does not recover the money. To challenge the charge itself, file a [dispute](/cards/disputes/overview) against the same transaction. The two are independent and neither requires the other.
***
## Alert lifecycle
| Status | Meaning |
| - | - |
| `PENDING` | Detected by fraud monitoring. Confirm or decline before `respondableUntil`. |
| `CONFIRMED` | Fraud confirmed. The card is blocked. |
| `DECLINED` | Cardholder said the transaction was legitimate. The card stays active. |
| `EXPIRED` | No response within the 72-hour window. The card stays active. You may still file a [manual report](#report-fraud-manually) while `reportableUntil` is in the future. |
| Origin | Meaning |
| - | - |
| `DETECTED` | Raised by fraud monitoring. Starts as `PENDING`. |
| `REPORTED` | Filed by you against a past transaction. Created as `CONFIRMED`. |
```mermaid theme={null}
flowchart LR
Detect["Detected"] --> Pending["PENDING"]
Pending -->|confirm| Confirmed["CONFIRMED"]
Pending -->|decline| Declined["DECLINED"]
Pending -->|no response in 72h| Expired["EXPIRED"]
Expired -->|manual report| Confirmed
Report["Manual report"] --> Confirmed
```
Detection alone does **not** block the card. Only confirm and manual report do.
***
## Deadlines
| Deadline | Field | Applies to |
| - | - | - |
| Respond to a detected alert | `respondableUntil` | Always present. 72 hours from when the alert was raised. |
| Report after expiry | `reportableUntil` | Set only when `status` is `EXPIRED`. Null otherwise. |
| First-time manual report | Enforced server-side | 180 days from the transaction date. Optionally tightened to 30 days from `cardholderNotifiedAt` when you send that field. |
Past `respondableUntil`, [Respond to alert](/api-reference/fraud-alerts/respond-to-alert) returns `FRAUD_RESPONSE_WINDOW_EXPIRED`. Use [Report fraud](/api-reference/fraud-alerts/report-fraud) instead while the reporting window is still open.
In sandbox, raise a detected alert by passing `triggerFraudAlert: true` on
[Simulate authorization](/api-reference/simulation/simulate-authorization).
Simulated fraud detections can expire after about **5 minutes**. You still get
[`CARD_FRAUD_ALERT_STATUS_UPDATED`](/api-reference/card-fraud-alert-status-updated)
with `status: EXPIRED`. Production uses the 72-hour `respondableUntil` window;
do not treat sandbox expiry timing as the live contract.
***
## Respond to a detected alert
Listen for [`CARD_FRAUD_ALERT_CREATED`](/api-reference/card-fraud-alert-created). When `origin` is `DETECTED` and `status` is `PENDING`, contact the cardholder before the deadline on `respondableUntil`.
Call [Respond to alert](/api-reference/fraud-alerts/respond-to-alert). Confirming an alert requires you to pass a [fraud type](#fraud-types).
You receive [`CARD_FRAUD_ALERT_STATUS_UPDATED`](/api-reference/card-fraud-alert-status-updated) with the new status.
On confirm, also listen for [`CARD_STATUS_UPDATED`](/api-reference/card-status-updated). The card moves to `BLOCKED` with `blockReason.type: FRAUD_ALERT_CONFIRMED`.
```mermaid theme={null}
sequenceDiagram
participant Reap
participant Backend as Your backend
participant Cardholder
Reap->>Backend: CARD_FRAUD_ALERT_CREATED
Backend->>Cardholder: Verify the transaction
Cardholder-->>Backend: Confirmed / declined
Backend->>Reap: POST /fraud-alerts/:id/respond
Reap->>Backend: CARD_FRAUD_ALERT_STATUS_UPDATED
Note over Reap,Backend: On confirm only
Reap->>Backend: CARD_STATUS_UPDATED (BLOCKED, FRAUD_ALERT_CONFIRMED)
```
If you miss the 72-hour window, the alert becomes `EXPIRED` and the card stays active. Check `reportableUntil` on the alert. While that timestamp is still in the future, file a [manual report](#report-fraud-manually) on the same transaction to confirm fraud and block the card.
***
## Report fraud manually
Use this when the cardholder later flags a transaction, or when a detected alert expired and `reportableUntil` is still ahead of now.
Take the card transaction ID from your records.
Call [Report fraud](/api-reference/fraud-alerts/report-fraud) with `transactionId`, `type`, and optionally `cardholderNotifiedAt`.
The report always confirms fraud and blocks the card. If a pending detected alert already exists for that transaction and is still inside the response window, the call confirms it instead of creating a second alert (transaction and alert are 1:1).
You receive [`CARD_FRAUD_ALERT_CREATED`](/api-reference/card-fraud-alert-created) or [`CARD_FRAUD_ALERT_STATUS_UPDATED`](/api-reference/card-fraud-alert-status-updated) with `status: CONFIRMED`, plus [`CARD_STATUS_UPDATED`](/api-reference/card-status-updated) with `blockReason.type: FRAUD_ALERT_CONFIRMED`.
***
## After a confirmed fraud alert
A fraud block reports `blockLiftable: true`, so [Unblock](/api-reference/cards/unblock-card) will lift it. Prefer deleting the card and issuing a replacement instead. The card number is compromised, and unblocking returns that same number to circulation. Unblock is there for the case where the cardholder confirmed the alert by mistake.
When the cardholder notified you of the suspected fraud, pass `cardholderNotifiedAt`. The reporting window becomes the earlier of 180 days from the transaction and 30 days from that notification. When omitted, only the 180-day transaction window is enforced server-side; the 30-day notification limb remains your obligation.
***
## Fraud types
Required when confirming a detected alert or filing a manual report.
| Value | Use when |
| - | - |
| `CARD_LOST` | The cardholder lost the card. |
| `CARD_STOLEN` | The card was stolen. |
| `ACCOUNT_NUMBER_MISUSE` | The cardholder still has the card, but the account number was used without it. |
| `OTHER` | Confirmed fraud that none of the above classify. |
# Managing Cards
Source: https://docs.reap.global/cards/managing-cards
Freeze, block, update, and delete cards.
These operations apply to both virtual and physical cards.
***
## Freeze and unfreeze
Freeze a card when the cardholder misplaces it or wants to pause spending. Call [Freeze a card](/api-reference/cards/freeze-card), and [Unfreeze a card](/api-reference/cards/unfreeze-card) to restore it. Either is available to you at any time.
A frozen card declines everything, including in-store and ATM use of a physical card and any mobile wallet it has been added to.
Freeze is the soft toggle. When you need a hard stop, block the card instead. See [freeze and block](/cards/overview#freeze-and-block-are-separate) for the difference.
***
## Block and unblock
Block a card when it needs a hard stop: the cardholder reports it compromised, a dispute is open, or you are holding the account pending review. Call [Block a card](/api-reference/cards/block-card). The card returns `status: BLOCKED` with `blockReason.type: CLIENT_REQUESTED`.
Call [Unblock a card](/api-reference/cards/unblock-card) to lift it. Read `blockLiftable` on the card first: a block Reap placed for risk or compliance reports `false` and returns `409 CARD_BLOCK_NOT_LIFTABLE`. See [blocked cards](/cards/overview#blocked-cards) for every reason, who can lift it, and what happens to a freeze the block was hiding.
***
## Update PIN
Call [Update PIN](/api-reference/cards/update-card-pin) to set or change the cardholder's PIN. The PIN is used for ATM withdrawals and at POS terminals that require chip-and-PIN verification.
PIN requirements:
* 4–12 digits
* No 3 or more consecutive ascending digits (e.g. `123`, `234`)
* No 3 or more repeated digits (e.g. `111`, `555`)
***
## Delete a card
Call [Delete a card](/api-reference/cards/delete-card) to delete a card when offboarding a user or closing their card program. Deletion is irreversible via the API.
***
## List cards
Call [List cards](/api-reference/cards/list-cards) to retrieve cards for your project. Use the `accountId` filter to scope results to a specific user's account. This is the most common use case when showing a cardholder their own cards.
# Overview
Source: https://docs.reap.global/cards/overview
Issue payment cards to your users.
Cards in Reap API are payment cards you issue to your users. Each card is linked to an account that holds the spending balance.
***
## Card types
| Type | Description |
| - | - |
| `VIRTUAL` | Digital-only card. Ready to use immediately after creation. |
| `PHYSICAL` | Mailed to the cardholder's address. Usable digitally right away; activate the physical card when it arrives. |
You choose `type` once, at creation, and it is fixed. A `PHYSICAL` card is a single card that works digitally right away, exactly like a virtual card, and can additionally be shipped as plastic. You do not create a separate virtual card for it, and there is no separate card ID for its digital and physical forms. Pick `VIRTUAL` for a digital-only card, `PHYSICAL` when the cardholder also needs plastic.
***
## Card statuses
| Status | Meaning |
| - | - |
| `ACTIVE` | Card is active and can be used for transactions. |
| `FROZEN` | Card is frozen and declines transactions. You or the cardholder can unfreeze it. |
| `BLOCKED` | Card is blocked. `blockReason` says why, and `blockLiftable` says whether you can lift it. |
| `EXPIRED` | Card has reached its expiration date and can no longer be used. |
`status` tracks the card's own state. It does not reflect the state of the account the card is issued on: a [restricted account](/accounts/overview#account-status) declines every authorization while its cards stay `ACTIVE`. Read both values when showing a cardholder whether a card can be used.
### Freeze and block are separate
A freeze and a block are two independent states a card can be in, and a card can be in both at once. `status` reports the block when both are set, so a frozen card that is then blocked reports `BLOCKED`, and the boolean `frozen` field is how you know the freeze is still there. Lifting the block returns that card to `FROZEN`, not to `ACTIVE`.
Each state is lifted on its own, in any order. Unfreezing a blocked card is allowed and clears the freeze; the card stays `BLOCKED` and goes to `ACTIVE` when the block lifts.
| | Freeze | Block |
| - | - | - |
| What it is for | The everyday toggle. A misplaced card, a cardholder pausing spend. | The hard stop. A compromised card, a dispute, an account on hold. |
| Who sets it | You, usually on behalf of the cardholder. | You, or Reap on a risk or fraud signal. |
| Reversing it | [Unfreeze](/api-reference/cards/unfreeze-card), always available to you. | [Unblock](/api-reference/cards/unblock-card), available when `blockLiftable` is `true`. |
Expose freeze in your app as a cardholder-facing switch. Reserve block for cases where you want the card stopped and a deliberate decision made before it is used again.
Either state stops the card everywhere. A physical card stops working at the terminal and the ATM alongside online payments, and so does the card in any mobile wallet it has been added to.
### Blocked cards
`blockReason.type` names the cause and `blockLiftable` tells you what you can do about it, so you never need to hardcode a list of reasons.
| `blockReason.type` | Cause | Liftable by you |
| - | - | - |
| `CLIENT_REQUESTED` | You called [Block](/api-reference/cards/block-card). | Yes |
| `SUSPECTED_FRAUD_CVV` | Too many incorrect CVV attempts. | Yes |
| `SUSPECTED_FRAUD_EXPIRY` | Too many incorrect expiry date attempts. | Yes |
| `SUSPECTED_FRAUD_PIN` | Too many incorrect PIN attempts. | Yes |
| `FRAUD_ALERT_CONFIRMED` | A cardholder confirmed a [fraud alert](/cards/fraud-alerts), or you reported the transaction as fraud. | Yes |
| `OTHER` | A risk or compliance block placed by Reap. | No, contact Reap support |
When `blockLiftable` is `false`, [Unblock](/api-reference/cards/unblock-card) returns `409 CARD_BLOCK_NOT_LIFTABLE` and the block stays in place. Surface that to your support team rather than retrying.
Blocking a card that is already blocked never weakens the block: a card blocked by Reap keeps that reason and stays non-liftable even if you call [Block](/api-reference/cards/block-card) on it afterwards.
For a card blocked because fraud was confirmed, prefer deleting the card and issuing a new one over unblocking it. The card number is compromised, and unblocking puts the same number back into circulation.
`status` is also independent of where a physical card is in production or delivery. A physical card is `ACTIVE` and usable online from creation, while its production and shipping progress is tracked separately on the `physicalCardStatus` field (and through shipment webhooks). So a physical card that is still in transit reports `status: ACTIVE` with a `physicalCardStatus` such as `IN_TRANSIT`. See [Physical card shipping](/cards/physical-cards/shipping).
### Simulating card statuses in sandbox
In sandbox, use [Simulate card status](/api-reference/simulation/simulate-card-status) to force a card into a status that is otherwise hard to trigger. Reap also emits a [`CARD_STATUS_UPDATED`](/api-reference/card-status-updated) webhook with the new status.
The main use cases are statuses you cannot reach through standard endpoints:
* `BLOCKED`: simulates a block placed by Reap, which in production needs a real fraud or compliance trigger. Defaults to `SUSPECTED_FRAUD_PIN`, which you can lift. Pass `blockReason: OTHER` to produce a block you cannot lift and exercise the `409 CARD_BLOCK_NOT_LIFTABLE` path.
* `EXPIRED`: simulates a card reaching its expiry date without waiting for it.
`FROZEN` and `ACTIVE` are also accepted here, but you can reach those through the standard [Freeze](/api-reference/cards/freeze-card) and [Unfreeze](/api-reference/cards/unfreeze-card) endpoints, and a `CLIENT_REQUESTED` block through [Block](/api-reference/cards/block-card).
***
## Issue your first card
Before issuing a card, the user must have `application.status: APPROVED` and the account must be `ACTIVE`. See the [KYC overview](/compliance/kyc/overview) if you haven't onboarded the user yet.
Call [Create a card](/api-reference/cards/create-card) with the user's ID, account ID, and `type`.
* `VIRTUAL`: the card is created with `status: ACTIVE` and is ready to use immediately.
* `PHYSICAL`: available for digital use immediately. The physical card is mailed to the cardholder and must be activated on arrival.
Pass `cardDesignId` to pick a design from those assigned to your project ([List card designs](/api-reference/card-designs/list-card-designs)); omit it to use the project default. The card returns the design it was issued with as `cardDesignId`.
The PAN, CVV, and expiry date are never returned by the API. To show them to the cardholder, call [Create reveal session](/api-reference/cards/reveal-card-details) from your backend. It returns a short-lived `revealUrl`. Pass this to your frontend and load it in an `
Reap sends a [`CARD_STATUS_UPDATED`](/api-reference/card-status-updated) webhook whenever a card's status changes. Listen for this event to keep your UI and internal records in sync.
Key statuses to handle:
* `FROZEN`: the cardholder or your app froze the card
* `BLOCKED`: read `blockReason.type` for the cause and `blockLiftable` for whether you can lift it
* `EXPIRED`: card has reached its expiration date
Do not poll for status changes. Webhooks are the source of truth.
If your card program includes ATM withdrawals or chip-and-PIN transactions, call [Update PIN](/api-reference/cards/update-card-pin) before the cardholder uses the card at a terminal. See [Update PIN](/cards/managing-cards#update-pin) for PIN requirements.
***
## Fraud monitoring
When fraud monitoring flags a transaction, or when a cardholder reports a past transaction as fraud, use [Fraud Alerts](/cards/fraud-alerts) to confirm or decline. Confirming or reporting blocks the card and emits [`CARD_STATUS_UPDATED`](/api-reference/card-status-updated) with `blockReason.type: FRAUD_ALERT_CONFIRMED`.
***
## Challenging a charge
Blocking a card stops further damage but does not recover money already spent. To challenge a cleared charge and take it to the card network, file a [dispute](/cards/disputes/overview). This covers unauthorised charges as well as everyday merchant problems such as goods never arriving or a subscription billed after cancellation.
***
## Adding a card to a mobile wallet
Cardholders can add their card to Apple Pay or Google Pay, either by entering the card details manually or, if you build it, with a one-tap button in your app. See [Digital Wallets](/cards/digital-wallets/overview) for both methods and how card verification works.
# Activation
Source: https://docs.reap.global/cards/physical-cards/activation
Activate a physical card once it reaches the cardholder.
Physical cards arrive locked. The cardholder activates the card with a 6-digit code printed on the mailer. After activation the card can be used at physical terminals.
***
## When to activate
Activate once the cardholder has the card in hand. The activate endpoint accepts cards in `DELIVERED`, `IN_TRANSIT`, or `OUT_FOR_DELIVERY` status - couriers report delivery on different schedules, and the cardholder often holds the card before our `physicalCardStatus` updates to `DELIVERED`. The 6-digit code in the mailer is the security boundary; no one can activate without it. You cannot activate a card that has not been shipped, has been canceled, or is already active.
```mermaid theme={null}
stateDiagram-v2
direction LR
IN_TRANSIT --> OUT_FOR_DELIVERY
OUT_FOR_DELIVERY --> DELIVERED
DELIVERED --> ACTIVATED : activate
IN_TRANSIT --> ACTIVATED : activate
OUT_FOR_DELIVERY --> ACTIVATED : activate
ACTIVATED --> [*]
```
***
## Activation flow
Prompt the cardholder for the 6-digit activation code from the card mailer. Authenticate them in your app first (login, biometric, in-app PIN) so a stolen mailer alone cannot activate a card.
Call [Activate physical card](/api-reference/cards/activate-physical-card) with the `cardId` and `activationCode`. The endpoint requires an `Idempotency-Key` header, so retries are safe.
On success the endpoint returns `204 No Content` and the card's `physicalCardStatus` flips to `ACTIVATED` on the next `GET /cards/:id`.
The card is ready for physical terminals. ATM withdrawals and contactless payments work immediately. The card's online (`ECOMMERCE`) usage is independent of activation, see [3D Secure](/cards/3d-secure) for the e-commerce flow.
***
## Lost activation codes
The mailer carries the only copy of the code the cardholder ever sees. If they lose it, retrieve the same code through the API and forward it via your own secure channel.
The retrieved code is the same dedicated code originally assigned to the card. It does not rotate. Treat it as sensitive: verify the cardholder's identity yourself before sharing, and deliver it over a channel you control (in-app message, verified email, SMS to a verified phone). Do not store the code in your application logs.
Call [Get activation code for a physical card](/api-reference/cards/get-activation-code) with the `cardId`. The response is the same 6-digit string the cardholder originally received.
Use this endpoint only for lost-code recovery. The standard flow is for the cardholder to read the code off the mailer.
***
## Best practices
* **Authenticate the cardholder before activation.** The mailer code alone is not strong enough. Combine it with your own login or biometric check.
* **Gate `GET /cards/:id/activation-code` carefully.** This endpoint returns the secret with no cardholder verification on our side. Verify identity through your own channel before calling it, and deliver the code only over channels you control.
* **Retry safely.** Activation is naturally idempotent - `physicalCardStatus = ACTIVATED` is sticky. A retry of a successful activate returns `400 CARD_CANNOT_BE_ACTIVATED` with `reason: 'ALREADY_ACTIVATED'`, which is still an unambiguous "card is active" signal. Partners may pass an `Idempotency-Key` for their own bookkeeping, but it is not required.
* **Treat the code as a secret.** Never log activation codes, never expose them in URLs, and never echo them back in error responses.
* **Do not poll for activation.** The `204` response is authoritative. The card's `physicalCardStatus` flips to `ACTIVATED` on the next `GET /cards/:id`.
# Shipping
Source: https://docs.reap.global/cards/physical-cards/shipping
Order physical cards and track production and delivery.
Ship one card or a batch of up to 200 cards in a single shipment. Reap handles production, courier handoff, and delivery tracking. You drive the flow with a draft-first API: build the shipment, review it, then submit when ready.
***
## Lifecycle
A shipment moves through production at the factory, courier handoff, and delivery.
```mermaid theme={null}
stateDiagram-v2
direction LR
[*] --> DRAFT
DRAFT --> PLACED : submit
DRAFT --> CANCELED : delete
PLACED --> IN_PRODUCTION
IN_PRODUCTION --> READY_TO_SHIP
READY_TO_SHIP --> IN_TRANSIT
IN_TRANSIT --> OUT_FOR_DELIVERY
OUT_FOR_DELIVERY --> DELIVERED
DELIVERED --> [*]
```
`DELIVERY_FAILED`, `EXCEPTION`, and `CANCELED` can also occur from most non-terminal states. `DELIVERY_FAILED` and `EXCEPTION` are recoverable - the courier may re-attempt and the shipment can move forward.
### Shipment statuses
| Status | Meaning |
| - | - |
| `DRAFT` | Created locally. Not yet sent to the manufacturer. Editable. |
| `PLACED` | Submitted to the manufacturer. Awaiting production. |
| `IN_PRODUCTION` | At least one card is being personalized. |
| `READY_TO_SHIP` | All cards produced. Awaiting courier pickup. |
| `IN_TRANSIT` | Picked up by the courier. |
| `OUT_FOR_DELIVERY` | Out for final delivery. |
| `DELIVERED` | Delivered to the recipient. |
| `DELIVERY_FAILED` | Delivery attempt failed. Courier may re-attempt. |
| `EXCEPTION` | Customs hold, weather delay, or similar. May still recover. |
| `CANCELED` | Shipment will not continue. |
Only `DELIVERED` and `CANCELED` are terminal. Some carriers skip intermediate states (for example, `OUT_FOR_DELIVERY`), so do not assume every status is visited.
### Per-card production status
Each card in the shipment carries its own `productionStatus`. This lets you tell which cards in a multi-card shipment are still being produced versus already complete.
| Status | Meaning |
| - | - |
| `PENDING` | Card has not yet been sent to the factory. |
| `REQUESTED` | Factory has accepted the production request. |
| `IN_PRODUCTION` | Card is being personalized. |
| `COMPLETED` | Production complete. |
| `ACTIVATED` | Cardholder has activated the card. |
| `ON_HOLD` | Production paused. Contact Reap for details. |
| `CANCELED` | Production canceled. |
| `BULK_SHIP_REJECTED` | The manufacturer rejected this specific card at submit time. The rest of the shipment still went through. |
***
## Single card vs multi-card shipments
One endpoint covers both. A shipment with one card and a shipment with up to 200 cards use the same draft-first flow.
| | Single card | Multi-card |
| - | - | - |
| Cards per shipment | 1 | 2 - 200 |
| Box-level destination | Must match the card's per-card address | Independent of per-card addresses |
| Cutoff window | None | Submit before `cutoffDate` returned at creation |
| Partial failure on submit | All-or-nothing | Surviving cards proceed; rejected cards stay behind |
Use multi-card shipments when you are dispatching cards in bulk to a single hub (a corporate office, a regional distribution point). Use single-card shipments when each card goes directly to the cardholder.
***
## Issue a shipment
Call [Create a card shipment](/api-reference/card-shipments/create-shipment-draft) with the destination address, recipient details, courier choice, and one or more cards. The shipment is created with `status: DRAFT`.
Each card entry includes the `cardId`, `cardDesignId`, and a per-card `shippingAddress`. The per-card address is the final destination printed on the card mailer; it can differ from the box-level destination on multi-card shipments.
The `cardDesignId` on a shipment entry is the design the plastic is produced from, and it can differ from the `cardDesignId` the card was issued with. The design must be physical-capable and assigned to your project.
Use [Update shipment](/api-reference/card-shipments/update-shipping-details), [Add a card](/api-reference/card-shipments/add-card-to-draft), and [Remove a card](/api-reference/card-shipments/remove-card-from-draft) to edit a `DRAFT` shipment. Once submitted, the shipment is frozen.
Updating the destination also updates each card whose shipping address still matches the previous destination. A card with a different shipping address keeps its own address.
Call [Submit shipment](/api-reference/card-shipments/submit-shipment). The shipment transitions to `PLACED` and the manufacturer starts production.
For multi-card shipments, a `cutoffDate` is returned. Cards submitted after this cutoff roll into the next production batch. Plan submission with this in mind.
On partial failure (some cards accepted, others rejected), the response returns **HTTP 207 Multi-Status**. The shipment still moves to `PLACED` with the surviving cards. Rejected cards appear in `cards[]` with `productionStatus: BULK_SHIP_REJECTED` and a `rejectedAt` timestamp.
Subscribe to [`CARD_SHIPMENT_STATUS_UPDATED`](/api-reference/card-shipment-status-updated) for shipment-level status changes. The webhook fires on every transition (`PLACED` → `IN_PRODUCTION`, `IN_TRANSIT`, etc.). The payload matches [Get shipment](/api-reference/card-shipments/get-shipment), including `cards[]` with each card's id and production status.
***
## Tracking and webhooks
`CARD_SHIPMENT_STATUS_UPDATED` is the source of truth for shipment status. Do not poll.
The payload includes the same `cards[]` array as [Get shipment](/api-reference/card-shipments/get-shipment), so you can map the shipment to its cards without a follow-up request.
When a status change includes courier tracking, `trackingNumber` and `trackingUrl` are populated on that payload. Pass the URL through to the cardholder so they can follow the delivery with the courier directly.
If you need the latest state on demand (e.g. a customer service agent looking up a specific shipment), call [Get shipment](/api-reference/card-shipments/get-shipment).
***
## Testing in sandbox
In sandbox there is no factory or courier, so submitted shipments stay at `PLACED` until you drive them. Use [Simulate shipment status](/api-reference/simulation/simulate-shipment-status):
```bash theme={null}
POST /simulation/card-shipments/{id}/status
{ "status": "IN_TRANSIT" }
```
Walk the shipment through the same statuses you see in production (`IN_PRODUCTION`, `READY_TO_SHIP`, `IN_TRANSIT`, `OUT_FOR_DELIVERY`, `DELIVERED`, and the recoverable / cancel paths). Each call returns `204`; observe the change via `CARD_SHIPMENT_STATUS_UPDATED`.
***
## Couriers and destinations
Set the courier on the draft:
| Courier `type` | `name` | When to use |
| - | - | - |
| `COURIER` | `DHL`, `FEDEX` | Tracked international delivery. Some couriers are not available in every country. |
| `POSTAL` | omitted | Local postal service. Tracking may be limited. |
Only `DHL`, `FEDEX`, and `POSTAL` can be selected on create or update. After the shipment is placed, the courier on GET responses and shipping webhooks reflects the manufacturer's assignment and may be `SFEXPRESS` for some manufacturer and destination combinations (for example cards shipped to Hong Kong), even if you requested `DHL` or `FEDEX`.
Some countries do not support physical card shipping or require additional fields. The API will reject a draft with a clear error if:
* The destination country is not supported (`SHIPPING_NOT_ALLOWED_FOR_COUNTRY`)
* A zone (state or province) is required for the destination (`ZONE_REQUIRED`) or the supplied zone is invalid (`INVALID_ZONE`)
* The chosen courier does not deliver to the destination (`COURIER_NOT_SUPPORTED`)
* The recipient phone number is invalid or its dial code does not match the destination country (`SHIPPING_PHONE_INVALID`, `SHIPPING_PHONE_DIAL_CODE_MISMATCH`)
Validate addresses up front in your UI when possible. The same checks are run on draft creation and on submission, so a draft that creates successfully will still pass these checks at submit time unless you change the address.
***
## What's next
Once a physical card is delivered, the cardholder activates it with the 6-digit code on the mailer. See [Activation](/cards/physical-cards/activation) for the activation flow and lost-code recovery.
# Changelog
Source: https://docs.reap.global/changelog
New endpoints, webhook events, and behavior changes in the Reap API.
Every change here is backwards compatible unless an entry says otherwise. Breaking changes
ship under a new `Reap-Version`, described in [Versioning & Compatibility](/api-reference/versioning).
### Production default rate limit is higher
The default production limit is now 600 requests per minute and 500,000 requests per day. The per-second limit stays at 20, and the tighter limits on card issuance, card operations, and identity verification are unchanged.
* [Rate limiting](/api-reference/rate-limiting) lists the limits for every tier
### User applications return rejection labels
The `application` object on a user now carries `rejectionLabels`, a list of the reject codes from the compliance review, such as `EXPIRATION_DATE` or `SELFIE_MISMATCH`. It is set when the status is `REJECTED` or `RETRY_REQUIRED` and `null` otherwise.
The list of codes can grow, so handle values you do not recognize. The codes are for your own use: do not show them to the end user.
* [Get user](/api-reference/users/get-user) returns the field on the application
* [Rejection labels](/compliance/kyc/rejection-labels) lists the codes and what they mean
### Statements can end yesterday
A statement period can now end yesterday once that day's card transaction data is final, from 10:00 UTC. Before then the latest end date is the day before yesterday.
Timestamps in statement files now carry milliseconds, for example `2026-07-14T09:21:07.000Z`. If you parse them with a fixed format, allow the fractional part.
* [Request statement](/api-reference/statements/request-statement) accepts the later end date
* [Statements overview](/statements/overview) lists the period limits and file format
### Card creation returns 400 for unsupported residence countries
For project on KYC\_ATTESTATIONS verification method, if the cardholder lives in a country where cards cannot be issued, [Create card](/api-reference/cards/create-card) returns a 400 `CARDHOLDER_COUNTRY_NOT_SUPPORTED` error. It previously returned a 500.
### Universal KYB addresses require line2
When you verify a company with [Universal KYB](/compliance/kyb/universal-kyb), both the registered and operating addresses must include a non-empty `line2`. A pack without it is rejected.
* [Advance company application](/api-reference/companies/advance-company-application) takes the Universal KYB pack
### Dispute refunds credit the cardholder, including prorated fees
When a dispute reaches `REFUNDED`, the dispute resource and
[`CARD_DISPUTE_STATUS_UPDATED`](/api-reference/card-dispute-status-updated) carry
`refundedAmount: { value, currency }`. It is `null` until the case settles.
`refundedAmount` is the cardholder credit: the network refund plus a proportional
share of FX markup and ATM variable fee. The per-withdrawal ATM fee stays charged.
The credit can be less than the disputed `amount`.
The same credit is appended to the disputed card transaction as a new `DISPUTE_REFUND`
event, delivered in `CARD_TRANSACTION_UPDATED`. It moves `amount.refunded` and
`amount.current` like a merchant `REFUND`. If you keep the cardholder's balance, as in
[External authorization](/transactions/external-authorization/overview), post the
credit from that event and not from the dispute webhook, or it is booked twice.
Transaction event `type` gains the `DISPUTE_REFUND` value. If you switch on event
types, handle it like `REFUND`.
* [Disputes](/cards/disputes/overview) - how the credit is calculated
* [Lifecycle](/transactions/lifecycle#dispute-refund) - the `DISPUTE_REFUND` event
* [Keeping your ledger in sync](/transactions/external-authorization/reconciliation#credit-a-dispute-refund) - how to book it
* [File dispute](/api-reference/disputes/file-dispute) and [Get dispute](/api-reference/disputes/get-dispute) return the same field
### Filing a dispute no longer requires a cardholder signature
This is a breaking change. A filed dispute is created as `RECEIVED` and Reap takes
the case from there. `requiresCardholderSignature` is gone from the dispute resource,
`PENDING_SIGNATURE` is gone from the status list, and `POST /disputes/:id/signature-link`
is removed. There is no replacement endpoint.
* [Disputes](/cards/disputes/overview) - filing and the status list
* [Dispute reasons](/cards/disputes/reasons) - the reason vocabulary
### Companies can be verified with Universal KYB
If your project is set up for Universal KYB, you can submit business verification you have already completed instead of sending the company through a hosted session. Send the verified business pack and supporting documents, then submit the application for Reap to review.
Company applications now include a `nextAction` that lists the documents still needed, or tells you to submit. Reap sends `COMPANY_STATUS_UPDATED` when the review finishes. Managed KYB works as before, and `nextAction` is `null` for it.
Companies also have a new `ACTION_REQUIRED` status, which means Reap needs more information.
* [Advance company application](/api-reference/companies/advance-company-application) sends the Universal KYB pack with `method: UNIVERSAL_KYB`, or starts a Managed KYB session (this endpoint was called Create company application)
* [Upload application documents](/api-reference/companies/upload-application-documents) adds company documents one type at a time
* [Upload related person documents](/api-reference/companies/upload-related-person-documents) adds identity documents for a beneficial owner
* [Submit company application](/api-reference/companies/submit-company-application) sends the application for review
* [Universal KYB](/compliance/kyb/universal-kyb) walks through the full flow
### Universal KYB companies can be tested in sandbox
[Simulate company status](/api-reference/simulation/simulate-company-status) approves, rejects, or asks for action on a submitted Universal KYB application, and sends the same `COMPANY_STATUS_UPDATED` webhook as a real review. Simulating before the application is submitted returns `SIMULATION_INVALID_STATE`.
* [Reset company application](/api-reference/simulation/reset-company-application) returns a sandbox application to pending submission and keeps the pack and uploaded documents, so you can submit again
### You generate the key pair for encrypted card details
For [Encrypted retrieval](/cards/card-details/encrypted-retrieval) in production, you generate the RSA key pair (2048 bits or larger) and send only the public key to your implementation manager. Reap does not generate or hold the private key.
### Checkouts reject a malformed quote ID and state retry timing
[Create checkout](/api-reference/agentic/create-checkout) returns `422` when `quoteId` is not a valid UUID, before it reaches the merchant. Checkout validation failures return `AGENTIC_REQUEST_REJECTED` with the `quoteId` or `enrollmentId` that caused them.
When a quote or checkout is temporarily unavailable, the `503` response has its own error code, `QUOTE_TEMPORARILY_UNAVAILABLE` or `CHECKOUT_TEMPORARILY_UNAVAILABLE`, and a `Retry-After` header with the number of seconds to wait.
* [Create quote](/api-reference/agentic/create-quote) documents the quote `503` response and its header
### Card transaction fee rows show the fees Reap charged you
[Card transaction fees](/statements/card-transaction-fees) statement rows add US dollar columns for each fee Reap charged you, from `authorizationFeeUsd` to `atmNonFinancialFeeUsd`. Each column matches a line on your invoice.
Rows with no transaction event, such as a wallet provisioning or a 3DS authentication, now fill in `cardId`, `cardLast4`, and `accountId` when the card is known.
### Quote a checkout URL from your own product discovery
[Create quote](/api-reference/agentic/create-quote) accepts either product variant ids or a
checkout URL that your application constructs. Checkout URLs can retain your attribution
parameters. The request also accepts an optional `offerCode` for either quote source.
* [One-time purchases](/agentic-payments/one-time-purchases) shows both request forms
### Quote errors state how to correct the request
[Create quote](/api-reference/agentic/create-quote) errors now identify checkout URLs that cannot
be used, unavailable card payment, invalid or expired offer codes, and quotes that the merchant
cannot fulfill. [Select shipping option](/api-reference/agentic/select-shipping-option) errors also
state when you must create a new quote or when the quote can no longer change.
### Quote retries preserve each attempt
Reap makes one merchant request for a quote attempt. Reusing the same idempotency key replays
the first response, including `503`. A new key starts a new attempt. See
[Idempotency](/api-reference/idempotency) for retry guidance.
### Card transaction merchants include an acceptor ID
Card transactions and the [`CARD_AUTHORIZATION_REQUEST`](/api-reference/card-authorization-request) webhook now include `cardAcceptorId` on `merchant`: the ID the merchant's acquirer assigns, unique only within that acquirer, and `null` when the network does not provide one. Match the same merchant across transactions with `cardAcceptorId`, `name`, `country`, and `mccCode` together. `merchant.id` is a different value on every transaction even for the same merchant, so it no longer serves that purpose.
* [Get card transaction](/api-reference/card-transactions/get-card-transaction) - returns `cardAcceptorId` on `merchant`
* [Transactions overview](/transactions/overview) - describes the merchant fields and how to match merchants across transactions
### Agentic API errors identify what went wrong
Enrollments, quotes, and checkouts under `/agentic` now return specific error codes instead of only the generic `AGENTIC_REQUEST_REJECTED` and `AGENTIC_RESOURCE_NOT_FOUND`. An expired quote returns `QUOTE_EXPIRED`, an unknown enrollment returns `ENROLLMENT_NOT_FOUND`, a sold-out variant returns `VARIANT_UNAVAILABLE`, and a reused idempotency key returns `IDEMPOTENT_PARAMETER_MISMATCH` or `IDEMPOTENCY_REQUEST_IN_PROGRESS`, among others. The generic codes still cover any case without a specific one, so existing error handling keeps working.
* [Create enrollment](/api-reference/agentic/create-enrollment) - lists the specific error codes it can return
* [Create quote](/api-reference/agentic/create-quote) - lists the specific error codes it can return
* [Create checkout](/api-reference/agentic/create-checkout) - lists the specific error codes it can return
### Checkouts can be completed instantly in sandbox
[Create checkout](/api-reference/agentic/create-checkout) accepts a new `X-Simulate-Checkout: COMPLETED` header in sandbox, which returns the checkout as `COMPLETED` without waiting on the underlying payment. Production requests reject the header.
### Idempotent retries of an empty response no longer fail
Retrying a call with the same [idempotency key](/api-reference/idempotency) against an endpoint that returns no body used to answer with a 500 on replay. It now replays the original empty response, the same as any other idempotent retry.
### Cross-currency card-transaction simulations require the original amount
Simulating an authorization, clearing, reversal, refund, decline, or 3DS challenge now requires `originalAmount` when the event lands on a transaction whose original currency differs from your billing currency, returning the existing `SIMULATION_INVALID_STATE` error if it is missing. The amount, currency, and timestamp recorded for the event now always match what the transaction webhooks report for it.
* [Simulate clearing](/api-reference/simulation/simulate-clearing) - documents `originalAmount`
* [Simulate reversal](/api-reference/simulation/simulate-reversal) - documents `originalAmount`
* [Simulate refund](/api-reference/simulation/simulate-refund) - documents `originalAmount`
### Encrypted card details can use RSA-OAEP with SHA-256
[Reveal PAN](/api-reference/cards/reveal-pan) can return `encryption: "RSA_OAEP_SHA256"` alongside the existing `RSA_OAEP_SHA1`. The value follows the public key registered for your program, so decrypt with the hash it names rather than a fixed one. Keys registered from now on use SHA-256.
* [Encrypted Card Details Retrieval](/cards/card-details/encrypted-retrieval) - decryption per scheme
### Sandbox refunds require a matching cleared transaction
[Simulate refund](/api-reference/simulation/simulate-refund) now checks that the transaction it
acts on can actually be refunded. Passing `transactionId` requires that transaction to have been
authorized before it cleared, so one created by offline or direct clearing cannot be refunded.
Omitting `transactionId` for a standalone refund now requires the card to already have an
authorized and cleared transaction in the refund currency, since the refund takes its merchant
and currency from that transaction instead of generating them at random. Either case that fails
returns the existing `SIMULATION_INVALID_STATE` error.
* [Testing in Sandbox](/transactions/testing-in-sandbox#scenario-4-refund-on-a-cleared-transaction)
walks through simulating a refund
### Company deactivation is reported separately from KYB status
Company resources now include `isDeactivated`. It is `true` while the verification provider has deactivated the company, and it is independent of `status`. The last KYB outcome stays on the company. Spend is stopped by moving the company to `RESTRICTED`. The company remains readable by id so you can still resolve users and cards that point at it.
[`COMPANY_STATUS_UPDATED`](/api-reference/company-status-updated) carries the same field.
* [Get company](/api-reference/companies/get-company) and [List companies](/api-reference/companies/list-companies) return `isDeactivated`
* [KYB overview](/compliance/kyb/overview#deactivation) describes the deactivation axis
### Archived related persons stay readable
Related person resources now include `isActive`. It is `false` when the person has been archived.
[Get related person](/api-reference/companies/get-related-person) still returns an archived person. [List related persons](/api-reference/companies/list-related-persons) omits them unless you pass `includeArchived=true`.
* [KYB overview](/compliance/kyb/overview#related-persons) covers archived related persons
### Statements itemise the fees Reap billed you
Request a statement for a period and Reap produces a CSV of every fee event it billed you for,
one row each. The rows carry the transaction, the fee category, and the amounts behind each one.
Ask for the whole file or name the columns you want. Files are prepared in the background and
stay available for 180 days.
* [Request statement](/api-reference/statements/request-statement) starts one for a period of up
to 31 days
* [Download statement](/api-reference/statements/download-statement) redirects to the file, or
returns a link if you send `Accept: application/json`
* [`STATEMENT_STATUS_UPDATED`](/api-reference/statement-status-updated) tells you when a file is
ready, so you do not have to poll
* [Card transaction fees](/statements/card-transaction-fees) documents every column the file
carries
### API keys can be restricted to a set of IP addresses
A key can be limited to a list of IPv4/IPv6 addresses or CIDR ranges. Once a list is set, a
request from any other address is rejected, including one where Reap cannot determine the
caller's address at all.
* [Authentication](/api-reference/authentication) documents the allowlist and the `403`
`API_KEY_IP_NOT_ALLOWED` error it returns
### Account balances report fiat credits alongside crypto
[Get account balance](/api-reference/accounts/get-account-balance) adds `assets.fiat`, the amount
credited to the account by bank transfer. Only the master collateral account can be funded that
way, so every other account keeps reporting `null` there.
[Get account assets](/api-reference/accounts/get-account-assets) itemizes the same credit as a
`FIAT` entry alongside the crypto rows, valued at par in the account's currency.
* [Bank transfers](/accounts/bank-transfers) covers funding the master account by wire
### Card shipment status webhooks include the cards in the shipment
[`CARD_SHIPMENT_STATUS_UPDATED`](/api-reference/card-shipment-status-updated) now carries the same `cards[]` array as [Get shipment](/api-reference/card-shipments/get-shipment). Each card includes `cardId` and `productionStatus`, so you can map a status change to the cards in the shipment without a follow-up request.
* [Physical card shipping](/cards/physical-cards/shipping) - subscribe and track production
### Sandbox authorization can raise a detected fraud alert
[Simulate authorization](/api-reference/simulation/simulate-authorization) and
[Simulate 3DS authorization](/api-reference/simulation/simulate-3ds-authorization) accept
optional `triggerFraudAlert: true`. That creates the charge and asynchronously raises a
detected fraud alert you can respond to or list like production.
* [Testing in Sandbox](/transactions/testing-in-sandbox) covers the end-to-end scenario
* [Fraud Alerts](/cards/fraud-alerts) documents respond and manual report
### Sandbox simulations carry your program's cardholder fees
Simulated authorizations, clearings, reversals, and refunds are priced like live
transactions. The `fees` on a simulated transaction reflect your program's rates, the ATM tier
resolved from the merchant country, and any override set on the account. The `amount` you
simulate is the merchant amount and the fees are charged on top of it.
Sandbox fees were always zero, so an integration that asserted `fees.atm` and `fees.fx` are `0`
on a simulated transaction needs to update that expectation. A simulation that omits `channel`
now runs as `ECOMMERCE` instead of picking a channel at random, so a same-currency purchase
stays free of fees unless you ask for an ATM withdrawal.
* [Testing in Sandbox](/transactions/testing-in-sandbox) - the simulation endpoints and the fee
scenarios you can drive through them
* [Cardholder fees](/transactions/fees) - the rates and ATM tiers being applied
### Cardholder fees are configurable per program and per account
The FX markup and the ATM withdrawal fees your cardholders pay are no longer fixed. Every
program carries a default rate per fee type, and you can override any of them for a single
account. ATM fees are split into a domestic and a cross-border tier, each with a rate-based
and a fixed component. See [Cardholder fees](/transactions/fees).
* [Create fee override](/api-reference/fees/create-fee-override) sets one fee type for one account
* [List effective fees](/api-reference/fees/list-effective-fees) shows what an account pays and where each rate comes from
* [List fees](/api-reference/fees/list-fees), [Get fee](/api-reference/fees/get-fee),
[Update fee value](/api-reference/fees/update-fee-value), and
[Delete fee override](/api-reference/fees/delete-fee-override) manage the rows
Set your program's default fees in the dashboard before launching in production: only an
organization Admin can change them, and only from the dashboard.
### Cards always report a design id
`cardDesignId` no longer returns `null`. Reap backfilled the design on every card issued before it
started recording one.
* [Get card](/api-reference/cards/get-card) and [List cards](/api-reference/cards/list-cards) carry
the field
* [`CARD_STATUS_UPDATED`](/api-reference/card-status-updated) carries the same field
### A card can only run one sandbox simulation at a time
[Simulate authorization](/api-reference/simulation/simulate-authorization) and
[Simulate decline](/api-reference/simulation/simulate-decline) now return a `409` with
`SIMULATION_IN_PROGRESS` when a simulation is already running for the card. Wait for the first one
to finish, then retry.
### Agentic Payments sandbox setup includes test cards and a one-time password
[Setup](/agentic-payments/setup) lists card numbers you can enter on the hosted card entry page to
run a full sandbox checkout, plus the fixed one-time password the verification step accepts.
### Singapore gets a dedicated API hostname
Singapore sandbox and production now have their own hostnames, `sg.sandbox.api.reap.global` and
`sg.prod.api.reap.global`, matching the pattern Mexico already uses. `sandbox.api.reap.global`
and `prod.api.reap.global` keep working as aliases.
* [Authentication](/api-reference/authentication) lists the base URL for every environment
### Enrollment and mandate owners are identified by type and id
An Agentic Payments owner now carries an `id` instead of a `reference`. It can also be a
`REAP_USER`, alongside the existing `CLIENT_REFERENCE`. A `REAP_USER` owner is derived from the
cardholder of an enrolled Reap card. You never send it on a request.
[List enrollments](/api-reference/agentic/list-enrollments) filters on `ownerId`, which replaces
`ownerReference`. The filter is now required, so a list is always scoped to one owner.
* [Get mandate](/api-reference/agentic/get-mandate) carries the same owner shape
### Enrollments are created per card source, and checkouts charge a stored enrollment
[Create enrollment](/api-reference/agentic/create-enrollment) request and response now depend on
`source`:
* An existing Reap card, enrolled directly
* A BIN-sponsor card, enrolled directly
* A hosted page that captures a new card
Enrolling an unknown Reap card returns a new `AGENTIC_CARD_NOT_FOUND` error.
[Create checkout](/api-reference/agentic/create-checkout) now takes the `enrollmentId` of an
`ACTIVE` enrollment and charges that enrollment's stored card and owner. It no longer accepts an
inline `owner`. A checkout can also reach an `EXPIRED` status, reported by
[Get checkout](/api-reference/agentic/get-checkout).
### Card authorization requests carry a transaction id
[`CARD_AUTHORIZATION_REQUEST`](/api-reference/card-authorization-request) now includes
`transactionId`, the id of the transaction the authorization belongs to. It matches `id` on the
transaction webhooks that follow it.
### KYC attestations require a second address line
An address on a [KYC attestation](/api-reference/users/advance-the-user-application) must now
include a non-empty `line2`. Use the district, area, or building name if the address has no
natural second line. An attestation with an empty or missing `line2` now fails at the attestation
call instead of later, at card issuance.
### Cards report the design they were issued with
[Get card](/api-reference/cards/get-card) and [List cards](/api-reference/cards/list-cards) now
return `cardDesignId`. For a physical card this is the design on the card record, which can
differ from the design on the shipment that produced the plastic. `cardDesignId` is `null` on
cards issued before Reap started recording the design.
* [Create card](/api-reference/cards/create-card), [Freeze card](/api-reference/cards/freeze-card),
[Unfreeze card](/api-reference/cards/unfreeze-card), [Block card](/api-reference/cards/block-card),
and [Unblock card](/api-reference/cards/unblock-card) carry the same field
* [List card designs](/api-reference/card-designs/list-card-designs) and
[Get card design](/api-reference/card-designs/get-card-design) look up a design by this id
### A product's variant can be resolved from its option choices
[Get product details](/api-reference/agentic/get-product-details) now returns a product's option
axes instead of resolving them to a variant itself. When a product has options, pass the chosen
option ids to [Resolve variant](/api-reference/agentic/resolve-variant) to get the variant id a
quote accepts.
[Get quote](/api-reference/agentic/get-quote) retrieves a quote you already created, so you no
longer have to hold onto the response from [Create quote](/api-reference/agentic/create-quote) to
check its state later.
### Sandbox card shipment and activation simulations no longer fail
Checking on a simulated shipment while it was between statuses, and activating a physical card
after simulating its delivery, could both return an unexpected server error in sandbox. Both now
complete normally.
* [Simulate shipment status](/api-reference/simulation/simulate-shipment-status) drives a shipment
through production and delivery in sandbox
* [Activate physical card](/api-reference/cards/activate-physical-card) activates a card with its
mailer code
### Encrypted card details retrieval is documented in the API reference
[Reveal PAN](/api-reference/cards/reveal-pan) returns a card's PAN, CVV, and expiry as an encrypted
payload, for PCI-scoped partners Reap approves for direct card-data access. The endpoint and its
[integration guide](/cards/pan-reveal) no longer require a private link to find.
The capability itself has not changed: it stays off by default until Reap reviews your compliance
status.
### ATM withdrawal fees can include a flat per-withdrawal component
The `atm` amount in a card transaction's `fees` can now carry a flat fee on top of the existing
rate-based markup, if your project's pricing includes one. A refund of a settled withdrawal never
carries the flat component, since the network cost behind it was already incurred.
* [Get card transaction](/api-reference/card-transactions/get-card-transaction) reports the
combined amount
### Policies can restrict or limit push-to-card transactions
`VISA_DIRECT` is a channel a channel-restriction, spend-limit, or authorization-count-limit policy
can now name, alongside `POS`, `ECOMMERCE`, and `ATM`.
* [Create policy](/api-reference/policies/create-policy) accepts it in `channels` and in a limit's
`filter`
* [Spend and Velocity Controls](/spend-policies/control-types) covers channel restriction and
filtered limits
### Funds can be withdrawn from an account to an external address
You can move tokens out of a User-Funded account's wallet to any address you name.
[Initiate crypto withdrawal](/api-reference/crypto-withdrawals/initiate-crypto-withdrawal) reserves
the funds and opens a signature window. The wallet owner signs a payload from
[Mint signing payload](/api-reference/crypto-withdrawals/mint-signing-payload), and you hand the
signature to [Submit withdrawal signature](/api-reference/crypto-withdrawals/submit-withdrawal-signature).
Reap covers the network fee and charges a flat fee per chain, taken out of the amount you request.
Subscribe to [`CRYPTO_WITHDRAWAL_CREATED`](/api-reference/crypto-withdrawal-created) and
[`CRYPTO_WITHDRAWAL_STATUS_UPDATED`](/api-reference/crypto-withdrawal-status-updated) to follow one
to completion. You can also read one with
[Get crypto withdrawal](/api-reference/crypto-withdrawals/get-crypto-withdrawal), or
[cancel](/api-reference/crypto-withdrawals/cancel-crypto-withdrawal) one that is still waiting for a
signature. Withdrawals appear in the [activity feed](/api-reference/activities/list-activities).
See [Withdrawing Funds](/accounts/withdrawals).
### Account assets report what can be withdrawn per chain
Each chain in the [assets endpoint](/api-reference/accounts/get-account-assets) now reports
`withdrawable`, the largest amount you can request on that chain, and `withdrawalFee`, what a
withdrawal there costs. Outstanding card spend is already netted out of `withdrawable`, so you can
show a user a figure they can act on.
See [The withdrawable cap](/accounts/withdrawals#the-withdrawable-cap).
### Account ownership is chosen per account
[Create account](/api-reference/accounts/create-account) accepts an optional `ownerType`
(`USER` or `COMPANY`). It defaults to `USER` when omitted. Company-owned accounts are only
available in corporate programs and return `400 COMPANY_ACCOUNT_NOT_ALLOWED` in consumer
programs. Account ownership is no longer a project-level setting.
See [Program Mode](/program-configuration/modes) and [Accounts](/accounts/overview).
### Deleting a user deletes their cards
[Delete user](/api-reference/users/delete-user) deletes every card the user holds, instead of
just blocking them. Deletion is permanent. The user's accounts stay open, since they can still
hold value or card debt to settle.
### Creating a user no longer needs terms acceptance
[Create user](/api-reference/users/create-user) no longer accepts or requires
`termsAcceptance`. Drop the field from any request that still sends it.
### Card shipment status can be simulated in sandbox
[Simulate shipment status](/api-reference/simulation/simulate-shipment-status) drives a
submitted shipment through production and delivery in sandbox. Watch the change arrive through
`CARD_SHIPMENT_STATUS_UPDATED`.
* [Shipping](/cards/physical-cards/shipping) covers card production and delivery tracking
### Users can be looked up by your own identifier
[Create user](/api-reference/users/create-user) accepts an optional `externalId`, your own
identifier for the user. It must be unique among your active users, so a request that reuses one
is rejected with `EXTERNAL_ID_ALREADY_EXISTS` rather than creating a duplicate. Deleting a user
releases the value for reuse.
Pass it to [List users](/api-reference/users/list-users) as `externalId` to resolve your
identifier back to a Reap user.
* [Get user](/api-reference/users/get-user) also returns the field
### Fiat deposit simulation takes the currency you send, not a conversion pair
Simulating a fiat deposit now takes a required `currency`, the currency of the simulated transfer.
It replaces the optional `originalAmount` and `originalCurrency` that previously simulated a
converted transfer.
The currency must be the one your card program bills in. Any other is rejected with
`SIMULATION_UNSUPPORTED_CURRENCY`, which lists the currencies the project accepts. A sandbox call
that still sends `originalAmount` or `originalCurrency` now fails validation.
* [Simulate a fiat deposit](/api-reference/simulation/simulate-a-fiat-deposit) takes a required
`currency` in place of `originalAmount` and `originalCurrency`
* [Bank transfers](/accounts/bank-transfers) covers the deposit flow
### Deposit addresses name the assets they accept
Each deposit address now names the assets it accepts. Each item uses the same `symbol`, `name`,
`decimals`, and `logoUri` fields used elsewhere for asset metadata, plus the `assetId` to deposit
against.
A deposit of anything else fails with `ASSET_NOT_ACCEPTED`. Read the list rather than hardcoding
one, since what a project accepts is a subset of what the platform supports.
List responses stay lean and omit it. `name` and `logoUri` are best-effort and may be `null`, so
render a fallback for both.
* [Get account](/api-reference/accounts/get-account) returns `acceptedAssets` on every
`chainAddresses` entry
* [Supported Assets](/accounts/supported-assets) lists the assets the platform can accept
### Sumsub Token Sharing decisions now arrive by webhook
Importing a Sumsub applicant share token no longer approves the user synchronously. A successful
import now moves the user to `IN_REVIEW`.
The decision, `APPROVED`, `REJECTED`, or `RETRY_REQUIRED`, then arrives on the same
`USER_APPLICATION_STATUS_UPDATED` webhook used by Managed KYC.
A failed import can now leave the user at `RETRY_REQUIRED` as well as `NOT_STARTED`.
* [Advance the user application](/api-reference/users/advance-the-user-application) with the
`SUMSUB_TOKEN_SHARING` method imports the token and moves the user to `IN_REVIEW`
* [`USER_APPLICATION_STATUS_UPDATED`](/api-reference/user-application-status-updated) carries the
decision
* [Managed KYC](/compliance/kyc/managed-kyc) explains the webhook-driven review flow
* [Sumsub Token Sharing](/compliance/kyc/sumsub-token-sharing) covers the import flow
### Choose the account owner type at creation
Creating an account now takes an explicit `ownerType`, `USER` or `COMPANY`, instead of inferring
it from the project.
It defaults to `USER`. `ownerId` is a user ID or a company ID to match. Requesting `COMPANY`
outside a corporate program now fails with `COMPANY_ACCOUNT_NOT_ALLOWED`.
* [Create account](/api-reference/accounts/create-account) takes `ownerType` and a matching
`ownerId`
* [Account ownership](/accounts/overview#ownership) explains user and company accounts
### Fund the master collateral account by bank transfer
[Get bank details](/api-reference/fiat-deposits/get-bank-details) returns the beneficiary name and
address, the account number, the bank name and address, the SWIFT code, and the domestic bank and
branch codes for wiring funds into the master collateral account, along with a `reference` to
quote on the transfer. It applies to Program-Funded projects enabled for bank transfers.
`FIAT_DEPOSITS_NOT_ENABLED` means the project has not been enabled for it.
See [Funding via Bank Transfer](/accounts/bank-transfers).
### Card shipping can be disabled for a project
[Submit shipment](/api-reference/card-shipments/submit-shipment) returns
`403 CARD_SHIPPING_NOT_ENABLED` when physical card shipping is not enabled for the project.
See [Shipping](/cards/physical-cards/shipping).
### Company simulation rejects restricted countries
[Simulate company status](/api-reference/simulation/simulate-company-status) now rejects
`country`, `registeredAddress`, and `operationalAddress` values for a country that cannot be used
for card issuance, such as `KP`, when simulating `ACTIVE`.
See [Getting started with KYB](/compliance/kyb/getting-started).
### Cards can be blocked and unblocked directly
You can block a card and unblock it when the block can be lifted. A block is a hard stop,
separate from freezing it.
`blockLiftable` on the card resource tells you whether unblock will work before you call it.
`frozen` reports a freeze that a block is hiding from `status`.
`blockReason.type` on a blocked card can be `CLIENT_REQUESTED`.
* [Block a card](/api-reference/cards/block-card) declines every attempt online, in-store, and at
the ATM
* [Unblock a card](/api-reference/cards/unblock-card) lifts a block you or Reap placed, when it
can be lifted
* [Card statuses](/cards/overview#card-statuses) covers how freeze and block show up in `status`
* [Managing cards](/cards/managing-cards#block-and-unblock) covers when to block versus freeze
### Card management stays available when an account is restricted
Card operations no longer fail with `ACCOUNT_NOT_ACTIVE` when the linked account is restricted.
`blockReason.type` can no longer be `ACCOUNT_RESTRICTED`. Restriction only affects transaction
authorization, tracked on the account.
* [Delete a card](/api-reference/cards/delete-card) removes a card when offboarding a user
* [Freeze a card](/api-reference/cards/freeze-card) pauses spending on a card
* [Unfreeze a card](/api-reference/cards/unfreeze-card) restores a frozen card
* [Update PIN](/api-reference/cards/update-card-pin) sets or changes the cardholder's PIN
* [Create reveal session](/api-reference/cards/reveal-card-details) returns a link that shows the
card details
* [Account status](/accounts/overview#account-status) explains how restriction is tracked on the
account
### File a dispute on a cleared card transaction
You can dispute a cleared card transaction and have Reap take the case to the card network.
Send the transaction ID and a reason. Reap derives the card and cardholder from the transaction,
along with its currency.
Some reasons need a signed statement from the cardholder before the case can move forward.
Subscribe to [`CARD_DISPUTE_STATUS_UPDATED`](/api-reference/card-dispute-status-updated) for the
outcome.
* [File dispute](/api-reference/disputes/file-dispute) opens a case on a cleared transaction
* [List disputes](/api-reference/disputes/list-disputes) returns the cases you have filed
* [Get dispute](/api-reference/disputes/get-dispute) returns one case
* [Disputes](/cards/disputes/overview) explains how filing and outcomes work
* [Dispute reasons](/cards/disputes/reasons) lists the reason vocabulary
### Fiat deposits are now available over the API
You can read a fiat deposit credited to an account. The response includes the amount as received
by the bank and the amount actually credited.
Those two differ when the transfer was converted on the way in.
In Program-Funded programs you can simulate a deposit in sandbox. Subscribe to
[`FIAT_DEPOSIT_CREATED`](/api-reference/fiat-deposit-created) for new deposits.
Fiat deposits also appear in the activity feed as `FIAT_DEPOSIT`.
* [Get fiat deposit](/api-reference/fiat-deposits/get-fiat-deposit) returns a credited fiat
deposit
* [Simulate a fiat deposit](/api-reference/simulation/simulate-a-fiat-deposit) credits a
project's master collateral account in sandbox
* [List activities](/api-reference/activities/list-activities) returns the ordered, paginated
feed
### The activity feed can return more than one activity type at once
`type` on the activity feed accepts a comma-separated list.
`type=CRYPTO_DEPOSIT,CARD_TRANSACTION` returns deposits and card transactions together in one
ordered, paginated feed.
Every other filter narrows within its own activity type instead of restricting the feed to it.
`cardId=` alone still returns the account's other activity alongside that card's
transactions. Combine it with `type=CARD_TRANSACTION` to see the card's transactions by
themselves.
* [List activities](/api-reference/activities/list-activities) returns the ordered, paginated
feed
* [Reading transactions](/transactions/overview) explains how the activity feed is filtered
### Master deposits can be charged a deposit fee
Deposits into a master account can carry a fee, set per project and agreed with Reap. The fee
comes out of the deposit.
Each deposit reports what it was charged in `feeAmount`, in the same token as `amount`. A
project with no fee reports `0`.
Deposits into user accounts are never charged and report `null`.
* [Deposit fee](/accounts/deposits#deposit-fee) explains when a master deposit is charged
### Simulate an authorization with a 3DS challenge
You can run the whole challenge flow in sandbox for cards using `3dsChallengeMethod: WEBHOOK`.
The simulation sends [`CARD_3DS_CHALLENGE_CREATED`](/api-reference/3ds-challenge-created)
without creating a transaction. Approving one creates the transaction, rejecting it does not.
* [Simulate 3DS authorization](/api-reference/simulation/simulate-3ds-authorization) runs the
challenge flow in sandbox
* [Testing in Sandbox](/transactions/testing-in-sandbox) covers the rest of the sandbox
challenge flow
### Responding to a closed 3DS challenge returns a specific error
A closed challenge returns a specific error if it has already been answered or has expired.
* [Respond to 3DS challenge](/api-reference/cards/respond-to-3ds-challenge) returns
`400 3DS_CHALLENGE_INVALID_STATE` when the challenge is already answered or expired
### Fraud alerts can be handled through the API
Reap raises an alert on a transaction it suspects is fraudulent. You can also report a past
transaction as fraud yourself.
You can list alerts and fetch one, then respond to confirm or decline. Confirming an alert or
reporting a transaction as fraud blocks the card and sends
[`CARD_STATUS_UPDATED`](/api-reference/card-status-updated) with
`blockReason.type: FRAUD_ALERT_CONFIRMED`.
[`CARD_FRAUD_ALERT_CREATED`](/api-reference/card-fraud-alert-created) fires when an alert is
raised. [`CARD_FRAUD_ALERT_STATUS_UPDATED`](/api-reference/card-fraud-alert-status-updated)
fires when its status changes.
An alert that nobody answers in time expires on its own.
* [Report fraud](/api-reference/fraud-alerts/report-fraud) reports a past transaction as fraud
* [Respond to alert](/api-reference/fraud-alerts/respond-to-alert) confirms or declines a
suspected-fraud alert
* [Fraud Alerts](/cards/fraud-alerts) explains how alerts work end to end
# Quickstart
Source: https://docs.reap.global/compliance/kyb/getting-started
Start KYB for a company and handle the outcome via webhooks.
## Goal
This guide walks you through the KYB flow end-to-end. By the end, you will have an `ACTIVE` company that is ready for account creation and card issuance.
Call [Create a company](/api-reference/companies/create-company) with a display name. The response includes a company `id` you will use for all subsequent calls. The company starts with `status: CREATED`.
Call [Advance company application](/api-reference/companies/advance-company-application) with the company `id`. The response includes a `sessionUrl`. Direct the company representative to this URL to complete the corporate verification form.
The company status advances to `PENDING` once the application is created.
During the verification flow, Reap identifies the company's officers and beneficial owners. Poll [List related persons](/api-reference/companies/list-related-persons) to detect when new related persons appear.
For each related person, retrieve their active application via [List related person applications](/api-reference/companies/list-related-person-applications) to get the `sessionUrl`. Send this URL to the individual so they can complete their KYC.
Repeat this for every related person. The company cannot advance to `IN_REVIEW` until all related persons reach `APPROVED`.
When all verification inputs are received and Reap's compliance review completes, Reap fires a `COMPANY_STATUS_UPDATED` event. See the [COMPANY\_STATUS\_UPDATED](/api-reference/company-status-updated) for the full payload schema.
Handle the `status` in the event:
* `IN_REVIEW`: inputs are received and the review is in progress.
* `ACTION_REQUIRED`: Reap needs more information. Your account manager will contact you.
* `ACTIVE`: company is verified. Proceed to account creation and card issuance.
* `REJECTED`: verification failed. Contact your account manager.
* `RESTRICTED`: company spend is stopped. Contact your account manager. Check `isDeactivated` if the company itself was deactivated.
***
## Testing in sandbox
In the sandbox environment, use the simulation endpoint to move a company to any target status without running real verification. The update is processed asynchronously. Wait for the `COMPANY_STATUS_UPDATED` webhook rather than polling.
The scenarios below use Managed KYB. For Universal KYB projects, see [Testing in sandbox](/compliance/kyb/universal-kyb#testing-in-sandbox).
See [Simulate company status](/api-reference/simulation/simulate-company-status) in the API reference for the full request schema, including the required `applicationDetails` fields when simulating `ACTIVE`.
Simulating `ACTIVE` rejects countries that cannot be used for card issuance (for example `KP`). Use a supported country such as `AE`, `HK`, or `SG` on `country`, `registeredAddress`, and `operationalAddress`.
### Scenario 1: Happy path (approved)
1. [Create a company](/api-reference/companies/create-company)
2. [Advance company application](/api-reference/companies/advance-company-application)
3. [Simulate company status](/api-reference/simulation/simulate-company-status) with `{ "status": "ACTIVE", "applicationDetails": { ... } }`
4. Confirm webhook received with `status: "ACTIVE"`
5. [Create an account](/api-reference/accounts/create-account) scoped to the company
### Scenario 2: In review, then approved
1. [Create a company](/api-reference/companies/create-company)
2. [Advance company application](/api-reference/companies/advance-company-application)
3. [Simulate company status](/api-reference/simulation/simulate-company-status) with `{ "status": "IN_REVIEW" }`
4. Confirm webhook received with `status: "IN_REVIEW"`
5. [Simulate company status](/api-reference/simulation/simulate-company-status) with `{ "status": "ACTIVE", "applicationDetails": { ... } }`
6. Confirm webhook received with `status: "ACTIVE"`
### Scenario 3: Rejected
1. [Create a company](/api-reference/companies/create-company)
2. [Advance company application](/api-reference/companies/advance-company-application)
3. [Simulate company status](/api-reference/simulation/simulate-company-status) with `{ "status": "REJECTED" }`
4. Confirm webhook received with `status: "REJECTED"`
# Overview
Source: https://docs.reap.global/compliance/kyb/overview
How Reap verifies companies and the statuses to expect during KYB.
Know Your Business (KYB) is the verification process every corporate entity must complete before it can hold accounts or issue cards on Reap. Your project uses one method, set when you onboard.
Reap hosts the verification session. You send `redirectUri`, receive a `sessionUrl`, and listen for `COMPANY_STATUS_UPDATED`.
You submit the verified business pack and supporting documents. Reap reviews them and returns a decision.
The rest of this page describes the shared company statuses and the Managed KYB session flow. For the pack-and-document flow see [Universal KYB](/compliance/kyb/universal-kyb).
***
## How it works
When you initiate KYB for a company, Reap creates a time-bound verification session. The company representative completes the session by submitting corporate documents, registration details, and ownership structure through Reap's hosted verification flow.
As the representative fills out the form, officers and beneficial owners are identified. These are called related persons. They are added to the company as the form is completed, either by the representative declaring them directly or through Reap's compliance review. Each related person must complete their own KYC before the company can advance to review.
You do not handle any identity documents. All document capture and storage happens inside Reap's verification infrastructure.
***
## Company statuses
A company's `status` reflects where it is in the KYB lifecycle.
| Status | Meaning |
| - | - |
| `CREATED` | Company created, verification not yet started |
| `PENDING` | Verification in progress. Either the company representative has not submitted the form, or one or more related persons have not completed KYC |
| `IN_REVIEW` | All verification inputs received. Under Reap compliance review |
| `ACTION_REQUIRED` | Reap needs more information. Your account manager will contact you |
| `ACTIVE` | KYB approved. The company can hold accounts and issue cards |
| `REJECTED` | Verification failed. Contact your account manager |
| `RESTRICTED` | Company spend is stopped. Accounts owned by the company and by its users are restricted. Set after a prior approval, or when the verification profile is deactivated |
```mermaid theme={null}
flowchart TD
CREATED --> PENDING
PENDING -->|"KYB submitted + all related persons approved"| IN_REVIEW
IN_REVIEW -->|"Reap approves"| ACTIVE
IN_REVIEW -->|"Reap rejects"| REJECTED
IN_REVIEW -->|"More information needed"| ACTION_REQUIRED
ACTION_REQUIRED -->|"Information received"| IN_REVIEW
ACTION_REQUIRED -->|"Reap approves"| ACTIVE
ACTION_REQUIRED -->|"Reap rejects"| REJECTED
ACTIVE -->|"Compliance action"| RESTRICTED
RESTRICTED -.->|"Resolved"| ACTIVE
REJECTED -.->|"Re-verification"| PENDING
```
### RESTRICTED vs REJECTED
`RESTRICTED` is not the same as `REJECTED`. A restricted company was previously able to operate and has been restricted following a re-verification failure or a compliance action, including deactivation of the company. Existing users remain, but their accounts are restricted: authorizations decline until the company returns to `ACTIVE`.
`REJECTED` means the initial KYB verification failed and the company was never approved. Contact your account manager in either case.
### Deactivation
A company can be deactivated independently of its compliance status. `isDeactivated` on the company resource (and on `COMPANY_STATUS_UPDATED`) is `true` while the verification profile is deactivated.
Deactivation does not overwrite the last KYB outcome. The company stays readable by id so you can still resolve users and cards that point at it. Spend is stopped by moving the company to `RESTRICTED`. A later approval, or an operator un-deactivation, clears `isDeactivated` and reapplies the persisted KYB outcome.
### From PENDING to IN\_REVIEW
A company advances from `PENDING` to `IN_REVIEW` only when two conditions are met simultaneously:
1. The company representative has submitted the company application form.
2. Every related person on the company has an `APPROVED` KYC status.
In practice, related persons are added to the company as the representative progresses through the verification form. Each one will need to complete their own KYC before the overall gate is met. The transition to `IN_REVIEW` happens automatically as soon as both conditions are satisfied.
***
## Application statuses
The company and related person application share common lifecycle and statues
| Status | Meaning |
| - | - |
| `INITIATED` | Session created. The URL is ready for the representative to open |
| `IN_PROGRESS` | Representative has submitted. Review is in progress |
| `APPROVED` | Verification passed |
| `REJECTED` | Verification failed. You can create a new application to retry |
| `REVIEW_REQUIRED` | Additional manual review is needed |
| `WORKFLOW_ERROR` | A technical issue occurred with the session. Create a new application |
| `EXPIRED` | The session URL expired before the representative completed the form. Create a new application |
The `sessionUrl` on an application is only usable while the application is in `INITIATED` status. Once submitted or expired, a new application must be created to get a fresh URL.
***
## Related persons
Related persons are the officers and beneficial owners associated with the company. They are added during the company application process as the representative declares them in the verification form, or when identified through Reap's compliance review. You cannot create or modify related persons via the API.
Each related person has a `status` reflecting their KYC verification progress.
| Status | Meaning |
| - | - |
| `NOT_STARTED` | Identified but verification not yet started |
| `IN_REVIEW` | Submitted, under review |
| `APPROVED` | KYC passed |
| `REJECTED` | KYC failed. A new KYC application can be created for this person |
Archived related persons have `isActive: false`. [Get related person](/api-reference/companies/get-related-person) still returns them. [List related persons](/api-reference/companies/list-related-persons) omits them unless you pass `includeArchived=true`.
Each related person has verification applications, each with a `sessionUrl` you must send to that individual. Retrieve active applications via [List related person applications](/api-reference/companies/list-related-person-applications).
All related persons must reach `APPROVED` before the company can advance to `IN_REVIEW`.
***
## Webhooks
Reap fires a `COMPANY_STATUS_UPDATED` event whenever a company status changes, including the transitions from `PENDING` to `IN_REVIEW` and from `IN_REVIEW` to `ACTIVE`. See the [COMPANY\_STATUS\_UPDATED](/api-reference/company-status-updated) for the full payload schema.
There are no dedicated webhook events for related person status changes. Poll [List related persons](/api-reference/companies/list-related-persons) to track individual KYC progress, or rely on the company `COMPANY_STATUS_UPDATED` webhook to detect when the overall gate has been passed.
***
## Key principles
* **You do not manage compliance.** Reap handles verification sessions, provider communication, and status updates.
* **Two conditions must be met for IN\_REVIEW.** The company representative must submit the KYB form AND all related persons must be individually approved before Reap begins its review.
* **Related persons are added during the application.** You do not create them. They are added as the representative fills out the verification form or through Reap's compliance review, and each gets their own KYC session.
* **Webhooks are the source of truth.** Listen for `COMPANY_STATUS_UPDATED` to track company status. Do not poll for status changes.
* **ACTIVE is a gate.** A company cannot hold accounts or issue cards until its status is `ACTIVE`.
# Universal KYB
Source: https://docs.reap.global/compliance/kyb/universal-kyb
Submit a verified business pack and supporting documents for Reap to review.
Universal KYB lets you bring business verification you have already completed. You submit the company pack and supporting documents, and Reap reviews them and returns a decision. Use this when your project is configured for Universal KYB.
This is one of Reap's company verification methods. See the [KYB overview](/compliance/kyb/overview) for the shared company statuses. Managed KYB (hosted session) is unchanged: send `redirectUri` to [Advance company application](/api-reference/companies/advance-company-application) and use `sessionUrl`.
***
## How it works
1. Create a company.
2. Submit the Universal KYB pack on [Advance company application](/api-reference/companies/advance-company-application) with `method: UNIVERSAL_KYB`. The first call creates the application. Later calls with the same method amend it.
3. Upload each outstanding document from `nextAction`. Company documents use [Upload application documents](/api-reference/companies/upload-application-documents). Beneficial-owner identity documents use [Upload related person documents](/api-reference/companies/upload-related-person-documents).
4. When `nextAction.type` is `SUBMIT`, call [Submit company application](/api-reference/companies/submit-company-application). The company moves to `IN_REVIEW`.
5. Reap reviews the application and sends `COMPANY_STATUS_UPDATED`.
```mermaid theme={null}
sequenceDiagram
participant BE as Your backend
participant Reap as Reap API
BE->>Reap: Create company
Reap-->>BE: companyId
BE->>Reap: Advance application (UNIVERSAL_KYB pack)
Reap-->>BE: nextAction DOCUMENT_UPLOAD
BE->>Reap: Upload company and UBO documents
Reap-->>BE: nextAction SUBMIT
BE->>Reap: Submit application
Reap-->>BE: company IN_REVIEW
Reap-->>BE: COMPANY_STATUS_UPDATED (ACTIVE / REJECTED / ACTION_REQUIRED)
```
Submit is explicit. Uploading the last required document does not lock the application, so you can still add optional documents such as `REGISTER_OF_DIRECTORS` and `BUSINESS_REGISTRY`.
***
## Pack
Send `method: UNIVERSAL_KYB` plus the verified business data: legal name, legal form, registration number, registered and operating addresses, email domains, line of business, and at least one beneficial owner. Both addresses require `line2`. Optional `ddq` and `edd` objects are accepted when you have them.
Declared ownership may sum to less than 100. A sum above 100 is rejected.
`nextAction` lists what is still required:
* `DOCUMENT_UPLOAD` with the outstanding company documents and any outstanding `UBO_IDENTITY` items (`relatedPersonId` + `uboDocumentType`)
* `SUBMIT` when required uploads are complete
* `null` after submit, or for Managed KYB
***
## Documents
Upload one document type per request.
### Company documents
Required before submit: `CERTIFICATE_OF_INCORPORATION`, `COMPANY_PROFILE`, `SHAREHOLDING_CHART`. Optional: `REGISTER_OF_DIRECTORS`, `BUSINESS_REGISTRY`, `SOURCE_OF_FUNDS`, `SOURCE_OF_WEALTH`.
Accepted formats: JPEG, PNG, GIF, PDF, Word (DOC, DOCX), Excel (XLS, XLSX), and CSV. Maximum 5 files per request and 5 MB total.
### Beneficial-owner documents
Each declared identity-document type needs a file on that related person. Accepted formats: JPEG, PNG, and PDF only. Maximum 2 files per request (front, then back) and 5 MB total.
***
## Incomplete submissions
If you submit before every required document is uploaded, the API returns `400` with `code: COMPANY_APPLICATION_INCOMPLETE`, plus `missing` and `invalid`, so you can fix every gap in one pass. When the outstanding items are documents, `documents` lists each one in the same shape as `nextAction.documents`, including `relatedPersonId` and `uboDocumentType` for beneficial-owner documents.
***
## Testing in sandbox
Simulation runs the same completeness check and sends the same `COMPANY_STATUS_UPDATED` webhook as a real review.
1. Submit a complete application as described in [How it works](#how-it-works).
2. Call [Simulate company status](/api-reference/simulation/simulate-company-status) with `ACTIVE`, `REJECTED`, `ACTION_REQUIRED`, or `CANCELLED`.
3. Wait for `COMPANY_STATUS_UPDATED`. The simulate call returns `204` and the new status arrives with the webhook.
Simulating before submit returns `SIMULATION_INVALID_STATE`. `IN_REVIEW` cannot be simulated because submit produces it. `ACTIVE` does not need `applicationDetails`. Reap uses the submitted application.
***
## Managed KYB
Projects configured for Managed KYB keep the existing flow. Send only `redirectUri` (or `method: MANAGED_KYB` with `redirectUri`). The response still includes `sessionUrl`. `nextAction` is `null`. Get and list application stay the same resource shape.
# Managed KYC
Source: https://docs.reap.global/compliance/kyc/managed-kyc
Reap runs identity verification through an embedded provider SDK and delivers the outcome to your webhook endpoint.
Managed KYC is one of Reap's verification methods, and applies if your project is configured for it. With Managed KYC, Reap runs the identity verification: you embed a provider SDK in your app, the user completes their checks, and the outcome arrives by webhook. You never handle identity documents directly.
This guide takes you from a new user to an `APPROVED` one, ready for account creation. See the [KYC overview](/compliance/kyc/overview) for the shared status model and how methods are assigned, or [Sumsub Token Sharing](/compliance/kyc/sumsub-token-sharing) for the alternative method.
***
## How it works
Reap integrates with a third-party identity verification provider. Today this is [Sumsub](https://sumsub.com); the integration is provider-agnostic and we may add others in the future.
When you advance a user's application, Reap returns a short-lived SDK token.
Your backend fetches this token and passes it to your frontend, which uses it to launch the Sumsub SDK inside your app.
* The user uploads their documents and completes liveness checks in the SDK.
* Reap receives the result from the provider and updates the user application status.
* You listen for webhooks to keep your system in sync.
```mermaid theme={null}
sequenceDiagram
participant BE as Your backend
participant Reap as Reap API
participant FE as Your frontend
participant SDK as Verification SDK
BE->>Reap: POST /users/:id/application
Reap-->>BE: nextAction.sdkToken
BE-->>FE: sdkToken
FE->>SDK: Launch SDK with token
SDK-->>SDK: User completes verification
Reap-->>BE: USER_APPLICATION_STATUS_UPDATED webhook
```
***
## What users are asked to provide
During verification, the user is guided through a set of checks. The exact steps depend on the user's country of residence and risk profile.
Typical checks include:
| Check | Description |
| - | - |
| Government-issued ID | Passport, national ID card, or driver's licence. Both sides may be required. |
| Nationality | Captured from the document and user input to determine eligibility and screening rules. |
| Residential address | Collected as part of the profile. |
| Liveness check | Selfie or short video to confirm the document matches a live person. |
| Proof of address | A recent document, such as a utility bill or bank statement, that shows the residential address. When required, the SDK asks for it as a separate stage after the identity checks. |
Capture and storage happen on the verification provider's infrastructure.
***
## Integrate
Managed KYC spans both your backend and your frontend. Your backend handles all Reap API calls and receives webhooks. Your frontend launches the Sumsub SDK using the SDK token your backend fetches.
Call [Advance the user application](/api-reference/users/advance-the-user-application) with the `userId` from [Create a user](/api-reference/users/create-user). The request body is optional for Managed KYC - it defaults to `method: MANAGED_KYC`, so you can omit it. The response returns an SDK token at `nextAction.sdkToken`. Return it to your frontend to launch the verification SDK.
Pass that SDK token to the Sumsub WebSDK in your app.
The SDK guides the user through document capture, liveness checks, and submission.
Follow the [Sumsub WebSDK integration guide](https://docs.sumsub.com/docs/get-started-with-web-sdk) for setup.
For mobile apps, refer to the Sumsub iOS or Android SDK docs.
Verification can run in several stages. Some users are asked for proof of address after they submit their ID and liveness check. Keep the SDK open until the user finishes every stage.
With the WebSDK, close the SDK on `idCheck.onApplicantVerificationCompleted`. Do not close it on `idCheck.onApplicantSubmitted`, which fires after each stage. On mobile, do not dismiss the SDK on the first submission event.
If you close the SDK early, the user never sees the proof of address step. The application can still move to `IN_REVIEW`, but it cannot be approved until the missing step is complete.
Keep your UI in a "verification in progress" state until your backend receives a webhook confirming the outcome.
When verification completes, Reap sends a `USER_APPLICATION_STATUS_UPDATED` event to your webhook endpoint.
See the [USER\_APPLICATION\_STATUS\_UPDATED event reference](/api-reference/user-application-status-updated) for the full payload schema.
Handle the `application.status` in the event:
* `APPROVED`: user is verified. Proceed to account creation.
* `REJECTED`: permanently rejected. Surface `application.rejectionReason` to the user. Do not re-initiate.
* `RETRY_REQUIRED`: user can try again. Surface `application.rejectionReason` and go back to Step 1.
If `application.status` is `RETRY_REQUIRED`, call [Advance the user application](/api-reference/users/advance-the-user-application) again to get a new token, then pass it to your frontend to relaunch the SDK.
Use `application.rejectionLabels` from the webhook to work out what the user needs to correct before they go through the SDK again. See [Rejection reasons](#rejection-reasons).
***
## Rejection reasons
If a user's application is `REJECTED` or `RETRY_REQUIRED`, two fields explain why:
* `rejectionReason` is a short message from the compliance review that you can show to the user. It is often generic.
* `rejectionLabels` lists the specific reject codes. Use them to tell the user what to fix.
Common codes include:
| Label | What it means | Typical action |
| - | - | - |
| `EXPIRATION_DATE` | The submitted ID document has expired | Ask the user to resubmit with a valid document |
| `UNSATISFACTORY_PHOTOS`, `SCREENSHOTS` | Image quality was too low, or the user uploaded a screenshot | Ask the user to retake photos of the original document in good lighting |
| `SELFIE_MISMATCH`, `BAD_FACE_MATCHING` | The liveness selfie did not match the ID document | Ask the user to redo the liveness check |
| `PROBLEMATIC_APPLICANT_DATA` | The entered details do not match the document | Ask the user to enter their name exactly as it appears on the ID |
| `FORGERY`, `NOT_DOCUMENT` | The document appears altered or is not an identity document | Do not re-initiate. Contact Reap support if you believe this is wrong |
See [Rejection labels](/compliance/kyc/rejection-labels) for the full list. It can grow, so handle codes you do not recognize.
`rejectionLabels` is for your internal use only. Do not display the codes to the end user or share them outside your organization. Some codes flag suspected fraud. Translate them into your own guidance instead.
A `REJECTED` status can also stem from an [unsupported jurisdiction](/compliance/kyc/overview#unsupported-jurisdictions). Unlike the examples above, this case is non-retriable - the user cannot resolve it by resubmitting documents.
***
## Testing in sandbox
In the sandbox environment, use the simulation endpoint to trigger `USER_APPLICATION_STATUS_UPDATED` webhooks without running real identity checks.
The update is processed asynchronously. Wait for the webhook rather than polling.
See [Simulate user application status](/api-reference/simulation/simulate-user-application-status) in the API reference for the full request schema.
### Scenario 1: Happy path
1. [Create a user](/api-reference/users/create-user)
2. [Advance the user application](/api-reference/users/advance-the-user-application)
3. [Simulate user application status](/api-reference/simulation/simulate-user-application-status) with `{ "status": "APPROVED" }`
4. Confirm webhook received with `status: "APPROVED"`
5. [Create an account](/api-reference/accounts/create-account)
### Scenario 2: Retry required, then approved
1. [Create a user](/api-reference/users/create-user)
2. [Advance the user application](/api-reference/users/advance-the-user-application)
3. [Simulate user application status](/api-reference/simulation/simulate-user-application-status) with `{ "status": "RETRY_REQUIRED" }`
4. Confirm webhook received with `status: "RETRY_REQUIRED"`, surface `rejectionReason` in your UI
5. [Advance the user application](/api-reference/users/advance-the-user-application) again
6. [Simulate user application status](/api-reference/simulation/simulate-user-application-status) with `{ "status": "APPROVED" }`
7. Confirm webhook received with `status: "APPROVED"`
8. [Create an account](/api-reference/accounts/create-account)
# Overview
Source: https://docs.reap.global/compliance/kyc/overview
How Reap verifies user identity, the methods available, and the application statuses to expect.
Know Your Customer (KYC) is the identity verification process every user must complete before they can hold an account or a card on Reap.
Reap supports three verification methods. Whichever you use, the outcome is the same: an `APPROVED` user, tracked by a single application status, with results delivered to your webhook endpoint.
***
## Verification methods
Reap runs the verification. You embed a provider SDK in your app, the user completes document and liveness checks, and the outcome arrives by webhook.
Reuse verifications from your own Sumsub account. You share a per-user token, Reap imports the existing verification, and the decision arrives on the `USER_APPLICATION_STATUS_UPDATED` webhook.
Bring verifications from your own KYC provider. You submit the verified data pack and supporting documents, and Reap reviews them and returns a decision.
Your project uses one of these methods, set when you onboard with Reap based on your agreement. The method is fixed per project, not chosen per user. If you are not sure which one applies to you, check with your account manager.
***
## Application statuses
User KYC is tracked via an application status on the user resource.
| Status | Meaning | Typical next step |
| - | - | - |
| `NOT_STARTED` | Application created, verification not yet initiated | [Advance the application](/api-reference/users/advance-the-user-application) using your verification method |
| `AWAITING_DOCUMENTS` | Data pack submitted, supporting documents pending upload (Universal KYC only) | [Upload the documents](/api-reference/users/upload-application-documents) listed in `nextAction` |
| `IN_REVIEW` | User has submitted documents, under review by compliance | Wait for `USER_APPLICATION_STATUS_UPDATED` |
| `APPROVED` | Identity verified, user can hold an account and cards | Proceed to account creation |
| `REJECTED` | Verification failed and cannot be recovered | Check `rejectionReason`, inform user, do not re-initiate |
| `RETRY_REQUIRED` | Submission incomplete or unclear, user can try again | [Advance the application](/api-reference/users/advance-the-user-application) again with a new submission |
The `application` object on a user includes `status`, `rejectionReason`, and `rejectionLabels`.
Both rejection fields are populated only when status is `REJECTED` or `RETRY_REQUIRED`:
* `rejectionReason` is a short message you can show to the user.
* `rejectionLabels` lists the reject codes from the compliance review, such as `FORGERY`, `SELFIE_MISMATCH`, or `EXPIRATION_DATE`. Use them to triage and support your users. See [Rejection labels](/compliance/kyc/rejection-labels) for the full list.
`rejectionLabels` is for your internal use only. Do not display the codes to the end user or share them outside your organization. Some codes flag suspected fraud. Translate them into your own guidance instead, for example "Your ID has expired, please upload a valid one".
[Universal KYC](/compliance/kyc/universal-kyc) adds `AWAITING_DOCUMENTS` while you upload the supporting documents, then moves to `IN_REVIEW`.
***
## Unsupported jurisdictions
Reap cannot onboard users from some jurisdictions due to regulatory restrictions.
A user who resides in a restricted jurisdiction cannot be onboarded, regardless of verification method.
For the current list of restricted jurisdictions, contact your account manager.
***
## Key principles
* **Reap orchestrates verification.** Reap manages provider communication, status tracking, and the application lifecycle. How the verification itself happens depends on your [verification method](#verification-methods).
* **One application per user.** A user has a single KYC application. If verification needs to be retried, you advance that user's application again.
* **KYC is a gate.** A user cannot have an account created or a card issued until their KYC status is `APPROVED`.
* **Webhooks are the source of truth.** Do not poll for status changes. Listen for `USER_APPLICATION_STATUS_UPDATED` events and update your UI and internal systems from there.
# Rejection Labels
Source: https://docs.reap.global/compliance/kyc/rejection-labels
Reference for the reject codes returned in rejectionLabels when a KYC application is rejected or needs a retry.
This page lists the codes Reap returns in `application.rejectionLabels` when a user's application is `REJECTED` or `RETRY_REQUIRED`. Use them to triage applications and to tell your users what to fix.
`rejectionLabels` is for your internal use only. Do not display the codes to the end user or share them outside your organization. Some codes flag suspected fraud or a sanctions match. Translate them into your own guidance instead.
## How to read the labels
* **A label does not decide whether the user can retry.** The same label can come with either status. Use `application.status` for that: `RETRY_REQUIRED` means the user can submit again, `REJECTED` is final.
* **One application can carry several labels.** For example, a failed selfie often returns `SELFIE_MISMATCH`, `BAD_FACE_MATCHING`, and `BAD_SELFIE` together.
* **The list can grow.** Handle codes you do not recognize, for example by falling back to `rejectionReason`.
## Document problems
| Label | Meaning |
| - | - |
| `ADDITIONAL_DOCUMENT_REQUIRED` | Another document is needed to pass the check. |
| `BACK_SIDE_MISSING` | The back side of the document is missing. |
| `BAD_PROOF_OF_ADDRESS` | The proof of address is poor quality. |
| `BAD_PROOF_OF_IDENTITY` | The ID document is poor quality. |
| `BLACK_AND_WHITE` | The document photos are black and white. |
| `DIGITAL_DOCUMENT` | The document is digital and cannot be accepted. |
| `DOCUMENT_DAMAGED` | The document is damaged. |
| `DOCUMENT_PAGE_MISSING` | Some pages of the document are missing. |
| `EXPIRATION_DATE` | The document has expired. |
| `FRONT_SIDE_MISSING` | The front side of the document is missing. |
| `ID_INVALID` | The identity document is not valid. |
| `INCOMPATIBLE_LANGUAGE` | The user must upload a translation of the document. |
| `INCOMPLETE_DOCUMENT` | Some information on the document is missing or only partly visible. |
| `LOW_QUALITY` | The document quality is too low to reach a decision. |
| `MORE_DOCUMENTS_REQUIRED` | More documents are needed to complete the review. |
| `NO_SUITABLE_DOCS` | The user could not provide a suitable document. |
| `NOT_DOCUMENT` | The upload is not a document relevant to verification. |
| `SCREENSHOTS` | The user uploaded screenshots instead of photos of the document. |
| `UNFILLED_ID` | The document has no signature or stamp. |
| `UNSATISFACTORY_PHOTOS` | The photos have problems, such as poor quality or hidden information. |
| `UNSUITABLE_DOCUMENT` | The document is the wrong type or lacks a required element, such as a seal. |
## Selfie and liveness
| Label | Meaning |
| - | - |
| `BAD_FACE_MATCHING` | The face check between the document and the selfie failed. |
| `BAD_SELFIE` | The selfie is poor quality. |
| `BAD_VIDEO_SELFIE` | The video selfie is poor quality. |
| `CAMERA_OR_LIGHTING_ISSUES` | The camera or lighting was not good enough. |
| `SELFIE_MISMATCH` | The selfie does not match the photo on the document. |
| `SELFIE_WITH_PAPER` | The user must upload a special selfie, for example holding a paper with the date. |
| `VIDEO_FILTERS_OR_MASKS` | Video filters or masks were applied. |
## Data mismatch
| Label | Meaning |
| - | - |
| `ADDITIONAL_DATA_REQUIRED` | More data is needed to complete the check. |
| `DB_DATA_MISMATCH` | The submitted data does not match an external database. |
| `DB_DATA_NOT_FOUND` | The submitted data was not found in an external database. |
| `DB_NO_DATA_SOURCE` | No available source supports the submitted data. |
| `INCONSISTENT_PROFILE` | Data or documents of different people were submitted for one user. |
| `INCORRECT_SOCIAL_NUMBER` | The tax or social security number is incorrect. |
| `INSUFFICIENT_DATA` | There is not enough data to reach a decision. |
| `PROBLEMATIC_APPLICANT_DATA` | The user's details do not match the document. Ask the user to enter their name exactly as it appears on the ID. |
| `REQUESTED_DATA_MISMATCH` | The submitted details do not match the data read from the document. |
| `WRONG_ADDRESS` | The address on the documents does not match the address the user provided. |
## Eligibility
| Label | Meaning |
| - | - |
| `AGE_REQUIREMENT_MISMATCH` | The user does not meet the age requirement. |
| `COUNTRY_BLOCKED` | The document or address country is not supported. See [unsupported jurisdictions](/compliance/kyc/overview#unsupported-jurisdictions). |
| `DUPLICATE` | The user has already been verified, and duplicates are not allowed. |
| `FOREIGNER` | Users from this country, or without a residence permit, are not accepted. |
| `IDV_DOC_TYPE_NOT_ALLOWED` | This document type is not accepted for the document's country. |
| `REGULATIONS_VIOLATIONS` | The user does not meet applicable regulations or compliance requirements. |
| `WRONG_USER_REGION` | Users from this region or country cannot be registered. |
## Fraud and risk
| Label | Meaning |
| - | - |
| `ADVERSE_MEDIA` | The user appears in adverse media. |
| `BLOCKLIST` | The user is on a blocklist. |
| `COMPLIANCE_SCREENING_FAILED` | The user failed compliance screening. |
| `COMPROMISED_PERSONS` | The user does not meet the compromised persons policy. |
| `CRIMINAL` | The user is involved in illegal activity. |
| `DOCUMENT_DEPRIVED` | The user has been deprived of the document. |
| `DOCUMENT_TEMPLATE` | The document is a template downloaded from the internet. |
| `FORGERY` | The document appears forged. |
| `FRAUDULENT_LIVENESS` | The user tried to bypass the liveness check. |
| `FRAUDULENT_PATTERNS` | Fraudulent behavior was detected. |
| `GRAPHIC_EDITOR` | The document was edited with a graphics editor. |
| `HIGH_RISK` | An unacceptably high level of risk was detected. |
| `PEP` | The user is a politically exposed person. |
| `RESTRICTED_PERSON` | The user is on a restricted list. |
| `SANCTIONS` | The user is on a sanctions list. |
| `SPAM` | The application was created by mistake or is spam, for example after too many attempts. |
| `SPONSORED_VERIFICATION` | The user is suspected of being paid to open the account. |
| `STOLEN_DOCS` | The documents are suspected stolen. |
| `SUSPICIOUS_DOCUMENT` | The document looks suspicious. |
| `THIRD_PARTY_GUIDANCE` | A third party misled or forced the user into the application. |
| `THIRD_PARTY_INVOLVED` | The user is verifying on behalf of a third party. |
## Incomplete or failed checks
| Label | Meaning |
| - | - |
| `CHECK_UNAVAILABLE` | An external check was unavailable at the time of review. |
| `IDV_VERIFICATION_FAILED` | The identity document check could not be completed. |
| `INCOMPLETE_SUBMISSION` | The verification was submitted without all required steps. |
| `LEVEL_ENFORCED_MISMATCH` | The verification did not meet the level your program requires. |
| `NOT_ALL_CHECKS_COMPLETED` | Some checks were not completed. |
| `POA_VERIFICATION_FAILED` | The proof of address check could not be completed. |
| `OTHER` | A reason outside the categories above. Check `rejectionReason`. |
# Sumsub Token Sharing
Source: https://docs.reap.global/compliance/kyc/sumsub-token-sharing
Reuse identity verifications you have already completed in your own Sumsub account, instead of asking your users to re-verify on Reap.
## Overview
If you already operate your own Sumsub account and have a verified user base, you do not have to put those users through KYC again when you onboard them to Reap.
Sumsub's Token Sharing feature lets you hand Reap a per-user share token from your Sumsub account. Reap verifies that shared applicant data and returns the decision via the same `USER_APPLICATION_STATUS_UPDATED` webhook as [Managed KYC](/compliance/kyc/managed-kyc).
This is offered alongside Managed KYC, where Reap runs the verification. Use this guide if your product runs Sumsub KYC under your own account by design and you want Reap to consume those verifications.
***
## Prerequisites
Before you can share tokens with Reap, one-time setup on both sides needs to be in place. Your Reap account manager will guide you through the Reap-side steps.
This authorises your company as a donor of KYC data and Reap as the recipient.
Use the setup token your Reap account manager will provide. Sumsub's [partner setup guide](https://docs.sumsub.com/docs/manage-sharing-partners) covers the exact steps.
Import or configure the applicant levels Reap expects for your program so share tokens are minted at a compatible level. A level mismatch returns `SUMSUB_TOKEN_LEVEL_MISMATCH` on ingest.
Until this Sumsub-side configuration is complete, token imports will fail regardless of how the API request is made.
***
## How it works
You mint a fresh share token in your Sumsub immediately before the call, then hand it to Reap by [advancing the user's application](/api-reference/users/advance-the-user-application) with the `SUMSUB_TOKEN_SHARING` method and the share token. A successful ingest returns `IN_REVIEW`; wait for the `USER_APPLICATION_STATUS_UPDATED` webhook for the final decision.
```mermaid theme={null}
sequenceDiagram
participant BE as Your backend
participant Sumsub as Your Sumsub account
participant Reap as Reap API
BE->>Reap: Create user
Reap-->>BE: userId
BE->>Sumsub: Mint share token for this applicant
Sumsub-->>BE: shareToken
BE->>Reap: Import share token (userId + shareToken)
Reap-->>BE: IN_REVIEW
Reap-->>BE: USER_APPLICATION_STATUS_UPDATED (APPROVED or REJECTED)
```
A failed import (for example, an expired token) leaves the user at `NOT_STARTED` or `RETRY_REQUIRED` and returns an error you can act on. You can retry by minting a fresh token and calling again.
An applicant that is approved in your Sumsub but missing identity fields returns `SUMSUB_APPLICANT_INCOMPLETE`. See [Constraints](#constraints) for what is required. Complete those fields on the applicant. Then mint a new share token and import again.
***
## How statuses update
The contract matches [Managed KYC](/compliance/kyc/managed-kyc):
* A successful submission transitions the user to `IN_REVIEW`.
* Reap emits `USER_APPLICATION_STATUS_UPDATED` when the review completes (`APPROVED`, `REJECTED`, or `RETRY_REQUIRED`).
* Your existing webhook handler picks it up. No new event type and no new payload to integrate.
See [Application statuses](/compliance/kyc/overview#application-statuses) for the full status model.
***
## Constraints
A few things are worth knowing up front because they affect how you build around the feature.
* **Share tokens are valid for 20 minutes.** Mint the token immediately before the import call.
* **Rate limited.** Reap accepts up to 3 imports per second per business. If you create users faster than that, you'll get rate limit errors.
* **Each import targets one Reap user.** You pass the Reap `userId` with the share token, and Reap attaches the imported verification to that user. Mint the share token for the Sumsub applicant that corresponds to that user.
* **Token Sharing assumes the user is already approved in your Sumsub.** Only share tokens for users who have completed KYC successfully on your side. Sharing tokens for in-review or rejected users is not supported.
* **The applicant must include the identity fields Reap needs to issue a card.** Required fields are name and date of birth, plus identity document type/number and a residential address (`line1` and country). Missing any of those returns `SUMSUB_APPLICANT_INCOMPLETE`.
The Reusable KYC Agreement, Sumsub partner setup (including adding Reap as a recipient and aligning applicant levels), and Reap program enablement are hard prerequisites. Token imports will fail until all are in place.
***
## Get started
Sumsub Token Sharing is enabled per program. Talk to your Reap account manager to:
Complete the Sumsub-side configuration: sign the Reusable KYC Agreement, add Reap as a sharing recipient, and import or align the applicant levels for your program.
Reap enables the method on your program once Sumsub configuration is confirmed.
Once enabled, the integration follows the flow above.
# Universal KYC
Source: https://docs.reap.global/compliance/kyc/universal-kyc
Submit identity verifications you have completed with your own KYC provider, along with the supporting documents, for Reap to review.
Universal KYC lets you bring verifications from your own KYC provider. You submit the full verified data pack and the supporting documents, and Reap reviews them and returns a decision. Use this when you run your own KYC and want Reap to review those results rather than run the verification itself.
This is one of Reap's verification methods and applies if your project is configured for it. See the [KYC overview](/compliance/kyc/overview) for the shared status model and how methods are assigned.
Unlike [Sumsub Token Sharing](/compliance/kyc/sumsub-token-sharing), which imports an existing verification without re-review, Universal KYC is reviewed by Reap. The user passes through `AWAITING_DOCUMENTS` and `IN_REVIEW` before a decision.
***
## How it works
Universal KYC has two steps: submit the data pack, then upload the supporting documents. Reap reviews the complete submission and delivers the decision to your webhook endpoint.
1. Submit the verified data pack by advancing the user's application. Reap responds with `AWAITING_DOCUMENTS` and the list of documents to upload.
2. Upload each document. When the last required document is received, the application moves to `IN_REVIEW`.
3. Reap reviews the submission and sends the decision by webhook.
```mermaid theme={null}
sequenceDiagram
participant BE as Your backend
participant Reap as Reap API
BE->>Reap: Create user
Reap-->>BE: userId
BE->>Reap: Advance application (data pack)
Reap-->>BE: status AWAITING_DOCUMENTS + documents to upload
BE->>Reap: Upload documents
Reap-->>BE: status IN_REVIEW
Reap-->>BE: USER_APPLICATION_STATUS_UPDATED (APPROVED / REJECTED / RETRY_REQUIRED)
```
***
## Step 1: Submit the data pack
[Advance the user's application](/api-reference/users/advance-the-user-application) with the `UNIVERSAL_KYC` method and the verified data you hold for the user:
* **Identity**: legal name, date of birth, and nationality.
* **Document metadata**: the type, number, and issuing country of the identity document and the proof of address.
* **Address**: the user's residential address.
* **Liveness**: the result of your liveness check.
* **Provider result**: the name of your KYC provider and the outcome it returned.
See the [advance endpoint reference](/api-reference/users/advance-the-user-application) for the exact fields.
On success the user moves to `AWAITING_DOCUMENTS`, and the response `nextAction` lists the documents you need to upload.
You submit document metadata (type, number, country) in the data pack. The document files themselves are uploaded separately in Step 2.
***
## Step 2: Upload the documents
The advance response's `nextAction.documents` lists the documents to provide, each with a `type` and its current upload state. [Upload the documents](/api-reference/users/upload-application-documents) as multipart form data, one document type per request:
* `documentType`: which document this request is for, one of the pending types in `nextAction.documents` (`IDENTITY`, `PROOF_OF_ADDRESS`, or `CURP`).
* `files`: the file or files for that document. Send more than one to cover multiple pages, or the front and back of a card.
Send one request per document type, in any order. Each response returns the updated status and the documents still outstanding in `nextAction`. When the last required document is received, the application moves to `IN_REVIEW` and Reap begins its review.
### File requirements
* Accepted formats: JPEG, PNG, GIF, PDF, Word (DOC, DOCX), Excel (XLS, XLSX), and CSV.
* Maximum size: 10 MB per file.
* Up to 15 files per request, and 30 MB total per request.
***
## Mexico card programs
Card programs that issue in Mexico require extra jurisdiction data in the data pack. Include the `jurisdictionDetails` object when you advance the application, with `jurisdiction` set to `MX` and the Mexico-specific fields (country of birth, economic activity, the issuer of the government identity document, and a beneficial-ownership attestation).
For Mexican nationals, the contact and identifier fields (phone, email, tax ID, and CURP) are required, and a `CURP` document must be uploaded alongside the identity and proof-of-address documents. For applicants who are not Mexican nationals, those fields are optional and no CURP document is needed.
See the [advance endpoint reference](/api-reference/users/advance-the-user-application) for the exact `jurisdictionDetails` fields.
`jurisdictionDetails` applies only to Mexico card programs. Submitting it on a program that does not issue in Mexico is rejected, and omitting it on a Mexico program is rejected.
***
## How statuses update
Universal KYC uses two statuses the other methods do not:
* `AWAITING_DOCUMENTS`: the data pack was accepted and Reap is waiting for the supporting documents.
* `IN_REVIEW`: all required documents are in and Reap is reviewing.
From there the outcome is the same as the other methods: `APPROVED`, `REJECTED`, or `RETRY_REQUIRED`. Reap emits a `USER_APPLICATION_STATUS_UPDATED` event on each transition. See [Application statuses](/compliance/kyc/overview#application-statuses) for the full model.
***
## Handling the decision
When review completes, Reap sends a `USER_APPLICATION_STATUS_UPDATED` event (see the [event reference](/api-reference/user-application-status-updated)). Handle `application.status` as described in the [status model](/compliance/kyc/overview#application-statuses). Surface `application.rejectionReason` to the user on both `REJECTED` and `RETRY_REQUIRED`.
For Universal KYC, `RETRY_REQUIRED` means the user can try again: advance the application again with a corrected data pack, then re-upload any documents the review flagged. Uploads add to the existing set, so you only need to resend what changed.
***
## Constraints
* **The data pack comes before the documents.** Advance the application first. Uploading documents before the pack is submitted is rejected.
* **Required documents depend on the user.** Every user needs an identity document and a proof of address. Mexican nationals on a Mexico card program also need a CURP document.
* **Uploading a document type that is not required is rejected.** Upload only the types listed in `nextAction`.
***
## Testing in sandbox
Uploading supporting documents in sandbox triggers the same review path as production. Placeholder or blank files are screened and can move the application to `RETRY_REQUIRED` or `REJECTED` on their own.
For most sandbox testing, skip document upload and drive the outcome with [Simulate user application status](/api-reference/simulation/simulate-user-application-status).
### Recommended sandbox path
1. [Create a user](/api-reference/users/create-user)
2. [Advance the application](/api-reference/users/advance-the-user-application) with the `UNIVERSAL_KYC` data pack
3. Skip document upload
4. [Simulate user application status](/api-reference/simulation/simulate-user-application-status) with desired status
5. Confirm the webhook, then continue with account or card creation
Use the full two-step upload flow only when you specifically want to exercise document upload and real review behavior.
***
## Get started
Universal KYC is enabled per program. Talk to your Reap account manager to enable it for your project. Once enabled, follow the two-step flow above.
# Introduction
Source: https://docs.reap.global/index
Modular fintech infrastructure. Cards, wallets, compliance, and payments via a single API.
Reap API is a modular, API-first fintech platform. It lets you launch card programs, onboard users with built-in compliance, accept stablecoin deposits across multiple chains, and track everything in a unified ledger.
You don't need every module on day one. Most partners start with cards and expand from there.
Make your first API call.
Endpoint reference.
Real-time event notifications.
## What you can build
* Issue and manage virtual or physical cards
* Onboard users with built-in KYC/KYB
* Accept stablecoin deposits on Base and Solana, with more chains coming
* Track transactions in a double-entry ledger
* Get real-time webhook notifications for all activity
# Authorization Mode
Source: https://docs.reap.global/program-configuration/authorization-mode
Who approves a card transaction - Reap, against balances you keep in sync, or your own systems in real time.
In a Program-Funded project you choose who makes the authorization decision on every card transaction: Reap, against the balances you keep in sync (**Managed**), or your own systems, in real time (**External**).
Authorization mode applies only to [Program-Funded](/program-configuration/funding-models#program-funded) projects. User-Funded projects are always authorized by Reap against the user's deposited balance. Like your funding model, authorization mode is set at project setup and cannot be changed after launch.
## The decision point
Whenever a cardholder taps, swipes, or checks out online, the card network asks Reap to approve or decline the transaction within a tight time budget. Authorization mode decides who answers.
```mermaid theme={null}
flowchart TD
S([Card transaction]) --> R[Reap]
R -->|Managed| B[Reap checks the balance you keep in sync]
R -->|External| Y[Reap asks your endpoint to decide]
B --> D{Approve or decline}
Y --> D
D --> N[Card network]
```
## Managed
Managed is the default, and most programs should use it. Reap evaluates and answers every authorization inside the card network's window, so you never build or operate a real-time authorization service or card-processing stack.
You mirror each user's spending power into Reap as [Virtual Assets](/virtual-assets/overview); Reap then authorizes each transaction against that balance, enforces your [spend policies](/spend-policies/overview) and platform limits, places and releases holds, and clears, reverses, and refunds against the same balance. Because the balance lives in Reap, the account exposes a readable spendable figure you can surface to the cardholder directly.
Managed fits when:
* You want Reap to own the authorization decision and the balance it draws against.
* Mirroring per-user balances into Reap (via Virtual Asset postings) fits how you operate.
## External
In External mode you make the decision. On every authorization Reap calls your endpoint with the transaction details and relays your approve or decline back to the card network. Your systems stay the source of truth for balance and risk, and you do not allocate Virtual Assets.
External fits when:
* You already run your own ledger, risk, or balance engine and need it to be authoritative.
* You cannot or do not want to mirror per-user balances into Reap.
* You need to act at authorization time in a way only your systems can, such as moving user funds or drawing on a credit line.
* You already integrate this way with another card issuer and want a symmetric integration.
External gives you more control, and more responsibility. It is not an upgrade over Managed. In return for owning the decision you take on:
* **An always-on endpoint on the authorization hot path.** Reap calls you synchronously on every authorization and you must return a decision within **1.6 seconds**. Time out, return a non-`2xx`, or be unreachable and the transaction is declined ([fail-closed](/transactions/external-authorization/authorization-request#responding-in-time)) - there is no retry on the live transaction.
* **Your own ledger.** Because you make the final approve or decline, you run the system of record for user balances and risk on your side and decide against it in real time. Reap holds no per-user spendable balance in External mode, so the authoritative ledger is yours to operate and keep correct.
* **Reconciliation from the event stream.** You keep that ledger in sync from the asynchronous [transaction webhooks](/transactions/lifecycle) - authorizations, clearings, reversals, and refunds - rather than reading balances back from Reap.
See [External Authorization](/transactions/external-authorization/overview) for the request and response contract, registering your endpoint, fail-closed behavior, and sandbox testing.
## Choosing between them
| | Managed | External |
| - | - | - |
| Who approves a transaction | Reap | You, in real time |
| Source of truth for balance | Reap, from the Virtual Assets you allocate | You |
| You run an authorization endpoint | No | Yes, always on, answering within 1.6s |
| If your systems are unavailable | Reap keeps authorizing against the last balance you set | Transactions are declined |
The distinction in one line: Reap is always the source of truth for **spend** - what actually cleared the network - while the source of truth for **balance** - what a user is allowed to spend - is Reap in Managed mode and you in External mode.
## What stays the same in both modes
Authorization mode changes only who approves a transaction. In both modes Reap clears, reverses, refunds, and settles with the card network, applies your [spend policies](/spend-policies/overview) and platform limits, and emits the same [transaction webhooks](/transactions/lifecycle). Your funding model and program mode are unaffected.
# Funding Model
Source: https://docs.reap.global/program-configuration/funding-models
How user funds are held, and how card spending is backed in your project.
Every Reap project is configured with a **funding model** at onboarding. The funding model determines where user funds sit, how card spending is backed, and how settlement works.
Your funding model is set by Reap during project setup and cannot be changed after launch. If you are unsure which model your project uses, contact your Reap account manager.
***
## The two models
Your funding model answers two questions: where user funds are held, and how card spending is backed.
| Model | Where user funds sit | Master collateral account | Card spend backed by |
| - | - | - | - |
| **User-Funded** | Reap-provisioned per-user wallets | Not used | The user's own wallet balance |
| **Program-Funded** | Your custody, off Reap | Required | A single master account you keep topped up |
A separate, independent choice, [authorization mode](/program-configuration/authorization-mode), decides who approves each card transaction. It applies only to Program-Funded projects.
***
## User-Funded
User-Funded is the default funding model. Each user deposits stablecoins into a Reap-provisioned wallet dedicated to them. Their balance stays self-custodied and isolated to that user, and card spending draws from that wallet.
```mermaid theme={null}
flowchart LR
U([User]) -->|Deposits stablecoins| W[User wallet on Reap]
W -->|Backs card spend| C[Card program]
C -->|Settlement| N[Card network]
```
### How funding works
1. You create an account for a verified user. Reap provisions deposit addresses for every supported chain.
2. The user sends supported stablecoins to those addresses. Once the deposit is approved, the balance becomes spendable.
3. The user taps their card. Reap authorizes against the account's available balance.
4. On a schedule, Reap settles cleared card spend by drawing stablecoins from the user's wallet.
### When User-Funded fits
* You want Reap to hold user funds end to end, so your users self-custody on the Reap platform.
* You are issuing retail cards where each user funds their own spending directly.
* You do not need to represent externally held assets or issue program credits.
***
## Program-Funded
Program-Funded is designed for programs where you already hold user funds: custodians, exchanges, crypto wallets, fund managers, brokerage platforms, and corporate treasuries.
Instead of users depositing into Reap, you keep a single **master collateral account** with us, topped up periodically to back card spending across your whole program. No per-user funds move to Reap; how each user's spending power is set and authorized depends on your [authorization mode](/program-configuration/authorization-mode).
This page describes the default authorization mode, **Managed**, where you allocate Virtual Assets and Reap authorizes spend against them. Program-Funded projects can instead use **External** authorization, where you approve each transaction in real time and do not allocate Virtual Assets. See [Authorization mode](/program-configuration/authorization-mode).
```mermaid theme={null}
flowchart LR
C([You]) -->|User funds stay with you| UH[Your custody / balance sheet]
C -->|Periodic top-up| M[Master collateral account on Reap]
C -.->|Set spending power per user| A[User accounts on Reap]
M -->|Backs card spend| Card[Card program]
Card -->|Settlement| N[Card network]
```
### How funding works
1. **You fund the master collateral account.** You send stablecoins to the master account's deposit addresses. It is the only account in your project that holds real crypto, and it collateralizes every card transaction across the program. See [Funding via Deposits](/accounts/deposits#master-collateral-funding). Projects enabled for it can top up by wire instead, described in [Funding via Bank Transfer](/accounts/bank-transfers).
2. **You set each user's spending power.** How depends on your [authorization mode](/program-configuration/authorization-mode). In the default **Managed** mode you mirror each user's balance into Reap as [Virtual Assets](/virtual-assets/overview) and Reap authorizes against it; in **External** mode you authorize each transaction in real time from your own systems. Either way, no per-user funds move to Reap.
3. **You keep the program collateralized.** Authorizations require the master account to still cover total outstanding spend across the program, so keep it topped up. Cleared spend is reconciled against it - in Managed mode via [`SETTLEMENT` postings](/virtual-assets/postings#settling-card-debt) on whatever cadence fits your operations.
### What Reap handles for you
Program-Funded preserves your custody model while handing off every part of running a card program. You get:
* Real-time authorization for every card swipe.
* Clearing, reversals, refunds, and fee accounting.
* Fraud monitoring, KYC, and transaction screening.
* Multi-chain stablecoin custody for the master account.
* Settlement with the card network.
* A unified ledger across fiat and crypto movement.
You decide who can spend what. You hold user funds. You settle on your own cadence. Reap does the rest.
### When Program-Funded fits
* You already custody user funds and want to keep them productive on your side rather than float them with Reap.
* Your users self-custody on your platform (wallet app, exchange account, custodian) and you want to issue cards against those balances without moving funds to Reap.
* You want to offer cards backed by assets you hold externally: stablecoins, non-stable crypto, fiat-backed units, or your own points.
* You want to issue program credits (cashback, sign-up bonuses, credit lines, salary allowances) without building card-processing infrastructure.
* You are a corporate treasury or trust structure pre-funding spending for a defined group of users.
***
## Funding model and program mode
Funding model is independent from [program mode](/program-configuration/modes). Both consumer and corporate programs can use either User-Funded or Program-Funded.
* **Program mode** determines who gets verified and who owns accounts.
* **Funding model** determines how those accounts are funded.
A typical consumer retail program uses **Consumer + User-Funded**. A custodian issuing cards against balances held on their platform uses **Consumer + Program-Funded**. A corporate expense program with company-held balances uses **Corporate + User-Funded** or **Corporate + Program-Funded** depending on whether the company custodies the funds or deposits them with Reap.
# Program Mode
Source: https://docs.reap.global/program-configuration/modes
How your program mode shapes the compliance flow, account structure, and card issuance model.
Every Reap program runs in one of two modes: **Consumer** or **Corporate**. Your mode is set by Reap when your program is configured, before you write a single line of integration code. It cannot be changed after launch.
Your mode determines who gets verified, who can own accounts, and how cards are issued. These are foundational constraints, not configuration options. Read this page before starting your integration.
Program mode is independent from your [funding model](/program-configuration/funding-models), which controls where user funds are held and how card spending is backed.
If you are unsure which mode your program uses, contact your Reap account manager.
***
## Consumer mode
Consumer mode is for programs that issue cards to individual users.
* Each user must complete KYC before an account or card can be created.
* Once approved, a user gets their own account with their own balance.
* Cards are issued directly to the user and spend against that user's account.
* Accounts are always user-owned (`ownerType: USER`).
***
## Corporate mode
Corporate mode is for programs that issue cards through a company or trust structure. KYB verification of the company is always required before users can be onboarded.
When you [create an account](/api-reference/accounts/create-account), you choose the owner type: `USER` or `COMPANY`. A corporate project can mix both.
### Company-owned accounts
Used for expense management and business card programs where the company controls and funds the balance.
* Create a company and initiate KYB.
* Once the company is approved, create a company account (`ownerType: COMPANY`). This is the shared account all cards spend from.
* Create employees and put each through KYC.
* Cards can be issued once an employee is approved.
### User-owned accounts
Used for trust and structured programs where a company or trust entity is required for compliance, but each individual funds and holds their own balance.
* Create a company (or trust) and initiate KYB.
* Create users associated with the company and put each through KYC.
* Once a user is approved, create a user account for them (`ownerType: USER`, the default) and issue cards. Spending draws from that user's own account.
***
## How the modes compare
| | Consumer | Corporate (company-owned account) | Corporate (user-owned account) |
| - | - | - | - |
| Compliance | KYC per user | KYB for company, KYC per employee | KYB for company or trust, KYC per user |
| Account owner | User | Company | User |
| Spending balance | Per user | Shared across all cards in the company | Per user |
| Cards issued to | User | Employee | User |
| Entities you create | Users | Companies + users | Companies + users |
***
## Key principles
* **Your program mode is fixed.** Reap sets it during onboarding. It cannot be changed after the program goes live.
* **Account ownership is chosen per account.** Pass `ownerType` when you create an account. `COMPANY` is only available in corporate programs.
* **KYC is always required for cardholders.** In all modes, every person who holds a card must have an approved KYC application.
* **Users in corporate mode belong to a company.** When you create a user in corporate mode, you associate them with a company. You can list and filter users by company.
# Overview
Source: https://docs.reap.global/program-configuration/overview
The choices Reap locks in when your project is set up.
Before you write any integration code, Reap configures two foundational choices for your project. Both are immutable after launch because they shape wallets, compliance flows, and settlement.
Consumer or Corporate. Determines who gets verified and who owns accounts.
User-Funded or Program-Funded. Determines where user funds are held and how card spending is backed.
## How they combine
Program mode and funding model are fully independent. Any combination is supported.
| Program mode | Funding model | Typical use |
| - | - | - |
| Consumer | User-Funded | Retail card where each user self-custodies their balance on Reap's wallet infrastructure |
| Consumer | Program-Funded | Consumer card where you already hold user assets on your own platform |
| Corporate | User-Funded | Business or trust program funding balances through Reap-provisioned wallets |
| Corporate | Program-Funded | Business or trust program where you hold the operating funds yourselves |
Account ownership (`USER` or `COMPANY`) is chosen when you [create an account](/api-reference/accounts/create-account), not as a project-level setting. Company-owned accounts are only available in corporate programs - see [Program Mode](/program-configuration/modes).
## What each configuration enables
A handful of entities and flows are gated by your funding model and (for Program-Funded projects) your [authorization mode](/program-configuration/authorization-mode). Everything else is identical across configurations.
| Capability | User-Funded | Program-Funded · Managed | Program-Funded · External |
| - | - | - | - |
| Per-account deposit wallets & [crypto deposits](/accounts/deposits) | Yes | No | No |
| [Virtual Assets](/virtual-assets/overview) (definitions, allocations, postings) | No | Yes | No |
| [Master collateral account](/program-configuration/funding-models#program-funded) | Not used | You fund it | You fund it |
| [External authorization endpoint](/transactions/external-authorization/overview) | No | No | Required |
| Who authorizes a transaction | Reap | Reap | You, in real time |
| Settling cleared card spend | Reap sweeps user wallets | You post `SETTLEMENT`s | You keep the master account topped up |
Everything else is common to all three: card issuance and the full [transaction lifecycle](/transactions/lifecycle), [spend policies](/spend-policies/overview), compliance (KYC/KYB), 3D Secure, digital-wallet provisioning, and [webhooks](/webhooks/overview).
## Changing configuration after launch
Program mode and funding model are fixed at project creation. To run a different configuration, create a new project. Most other settings (cards, users, limits, webhooks) are configurable after launch.
# Quickstart
Source: https://docs.reap.global/quickstart
Make your first Reap API call.
Contact the Reap team to get your sandbox credentials. You'll receive an API key and base URL for your environment.
All requests require:
* `Authorization` header with your API key as a Bearer token
* `Reap-Version` header specifying the API version
```bash theme={null}
curl https://sandbox.api.reap.global/users \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Reap-Version: 2025-02-14"
```
```bash theme={null}
curl -X POST https://sandbox.api.reap.global/users \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Reap-Version: 2025-02-14" \
-H "Content-Type: application/json" \
-d '{
"externalId": "cardholder_8f21a",
"email": "alice@example.com",
"phoneNumber": "+14155552671",
"firstName": "Alice",
"lastName": "Smith"
}'
```
`externalId` is optional and holds your own identifier for the user. It must be unique
among your active users, so a retry that reuses one is rejected rather than creating a
duplicate. Pass it to [List users](/api-reference/users/list-users) as `?externalId=` to
resolve your identifier back to a Reap user if you ever lose the mapping. Deleting a user
releases the value for reuse.
Browse the [API Reference](/api-reference/overview) or set up [Webhooks](/webhooks/overview).
# Control Types
Source: https://docs.reap.global/spend-policies/control-types
Every policy type and the configuration it accepts.
There are five policy types, in two families: **restrictions** that block transactions by what or where, and **limits** that cap by how much or how often. Every policy carries a `type`, a [`scope`](/spend-policies/scopes), a `name`, and a `config` whose shape depends on the type.
All amounts are in your program's billing currency (for example USD or HKD).
## Restrictions
Restrictions block a transaction based on the single transaction in front of them. They hold no running state.
### Merchant restriction
`type: MERCHANT_RESTRICTION`. Blocks transactions whose merchant matches the criteria. The match works on one dimension at a time:
| Dimension | Matches on | Value |
| - | - | - |
| `MCC` | Merchant Category Code | A list of 4-digit codes |
| `MERCHANT_COUNTRY` | The merchant's country | A list of ISO 3166-1 alpha-2 codes |
Block a set of merchant categories (here, gambling and dating):
```json theme={null}
{
"type": "MERCHANT_RESTRICTION",
"scope": { "type": "CARD", "id": "" },
"name": "Block gambling and dating",
"config": {
"match": { "dimension": "MCC", "codes": ["7995", "7273"] }
}
}
```
Block transactions in specific countries:
```json theme={null}
{
"config": {
"match": { "dimension": "MERCHANT_COUNTRY", "countries": ["RU", "KP"] }
}
}
```
The merchant MCC and country are visible on every transaction under `merchant`, so you can confirm what a given transaction would match against.
For a full list of Merchant Category Codes, see page 24 of the [Visa Merchant Data Standards Manual](https://usa.visa.com/content/dam/VCOM/download/merchants/visa-merchant-data-standards-manual.pdf).
### Channel restriction
`type: CHANNEL_RESTRICTION`. Blocks transactions on the listed channels. A transaction is blocked if its channel is in the list.
| Channel | Description |
| - | - |
| `POS` | In-store, card present |
| `ECOMMERCE` | Online / card-not-present |
| `ATM` | Cash withdrawal |
Disable ATM withdrawals on a card:
```json theme={null}
{
"type": "CHANNEL_RESTRICTION",
"scope": { "type": "CARD", "id": "" },
"name": "No ATM",
"config": { "channels": ["ATM"] }
}
```
## Limits
Limits cap value. The per-transaction cap looks only at the current transaction; the spend and count limits track usage over a calendar window.
### Transaction amount limit
`type: TRANSACTION_AMOUNT_LIMIT`. Rejects any single transaction above `maxAmount`. Has no window.
```json theme={null}
{
"type": "TRANSACTION_AMOUNT_LIMIT",
"scope": { "type": "CARD", "id": "" },
"name": "Max $1,000 per transaction",
"config": { "maxAmount": 1000 }
}
```
### Spend limit
`type: SPEND_LIMIT`. Caps cumulative spend over a calendar window at `maxAmount`. A transaction is rejected if it would push spend in the current window past the cap.
```json theme={null}
{
"type": "SPEND_LIMIT",
"scope": { "type": "CARD", "id": "" },
"name": "Daily card limit",
"config": {
"window": { "type": "CALENDAR", "period": "DAILY" },
"maxAmount": 500
}
}
```
### Transaction count limit
`type: AUTHORIZATION_COUNT_LIMIT`. Caps the number of transactions over a calendar window at `maxCount`.
```json theme={null}
{
"type": "AUTHORIZATION_COUNT_LIMIT",
"scope": { "type": "CARD", "id": "" },
"name": "Max 10 transactions a day",
"config": {
"window": { "type": "CALENDAR", "period": "DAILY" },
"maxCount": 10
}
}
```
### Windows
Spend and count limits track usage over a calendar window. The window is fixed when the policy is created; to change the period, create a new policy.
| Period | Resets |
| - | - |
| `DAILY` | Start of each day |
| `WEEKLY` | Start of each ISO week (Monday) |
| `MONTHLY` | Start of each month |
| `YEARLY` | Start of each year |
| `LIFETIME` | Never |
Windows are calendar-based in UTC. A daily limit resets at 00:00 UTC, not on a rolling 24-hour basis. When a window rolls over, usage starts fresh automatically.
Spend and count limits accept an optional `filter` so the limit only counts transactions that match it. This lets you cap one slice of spend without affecting the rest. The filter narrows by channel, by merchant, or both (a transaction must match every dimension present).
Cap ATM cash to \$300 a day without limiting other spend:
```json theme={null}
{
"type": "SPEND_LIMIT",
"scope": { "type": "CARD", "id": "" },
"name": "Daily ATM cash cap",
"config": {
"window": { "type": "CALENDAR", "period": "DAILY" },
"maxAmount": 300,
"filter": { "channels": ["ATM"] }
}
}
```
A filtered limit tracks its own usage, independent of any unfiltered limit on the same card and window. The `merchant` filter takes the same shape as a [merchant restriction](#merchant-restriction)'s `match`.
### How usage is counted
Spend and count limits track usage as transactions are authorized. Usage mostly only goes up, but a few later events adjust it.
* **Approved transactions** add to usage: a spend limit by the authorized amount, a count limit by one.
* **Declined transactions** never count.
* **Reversals release usage.** When the network reverses an authorization before it settles, a full reversal frees both the amount and the transaction from the window. A partial reversal frees only the reversed amount, and the transaction still counts toward a count limit because it did occur.
* **Expired or voided authorizations** that never settle are released the same way as a full reversal.
* **Refunds do not restore usage.** A refund is a separate credit that can arrive days or weeks later, so it does not add headroom back to the window. Spending `$1,000` and later receiving a `$1,000` refund still leaves only the original headroom for that window.
Usage reflects the amount authorized. If a transaction later settles for more than was authorized (a restaurant tip or a fuel pump hold, for example), the limit was already evaluated at authorization and the difference is not re-checked.
## Common use cases
Each policy type maps to real controls you can ship. The examples below are grouped by who the control serves: the cardholder configuring their own card, and the program enforcing rules across cardholders. Every payload is a complete `POST /policies` body.
### Cardholder-driven controls
Controls a cardholder sets on their own card, usually exposed through your app. These attach at the `CARD` scope.
A cardholder caps their own card at `$200` a day. Editing the amount later with `PUT /policies/{id}/config` keeps the day's recorded spend.
```json theme={null}
{
"type": "SPEND_LIMIT",
"scope": { "type": "CARD", "id": "" },
"name": "My daily limit",
"config": {
"window": { "type": "CALENDAR", "period": "DAILY" },
"maxAmount": 200
}
}
```
A cardholder turns off card-not-present spend while keeping in-store and ATM use. Re-enabling it later is a single `POST /policies/{id}/disable`.
```json theme={null}
{
"type": "CHANNEL_RESTRICTION",
"scope": { "type": "CARD", "id": "" },
"name": "Online payments off",
"config": { "channels": ["ECOMMERCE"] }
}
```
A cardholder blocks any transaction over `$300` on their card, leaving the daily total uncapped.
```json theme={null}
{
"type": "TRANSACTION_AMOUNT_LIMIT",
"scope": { "type": "CARD", "id": "" },
"name": "Max $300 per purchase",
"config": { "maxAmount": 300 }
}
```
### Program-driven controls
Rules you enforce across the program or per cardholder, independent of what the cardholder sets. Restrictions can attach at the `PROJECT` scope to cover every card; spend and count limits attach per cardholder (`USER`) or per card (`CARD`).
A program-wide restriction blocking gambling and dating merchants on every card. Attaching at the `PROJECT` scope means new cards inherit it automatically.
```json theme={null}
{
"type": "MERCHANT_RESTRICTION",
"scope": { "type": "PROJECT" },
"name": "Block gambling and dating",
"config": {
"match": { "dimension": "MCC", "codes": ["7995", "7273"] }
}
}
```
No card in the program can withdraw cash. To allow it on a specific card, leave this off and apply ATM rules per card instead.
```json theme={null}
{
"type": "CHANNEL_RESTRICTION",
"scope": { "type": "PROJECT" },
"name": "No ATM withdrawals",
"config": { "channels": ["ATM"] }
}
```
A `$5,000` monthly cap at the `USER` scope counts the combined spend of every card the cardholder holds, not each card on its own.
```json theme={null}
{
"type": "SPEND_LIMIT",
"scope": { "type": "USER", "id": "" },
"name": "Monthly cardholder cap",
"config": {
"window": { "type": "CALENDAR", "period": "MONTHLY" },
"maxAmount": 5000
}
}
```
A program-wide ceiling on how many transactions each card can make per day is a useful guardrail against runaway or automated charges, applied uniformly without configuring each card.
Count limits cannot attach at the `PROJECT` scope through the API, so this is not something you can set up yourself. Contact the Reap team and we will configure a program-wide transaction-count limit for you.
For capping one slice of spend on its own (such as ATM cash), see [restricting a limit to certain transactions](#windows).
## Next
* [Scopes](/spend-policies/scopes): where each of these attaches and how overlapping policies resolve.
# Overview
Source: https://docs.reap.global/spend-policies/overview
Declarative rules that control where, how much, and how often a card can be used.
A **policy** is a rule Reap enforces when authorizing a card transaction. You decide what is allowed, attach the rule at the right level of your program, and Reap applies it on every transaction in real time. You do not run your own authorization service, and you do not need to react to each transaction as it happens.
Reap makes the authorization decision. Policies are how you tell Reap what that decision should be.
## What you can control
Policies fall into two families.
Block transactions by **merchant** (category, specific merchant, or country) or by **channel** (in-store, online, ATM, push-to-card). Evaluated against the single transaction in front of it.
Cap a **single transaction** amount, total **spend over a period**, or the **number of transactions** over a period. Periods are calendar windows from daily through yearly, plus lifetime.
See [Control types](/spend-policies/control-types) for the full list and the configuration each one takes.
## How policies work
Every policy has three parts:
* A **type**, which decides what it checks (a merchant restriction, a spend limit, and so on).
* A **scope**, which decides what it applies to: your whole program, one cardholder, or one card. See [Scopes](/spend-policies/scopes).
* A **configuration**, the values specific to the type (the merchants to block, the amount cap, the window).
When a card is used, Reap gathers every active policy that applies to that card, from the card itself up through the cardholder and the program, and checks the transaction against all of them. If any one of them would block the transaction, it is declined. The strictest applicable rule always wins, so a permissive rule at one level can never loosen a stricter rule at another.
```mermaid theme={null}
flowchart TD
P[Program] --> U[Cardholder]
U --> C[Card]
C --> T[Transaction checked against
every policy at, above, or on the card]
```
A new card has no policies and is unrestricted, beyond the platform-enforced limits that apply to every program (see [ATM withdrawal limits](/transactions/atm-limits) and [Restricted MCCs](/transactions/restricted-mccs)). You add policies as you need them.
## When a policy declines a transaction
A blocked transaction is recorded as `DECLINED` with a `declineReason` code that identifies which kind of rule stopped it, and a `policy` reference that names the exact policy responsible. The reference is captured on the transaction at decline time, so it stays accurate even if you later edit or remove that policy.
```json theme={null}
{
"status": "DECLINED",
"declineReason": {
"code": "SPEND_LIMIT_EXCEEDED",
"message": "This transaction would exceed the spending limit on this card. Try a smaller amount or try again later."
},
"policy": {
"id": "9b7d3c2a-1f4e-4a2b-8c6d-0e1f2a3b4c5d",
"type": "SPEND_LIMIT",
"scopeType": "CARD",
"name": "Daily card limit"
}
}
```
See [Decline reasons](/transactions/decline-reasons#spending-policy-controls) for every code a policy can produce.
## Managing policies
| Operation | Endpoint |
| - | - |
| Create a policy | `POST /policies` |
| List your policies | `GET /policies` |
| See what's in force for a card, cardholder, or program | `GET /policies/effective` |
| Get a policy | `GET /policies/{id}` |
| Rename a policy | `PATCH /policies/{id}` |
| Replace a policy's configuration | `PUT /policies/{id}/config` |
| Stop enforcing a policy | `POST /policies/{id}/disable` |
| Resume enforcing a policy | `POST /policies/{id}/enable` |
### Editing a policy
Most of a policy can be changed in place. A few properties are fixed at creation, because changing them would change what the policy fundamentally is.
* Name, with `PATCH /policies/{id}`
* Status, via enable / disable
* Amounts, counts, merchants, and channels, with `PUT /policies/{id}/config`
* Type
* Scope
* A limit's calendar window
To change a fixed property, create a new policy and disable the old one. Replacing a limit's amount preserves recorded usage: raising a daily spend limit from `$500` to `$1,000` mid-day does not reset what has already been spent, it just raises the ceiling.
### Turning a policy off
To stop enforcing a policy, disable it with `POST /policies/{id}/disable`. A disabled policy is skipped during authorization but keeps its recorded usage, so re-enabling it with `POST /policies/{id}/enable` resumes from where it left off. Enabling an already-enabled policy, or disabling an already-disabled one, returns it unchanged.
## Seeing what applies and what remains
`GET /policies` lists the policies you created. To see the full set actually **in force** for one card, cardholder, or your program - including the policies inherited from a higher scope - use [List effective policies](/api-reference/policies/list-effective-policies). This is also how you read how much of each limit is left.
Pass the scope as query parameters: `scopeType` (`CARD`, `USER`, or `PROJECT`) plus, for `CARD` and `USER`, the `scopeId`. The response lists every active policy that applies, most specific first. Each spend or transaction count limit carries a `usage` object with the headroom remaining in the window and a `resetsAt` timestamp for when it rolls over; restrictions and per-transaction caps report `usage: null`. This is the endpoint behind a "how much can I still spend today?" view, and only active policies are returned. See [List effective policies](/api-reference/policies/list-effective-policies) for the full request and response shape.
## Next
Every policy type and the configuration it takes.
Where policies attach and how the effective set is resolved.
# Scopes
Source: https://docs.reap.global/spend-policies/scopes
Where a policy attaches, what it governs, and how overlapping policies resolve.
A policy's `scope` decides what it applies to. Scopes form a hierarchy that follows how your program is structured: the program contains cardholders, and each card belongs to a cardholder.
| Scope | `scope` shape | Governs |
| - | - | - |
| Program | `{ "type": "PROJECT" }` | Every card in your program |
| Cardholder | `{ "type": "USER", "id": "" }` | Every card held by that cardholder |
| Card | `{ "type": "CARD", "id": "" }` | That one card |
In the API, `PROJECT` is the identifier for the program scope and `USER` for the cardholder scope. The program scope carries no id.
## What a scope means
For a **restriction**, the scope is the set of cards it governs. A merchant restriction at the program scope blocks that merchant on every card; the same restriction at the card scope blocks it on one card.
For a **limit**, the scope is also the entity whose usage is counted:
* A spend limit on a **card** counts that card's spend.
* A spend limit on a **cardholder** counts the combined spend across all of that cardholder's cards. A `$1,000` monthly cardholder limit means \$1,000 total, no matter how many cards they hold.
This distinction matters when a cardholder has more than one card. Use the card scope to cap each card on its own; use the cardholder scope to cap the person.
### What attaches where
| | Program | Cardholder | Card |
| - | - | - | - |
| Merchant restriction | yes | yes | yes |
| Channel restriction | yes | yes | yes |
| Transaction amount limit | yes | yes | yes |
| Spend limit | no | yes | yes |
| Transaction count limit | no | yes | yes |
Running-total limits (spend and transaction count) cannot attach at the program scope, because a program-wide running total has no counted entity. To apply the same limit across many cards, attach it per card or per cardholder. If you need a program-wide rollout across a large number of cards, contact Reap.
## How overlapping policies resolve
When a card is used, Reap collects every active policy attached to that card, to its cardholder, and to the program, and checks the transaction against all of them. A transaction is declined if **any** applicable policy blocks it. The strictest rule wins, so a permissive policy at one level never overrides a stricter policy at another.
If more than one policy would block a transaction, the one reported in the `policy` field of the decline is the most specific: a card policy is reported ahead of a cardholder policy, which is reported ahead of a program policy. This only affects which policy is named as the reason; the transaction declines regardless.
Platform-enforced limits ([ATM withdrawal limits](/transactions/atm-limits) and [restricted MCCs](/transactions/restricted-mccs)) apply on top of your policies on every program and cannot be overridden.
## Seeing a card's or cardholder's policies
Fetching a single [card](/cards/overview) or cardholder returns the policies attached directly to it under a `policies` array. This is the directly-attached set only; it does not include policies inherited from a higher scope or the platform-enforced limits. To list all of your policies and filter by scope, use `GET /policies` with the `scopeType` and `scopeId` query parameters.
To see the **full set in force** for a card or cardholder - the directly-attached policies plus everything inherited from a higher scope, with the headroom remaining on each limit - use [`GET /policies/effective`](/spend-policies/overview#seeing-what-applies-and-what-remains).
# Card transaction fees
Source: https://docs.reap.global/statements/card-transaction-fees
One row per fee event on a card transaction.
`CARD_TRANSACTION_FEES` itemises what Reap billed you for that arose from card use: purchases, ATM withdrawals, refunds, wallet provisioning, and 3DS authentication. Request it as described on the [statements overview](/statements/overview).
## What a row is
A row is one fee event, not one transaction. A single transaction produces several rows when it is authorized, captured in parts, refunded, or later corrected. Join them on `transactionId`.
Some rows carry no transaction at all, because the fee is priced per occurrence: provisioning a card into Apple Pay or Google Pay, a 3DS authentication, and non-financial ATM actions such as a PIN change or a balance enquiry. Those rows leave the event columns empty, and `feeType` says what happened. Their transaction amount columns read zero, which is their real value rather than a placeholder, and the fee Reap charged for the occurrence is in its fee column.
A `TRANSACTION` row with no `eventId` is a zero-value authorization: a merchant checking that the card is valid, for example when it is saved for later payments. The network charges for the check, but no money moves, so it raises no card transaction event. The card and merchant columns identify it.
Every row has an `id` that stays the same in every statement that includes it. Use it to tell a row you have already imported from a new one when you request an overlapping period, since rows with no `eventId` have nothing else unique to match on.
### Adjustments
An `ADJUSTMENT` row corrects a fee assessed on an earlier transaction, and they are extremely rare. The original row is never rewritten. The adjustment records the difference instead, and carries the `transactionId` of the transaction it corrects. Its `occurredAt` is when the correction was issued, and its event columns are empty.
### Signs
Amounts follow the cardholder's money. Spend is positive, refunds and reversals are negative, so summing a column over a period gives you a net figure with no sign handling of your own.
Fees you collected and interchange you earned are signed from your point of view: positive, and negative where they were given back. The fees Reap charged you are positive as charged, and negative only where one was credited back to you. `ADJUSTMENT` rows are corrections issued against a fee assessed earlier, and they carry whichever sign undoes the original. Include them in every total.
### Currencies
Two denominations coexist in one file.
`currency` is your card program currency, and governs `amount`, `billAmount`, `originalAmount`, `fxFee`, `atmVariableFee`, and `atmFixedFee`.
Columns whose names end in `Usd` are US dollars on every program, whatever your program currency. `billAmountUsd` is `billAmount` converted at the rate for the day the transaction happened.
### Amounts
Three amount columns describe the same transaction from different angles:
* `billAmount` is the transaction before the fees you applied.
* `amount` is what the cardholder was charged, so it equals `billAmount` plus `fxFee`, `atmVariableFee`, and `atmFixedFee`. It matches what [the transactions API](/transactions/overview) returns.
* `billAmountUsd` is `billAmount` in US dollars.
The difference between `amount` and `billAmount` is exactly the fees you collected from your cardholder.
### Fees Reap charged you
The columns from `authorizationFeeUsd` to `atmNonFinancialFeeUsd` carry what Reap charged you on each row, in US dollars. Each one corresponds to a line on your invoice, so summing it over the invoice period gives that line's amount. A fee that your pricing does not charge separately reads zero.
### Interchange
Interchange is reported in full, as earned on the transaction. `grossInterchangeUsd` is before scheme deductions and `netInterchangeUsd` is after them. Neither is your share of it: the share is a percentage set out in your pricing schedule, which the file does not carry.
## Columns
Request a subset with `fields` and you get exactly these names, in the order you asked for.
| Column | Description |
| - | - |
| `id` | Identifier of this row. It stays the same in every statement that includes the row, so use it to tell a row you have already seen from a new one, including rows with no eventId. |
| `occurredAt` | When the network event occurred, UTC. Statement periods are bucketed on this column. On an ADJUSTMENT row it is when the correction was issued. |
| `eventType` | Lifecycle event the fee was assessed on. Empty on rows with no card transaction event, which feeType explains. |
| `eventId` | Card transaction event this fee was assessed on, as returned by the API. |
| `transactionId` | Card transaction the event belongs to. Several rows share one transaction: multiple captures, refunds and adjustments. |
| `cardId` | Card the fee is attributed to. Present on rows with no transaction event, which is what makes a per-occurrence fee traceable. Empty only if the card could not be resolved. |
| `cardLast4` | Last four digits of the card number. Empty when cardId is. |
| `accountId` | Account the card belongs to. Empty when cardId is. |
| `channel` | How the transaction was initiated. ATM invoice lines filter on this column. |
| `feeType` | Fee category the row was assessed under. Explains rows with no transaction event, and carries the per-occurrence lines such as ATM non-financial actions and 3DS authentication. ADJUSTMENT rows are corrections to a fee assessed earlier; include them in every total. |
| `locality` | Whether the merchant sits in your card program market. The international transaction fee and the ATM rates split on this column. |
| `hasFx` | Whether the transaction involved a currency conversion. The FX fee line sums the volume of these rows. |
| `currency` | Currency of amount, billAmount, fxFee, atmVariableFee and atmFixedFee: your card program currency, as an ISO 4217 alphabetic code. |
| `amount` | What the cardholder was charged, in the card currency. Equals billAmount plus fxFee, atmVariableFee and atmFixedFee, and matches the transaction amount the API returns. Zero on a row that carries a fee but no transaction. |
| `billAmount` | Transaction amount before the fees you applied, in the card currency. The difference between this and amount is exactly the fees you collected. |
| `billAmountUsd` | billAmount - not amount - converted to US dollars at the transaction-day rate. Every percentage line on your invoice is computed on this column, whatever your card program currency. |
| `originalAmount` | Amount in the currency the merchant charged. Empty on rows with no merchant. |
| `originalCurrency` | Currency the merchant charged in. |
| `fxFee` | Foreign exchange fee you collected from the cardholder, in the card currency. |
| `atmVariableFee` | Percentage-based ATM fee you collected from the cardholder, in the card currency. |
| `atmFixedFee` | Per-withdrawal ATM fee you collected from the cardholder, in the card currency. |
| `authorizationFeeUsd` | Authorization fee Reap charged you on this row, in US dollars. Where your pricing includes 3DS, fraud monitoring or tokenization in the authorization fee, it is included here and those columns read zero. |
| `settlementFixedFeeUsd` | Per-settlement fee Reap charged you on this row, in US dollars. |
| `settlementVariableFeeUsd` | Percentage-based settlement fee Reap charged you on this row, in US dollars, computed on billAmountUsd. Where your pricing has an international transaction fee, this is that fee, and it is charged on cross-border settlements only. |
| `threeDsFeeUsd` | 3DS authentication fee Reap charged you on this row, in US dollars. |
| `fraudMonitoringFeeUsd` | Fraud monitoring fee Reap charged you on this row, in US dollars. |
| `tokenFeeUsd` | Tokenization fee Reap charged you on this row, in US dollars: provisioning a card into a wallet, and authorizing with a wallet token. |
| `applePayFeeUsd` | Apple Pay fee Reap charged you on this row, in US dollars. |
| `atmNonFinancialFeeUsd` | Fee Reap charged you for an ATM non-financial action such as a PIN change or a balance enquiry, in US dollars. |
| `grossInterchangeUsd` | Interchange earned on this settlement before scheme deductions, in US dollars. This is the full figure, not your share of it. |
| `netInterchangeUsd` | Gross interchange less the scheme settlement fees attributable to it, in US dollars. This is the full figure; your revenue share is the percentage set out in your pricing schedule. |
| `merchantName` | Merchant as named in the network message. |
| `merchantCountry` | Two-letter ISO 3166 country code of the merchant. |
| `mcc` | Merchant category code of the merchant's business. |
| `settlementDate` | Day the transaction settled, UTC. Empty while it is still authorized. |
## Scale and ordering
Card-currency columns carry two decimal places. `Usd` columns carry seven, which is the scale Reap computes and bills at, so round once at the end rather than per row.
Rows are ordered by `occurredAt`, oldest first. See the [file format](/statements/overview#file-format) for the dialect every statement shares.
# Overview
Source: https://docs.reap.global/statements/overview
Request an itemised file of the fees Reap billed you over a period.
A statement is a file listing every fee event Reap billed you for over a period, one row each. It carries the transaction, the fee category, and the amounts behind each row.
Statements are produced as CSV files. You request one for a date range, Reap prepares it, and you download it when it is ready.
Statements are in beta. The columns a file carries can still change in the coming weeks.
## Requesting a statement
Call [Request statement](/api-reference/statements/request-statement) with a type and a period. The period is a range of calendar days in UTC, inclusive at both ends.
Three limits apply to the period:
* It covers at most 31 days, which is enough for one monthly billing cycle.
* It ends no later than yesterday. A day's card transaction data is final by 10:00 UTC the following day, so a period ending yesterday is accepted from 10:00 UTC. Before then, the latest day you can include is the day before yesterday.
* It starts no earlier than 1 June 2026.
Five statements can be preparing at once. Request a sixth while five are still pending and the call is rejected until one of them finishes.
By default the file contains every column of the type. Pass `fields` to narrow it to the columns you want, in the order you want them. The response echoes your request as a statement resource with an `id`.
## Waiting for the file
A statement starts as `PENDING` while Reap prepares it, then becomes `GENERATED` when the file is ready. Most statements are ready within a couple of minutes.
Subscribe to [`STATEMENT_STATUS_UPDATED`](/api-reference/statement-status-updated) rather than polling. The payload is the statement resource, so `status` tells you when to download. If you do poll, use [Get statement](/api-reference/statements/get-statement).
A statement can also read `FAILED`, which means no file will arrive and you should request the period again.
Once generated, the resource also reports `rowCount` and `byteSize`, plus `dataAsOf`: how fresh the underlying records were when the file was produced.
## Downloading
Call [Download statement](/api-reference/statements/download-statement). It answers `302` with a redirect to a short-lived URL that serves the file, so an HTTP client that follows redirects gets the file in one call. Fetch that URL without your API credentials, because it carries its own.
Send `Accept: application/json` instead and you get the URL and its expiry in the body, which suits a browser download or a signed link you pass to another system.
Download links last five minutes, but you can ask for a new one at any time. Files themselves stay available for 180 days from the day the statement was generated.
## File format
Every statement follows the same dialect, whatever its type.
Files are UTF-8 CSV with a header row, comma separated, `\n` line endings, and `"` quoting for values containing a comma, a quote, or a newline. Empty columns are written as an empty field rather than the word `null`.
Timestamps are ISO 8601 in UTC with millisecond precision (`2026-07-14T09:21:07.000Z`) and dates are `YYYY-MM-DD`. Amounts are plain decimal strings with no thousands separator and no currency symbol. Rows arrive oldest first.
Each type documents the columns it carries and the scale of its amounts.
## Available types
One row per fee event on a card transaction: purchases, ATM withdrawals,
refunds, wallet provisioning, and 3DS authentication.
## Column stability
Columns are tied to the API version the statement was requested under, reported as `version` on the resource. Within a version a column never changes name, type, or meaning, and columns are only ever added to the end of the file. Read columns by their header name rather than by position and an added column will not break your parser. See [versioning](/api-reference/versioning) for how versions work.
# Amounts
Source: https://docs.reap.global/transactions/amounts
Per-status amount fields, currency, and the conversion rate Reap applies.
The amount fields on a card transaction change shape based on `status`. A `PENDING` transaction tracks authorized vs reversed amounts because both can still move. A `CLEARED` transaction tracks cleared vs refunded amounts for the same reason. A `DECLINED` transaction is final and only records what was attempted.
This page describes the field shape per status and how the numbers evolve over the transaction's life. For the underlying scenarios (reversals, clearings, refunds) see [Lifecycle](/transactions/lifecycle). For the rates behind the `fees` on each transaction see [Cardholder fees](/transactions/fees).
## Currencies
Every transaction carries two currency fields.
| Field | Description |
| - | - |
| `currency` | The cardholder's billing currency, in which the cardholder is ultimately charged. Fixed per card program (for example `USD` or `HKD`). |
| `originalCurrency` | The currency the merchant charged in (ISO 4217). Equal to `currency` for same-currency transactions; otherwise the merchant's local currency (e.g. `EUR`, `JPY`, `HKD`). |
Same-currency transactions and cross-currency transactions use the same fields. When the two currencies are equal there is no FX involved and `conversionRate` is `1`.
## Amount shape by status
### PENDING
Pending transactions have not settled yet. The authorized amount can still grow (incremental authorization) or shrink (reversal), so both numbers are tracked alongside the current effective amount.
```json theme={null}
{
"status": "PENDING",
"currency": "USD",
"originalCurrency": "EUR",
"amount": {
"authorized": 110.00,
"reversed": 10.00,
"current": 100.00
},
"originalAmount": {
"authorized": 101.20,
"reversed": 9.20,
"current": 92.00
},
"fees": {
"atm": 0,
"fx": 0.50
},
"conversionRate": 1.086957
}
```
| Field | Meaning |
| - | - |
| `amount.authorized` | Sum of all authorization events on the transaction, in `currency`. |
| `amount.reversed` | Sum of all reversal events. |
| `amount.current` | Effective hold on the account: `max(0, authorized - reversed)`. This is the number to show as the pending amount. |
| `originalAmount.*` | Same three values expressed in `originalCurrency`. |
| `fees` | Net cardholder fees in `currency`: `{ "atm", "fx" }`, using the same auth − reversal legs as `amount.current` (clamped at 0). |
### CLEARED
Cleared transactions have settled. They can still receive refunds, and refunds adjust the amount fields without changing the status.
```json theme={null}
{
"status": "CLEARED",
"currency": "USD",
"originalCurrency": "USD",
"amount": {
"cleared": 100.00,
"refunded": 25.00,
"current": 75.00
},
"originalAmount": {
"cleared": 100.00,
"refunded": 25.00,
"current": 75.00
},
"fees": {
"atm": 0,
"fx": 0.40
},
"conversionRate": 1.0
}
```
| Field | Meaning |
| - | - |
| `amount.cleared` | Sum of all clearing events on the transaction. |
| `amount.refunded` | Sum of all `REFUND` and `DISPUTE_REFUND` events: merchant refunds and dispute credits. |
| `amount.current` | Effective net charge: `max(0, cleared - refunded)`. |
| `originalAmount.*` | Same three values expressed in `originalCurrency`. |
| `fees` | Net cardholder fees in `currency`: `{ "atm", "fx" }`, using the same clearing − refund legs as `amount.current` (clamped at 0). |
A full refund leaves `amount.current` at `0` while the status remains `CLEARED`. See [Refund on a cleared transaction](/transactions/lifecycle#refund-on-a-cleared-transaction-related-refund).
### VOID
Void transactions are either authorizations that were fully reversed before clearing, or zero-amount card validity checks. The shape mirrors `PENDING` but `current` is `0`.
```json theme={null}
{
"status": "VOID",
"currency": "USD",
"originalCurrency": "USD",
"amount": {
"authorized": 250.00,
"reversed": 250.00,
"current": 0.00
},
"originalAmount": {
"authorized": 250.00,
"reversed": 250.00,
"current": 0.00
},
"fees": {
"atm": 0,
"fx": 0
}
}
```
`VOID` transactions do not carry a `conversionRate`. `fees` is still present (usually zeros after a full reversal).
### DECLINED
Declined transactions were rejected at authorization. No funds were ever held on the cardholder `amount`, so the response records the attempted merchant amount, the reason, and any cardholder fees computed for that attempt.
```json theme={null}
{
"status": "DECLINED",
"currency": "USD",
"originalCurrency": "EUR",
"originalAmount": 50.00,
"fees": {
"atm": 0,
"fx": 0
},
"declineReason": {
"code": "INSUFFICIENT_BALANCE",
"message": "Insufficient balance for this transaction."
}
}
```
| Field | Meaning |
| - | - |
| `originalAmount` | Flat number. The amount the merchant attempted to charge, in `originalCurrency`. |
| `fees` | Cardholder fees from the decline event in `currency`: `{ "atm", "fx" }`. Always present; usually zeros. |
| `declineReason` | Why the transaction was declined. See [Decline reasons](/transactions/decline-reasons). |
There is no `amount` field on a declined transaction, because nothing was charged as a cardholder hold in `currency`. There is no `conversionRate` either, because no conversion took place. `fees` can still be non-zero when a cardholder fee was computed for the attempt.
## Which field to display
`amount.current` is the single field most product surfaces want to render. It always reflects the latest effective amount on the transaction, recomputed every time a new event arrives. The [lifecycle scenarios](/transactions/lifecycle#scenarios) describe what changes it under each event type.
If you need a step-by-step view, the `events` array carries the precise amount and timestamp of every authorization, clearing, reversal, and refund.
## Conversion rate
`conversionRate` is the rate Reap applied to convert `originalAmount` into `amount`, expressed as the multiplier such that `amount ≈ originalAmount * conversionRate`. It is rounded to 6 decimal places.
* `1.0` whenever `currency` and `originalCurrency` are the same.
* Otherwise, the effective `currency`-per-`originalCurrency` rate, as of authorization time.
`conversionRate` is present on `PENDING` and `CLEARED`, and absent on `VOID` and `DECLINED`.
## Per-event amounts
The `events` array on every transaction (except `DECLINED`, which has a single decline event) uses the same currency model as the parent transaction. Each event carries:
| Field | Description |
| - | - |
| `type` | One of `AUTHORIZATION`, `CLEARING`, `REVERSAL`, `REFUND`, `DISPUTE_REFUND`, `DECLINE`. `DISPUTE_REFUND` is the credit from a [dispute](/transactions/lifecycle#dispute-refund) resolved in the cardholder's favor. |
| `amount` | Event amount in `currency`. |
| `originalAmount` | Event amount in the event's `originalCurrency`. |
| `originalCurrency` | The currency the event was reported in. |
| `fees` | Cardholder fees charged on this event in `currency`: `{ "atm", "fx" }`. Always present; zeros when no fee applies. |
| `occurredAt` | When the event happened on the card network. |
The transaction-level amount breakdowns and net `fees` above are aggregates over `events`. If you need an audit trail of exactly what was authorized, reversed, cleared, or refunded and when, read the `events` array directly.
# ATM Withdrawal Limits
Source: https://docs.reap.global/transactions/atm-limits
Platform-enforced limits and geographic restrictions on ATM cash withdrawals.
ATM cash withdrawals are subject to platform-enforced velocity limits and geographic restrictions. These exist for scheme-compliance and risk reasons. They apply to every card program, sit on top of the cardholder's available balance, and cannot be overridden through the API or on a per-card basis.
These limits are independent of spending controls you configure yourself. They constrain ATM cash access specifically, not point-of-sale or e-commerce spend.
## Limits
Amount caps are denominated in the program's billing currency. Withdrawal counts are the same regardless of currency.
For a `USD` program:
| Window | Amount cap | Withdrawal count |
| - | - | - |
| Daily | USD 3,000 | 3 withdrawals |
| Monthly | USD 10,000 | 30 withdrawals |
| Yearly | USD 100,000 | No count limit |
For an `HKD` program:
| Window | Amount cap | Withdrawal count |
| - | - | - |
| Daily | HKD 25,000 | 3 withdrawals |
| Monthly | HKD 80,000 | 30 withdrawals |
| Yearly | HKD 800,000 | No count limit |
A withdrawal is rejected if it would push the cardholder past either the amount cap or the count cap for any active window. Windows are calendar-based and reset at the start of the next day, month, or year.
When a withdrawal is declined for exceeding one of these limits, the transaction carries one of the following [decline reasons](/transactions/decline-reasons#network-mandated-limits):
* `ATM_WITHDRAWAL_LIMIT_EXCEEDED` - the amount cap for the current window was exceeded.
* `ATM_WITHDRAWAL_FREQUENCY_EXCEEDED` - the withdrawal count for the current window was reached.
## Geographic restrictions
ATM withdrawals are blocked in restricted jurisdictions, including mainland China, FATF-blacklisted countries, and countries subject to OFAC sanctions. A withdrawal attempted at an ATM in a restricted country is declined with `TRANSACTION_TYPE_NOT_ALLOWED`.
This restriction applies only to ATM cash withdrawals. Point-of-sale and e-commerce transactions are governed by the [restricted MCC](/transactions/restricted-mccs) list and standard authorization rules.
The limits above are the program defaults and can be increased for your program on request, subject to review and approval by the Reap team. Reach out to your Reap contact if your program needs higher thresholds.
# Decline Reasons
Source: https://docs.reap.global/transactions/decline-reasons
Reference for every code that can appear on a declined card transaction.
When a transaction has `status: DECLINED`, the response carries a `declineReason` object that explains why.
```json theme={null}
{
"status": "DECLINED",
"declineReason": {
"code": "INSUFFICIENT_BALANCE",
"message": "Insufficient balance for this transaction."
}
}
```
**`code` is for your integration; `message` is for the cardholder.**
* **`code`** is a machine-readable identifier intended for your backend logic - branching on it to route the cardholder to a top-up screen, prompt for a PIN re-entry, surface a "card blocked" support flow, and so on. Do not render the code string itself to end users.
* **`message`** is human-readable English copy safe to display directly to the cardholder as a sensible default. You can also write your own copy keyed off `code` whenever you want fuller control over UX.
This page groups every code that can appear in `declineReason.code` by what your integration or the cardholder can do about it. For lifecycle context (when a decline is recorded, what webhook fires) see [Decline at authorization](/transactions/lifecycle#decline-at-authorization).
## Cardholder can self-resolve
The cardholder can take an action and retry.
| Code | When it happens | Suggested next step |
| - | - | - |
| `INSUFFICIENT_BALANCE` | The account's available balance is below the transaction amount. | Prompt the cardholder to top up, then retry. |
| `CARD_FROZEN` | The card is in `FROZEN` status. | Unfreeze the card from your app (or have the cardholder do it) and retry. |
| `CARD_NOT_ACTIVATED` | The card was issued but never activated. | Activate the card, then retry. |
| `INCORRECT_PIN` | The PIN entered at the terminal could not be verified. Covers both wrong PIN and PIN verification failures from the network. | Cardholder retries with the correct PIN. |
| `INCORRECT_CVV` | The CVV entered at checkout was wrong. | Cardholder checks and retries. |
| `INVALID_CARD_DETAILS` | One or more of the card details (PAN, expiry, CVV) did not match. | Cardholder rechecks the card details and retries. |
| `CONTACTLESS_NOT_SUPPORTED` | The contactless tap could not be processed. | Cardholder inserts or swipes the card instead. |
| `TERMINAL_ERROR` | The terminal could not complete the transaction. | Cardholder retries at the same or a different terminal. |
| `CASH_SERVICE_NOT_AVAILABLE` | The ATM is not providing cash service at the moment. | Cardholder tries a different ATM. |
## Network-mandated limits
The transaction exceeded a limit enforced by the platform. Trying a smaller amount or waiting for the limit window to reset usually resolves it.
| Code | When it happens |
| - | - |
| `ATM_WITHDRAWAL_LIMIT_EXCEEDED` | The per-window ATM amount cap has been exceeded. |
| `ATM_WITHDRAWAL_FREQUENCY_EXCEEDED` | The maximum number of ATM withdrawals allowed in the current window has been reached. |
## Merchant or transaction restriction
The card cannot be used for this particular transaction. The cardholder should use a different payment method or merchant; retrying the same flow will not help.
| Code | When it happens |
| - | - |
| `MERCHANT_CARD_TYPE_MISMATCH` | The merchant does not accept this card brand or product type. |
| `MERCHANT_CATEGORY_CODE_NOT_ALLOWED` | The merchant's category (MCC) is restricted. See [Restricted MCCs](/transactions/restricted-mccs). |
| `TRANSACTION_TYPE_NOT_ALLOWED` | The card cannot be used for this transaction type. |
| `TRANSACTION_NOT_ALLOWED` | Your own authorization endpoint declined the transaction on your controls. [External authorization](/transactions/external-authorization/overview) projects only; it is the verbatim reason your endpoint returned. |
| `WITHDRAWALS_NOT_ALLOWED` | Cash withdrawals are not enabled on this card. |
| `NETWORK_DECLINE` | Generic decline returned by the card network (Visa) without a more specific reason. Cardholder can retry or use a different payment method. |
## Spending policy controls
The transaction was blocked by a [spend policy](/spend-policies/overview) you configured. The declined transaction also carries a `policy` reference naming the exact policy responsible, so you can tell which rule fired. Retrying the same transaction will not help until the limit window resets or the policy is changed.
| Code | When it happens |
| - | - |
| `MERCHANT_NOT_ALLOWED` | A [merchant restriction](/spend-policies/control-types#merchant-restriction) blocks this merchant, category, or country. |
| `CHANNEL_NOT_ALLOWED` | A [channel restriction](/spend-policies/control-types#channel-restriction) blocks this channel (in-store, online, ATM, or push-to-card). |
| `TRANSACTION_AMOUNT_LIMIT_EXCEEDED` | The transaction is above the [per-transaction limit](/spend-policies/control-types#transaction-amount-limit). |
| `SPEND_LIMIT_EXCEEDED` | The transaction would push spend past a [spend limit](/spend-policies/control-types#spend-limit) for the current window. |
| `AUTHORIZATION_COUNT_LIMIT_EXCEEDED` | The [transaction-count limit](/spend-policies/control-types#transaction-count-limit) for the current window has been reached. |
## Requires support or program action
The card or account is in a state that the cardholder cannot resolve themselves - your team needs to act, or the cardholder needs to contact support.
| Code | When it happens | What to do |
| - | - | - |
| `CARD_NOT_ACTIVE` | The card has been deactivated or is otherwise not in an active status. | Check the card status via the API. Issue a replacement if the card has been permanently deactivated. |
| `CARD_EXPIRED` | The card has passed its expiry date. | Issue a replacement card. |
| `CARD_BLOCKED` | The card has been blocked. Covers blocks you placed as well as auto-blocks from repeated incorrect PIN, CVV, or expiry-date entries. | Read `blockLiftable` on the card. When it is `true`, call [unblock](/api-reference/cards/unblock-card); when it is `false`, the block is a Reap risk or compliance decision and only Reap support can lift it. See [Card statuses](/cards/overview#card-statuses). |
| `CARD_REPORTED_LOST` | The card has been reported lost. | The cardholder requests a replacement card. |
| `CARD_REPORTED_STOLEN` | The card has been reported stolen. | The cardholder requests a replacement card. |
| `ACCOUNT_NOT_ACTIVE` | The account linked to the card is not active. | Check the account status. A `RESTRICTED` account typically reflects a compliance issue that needs to be resolved. |
| `INSUFFICIENT_MASTER_BALANCE` | The master collateral account does not have enough balance to authorize the transaction. The cardholder's own balance is fine; the program-level collateral is the limiting factor. Specific to [Program-Funded](/program-configuration/funding-models#program-funded) programs. | Top up the master collateral account, then retry. |
## Security
| Code | When it happens | What to do |
| - | - | - |
| `SUSPECTED_FRAUD` | The transaction was declined for security reasons by Reap, the card network, or our fraud-detection partners. | The cardholder can retry or contact support if they believe the decline was wrong. Persistent suspected-fraud declines on the same card may warrant proactive review. |
## Catch-all
| Code | When it happens |
| - | - |
| `INTERNAL_ERROR` | A bucket reason returned when the transaction could not be authorized due to an underlying system error. |
If you need the underlying reason for a specific transaction that came back as `INTERNAL_ERROR`, contact Reap support with the transaction ID.
In [External authorization](/transactions/external-authorization/overview) projects, `INTERNAL_ERROR` is also what the cardholder sees when your authorization endpoint does not respond in time or returns an invalid response. Reap fails closed and records the underlying cause on its side.
# The Authorization Request
Source: https://docs.reap.global/transactions/external-authorization/authorization-request
The CARD_AUTHORIZATION_REQUEST contract - what Reap sends, what you return, and your time budget.
This page is the wire contract for [External authorization](/transactions/external-authorization/overview): what Reap sends on every transaction, what you return, and how long you have to respond.
## The request
Reap sends a `CARD_AUTHORIZATION_REQUEST` as a signed HTTPS `POST` to your registered endpoint. It carries the same `{ id, type, data }` envelope and `X-Reap-Webhook-*` headers as every other Reap webhook, so you can [verify the signature](/webhooks/signature-verification) - over the raw request bytes - and route it through the same code path. The one difference is that Reap consumes your HTTP response as the decision.
The body is the envelope: `type` is always `CARD_AUTHORIZATION_REQUEST` and `data` carries the authorization payload.
```json theme={null}
{
"id": "9b2c1f7e-1a4d-4c8e-9b2a-2f7e1a4d4c8e",
"type": "CARD_AUTHORIZATION_REQUEST",
"data": {
"transactionId": "8a1d0e6f-2b3c-4d5e-8f1a-3c4d5e6f7a8b",
"eventId": "9b2c1f7e-1a4d-4c8e-9b2a-2f7e1a4d4c8e",
"accountId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"cardId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"channel": "ECOMMERCE",
"digitalWallet": null,
"currency": "USD",
"amount": 42.50,
"originalCurrency": "EUR",
"originalAmount": 39.10,
"fees": {
"atm": 0,
"fx": 0.01
},
"merchant": {
"cardAcceptorId": "000123456789012",
"name": "ACME Store",
"city": "Berlin",
"postCode": "10115",
"state": null,
"country": "DE",
"mccCode": "5732",
"mccCategory": "Electronics Stores"
},
"occurredAt": "2024-01-15T10:30:00Z"
}
}
```
The `data` fields:
| Field | Description |
| - | - |
| `transactionId` | Identifier of the transaction this authorization belongs to. On the first authorization it is the id the transaction is created with. On an [incremental authorization](/transactions/lifecycle#incremental-authorizations) it is the id of the existing pending transaction. Matches `id` on the subsequent [transaction webhooks](/transactions/lifecycle). |
| `eventId` | Identifier for this authorization event, equal to the envelope `id`. Whichever way you decide, the same id appears in the resulting transaction's `events` and as `triggerEventId` on the [transaction webhooks](/transactions/lifecycle) that follow - as the authorization event if you approve, as the decline event if you decline. Use it to correlate this request with the specific event. |
| `accountId` | The account the card belongs to. |
| `cardId` | The card used for this transaction. |
| `channel` | `POS`, `ECOMMERCE`, `ATM`, or `VISA_DIRECT`. |
| `digitalWallet` | `APPLE_PAY`, `GOOGLE_PAY`, `MERCHANT_TOKEN`, or `null` for a physical card. |
| `currency`, `amount` | The amount to authorize in the card's billing currency (fixed per card program, for example `USD` or `HKD`). A zero `amount` is a [balance-check authorization](#zero-amount-checks). |
| `originalCurrency`, `originalAmount` | The same amount in the merchant's local presentment currency. |
| `fees` | Cardholder fee breakdown in the billing currency: `atm` (ATM markup) and `fx` (FX markup). Always present; zeros when no fee applies. |
| `merchant` | Merchant `cardAcceptorId`, `name`, `city`, `postCode`, `state`, `country` (ISO 3166-1 alpha-2), `mccCode` (ISO 18245), and `mccCategory`. `cardAcceptorId` is the merchant ID assigned by the merchant's acquirer, the same value the transaction webhooks carry. It is unique only within one acquirer and can be `null`. |
| `occurredAt` | ISO 8601 timestamp of when the authorization was initiated. |
The payload is a subset of the card transaction resource and reuses its field names, types, and enums. The generated [`CARD_AUTHORIZATION_REQUEST`](/api-reference/card-authorization-request) webhook reference is the authoritative schema - build your integration against it. For the shared formats of `channel`, `digitalWallet`, and `merchant`, see the [card transaction resource](/api-reference/card-transactions/get-card-transaction).
## Your response
Respond with `200` and a JSON body carrying your decision. The body is a discriminated object keyed on `decision`.
To approve:
```json theme={null}
{ "decision": "APPROVE" }
```
To decline, include a `reason`:
```json theme={null}
{ "decision": "DECLINE", "reason": "INSUFFICIENT_BALANCE" }
```
| Decline reason | Use when |
| - | - |
| `INSUFFICIENT_BALANCE` | The cardholder does not have enough balance on your side for this transaction. |
| `TRANSACTION_NOT_ALLOWED` | You are rejecting the transaction on your own controls (risk, a blocked merchant or category, a disabled card on your side, and so on). |
These are the only reasons you return. Card and account state declines are decided by Reap before you are called, so they are not yours to return. The decision object is intentionally small so it can gain options later, such as partial approval, without breaking your integration.
## Responding in time
You must return a decision within **1.6 seconds**. The card network does not wait, and there is no retry on the live transaction.
Reap fails closed. If your endpoint times out, is unreachable, returns a non-`2xx` status, or returns a body Reap cannot parse, Reap declines the transaction. The cardholder sees a generic decline ([`INTERNAL_ERROR`](/transactions/decline-reasons#catch-all)), so keep your endpoint highly available and monitor it. Reap records the underlying cause on its side for support.
Because authorization fails closed, your endpoint is on the critical path of every transaction. Treat it as a high-availability, low-latency service. Downtime or slow responses decline real cardholder transactions. See [Handling requests](/transactions/external-authorization/handling-requests) for how to build for this.
## Zero-amount checks
Some merchants send a zero-amount authorization to confirm a card is valid without charging it, common for subscription sign-ups and saved-card setups. Reap forwards these to you like any other authorization, and you approve or decline them. A zero-amount authorization places no hold whichever way you decide; your decision only signals to the merchant whether the card is usable.
# Handling Requests
Source: https://docs.reap.global/transactions/external-authorization/handling-requests
Build an authorization endpoint that is fast, available, and correct under concurrency.
Your authorization endpoint sits on the critical path of every card transaction in the program. This page covers how to build it so it stays correct and fast under real traffic. It assumes you have read [The authorization request](/transactions/external-authorization/authorization-request) contract.
## Treat it as a high-availability, low-latency service
Authorization [fails closed](/transactions/external-authorization/authorization-request#responding-in-time): if you time out, return a non-`2xx`, return an unparseable body, or are unreachable, Reap declines the transaction and the cardholder sees a decline at the terminal. There is no retry on the live transaction.
* Budget your entire round trip - network, decoding, signature check, your decision, and encoding - to comfortably fit inside the **1.6 second** window.
* Run the endpoint with redundancy and autoscaling. A cold start or a single slow dependency during a traffic spike declines real transactions.
* Monitor decline-by-timeout and error rates as a first-class SLO. A rise there is lost cardholder spend, not just noise.
## Verify every request
Reap signs each request with your endpoint's signing secret. Before you trust any field in the body, [verify the signature](/webhooks/signature-verification) over the exact raw request bytes - not the re-serialized JSON. Reject anything that fails verification with a non-`2xx` (which fails closed to a decline). This is the same scheme as your other webhooks, so you can reuse that verification code.
## Decide fast and deterministically
Reap has already applied card state, [spend policies](/spend-policies/overview), and platform limits before calling you, so your endpoint does not re-implement those. Your job is the money decision: does this cardholder have the balance, and do your own risk controls allow it?
Keep the decision path to cheap, local reads and writes:
* Read the cardholder's available balance from your own ledger and compare against `amount`.
* Apply only controls and checks you can evaluate within the latency budget.
* Keep slow or best-effort work — analytics, notifications, and any dependency you cannot bound — **off** the decision path, or behind a hard timeout that fails to a safe default well inside your budget. Never let an optional dependency block the response.
Return only the two [decline reasons](/transactions/external-authorization/authorization-request#your-response) that are yours to return (`INSUFFICIENT_BALANCE`, `TRANSACTION_NOT_ALLOWED`); everything else is decided by Reap before you are called.
## Reserve funds atomically when you approve
An approval is a commitment: Reap relays it to the network and a hold is placed. Multiple authorizations for the same card can arrive concurrently, so guard your balance the way Reap guards its own ledger - place the reservation atomically at the moment you approve, so two in-flight authorizations cannot both pass the balance check and overdraw.
Reserve the **authorized** `amount`, and treat that reservation as provisional. The final settled amount frequently differs from what you approved:
* A **partial clearing** settles less than authorized and releases the remainder.
* An **over-clearing** (tips, fuel, hotel incidentals) settles more than authorized.
* A **reversal** releases the hold entirely.
You do not learn the final amounts from the authorization request - you learn them from the asynchronous transaction lifecycle. Release and adjust reservations there, not here.
## The request is not your record
The authorization request is the live decision point and nothing more. It does not tell you whether the transaction ultimately cleared, was reversed, or was refunded, and it is not durable. Your ledger's source of truth is the asynchronous [transaction lifecycle](/transactions/external-authorization/reconciliation) that follows an approval. Key reservations on `transactionId` so an incremental authorization attaches to a transaction you already hold. Use `eventId` for the specific authorization.
# Overview
Source: https://docs.reap.global/transactions/external-authorization/overview
Approve or decline every card transaction in real time from your own systems.
In an External-mode project you own the money decision on every card transaction. Reap calls your endpoint, you return an approve or decline, and Reap relays that decision to the card network. Your systems are the source of truth for the per-user balance, the risk decision, and the per-user ledger - Reap keeps no per-user balance of its own to authorize against.
Reap does not step out of the loop, though. It still enforces its program-wide guardrails on every transaction before and around your decision - card and account state, your spend policies and limits, fraud monitoring, card-network rules, and the [master collateral](/program-configuration/funding-models#program-funded) that backs the program - any of which can decline a transaction regardless of your answer. See [Division of responsibility](#division-of-responsibility).
Applies to [Program-Funded](/program-configuration/funding-models#program-funded) projects using **External** [authorization mode](/program-configuration/authorization-mode). In Managed mode Reap authorizes against the balances you allocate, and these pages do not apply.
## Division of responsibility
External authorization is a split contract. Reap still runs the deterministic, program-wide guardrails; you own the money decision.
| Concern | Who decides |
| - | - |
| Card and account status (frozen, blocked, closed) | **Reap** - declined locally before you are called |
| [Spend policies](/spend-policies/overview) and platform limits | **Reap** - declined locally before you are called |
| Card-network (Visa) rules | **Reap and the card network** - can decline independent of your answer |
| Fraud monitoring | **Reap** - can decline a suspected-fraud transaction independent of your answer |
| Per-cardholder balance and your own risk / controls | **You** - via the authorization request |
| [Master collateral](/program-configuration/funding-models#program-funded) covers the spend | **Reap** - checked after you approve |
| Relaying the final outcome to the card network | **Reap** |
Reap runs its deterministic guardrails - card state, spend policies, and limits - before it calls you, so your endpoint is only asked about transactions that have already passed them; you never see one Reap has already declined. Card-network rules and fraud monitoring apply on top and can decline a transaction independent of your answer, including before your endpoint is ever called. That leaves your endpoint exactly one question: given the transaction in front of you, does the cardholder have the balance and do your own controls allow it?
## How it works
```mermaid theme={null}
sequenceDiagram
participant Network as Card network
participant Reap
participant You as Your endpoint
Network->>Reap: Authorization request
Reap->>Reap: Local checks (card and account status, spend policies, limits)
Reap->>You: CARD_AUTHORIZATION_REQUEST
You-->>Reap: Approve or decline (within 1.6s)
Reap->>Reap: Master collateral check
Reap-->>Network: Approved or declined
Note over Network,You: If approved, the async transaction lifecycle begins
Reap->>You: CARD_TRANSACTION_CREATED, then UPDATED ...
```
## The authorization flow
The cardholder taps, swipes, or enters their card. The card network routes an authorization request to Reap through the card processor.
Reap validates card and account status, evaluates your [spend policies](/spend-policies/overview), and applies platform limits. Anything that fails is declined locally with a [decline reason](/transactions/decline-reasons) and never reaches you.
Reap sends a signed [`CARD_AUTHORIZATION_REQUEST`](/transactions/external-authorization/authorization-request) as an HTTPS `POST` to your registered endpoint, carrying the card, amount, and merchant details.
Check your own ledger balance and risk controls, then return `APPROVE` or `DECLINE` within **1.6 seconds**. See [Handling requests](/transactions/external-authorization/handling-requests) for the response contract and latency budget.
If you approve, Reap verifies your [master collateral account](/program-configuration/funding-models#program-funded) covers the spend. In the rare case it does not, Reap declines regardless of your approval.
Reap forwards the approve or decline to the card network, which releases the goods or declines at the terminal.
If the transaction was approved, the [transaction lifecycle](/transactions/lifecycle) begins: a `CARD_TRANSACTION_CREATED` webhook, then `CARD_TRANSACTION_UPDATED` for each clearing, reversal, or refund. These are the durable record you [reconcile your ledger](/transactions/external-authorization/reconciliation) against - the authorization request is the live decision, not the record.
## Next
The request and response contract: envelope, payload, decline reasons, timeout, and fail-closed behavior.
Build an endpoint that stays fast, verifies signatures, and dedupes safely on the authorization hot path.
Reconcile holds, clearings, reversals, and refunds against the async transaction lifecycle.
Register your authorization endpoint and exercise it end to end in sandbox.
# Keeping Your Ledger in Sync
Source: https://docs.reap.global/transactions/external-authorization/reconciliation
Reconcile your ledger against the asynchronous transaction lifecycle after you approve.
In External mode you own the per-user ledger, so you also own reconciliation. The [authorization request](/transactions/external-authorization/authorization-request) places a provisional reservation; the asynchronous [transaction lifecycle](/transactions/lifecycle) is what tells you the transaction actually cleared, was reversed, or was refunded. This page is how you keep the two aligned.
## The lifecycle is your source of truth
After an approval, Reap emits the standard transaction webhooks:
* `CARD_TRANSACTION_CREATED` - a transaction is recorded for the first time.
* `CARD_TRANSACTION_UPDATED` - an existing transaction gains a new event (clearing, reversal, refund, or dispute refund).
Each webhook carries the **full transaction object**. The [`events` array](/transactions/amounts#per-event-amounts) is the authoritative record you reconcile against: every `AUTHORIZATION`, `CLEARING`, `REVERSAL`, `REFUND`, and `DISPUTE_REFUND`, each with its own `amount`. Post those to your ledger - they, not the authorization request, tell you what actually settled. The request only ever told you what you approved.
Reap also surfaces an aggregate [`amount` breakdown](/transactions/amounts) (`authorized`, `reversed`, `cleared`, `refunded`, and the net `current`) computed over those same events. It is a convenient mirror of Reap's own view - useful to cross-check your position against.
## Correlate with the authorization request
The `transactionId` you received in the authorization request is the transaction `id` on every subsequent webhook. An incremental authorization carries the same `transactionId` as the first one, so you can attach the new reservation to a transaction you already hold.
The `eventId` appears again as `triggerEventId` and inside `events`. Use it to tie this reservation to the specific authorization event.
A decline is correlatable the same way. The transaction Reap records for it uses that `transactionId` and carries a `DECLINE` event under that same `eventId`. You can release the decision you logged without matching on amounts and timestamps.
## Process events idempotently
Update your ledger as events arrive. Deliveries can repeat and are not guaranteed to arrive in order, so make your handling idempotent - key on the event id and skip anything you have already applied.
Expect the settled amounts to differ from what you approved: partial clearings release the unused portion, over-clearings settle more than authorized, and reversals release the hold entirely. Release your provisional reservation and post the actual amounts as those events arrive. See the [lifecycle](/transactions/lifecycle) for each scenario.
## Handle events with no prior authorization
Reap forwards **every** presentment the card network sends, including ones that never passed through your authorization endpoint. Your reconciliation must not assume a matching authorization always exists:
* **Offline and direct clearing.** Transit gates and in-flight purchases clear without a real-time authorization. You receive `CARD_TRANSACTION_CREATED` with `status: CLEARED` and a single `CLEARING` event - a charge with no hold to release. Book it as debt directly.
* **Standalone (unrelated) refund.** A tax refund, goodwill credit, or a return long after the purchase arrives as `CARD_TRANSACTION_CREATED` with `status: CLEARED` and a single `REFUND` event, with no preceding transaction. Credit the cardholder.
* **A clearing after a decline or reversal.** The network can still present a clearing for a transaction you never approved or that was already reversed. Reap books it as debt; mirror the charge and reconcile.
Treating the lifecycle as authoritative - rather than only settling transactions you remember approving - is what keeps your ledger correct when the network behaves this way.
These are the same edge cases Reap handles internally when it owns the ledger in Managed mode. In External mode the responsibility moves to you, which is why your ledger, not the authorization request, is the system of record.
## Credit a dispute refund
A [dispute](/cards/disputes/overview) that reaches `REFUNDED` credits the cardholder. The credit arrives on the disputed transaction as a `DISPUTE_REFUND` event in `CARD_TRANSACTION_UPDATED`. Post it the same way you post a `REFUND`: credit the cardholder the event's `amount`, keyed on the event id. See [Dispute refund](/transactions/lifecycle#dispute-refund).
The event's `amount` equals the dispute's `refundedAmount`. It is the refunded bill plus a prorated share of FX markup and ATM variable fee. It does not include the per-withdrawal ATM fixed fee, which stays charged. It can be less than the disputed `amount`. See [How a refund is calculated](/cards/disputes/overview#how-a-refund-is-calculated).
Credit the cardholder from the `DISPUTE_REFUND` event only. [`CARD_DISPUTE_STATUS_UPDATED`](/api-reference/card-dispute-status-updated) with `status: REFUNDED` reports the same credit on the dispute. Crediting from both books the refund twice.
# Setup and Testing
Source: https://docs.reap.global/transactions/external-authorization/setup
Register your authorization endpoint and exercise it end to end in sandbox.
## Registering your authorization endpoint
Your authorization endpoint is a webhook registered in `REQUEST` mode. Call [Create a webhook endpoint](/api-reference/webhooks/create-webhook-endpoint) with an HTTPS `url` and `mode` set to `REQUEST`. The response returns a `signingSecret` once. Store it - you need it to [verify request signatures](/webhooks/signature-verification).
* **External projects only.** Registering a `REQUEST` endpoint on a project that is not in External authorization mode returns a `403`.
* **One per project.** Registering a second `REQUEST` endpoint returns a `409`. To change the URL, update it in place.
* **Rotate-only.** You can rotate the URL and signing secret, but the endpoint cannot be disabled or deleted, so your card program always has a live authorization endpoint.
* **Signed.** Requests are signed with the same scheme as your other webhooks - [verify the signature](/webhooks/signature-verification) before trusting a payload.
* **Required before issuing cards.** In an External project, [card creation](/cards/overview) fails until this endpoint is registered. This guarantees every card in the program has a live decision endpoint behind it.
The authorization endpoint is separate from your notification webhooks. It receives only `CARD_AUTHORIZATION_REQUEST` and never the asynchronous event stream. Register your notification endpoints separately to receive the [transaction lifecycle](/transactions/external-authorization/reconciliation) you reconcile against.
## Testing in sandbox
In a sandbox External project, [Simulate authorization](/api-reference/simulation/simulate-authorization) fires a real signed `CARD_AUTHORIZATION_REQUEST` to your registered endpoint and authorizes based on the decision you return, so you can exercise your decisioning end to end - signature verification, your balance check, and the approve/decline response.
A full run-through, including the follow-on lifecycle webhooks you reconcile against, is in [Testing in Sandbox](/transactions/testing-in-sandbox#scenario-5-external-authorization).
# Cardholder fees
Source: https://docs.reap.global/transactions/fees
The FX and ATM fees charged to cardholders, how to set them for your program, and how to override them for a single account.
Cardholder fees are charged to the cardholder on top of the merchant amount when a card transaction is authorized, so the amount debited from the account is higher than the amount the merchant asked for. Every program carries a default rate for each fee type. You can change those defaults and override any of them for a single account.
## Fee types
| Type | Charged when | Unit |
| - | - | - |
| `FX_MARKUP` | The merchant charges in a currency other than your billing currency | Basis points of the bill amount |
| `ATM_DOMESTIC_VARIABLE` | A domestic ATM withdrawal | Basis points of the bill amount |
| `ATM_DOMESTIC_FIXED` | A domestic ATM withdrawal | Fixed amount in your billing currency |
| `ATM_CROSS_BORDER_VARIABLE` | A cross-border ATM withdrawal | Basis points of the bill amount |
| `ATM_CROSS_BORDER_FIXED` | A cross-border ATM withdrawal | Fixed amount in your billing currency |
100 basis points is 1%. A rate of `0` or an amount of `0` turns a fee off without deleting it.
An ATM withdrawal is **domestic** when the ATM is in the same country as your card program, and **cross-border** otherwise. A cross-currency ATM withdrawal pays the FX markup as well as the ATM pair for its tier.
The fixed ATM fee is charged once per withdrawal.
## Defaults and overrides
Each fee has a **scope**:
* `PROJECT`: the default for your whole program. Every project has exactly one row per fee type. You can change its value but you cannot create or delete it. The project is the one you authenticate as, so this scope carries no `id`.
* `ACCOUNT`: an override for one account. It replaces the project default for that fee type on every card of that account. An account can have at most one override per fee type.
When Reap prices a transaction it uses the account override where one exists and the project default otherwise. Each transaction records the fee row that priced it and the version that row was on. Changing a fee later never rewrites the history of settled transactions.
Set your program's default fees in the dashboard before you launch in production. Only an organization Admin can change project defaults, and only from the dashboard; an API key cannot update them. Account overrides can be managed from the dashboard or the API.
## Set an override
Create an override for one fee type on one account. The `value` says how the fee is denominated: `unit: "BPS"` with a `bps` rate for a rate-based type, `unit: "AMOUNT"` with an `amount` for a fixed one.
```bash theme={null}
curl -X POST https://api.reap.global/fees \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "ATM_DOMESTIC_FIXED",
"scope": { "type": "ACCOUNT", "id": "1c4e0c5e-9b6b-4d5a-8e2e-3f1b2c7a9d10" },
"value": { "unit": "AMOUNT", "amount": 1.5 }
}'
```
```json theme={null}
{
"id": "6f0b3c6e-6d2f-4f0e-9c1e-1d2f3a4b5c6d",
"type": "ATM_DOMESTIC_FIXED",
"value": { "unit": "AMOUNT", "amount": 1.5, "currency": "USD" },
"scope": { "type": "ACCOUNT", "id": "1c4e0c5e-9b6b-4d5a-8e2e-3f1b2c7a9d10" },
"version": 1,
"createdAt": "2026-09-11T09:12:44.000Z",
"updatedAt": "2026-09-11T09:12:44.000Z"
}
```
A fixed fee is always denominated in your program's billing currency, which the response reports as `currency`. A rate-based value carries no currency: it applies to the bill amount whatever that currency is.
Update a fee with [Update fee value](/api-reference/fees/update-fee-value); the `version` increments on every change. Delete an override with [Delete fee override](/api-reference/fees/delete-fee-override) and the account falls back to the project default. Creating a second override for the same type and account returns `409 DUPLICATE_FEE`.
## Read what an account pays
[List effective fees](/api-reference/fees/list-effective-fees) returns one entry per fee type for an account, each naming the row that supplies it.
```bash theme={null}
curl "https://api.reap.global/fees/effective?accountId=1c4e0c5e-9b6b-4d5a-8e2e-3f1b2c7a9d10" \
-H "Authorization: Bearer $API_KEY"
```
```json theme={null}
{
"items": [
{
"type": "FX_MARKUP",
"value": { "unit": "BPS", "bps": 150 },
"source": {
"feeId": "0b1f6a2e-4a3c-4d8e-9f10-2a3b4c5d6e7f",
"scope": { "type": "PROJECT" }
}
},
{
"type": "ATM_DOMESTIC_FIXED",
"value": { "unit": "AMOUNT", "amount": 1.5, "currency": "USD" },
"source": {
"feeId": "6f0b3c6e-6d2f-4f0e-9c1e-1d2f3a4b5c6d",
"scope": { "type": "ACCOUNT", "id": "1c4e0c5e-9b6b-4d5a-8e2e-3f1b2c7a9d10" }
}
}
]
}
```
[List fees](/api-reference/fees/list-fees) returns the rows themselves, filtered by `type`, `scopeType`, or `scopeId`.
## What appears on the transaction
Each transaction reports the fees it charged under `fees`: `fx` for the FX markup and `atm` for the ATM pair combined, both in your billing currency. See [Amounts](/transactions/amounts) for how these aggregate across authorization, clearing, and refund events.
A [dispute refund](/cards/disputes/overview#how-a-refund-is-calculated) reverses FX markup and the ATM variable fee in proportion to the bill that came back. The per-withdrawal ATM fixed fee stays charged, the same way a merchant refund leaves it charged.
# Lifecycle
Source: https://docs.reap.global/transactions/lifecycle
The four transaction statuses and the scenarios that drive them.
A card transaction is created the first time the card network sends Reap an event for it, and evolves as more events arrive. The transaction `id` is stable for the entire life of the transaction. The `status` and the `events` array are what change.
## Statuses
| Status | Meaning |
| - | - |
| `PENDING` | The authorization was approved and funds are reserved on the account. The transaction has not yet been settled by the merchant. |
| `CLEARED` | The merchant has collected and the cardholder has been charged. |
| `VOID` | The authorization was fully reversed before clearing. No funds were charged. Also used for zero-amount card validity checks. |
| `DECLINED` | The authorization was rejected. No funds were reserved or charged. |
`CLEARED` is terminal in the sense that the transaction stays `CLEARED` even when refunds arrive later. Refunds append a new event and adjust the amount fields, but never move the status. See [Refund on a cleared transaction](#refund-on-a-cleared-transaction-related-refund).
```mermaid theme={null}
stateDiagram-v2
[*] --> PENDING: authorization approved
[*] --> DECLINED: authorization rejected
[*] --> VOID: zero-amount verification
[*] --> CLEARED: standalone refund
[*] --> CLEARED: offline / direct clearing
PENDING --> CLEARED: merchant settles
PENDING --> VOID: full reversal (incl. auto-reversal)
CLEARED --> CLEARED: additional clearing / refund
```
## Balances during a transaction
Card spend moves an account's balance in two phases, mirroring the network:
* **Authorization** places a *hold*. The authorized amount is reserved and the account's available balance drops immediately, but nothing has settled yet. Holds are provisional and are released by a reversal or replaced by a real charge at clearing.
* **Clearing** converts the hold into a settled charge. A reversal releases the hold instead; a refund credits the account after clearing.
The `events` array is the audit trail of these movements. The transaction's `amount` breakdown tracks `authorized` vs `reversed` while `PENDING` and `cleared` vs `refunded` once `CLEARED`, with `amount.current` always carrying the net effect - see [Amounts](/transactions/amounts). In User-Funded and Managed projects this is the balance Reap authorizes against. In [External-authorization](/transactions/external-authorization/overview) projects Reap forwards the decision to you, and you track these same movements in your own ledger.
## Scenarios
The scenarios below cover almost every transaction you will see. Each describes how the status, the `events` array, and the amount fields change, which webhook fires, and the balance effect.
### Authorization and clearing
The default path. The cardholder presents the card, the merchant requests an authorization, Reap approves it, and one to several days later the merchant submits the charge for settlement.
```mermaid theme={null}
sequenceDiagram
participant Network as Card network
participant Reap
participant Backend as Your backend
Network->>Reap: Authorization request
Reap-->>Network: Approved
Reap->>Backend: CARD_TRANSACTION_CREATED (status: PENDING)
Note over Network,Backend: Hours to several days later
Network->>Reap: Clearing
Reap->>Backend: CARD_TRANSACTION_UPDATED (status: CLEARED)
```
The transaction is created in `PENDING` with one `AUTHORIZATION` event, then transitions to `CLEARED` when a `CLEARING` event arrives. The hold placed at authorization becomes a settled charge at clearing.
#### Clearing amount vs. authorized amount
The cleared amount frequently differs from what was authorized. The transaction still moves `PENDING → CLEARED`; only the amounts and the residual hold differ.
* **Equal.** The merchant clears exactly the authorized amount. `amount.cleared` equals `amount.authorized`; the hold converts to a charge with no remainder.
* **Less than authorized (partial clearing).** The merchant clears less, and the unused portion of the hold is released immediately. Classic case: a hotel authorizes the room rate plus an incidentals buffer, then clears only the room rate at check-out. Auth `$200` → clearing `$150` → status `CLEARED`, `amount.cleared` `$150`, and the remaining `$50` returns to the account.
* **More than authorized (over-clearing).** The merchant clears more than authorized with no incremental authorization beforehand - common with tips (restaurants) and automated fuel dispensers. Auth `$200` → clearing `$220` → `amount.cleared` `$220`; the extra `$20` is charged on top of the released hold.
* **Additional clearings.** A single authorization can receive more than one clearing (multi-leg travel, split shipments). Each appends a `CLEARING` event and adds to `amount.cleared` while the status stays `CLEARED`.
### Offline and direct clearing
Some transactions clear without Reap ever seeing an authorization. Offline-capable terminals - transit gates, in-flight purchases - accept the card without a real-time authorization and submit the charge later. Reap creates the transaction directly in `CLEARED` with a single `CLEARING` event and the full amount as debt. There is no hold to release, because none was ever placed.
```mermaid theme={null}
sequenceDiagram
participant Network as Card network
participant Reap
participant Backend as Your backend
Network->>Reap: Clearing (no prior authorization)
Reap->>Backend: CARD_TRANSACTION_CREATED (status: CLEARED)
```
You receive a `CARD_TRANSACTION_CREATED` webhook (not `UPDATED`) with `status: CLEARED` and a single `CLEARING` event.
### Decline at authorization
The card network requests an authorization that Reap rejects (insufficient funds, frozen card, blocked merchant category, and so on). No funds are held and the transaction is created directly in `DECLINED`. The `declineReason` field on the response tells you why; see [Decline reasons](/transactions/decline-reasons) for the full code list. No further events follow.
You receive one `CARD_TRANSACTION_CREATED` webhook with `status: DECLINED`.
Not every declined attempt becomes a transaction in Reap. When the merchant or the card network rejects the attempt before it reaches Reap (for example, an obvious fraud signal at the terminal), no record is created. A `DECLINED` status in Reap always means Reap declined it, which is why `declineReason` is always populated.
### Full reversal
The merchant cancels the authorization before settling. The entire hold is released back to the account and the status becomes `VOID`. The classic example is a hotel or rental-car pre-authorization released at check-out once the final amount is known.
```mermaid theme={null}
sequenceDiagram
participant Network as Card network
participant Reap
participant Backend as Your backend
Network->>Reap: Authorization ($200)
Reap->>Backend: CARD_TRANSACTION_CREATED (PENDING)
Network->>Reap: Reversal ($200)
Reap->>Backend: CARD_TRANSACTION_UPDATED (VOID)
```
You receive one `CARD_TRANSACTION_UPDATED` webhook with `status: VOID` and a new `REVERSAL` event appended to `events`.
### Partial reversal
The merchant releases part of the authorized amount without settling yet. The transaction stays `PENDING`, but the `amount.reversed` field grows and `amount.current` drops to reflect the remaining hold.
A common pattern: a hotel pre-authorizes the nightly rate plus an incidentals hold at check-in, releases the unused incidentals at check-out, then clears for just the actual charges. You see one `AUTHORIZATION`, then a `REVERSAL`, then a `CLEARING`, and the status moves `PENDING` → `PENDING` → `CLEARED`.
You receive `CARD_TRANSACTION_UPDATED` for each reversal.
### Automatic reversal
If a `PENDING` authorization is never cleared or reversed by the merchant, the card network eventually releases it and Reap processes that like any other reversal: a `REVERSAL` event is appended, the hold is released, and the status becomes `VOID`. Stale holds are released by the network after set amount of days.
You receive one `CARD_TRANSACTION_UPDATED` webhook with `status: VOID`.
### Refund on a cleared transaction (related refund)
The merchant refunds a transaction that has already cleared. The transaction stays `CLEARED`; a `REFUND` event is appended to `events`, `amount.refunded` grows, and `amount.current` drops by the refunded amount. A full refund leaves `amount.current` at `0` while the status remains `CLEARED`. The refunded amount is credited back to the account.
```mermaid theme={null}
sequenceDiagram
participant Network as Card network
participant Reap
participant Backend as Your backend
Network->>Reap: Authorization + Clearing
Reap->>Backend: CARD_TRANSACTION_UPDATED (CLEARED)
Note over Network,Backend: Days later
Network->>Reap: Refund
Reap->>Backend: CARD_TRANSACTION_UPDATED (CLEARED, + REFUND event)
```
You receive a `CARD_TRANSACTION_UPDATED` webhook with the full transaction object, including the new refund event.
### Standalone refund (unrelated refund)
Sometimes a merchant sends a refund that does not correspond to any existing transaction on Reap - a tax refund on an overseas purchase, a goodwill credit, or a return long after the original purchase. Reap creates a brand new transaction in `CLEARED` with a single `REFUND` event and no preceding authorization or clearing.
You receive a `CARD_TRANSACTION_CREATED` webhook (not `UPDATED`) with `status: CLEARED` and a single event of type `REFUND`. From your side, this transaction is treated like any other cleared transaction in the activity feed.
### Dispute refund
A [dispute](/cards/disputes/overview) resolved in the cardholder's favor credits the disputed transaction. The transaction stays `CLEARED`; a `DISPUTE_REFUND` event is appended to `events`, `amount.refunded` grows, and `amount.current` drops by the credit. It moves the amount fields exactly like a merchant `REFUND`. The event type tells you the credit came from a dispute rather than from the merchant.
The credit is the dispute's `refundedAmount`: the refunded bill plus a proportional share of FX markup and ATM variable fee. The per-withdrawal ATM fee stays charged. See [How a refund is calculated](/cards/disputes/overview#how-a-refund-is-calculated).
```mermaid theme={null}
sequenceDiagram
participant Reap
participant Backend as Your backend
Note over Reap,Backend: Dispute filed on a CLEARED transaction
Reap->>Backend: CARD_DISPUTE_STATUS_UPDATED (SUBMITTED)
Note over Reap,Backend: Days or weeks later
Reap->>Backend: CARD_TRANSACTION_UPDATED (CLEARED, + DISPUTE_REFUND event)
Reap->>Backend: CARD_DISPUTE_STATUS_UPDATED (REFUNDED)
```
You receive a `CARD_TRANSACTION_UPDATED` webhook with the full transaction object, including the new `DISPUTE_REFUND` event, alongside the `CARD_DISPUTE_STATUS_UPDATED` webhook for the dispute. Apply the balance change from the transaction webhook; the dispute webhook reports the case outcome. Webhooks are not guaranteed to arrive in order.
## Other cases worth knowing
### Zero-amount verifications
Some merchants run a zero-amount authorization to check the card is valid without charging it (typical for subscription sign-ups and saved-card flows). Reap records this as a transaction created directly in `VOID` with a single `AUTHORIZATION` event of amount `0`. No funds are reserved and no further events arrive.
### Incremental authorizations
A merchant can raise the authorized amount on a transaction without cancelling the original auth. Restaurants adding a tip and hotels topping up an open pre-authorization both do this. The transaction keeps the same `id` and stays `PENDING`. A second `AUTHORIZATION` event is appended and `amount.authorized` grows by the incremental amount. In [External authorization](/transactions/external-authorization/authorization-request), the incremental request carries that same `transactionId`. The eventual clearing may match the total authorized amount or differ from it - see [Clearing amount vs. authorized amount](#clearing-amount-vs-authorized-amount).
### 3D Secure on ECOMMERCE authorizations
Online (`channel: ECOMMERCE`) authorizations may include a 3D Secure step in which the issuer must confirm the cardholder before the authorization can proceed. Reap handles 3DS for you and exposes a per-card setting that controls how challenges are resolved - SMS OTP by default, or an in-app flow you drive from your backend. See [3D Secure](/cards/3d-secure).
## Testing the lifecycle
The sandbox lets you drive each scenario end-to-end without a real card network. See [Testing in Sandbox](/transactions/testing-in-sandbox) for the simulation endpoints that produce authorizations, clearings, reversals, refunds, and declines.
# Overview
Source: https://docs.reap.global/transactions/overview
How card transactions are represented, retrieved, and tracked over their lifecycle.
A card transaction represents one card use end-to-end: an in-store purchase, an online checkout, an ATM withdrawal, or a third-party push to the card. Reap captures every authorization, clearing, reversal, refund, and decline that the card network sends, and exposes the result as a single transaction resource that evolves over time.
You do not poll the card network or reconcile authorizations against clearings yourself. Reap does that, and you read the current state through the API or react to it through webhooks.
## Anatomy of a transaction
Every transaction carries the same envelope of identifying fields, plus a status-specific amount shape and a chronological event log.
| Field | Description |
| - | - |
| `id` | The transaction ID. Stable across the full lifecycle. |
| `accountId`, `cardId` | The account and card the transaction was made on. |
| `status` | `PENDING`, `CLEARED`, `DECLINED`, or `VOID`. See [Lifecycle](/transactions/lifecycle). |
| `channel` | How the transaction was initiated: `POS`, `ECOMMERCE`, `ATM`, or `VISA_DIRECT`. |
| `digitalWallet` | `APPLE_PAY`, `GOOGLE_PAY`, `MERCHANT_TOKEN` (the merchant stored the card for a recurring or saved-card payment), or `null` if no digital wallet was used. |
| `merchant` | Merchant `id`, `cardAcceptorId`, `name`, `city`, `postCode`, `state`, `country`, `mccCode`, and `mccCategory`. `cardAcceptorId`, `postCode`, and `state` are nullable. `merchant.id` identifies the merchant details recorded on this one transaction and differs on every transaction, even for the same merchant, so do not use it to identify or group merchants. To match merchants across transactions, use `cardAcceptorId`, the merchant ID assigned by the merchant's acquirer. It is unique only within one acquirer, so combine it with `name`, `country`, and `mccCode`. |
| `currency` | The card's billing currency (ISO 4217). Fixed per card program (for example `USD` or `HKD`). |
| `originalCurrency` | The currency the merchant charged in. Equal to `currency` for same-currency transactions, otherwise the merchant's local currency. |
| `amount`, `originalAmount` | Amount charged to the cardholder (in `currency`) and amount the merchant charged (in `originalCurrency`). The shape of these fields varies by status. See [Amounts](/transactions/amounts). |
| `fees` | Cardholder fee breakdown in `currency`: `{ "atm", "fx" }`. Always present on every status (including `DECLINED`); zeros when no fee applies. Net of the same event legs as cardholder `amount` for `PENDING` / `VOID` / `CLEARED`. |
| `conversionRate` | Effective rate Reap applied to convert `originalAmount` to `amount`. Present on `PENDING` and `CLEARED`; absent on `VOID` and `DECLINED`. |
| `events` | Chronological log of every lifecycle event (authorization, clearing, reversal, refund, decline). Oldest first. Each event includes its own `fees`. |
| `declineReason` | Present only on `DECLINED` transactions. See [Decline reasons](/transactions/decline-reasons). |
| `occurredAt` | When the underlying card activity happened. |
| `createdAt`, `updatedAt` | When Reap first recorded the transaction and last modified it. |
The response shape is a discriminated union on `status`. `DECLINED` transactions carry a `declineReason` and a flat `originalAmount`. `PENDING`, `CLEARED`, and `VOID` carry breakdowns instead, because their amounts can move as reversals, clearings, or refunds arrive. See [Amounts](/transactions/amounts) for the per-status field shapes and [Lifecycle](/transactions/lifecycle) for how they change.
## Retrieving transactions
There are two endpoints, used for different jobs.
### List activities
Use [List activities](/api-reference/activities/list-activities) to fetch a user's card history. This is the primary way to read transactions, because card transactions are part of a unified activity feed that also contains crypto deposits and (in Program-Funded programs) virtual asset postings. The feed is chronologically ordered and paginated, which matches what most product surfaces (transaction history screens, statements, exports) actually need.
`type` controls which kinds of activity the feed returns. It is the only parameter that does, so pass `type=CARD_TRANSACTION` whenever you want card transactions alone. It accepts several types, so `type=CRYPTO_DEPOSIT,CARD_TRANSACTION` returns deposits and transactions in one ordered, paginated feed.
Every other filter narrows within its own kind and leaves the others untouched:
* `accountId=` scopes to one account (typically a user).
* `cardId=` restricts which card transactions come back, to a specific card.
* `cardTransactionStatus=PENDING,CLEARED` restricts which card transactions come back, by status (comma-separated).
So `cardId=` on its own still returns that account's crypto deposits alongside the card's transactions. Combine it with `type=CARD_TRANSACTION` to get the card's transactions by themselves.
Each item in the response carries the full card transaction object under `data`, identical to what `Get a card transaction by ID` returns.
### Get a card transaction by ID
Use [Get a card transaction by ID](/api-reference/card-transactions/get-card-transaction) when you already have a transaction ID and want its current state. Common cases: rendering a transaction detail screen or refreshing a single row in your UI. The response schema is identical to items returned by the activities endpoint.
## Reacting to changes
Subscribe to webhooks instead of polling.
| Event | When it fires |
| - | - |
| [`CARD_TRANSACTION_CREATED`](/api-reference/card-transaction-created) | A new transaction is recorded for the first time (a new authorization, a decline, or a standalone refund). |
| [`CARD_TRANSACTION_UPDATED`](/api-reference/card-transaction-updated) | An existing transaction receives a new lifecycle event (clearing, reversal, refund, or a previously pending transaction transitioning to declined). |
Both events carry the full transaction object. The `events` array on the payload always reflects the complete history at the time of delivery, ordered oldest first, so you do not need to stitch events together across deliveries.
To know which event triggered a given webhook, read `triggerEventId` on the payload and resolve it against `events` for the exact event type and amounts that drove the change. Do not assume it is the last entry in `events`; use `triggerEventId` as the authoritative pointer.
Deliveries are not ordered, so upsert on `data.id` rather than expecting the create to arrive first, and ignore a payload whose `updatedAt` is older than what you have stored.
Use the event `id` for idempotency. See [Webhooks](/webhooks/overview) for delivery guarantees, retries, and signature verification.
## Next
The four statuses and the scenarios that drive them.
Per-status amount fields, currency, and conversion rate.
Every code that can appear on a declined transaction.
# Restricted MCCs
Source: https://docs.reap.global/transactions/restricted-mccs
Merchant Category Codes that the platform always declines.
The platform enforces a list of Merchant Category Codes (MCCs) for which all card transactions are declined, regardless of card- or account-level configuration. These restrictions exist for scheme-compliance and risk reasons and cannot be overridden on a per-card or per-program basis.
When a transaction matches one of these MCCs, the decline is surfaced as:
```json theme={null}
{
"status": "DECLINED",
"declineReason": {
"code": "MERCHANT_CATEGORY_CODE_NOT_ALLOWED",
"message": "This merchant category is not allowed on this card. Please use a different payment method."
}
}
```
The merchant MCC is also visible on the transaction itself under `merchant.mccCode` and `merchant.mccCategory`, so you can confirm which category triggered the decline.
## Restricted categories
| MCC | Description |
| - | - |
| `5542` | Automated Fuel Dispensers (AFD) |
Most pay-at-the-pump fuel dispensers fall under MCC `5542`. Inside-the-store fuel purchases (e.g. paying at the cashier inside a gas station) typically use MCC `5541` and are not restricted by this list.
# Testing in Sandbox
Source: https://docs.reap.global/transactions/testing-in-sandbox
Drive every transaction lifecycle scenario end-to-end without a real card network.
The sandbox environment exposes simulation endpoints that produce authorizations, clearings, reversals, refunds, and declines on demand. Use them to test every state your integration needs to handle without waiting for a real card network event.
Simulation is available in all sandbox projects. These endpoints return an error if called in a production environment.
Simulated transactions are priced like live traffic. The `amount` you pass is the merchant amount, and your program's [cardholder fees](/transactions/fees) are charged on top of it. The amount on the resulting transaction is therefore the merchant amount plus `fees`, in the same way a real transaction reports it. See [Amounts](/transactions/amounts) for the full breakdown.
Fees apply on the same terms as in production. An ATM withdrawal is charged the domestic or the cross-border tier depending on `merchant.country`, a purchase in another currency is charged the FX markup, and an account with its own override is charged that instead. To exercise a fee, pass `channel: ATM` or an `originalCurrency` other than your billing currency; for the cross-border ATM tier, also pass a `merchant.country` other than your card program's.
A simulation is deliberately predictable about what it charges: omitting `channel` runs as `ECOMMERCE` and omitting `merchant.country` places the merchant in your card program's own country. A request that names neither therefore carries no fees, and the same request always charges the same amount.
For the underlying status transitions and what to expect on each event, see [Lifecycle](/transactions/lifecycle).
***
## Scenario 1: Authorization and clearing
1. [Create a card](/api-reference/cards/create-card) with `type: VIRTUAL`
2. [Simulate authorization](/api-reference/simulation/simulate-authorization) with a `cardId` and `amount`
3. Confirm [`CARD_TRANSACTION_CREATED`](/api-reference/card-transaction-created) webhook received with `status: PENDING`
4. [Simulate clearing](/api-reference/simulation/simulate-clearing) with the `transactionId`
5. Confirm [`CARD_TRANSACTION_UPDATED`](/api-reference/card-transaction-updated) webhook received with `status: CLEARED`
***
## Scenario 2: Declined transaction
1. [Create a card](/api-reference/cards/create-card)
2. [Simulate decline](/api-reference/simulation/simulate-decline) with a `cardId`, `amount`, and optional `declineReason` (defaults to `INSUFFICIENT_BALANCE`)
3. Confirm [`CARD_TRANSACTION_CREATED`](/api-reference/card-transaction-created) webhook received with `status: DECLINED`
See [Decline reasons](/transactions/decline-reasons) for the full set of codes you can pass as `declineReason`.
***
## Scenario 3: Decline against a frozen card
1. [Freeze a card](/api-reference/cards/freeze-card)
2. Confirm [`CARD_STATUS_UPDATED`](/api-reference/card-status-updated) webhook received with `status: FROZEN`
3. [Simulate authorization](/api-reference/simulation/simulate-authorization) (confirm it is declined)
4. [Unfreeze a card](/api-reference/cards/unfreeze-card)
5. Confirm [`CARD_STATUS_UPDATED`](/api-reference/card-status-updated) webhook received with `status: ACTIVE`
***
## Scenario 4: Refund on a cleared transaction
1. [Create a card](/api-reference/cards/create-card)
2. [Simulate authorization](/api-reference/simulation/simulate-authorization)
3. [Simulate clearing](/api-reference/simulation/simulate-clearing) with the `transactionId`
4. [Simulate refund](/api-reference/simulation/simulate-refund) with the `transactionId`
5. Confirm [`CARD_TRANSACTION_UPDATED`](/api-reference/card-transaction-updated) webhook received
***
## Scenario 5: External authorization
Applies to **External** [authorization mode](/program-configuration/authorization-mode) projects only. In Managed projects, Reap decides the authorization and there is no endpoint to call.
In an External project, simulation fires a real signed `CARD_AUTHORIZATION_REQUEST` to your registered authorization endpoint and authorizes based on the decision you return. See [The authorization request](/transactions/external-authorization/authorization-request) for the contract.
1. Register your authorization endpoint (a webhook with `mode: REQUEST`) and [create a card](/api-reference/cards/create-card)
2. [Simulate authorization](/api-reference/simulation/simulate-authorization) with a `cardId` and `amount`
3. Your endpoint receives a `CARD_AUTHORIZATION_REQUEST`; return `{ "decision": "APPROVE" }` or a decline
4. Confirm the [`CARD_TRANSACTION_CREATED`](/api-reference/card-transaction-created) webhook reflects your decision: `status: PENDING` on approve, `status: DECLINED` on decline
***
## Scenario 6: Authorization with 3DS challenge (WEBHOOK)
Use this when the card uses `3dsChallengeMethod: WEBHOOK` and you need to exercise the challenge → respond → transaction path end-to-end.
1. [Create a card](/api-reference/cards/create-card) with `3dsChallengeMethod: WEBHOOK` (or [update](/api-reference/cards/update-3ds-challenge-method) an existing card)
2. [Simulate authorization with 3DS](/api-reference/simulation/simulate-3ds-authorization) with a `cardId` and `amount`
3. Confirm [`CARD_3DS_CHALLENGE_CREATED`](/api-reference/3ds-challenge-created) — there is **no** `CARD_TRANSACTION_CREATED` yet
4. [Respond to 3DS challenge](/api-reference/cards/respond-to-3ds-challenge) with `approve: true` or `false`
5. On **approve**, confirm [`CARD_TRANSACTION_CREATED`](/api-reference/card-transaction-created) with `status: PENDING`. On **reject**, the challenge becomes `REJECTED` and **no** transaction is created (same as production — a rejected 3DS never produces a network authorization).
***
## Scenario 7: Detected fraud alert
1. [Create a card](/api-reference/cards/create-card)
2. [Simulate authorization](/api-reference/simulation/simulate-authorization) with `triggerFraudAlert: true`
3. Confirm [`CARD_TRANSACTION_CREATED`](/api-reference/card-transaction-created) webhook received with `status: PENDING`
4. Confirm [`CARD_FRAUD_ALERT_CREATED`](/api-reference/card-fraud-alert-created) webhook received with `origin: DETECTED` and `status: PENDING`
5. [Respond to alert](/api-reference/fraud-alerts/respond-to-alert) with confirm or decline
Detection alone does not block the card. See [Fraud Alerts](/cards/fraud-alerts) for respond and manual-report flows. You can also [report fraud](/api-reference/fraud-alerts/report-fraud) against transactions from any simulation path. Some older simulated transactions might not support fraud reporting, please use a newer transaction in that case.
***
## Simulating card statuses
To force a card into a status that is otherwise hard to trigger (`BLOCKED`, `EXPIRED`), see [Simulating card statuses](/cards/overview#simulating-card-statuses-in-sandbox).
# Defining Virtual Assets
Source: https://docs.reap.global/virtual-assets/definitions
Declare an asset once, pick how its rate is priced, and use it across every account in your project.
Applies to **Program-Funded** programs using **Managed** [authorization mode](/program-configuration/authorization-mode). See [Overview](/virtual-assets/overview) for the bigger picture.
A **virtual asset definition** describes a single unit of value you want Reap to track. Once defined, you can [post deposits, withdrawals, and settlements](/virtual-assets/postings) against it on any account in the project. Each account can hold balances in multiple virtual assets at once, and their combined value (in your program's billing currency) contributes to the account's available balance.
You decide what a virtual asset represents. Stablecoins, non-stable crypto, yield-bearing share tokens, fiat-backed units, points, cashback, and program credits all work through the same primitive: a symbol, a decimal precision, and a way to price it in your program's billing currency.
## Anatomy of a definition
| Field | What it is |
| - | - |
| `symbol` | Short uppercase identifier for the asset (e.g. `USDC`, `BTC`, `GOLD`, `POINTS`). |
| `name` | Human-readable name (e.g. "USD Coin"). |
| `decimals` | Number of decimal places. Match what you use on your side so you never deal with rounding surprises. |
| `rateSource` | `FIXED` or `HTTP`. Determines how the rate is priced. See below. |
| `status` | `ACTIVE` or `DISABLED`. Disabled assets are excluded from balance computation. |
Create a virtual asset via [`POST /virtual-assets`](/api-reference/virtual-assets/create-virtual-asset). Update its name or HTTP endpoint via [`PATCH /virtual-assets/:id`](/api-reference/virtual-assets/update-virtual-asset).
Pick the `symbol` carefully. It surfaces in the account assets breakdown, in the postings payload, and in webhook events. It cannot be changed once the asset is created.
## Rate sources
Every virtual asset has an exchange rate in your program's billing currency. The rate determines how the asset's raw units translate into that currency for balance computation, for authorization, and for settling card debt. See the [worked example](/virtual-assets/postings#worked-example) for the arithmetic on a settlement.
Reap supports two rate sources:
You set the rate, Reap stores it. The rate stays constant until you explicitly update it. Best for stable or client-priced units (USD-denominated credits, loyalty points, internal reference prices).
Reap polls an endpoint you host. The rate updates whenever your endpoint returns a new value. Best for assets priced against a moving market you already have data for (volatile crypto, FX rates, NAV).
Choose per asset. A project can mix both.
### FIXED
Set the initial rate when you create the asset. Update it any time via [`PUT /virtual-assets/:id/rate`](/api-reference/virtual-assets/set-rate-for-virtual-asset). The update takes effect immediately for balance computation.
Typical values:
* `1.00` for a USD-denominated credit like cashback, sign-up bonus, or credit line.
* `0.01` for a point system (one hundred points per USD).
* Any other ratio you want to peg, like a fixed internal conversion for a stablecoin you custody.
Every rate change is stored as a snapshot with a timestamp. Past balances always recompute against the rate that was active at the time the snapshot was taken.
### HTTP
Provide an endpoint URL when you create the asset. Reap polls it every **5 minutes** and stores each new value as a rate snapshot. Card authorizations use the most recent snapshot, not a live fetch. Your endpoint must respond to a `GET` request with this JSON:
```json theme={null}
{ "rate": 3487.42 }
```
Where `rate` is the value of one unit of the virtual asset in your program's billing currency. On creation, Reap fetches your endpoint synchronously to validate it. If the endpoint is unreachable or returns an invalid response, the asset is not created.
Your endpoint should return quickly (within a few seconds) and should not require authentication. Reap caches the most recent successful rate, so transient failures do not stall spending, but prolonged outages will leave balances priced at the last known rate until the endpoint recovers.
You can point the endpoint at anything: your market data service, a cached feed from a pricing provider, a simple proxy to an upstream API. Reap does not prescribe the source.
Update the endpoint URL any time via [`PATCH /virtual-assets/:id`](/api-reference/virtual-assets/update-virtual-asset).
### When to choose which
* The unit is priced in USD already (credits, cashback, allowances).
* The rate is pegged by program design, not by market movement.
* You want deterministic rate changes tied to explicit business decisions.
* The asset tracks a volatile underlying (BTC, ETH, other tokens).
* The asset is a yield-bearing share token whose USD value drifts continuously (vault shares, LST/LRT tokens, money-market deposit receipts). Point the endpoint at whatever source you trust for the live share price.
* You already have a pricing pipeline and do not want to push updates every time a price ticks.
* The rate should reflect your platform's live price, not a manual snapshot.
## Lifecycle
```mermaid theme={null}
flowchart LR
C[Create] --> A[ACTIVE]
A -->|PATCH /virtual-assets/:id
status=DISABLED| D[DISABLED]
```
* **Create.** Defines the asset, validates the rate source (fetches once for HTTP, stores the initial rate for FIXED), activates it. The asset can now receive allocations.
* **Update.** You can edit `name` and, for HTTP assets, `httpEndpoint`. The `symbol`, `decimals`, and `rateSource` are immutable.
* **Set rate.** Available only for FIXED assets. Inserts a new rate snapshot.
* **Disable.** One-way. Disabled assets cannot receive new allocations, and their existing balances are excluded from available balance. Historical entries remain on the ledger for audit.
Disabling a virtual asset is permanent. If users still hold non-zero balances in that asset, their available balance will drop immediately. [Settle](/virtual-assets/postings#settling-card-debt) or [withdraw](/virtual-assets/postings) outstanding balances before disabling if you need to wind the asset down cleanly.
# Overview
Source: https://docs.reap.global/virtual-assets/overview
Unit balances Reap tracks on behalf of your users, contributing to their available balance.
Applies to **Program-Funded** programs using **Managed** [authorization mode](/program-configuration/authorization-mode), the default. See [Funding Model](/program-configuration/funding-models) for where this fits.
**Virtual Assets** are unit balances Reap tracks on behalf of your users. You define the asset (a symbol, a display name, a rate in your program's billing currency), allocate amounts to each account, and Reap turns those allocations into spendable balance the user can use on their card.
You never move real crypto for a virtual asset. The backing is your [master collateral account](/program-configuration/funding-models#program-funded), which covers card spending across the whole program. Virtual Assets are how you tell Reap who gets to spend how much, and in what currency.
## Why they exist
Running a card program usually forces a choice: custody your users' funds with the card issuer, or build your own authorization, clearing, and settlement stack so you can keep custody. Virtual Assets remove that choice.
* You keep the user relationship, the custody model, and the currency denomination you already use.
* Reap authorizes, clears, and settles every card transaction against the master account.
* The user's spending power on Reap is a single number per account per asset, set by you.
## Two primary patterns
Virtual Assets support two patterns that cover almost every program shape we see. They use the same API and the same lifecycle. What differs is what the balance represents on your side.
The virtual balance represents real value you hold for the user off-platform: stablecoins in a custody wallet, crypto on an exchange, fiat in a trust account, gold in vaulted storage. You mirror your internal balances into Reap.
The virtual balance represents value you grant to the user from your own program funds: cashback, sign-up bonuses, salary allowances, credit lines. Nothing to custody, you are the source.
The two patterns can coexist in the same project. A wallet app can allocate **USDC** from custody and **POINTS** as cashback on the same account. The card sees one available balance.
## Use-case vignettes
Users hold USDC, USDT, ETH, or BTC on your platform. Define one virtual asset per token with appropriate rate sources (FIXED for stablecoins, HTTP for volatile assets). Mirror each user's balance into Reap. Their card spends against what they already own.
Define a `POINTS` or `CASHBACK` virtual asset with a FIXED rate (e.g. 1 point = 0.01 USD). Allocate on qualifying transactions. Users spend their rewards through the same card, no separate redemption flow.
Define a `BONUS_USD` virtual asset. Allocate the bonus when onboarding completes. Users see it in their available balance immediately and can spend it on their first transaction.
Define a `CREDIT_USD` virtual asset. Allocate the approved limit to each user who qualifies. The user spends against it. You top up the master account to cover; you handle repayment on your side.
Define a `PAYROLL_USD` virtual asset. Allocate the monthly amount to each employee on payroll day. Optionally deallocate unused balance at period end, or let it roll over.
Allocate **USDC** from real custody for everyday spending, and **CASHBACK\_USD** for rewards earned through the program. Both show up as part of the same available balance on the same card.
## How they feed into available balance
Virtual Asset allocations contribute to the `assets` side of the balance equation on an account:
```
availableBalance = assets - liabilities
```
Each virtual asset is valued in your program's billing currency using its exchange rate. All active allocations on an account are summed. Card debt is subtracted as liabilities.
See [Accounts Overview](/accounts/overview) for the full balance model.
## Lifecycle at a glance
```mermaid theme={null}
flowchart LR
D[Define virtual asset] --> A[Allocate balance to account]
A --> S[User spends on card]
S --> R[Settle card spend]
R --> A
```
1. [Define the virtual asset](/virtual-assets/definitions) once at the project level. Pick a rate source and, for FIXED rates, an initial rate.
2. [Allocate balance](/virtual-assets/postings) to each user's account by posting a `DEPOSIT`. The balance becomes spendable immediately.
3. The user spends. Reap authorizes against the available balance and settles card network debt from the master account. You do not touch this flow.
4. [Settle card spend](/virtual-assets/postings#settling-card-debt) by posting a `SETTLEMENT`. The user's virtual balance drops and the outstanding card debt is cleared in one atomic step.
## What Reap does, what you do
* Authorizes every card swipe against the user's available balance.
* Clears, reverses, and settles card transactions with the card network.
* Custodies the master collateral account.
* Fetches HTTP-source rates on a schedule and stores a snapshot for every update.
* Atomically applies your postings against the user's balance and card debt.
* Decide which virtual assets to offer and their rate sources.
* Post deposits and withdrawals to keep each user's spendable balance in sync with your side.
* Keep the master account topped up to back outstanding spend.
* Post settlements after card spend so the user's virtual balance reflects what they have spent.
## Next
* [Defining virtual assets](/virtual-assets/definitions): rate sources, HTTP endpoint contract, disabling.
* [Virtual Asset postings](/virtual-assets/postings): how to credit, debit, and settle account balances, with patterns per use case.
# Virtual Asset Postings
Source: https://docs.reap.global/virtual-assets/postings
Credit, debit, and settle virtual asset balances on an account through a single endpoint.
Applies to **Program-Funded** programs using **Managed** [authorization mode](/program-configuration/authorization-mode). See [Overview](/virtual-assets/overview) for the bigger picture.
A **virtual asset posting** is how you update an account's virtual asset balances. Every posting carries a `type` that says what kind of change you mean, plus one or more `entries` listing the virtual assets and amounts involved. The whole posting is applied atomically: either all entries land, or none do.
## The three types
| `type` | What it does | Typical use |
| - | - | - |
| `DEPOSIT` | Credits the entries to the account's virtual balance. | User deposits with you, sign-up bonus, cashback, salary, top-up. |
| `WITHDRAWAL` | Debits the entries from the account's virtual balance. Rejected if any entry would take a balance below zero. | User withdraws from you, expiring a bonus, revoking unspent allowance. |
| `SETTLEMENT` | Debits the entries from the account's virtual balance **and** applies their value (in the program's billing currency) to outstanding card debt. | Closing the loop after card spend. |
All three go through the same call:
```
POST /postings
```
See [`POST /postings`](/api-reference/virtual-asset-postings/create-posting) for the full request and response contract, including `settledAmount` and per-entry `rateSnapshot` returned for `SETTLEMENT`.
Submit multiple `entries` in one posting to apply correlated changes together. A multi-asset deposit, a multi-asset settlement, or a deposit-and-bonus stack all go in a single call, so partial states never become visible.
## Patterns
Most programs fall into one of two shapes. They use the same endpoint and the same operations; what differs is what the balance represents on your side, and which postings you actually need.
### Custodial mirror
You hold the user's funds on your side - a custody wallet, an exchange account, a fund - and the virtual balance on Reap mirrors yours so the user can spend on their card.
* **User deposits with you** → `DEPOSIT` for the matching asset.
* **User withdraws from you** → `WITHDRAWAL` for the same asset.
* **User's card spend has cleared** → `SETTLEMENT` for the asset they are paying with. Debits their virtual balance and clears the matching card debt in one step.
The third operation is what makes `SETTLEMENT` distinct from `WITHDRAWAL`. After a card swipe clears, you need to pull from the user's holdings on your side to cover their card spend. A plain `WITHDRAWAL` for that pull would drop the user's available balance a second time - once when the card debt accrued, again when the withdrawal posted. `SETTLEMENT` nets the two against each other so the user's spending position only moves once and matches what they swiped.
Each entry's `amount` is the delta to apply, not the new total. A user depositing 100 USDC becomes a `DEPOSIT` with `entries: [{ virtualAssetId: , amount: "100" }]`; a later 30 USDC withdrawal becomes a `WITHDRAWAL` with `amount: "30"`.
### Issued credits
You give the user balance - cashback, sign-up bonus, salary allowance - funded by your program rather than the user's own holdings. Define one virtual asset per credit type (e.g., `BONUS_USD` with a FIXED rate of `1.00`) and:
* **Grant** → `DEPOSIT`. The user can spend it on their next card swipe.
* **Revoke unused balance** → `WITHDRAWAL`.
Whether you also need `SETTLEMENT` against the granted asset depends on how you want card debt to behave on the account; see the next section.
## Settling card debt
`SETTLEMENT` is how you close card debt against a user's virtual balance. You debit the user's balance for the assets they are paying with, and Reap converts those amounts to the program's billing currency and applies them against the account's outstanding card debt in the same atomic step.
```json theme={null}
{
"type": "SETTLEMENT",
"entries": [
{ "virtualAssetId": "", "amount": "12.50" }
]
}
```
You only pass virtual asset units. Reap computes the settled value in the program's billing currency using each asset's rate at posting time, returns it as `settledAmount` on the response.
### Worked example
Take a USD-billed program with two virtual assets: `YUSD`, 6 decimals, [FIXED](/virtual-assets/definitions#fixed) rate `1`, and `WETH`, 18 decimals, [HTTP](/virtual-assets/definitions#http) rate whose latest snapshot is `3487.42`. An account holds both and has \$150.00 of cleared card debt.
Settle 100 `YUSD` and 0.01 `WETH` in one posting:
```json theme={null}
{
"type": "SETTLEMENT",
"entries": [
{ "virtualAssetId": "", "amount": "100" },
{ "virtualAssetId": "", "amount": "0.01" }
]
}
```
The response echoes the rate each entry was priced at:
```json theme={null}
{
"type": "SETTLEMENT",
"entries": [
{
"virtualAssetId": "",
"amount": "100.000000",
"rateSnapshot": 1
},
{
"virtualAssetId": "",
"amount": "0.010000000000000000",
"rateSnapshot": 3487.42
}
],
"settledAmount": 134.87
}
```
The `YUSD` entry is worth `100 × 1 = 100.00`. The `WETH` entry is worth `0.01 × 3487.42 = 34.8742`, rounded to `34.87` for a USD-billed program. Each entry is rounded to the billing currency on its own, then the entries are summed, so `settledAmount` is `134.87`. That drains the account's cleared debt from \$150.00 to \$15.13, and both virtual balances go down by the amounts you posted.
Because the `WETH` rate moves, the same 0.01 `WETH` settles a different amount tomorrow. `rateSnapshot` is the rate that was actually applied, so you can reconcile a posting without having to guess which snapshot was live when you called.
### When you need it
For [custodial mirror](#custodial-mirror) programs, `SETTLEMENT` is essential. It is how you pull from the user's holdings to cover their card spend without dropping the user's available balance twice.
For [issued-credit](#issued-credits) programs, `SETTLEMENT` is optional. The available balance equation `assets - liabilities` already shows the right number for the user - card debt subtracts from it as soon as a swipe clears, regardless of whether you post anything. Whether you also post `SETTLEMENT` against the credit depends on how you want the books to read:
* **Post `SETTLEMENT`** to drain the credit's virtual balance as the user spends. The virtual balance then reflects credit-still-available, and `cardDebt.cleared` returns to zero. Cleaner per-asset reporting; more API calls.
* **Skip `SETTLEMENT`** and let `cardDebt.cleared` grow as the running total of granted-and-spent. The virtual balance reflects credit-ever-granted, and the user's available position is still correct. Fewer calls; the per-asset view shows totals rather than remaining.
Pick what matches how you reconcile on your side. Both are valid.
### How card debt is drained
Card debt on an account is split into two buckets:
* `pending` - authorizations that have been approved but not yet cleared by the network.
* `cleared` - cleared transactions awaiting settlement from you.
Each `SETTLEMENT` drains `cleared` first, then any overflow drains `pending`. You can read the current split from [`GET /accounts/:id/balance`](/api-reference/accounts/get-account-balance) under `liabilities.cardDebt`.
### Card debt can go negative
If you settle more than the user owes (or the user gets a refund after settlement), `liabilities.cardDebt.total` goes negative. Negative card debt is real credit on the account: it absorbs future card spend before any virtual balance is drained. Reap surfaces the structured split (`pending`, `cleared`, `total`) on the balance endpoint so you can decide how to present it.
## Sizing WITHDRAWAL postings
A `WITHDRAWAL` only checks that its entry does not take the virtual asset's own balance below zero. It does **not** check `availableBalance`. Posting a `WITHDRAWAL` that ignores outstanding card debt can leave the account with a negative `availableBalance` - and the master collateral account holding the bag for spend you have already let the user pull off your side.
To size a `WITHDRAWAL` safely, read the `withdrawable` field on each row of [`GET /accounts/:id/assets`](/api-reference/accounts/get-account-assets). It is the maximum amount of that virtual asset you can debit without taking `availableBalance` below zero. Outstanding card debt - including pending authorizations - is netted out, so the value matches what the account can actually release at the moment of the call.
`withdrawable` is a **per-asset cap, not a budget that adds up across rows**. Each value assumes only that asset is being debited and every other balance stays untouched. Example on an account holding 100 `USDC` virtual and 500 `YUSD` virtual with \$100 of card debt and \$525 of headroom:
* The `USDC` row shows `withdrawable: "100.000000"` (the full balance - well under the \$525 cap).
* The `YUSD` row shows `withdrawable: "500.000000"` (the full balance - same headroom, different asset).
You cannot do both. After each posting, call the endpoint again to read the updated caps.
## Best practices
### Order postings around your actual fund movements
The posting changes Reap's view of what the user can spend. When the matching change happens on your side matters.
* **`DEPOSIT` only after on-chain finalization.** When the deposit you are mirroring is a chain transfer into your custody, wait for it to be finalized (irreversible), not just confirmed once. If you credit on confirmation and the transfer is later reorged out, you have enabled spend the user cannot back. Plain confirmation depth is not enough on chains that re-org.
* **`WITHDRAWAL` before the user withdraws.** If you debit Reap after letting the user pull funds, the user can race the withdrawal against a card swipe and overspend. Post the `WITHDRAWAL` first sized via [`withdrawable`](#sizing-withdrawal-postings); once it returns successfully, you know Reap will reject further spend against that amount, and you can release the funds on your side.
* **`SETTLEMENT` after card spend is observable.** Settle on the [`CARD_TRANSACTION_UPDATED`](/api-reference/card-transaction-updated) clearing webhook or on a periodic cadence using `liabilities.cardDebt.cleared` as the target.
### Use the `Idempotency-Key` header
Every successful posting is a new ledger event, so a retry without idempotency would double-apply. The header is therefore **required** on `POST /postings` - a request without it returns `422`. Pass a stable, request-scoped value (UUID v4 is fine) and keep it across every retry of the same posting; Reap replays the original result instead of creating a duplicate. See [Idempotency](/api-reference/idempotency) for the full behavior.
### One posting per real-world event
A multi-asset deposit on your side becomes one `DEPOSIT` posting with multiple entries, not one posting per asset. This avoids brief windows where Reap shows half the deposit applied, and keeps every entry in the same atomic ledger event for audit.
## What postings are not
* **Not a cap on card spend.** Available balance is the cap. Postings feed into available balance but do not block individual transactions on their own.
* **Not a log of your internal movements.** Only post when you want Reap's view of the user's balance to change. Internal transfers between two of your own buckets do not need a posting.
* **Not how Reap settles with the card network.** That happens automatically against the master collateral account. Postings settle the user side: your obligation to the user, and the user's spend on Reap.
# Overview
Source: https://docs.reap.global/webhooks/overview
Real-time event notifications when resources change.
Reap sends HTTP POST requests to your endpoint whenever resources change. Use webhooks to stay in sync without polling.
## Setup
Register an endpoint by calling [Create a webhook endpoint](/api-reference/webhooks/create-webhook-endpoint) with a `name` and an HTTPS `url`. The response includes a `signingSecret` returned exactly once. Store it securely. Reap does not retain a copy, and there is no way to read it back later.
```json theme={null}
{
"name": "production-events",
"url": "https://api.example.com/webhooks/reap"
}
```
You can register up to 5 active endpoints per project. Every event type is delivered to every active endpoint. To stop deliveries to an endpoint, call [Disable a webhook endpoint](/api-reference/webhooks/disable-webhook-endpoint). Disabling is one-way, so create a new endpoint if you need to resume delivery to the same URL.
To rotate a signing secret (for example if the previous value leaked), call [Rotate the signing secret](/api-reference/webhooks/rotate-signing-secret). The old secret is invalidated immediately. Update your verifier with the new value before the next event arrives, or in-flight deliveries will fail HMAC verification on your endpoint and be retried automatically.
Your endpoint must return a `2xx` status code to acknowledge each delivery. Non-`2xx` responses or timeouts trigger automatic retries with exponential backoff over approximately 48 hours.
## Source IP addresses
Webhook deliveries originate from a fixed set of egress IP addresses per environment. If your endpoint sits behind a firewall or allowlist, permit inbound traffic from all of the addresses for the environment you integrate with.
| Environment | Egress IP addresses |
| - | - |
| Singapore sandbox | `13.228.84.42/32` |
| Singapore production | `13.251.96.90/32`, `13.229.17.218/32`, `3.1.46.185/32` |
| Mexico sandbox | `78.14.100.165/32` |
| Mexico production | `78.13.78.56/32`, `78.12.158.189/32`, `78.14.99.200/32` |
Allowlist every address listed for your environment. These addresses are stable, but we will notify you in advance if they ever change.
## Event envelope
Every delivery wraps event data in a standard envelope:
```json theme={null}
{
"id": "evt_01J5K7Q8R9T0V1W2X3Y4Z5A6B7",
"type": "CRYPTO_DEPOSIT_CREATED",
"data": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"accountId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"chainId": "BASE",
"status": "PENDING",
"amount": "100.00"
}
}
```
| Field | Type | Description |
| - | - | - |
| `id` | `string` | Unique event ID. Use for idempotency. |
| `type` | `string` | Event type, e.g. `CRYPTO_DEPOSIT_CREATED`. |
| `data` | `object` | Payload matching the corresponding API resource. |
## HTTP headers
| Header | Description |
| - | - |
| `X-Reap-Webhook-Id` | Unique event ID (same as `id` in the body). |
| `X-Reap-Webhook-Timestamp` | Unix timestamp (seconds) of signature generation. |
| `X-Reap-Webhook-Signature` | HMAC-SHA256 signature. See [Signature Verification](/webhooks/signature-verification). |
| `Content-Type` | Always `application/json`. |
## Event types
| Event | Description |
| - | - |
| `USER_APPLICATION_STATUS_UPDATED` | User application status changed (KYC approval, rejection, etc.) |
| `COMPANY_STATUS_UPDATED` | Company status changed (KYB approval, rejection, etc.) |
| `CRYPTO_DEPOSIT_CREATED` | New on-chain deposit detected |
| `CRYPTO_DEPOSIT_STATUS_UPDATED` | Deposit status changed (approved, rejected, etc.) |
| `CARD_STATUS_UPDATED` | Card status changed (activated, frozen, blocked, etc.) |
| `ACCOUNT_STATUS_UPDATED` | Account status changed (restricted, reactivated, etc.) |
| `CARD_TRANSACTION_CREATED` | New card transaction |
| `CARD_TRANSACTION_UPDATED` | Card transaction lifecycle event (clearing, reversal, refund, dispute refund, etc.) |
| `CARD_3DS_CHALLENGE_CREATED` | 3DS challenge created for a cardholder (see [3D Secure](/cards/3d-secure)) |
| `CARD_SHIPMENT_STATUS_UPDATED` | Physical card shipment status changed |
| `CARD_TOKENIZATION_REQUESTED` | OTP for adding a card to a digital wallet via SMS verification (valid for a short window) |
| `CARD_FRAUD_ALERT_CREATED` | Fraud alert created (detected or reported). See [Fraud Alerts](/cards/fraud-alerts) |
| `CARD_FRAUD_ALERT_STATUS_UPDATED` | Fraud alert status changed (confirmed, declined, or expired) |
| `CARD_DISPUTE_STATUS_UPDATED` | Card dispute status changed |
Payloads match the shape of the corresponding API resource. For example, `CRYPTO_DEPOSIT_CREATED` data matches `GET /crypto-deposits/:id`.
## Delivery guarantees
Every event is delivered **at least once**, and **in no particular order**.
A delivery that times out or returns a non-`2xx` status is retried with exponential backoff over approximately 48 hours. A response we never see looks the same to us as a failure, so an event you already processed can arrive again. Deduplicate on the envelope `id`, which is stable across every redelivery.
Ordering is not guaranteed for any pair of events, including two events about the same resource. Treat each delivery as a statement about one resource at a point in time rather than a step in a sequence: upsert on the resource id in `data`, and ignore a payload whose `updatedAt` is older than what you have already stored.
## Best practices
* **Return quickly.** Respond with `2xx` within a few seconds. Process heavy work in the background.
* **Be idempotent.** Events may be delivered more than once. Use the `id` field to skip duplicates.
* **Do not rely on ordering.** Reconcile from the payload rather than the arrival sequence.
* **Verify signatures.** Always [verify the signature](/webhooks/signature-verification) before processing.
# Signature Verification
Source: https://docs.reap.global/webhooks/signature-verification
Verify webhook requests originate from Reap.
Every webhook is signed with HMAC-SHA256 using your project's signing secret. Verify the signature before processing any event.
## Signature format
The `X-Reap-Webhook-Signature` header looks like this:
```
t=1709312400,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8f9
```
`t` is a Unix timestamp (seconds). `v1` is the hex-encoded HMAC-SHA256 signature.
## How to verify
Split `X-Reap-Webhook-Signature` on `,`. Extract the `t=` timestamp and `v1=` signature.
Concatenate timestamp and raw request body with a `.` separator: `{timestamp}.{raw_body}`
Use the raw request body, not a re-serialized version. Re-stringifying JSON changes whitespace and key order, which breaks verification.
Compute HMAC-SHA256 of the signed payload using your secret. Compare the hex digest to `v1` using constant-time comparison.
Reject requests where the timestamp is more than 5 minutes old to prevent replay attacks.
## JavaScript example
```javascript theme={null}
import { createHmac, timingSafeEqual } from "crypto";
const WEBHOOK_SECRET = process.env.REAP_WEBHOOK_SECRET;
const TOLERANCE_SECONDS = 300;
function verifyWebhook(rawBody, signatureHeader) {
const parts = signatureHeader.split(",");
let timestamp;
let signature;
for (const part of parts) {
const [key, value] = part.split("=", 2);
if (key === "t") timestamp = parseInt(value, 10);
if (key === "v1") signature = value;
}
if (!timestamp || !signature) {
throw new Error("Invalid signature header");
}
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > TOLERANCE_SECONDS) {
throw new Error("Timestamp outside tolerance");
}
const signedPayload = `${timestamp}.${rawBody}`;
const expected = createHmac("sha256", WEBHOOK_SECRET)
.update(signedPayload)
.digest("hex");
const expectedBuf = Buffer.from(expected, "hex");
const signatureBuf = Buffer.from(signature, "hex");
if (
expectedBuf.length !== signatureBuf.length ||
!timingSafeEqual(expectedBuf, signatureBuf)
) {
throw new Error("Invalid signature");
}
return JSON.parse(rawBody);
}
```
## Express.js example
Capture the raw body before JSON parsing:
```javascript theme={null}
import express from "express";
const app = express();
app.use("/webhooks", express.raw({ type: "application/json" }));
app.post("/webhooks", (req, res) => {
try {
const event = verifyWebhook(
req.body.toString("utf-8"),
req.headers["x-reap-webhook-signature"]
);
// process event
res.sendStatus(200);
} catch (err) {
res.status(400).json({ error: err.message });
}
});
```
## Security notes
* Always use constant-time comparison (`timingSafeEqual`). String equality leaks timing information.
* 5 minutes is the recommended timestamp tolerance.
* Keep your signing secret safe. If it leaks, rotate it via [Rotate the signing secret](/api-reference/webhooks/rotate-signing-secret). The previous value is invalidated immediately.