# Fyatu Developer Docs Source: https://docs.fyatu.com/home API references, integration guides, and webhook documentation. Select the platform you're building on.
# Fyatu Developer Docs

API references, integration guides, and webhook documentation.
Select the platform you're building on.

Fyatu Business
V3 · Stable Basic

For early-stage startups getting started with card issuing. Issue virtual prepaid Visa and Mastercard cards, accept payments from Fyatu users, and send payouts — all through a single REST API.

Fyatu Business dashboard
Self-service — upgrade your Fyatu Individual account to Business from the dashboard. No approval required.
Fyatu CaaS
V3.20 · CaaS Advanced

For companies managing card programs at scale. Run your own branded card program — create products, manage cardholders, issue cards, and monitor spending in real time.

Fyatu CaaS portal dashboard
By invitation — contact the Fyatu sales team, sign an agreement, and pay the program setup fee to get access.
# Generate Deposit Address Source: https://docs.fyatu.com/v3/api-reference/account/deposit-address v3/openapi.json POST /account/deposit-address Generate a USDT deposit address to fund your Fyatu business wallet. POST /account/deposit-address. ## Overview Generate a deposit address for receiving cryptocurrency. Select your preferred currency (USDT or USDC) and network (TRON, ETH, BSC, etc.). ## Supported Currencies | Currency | Description | | -------- | ----------- | | `USDT` | Tether USD | | `USDC` | USD Coin | ## Supported Networks | Network | Address Type | Compatible Currencies | | ----------- | ------------ | --------------------- | | `TRON` | TRC20 | USDT, USDC | | `ETH` | ERC20 | USDT, USDC | | `BSC` | ERC20 | USDT, USDC | | `POLYGON` | ERC20 | USDT, USDC | | `ARBITRUM` | ERC20 | USDT, USDC | | `AVALANCHE` | ERC20 | USDT, USDC | Networks are grouped by address format. TRC20 addresses work only on TRON. ERC20/BEP20 addresses use the same format and work on all EVM-compatible chains (ETH, BSC, Polygon, Arbitrum, Avalanche). ## Address Reuse If you already have an address for the requested address type (TRC20 or ERC20), the existing address is returned instead of generating a new one. The response includes `isNew: false` in this case. ## Example Usage ```javascript theme={null} const response = await fetch('https://api.fyatu.com/api/v3/account/deposit-address', { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ currency: 'USDT', network: 'TRON' }) }); const { data } = await response.json(); console.log(`Address: ${data.address}`); console.log(`Type: ${data.addressType}`); // TRC20 or ERC20 console.log(`New address: ${data.isNew}`); ``` Always verify you're sending on the correct network. Sending crypto on the wrong network may result in permanent loss of funds. ## Compatible Networks The response includes a `compatibleNetworks` array showing which networks can use this address: * **TRC20**: Only `["TRON"]` * **ERC20**: `["ETH", "BSC", "POLYGON", "ARBITRUM", "AVALANCHE"]` # Get Invoices Source: https://docs.fyatu.com/v3/api-reference/account/invoices v3/openapi.json GET /account/invoices List your business billing invoices — subscription charges, outstanding amounts, and payment status. GET /account/invoices. ## Overview Retrieve a paginated list of billing invoices issued to your business account. Invoices are generated monthly for subscription-based pricing plans and may also be issued for outstanding fees. ## Pagination | Parameter | Default | Max | | --------- | ------- | --- | | `page` | 1 | - | | `perPage` | 20 | 100 | ## Response Fields | Field | Type | Description | | ------------- | ------------ | --------------------------------------------------------- | | `invoiceId` | string | Unique invoice identifier | | `period` | string | Billing period in `YYYY-MM` format | | `subtotal` | number | Amount before any adjustments | | `totalAmount` | number | Total amount due | | `paidAmount` | number | Amount already paid | | `currency` | string | Always `USD` | | `status` | string | `PENDING`, `PAID`, `PARTIALLY_PAID`, `OVERDUE`, or `VOID` | | `issuedAt` | string | ISO 8601 timestamp when invoice was issued | | `dueDate` | string\|null | Due date in `YYYY-MM-DD` format | | `paidAt` | string\|null | ISO 8601 timestamp when invoice was fully paid | ## Invoice Statuses | Status | Description | | ---------------- | ------------------------------------------------------- | | `PENDING` | Invoice issued, payment not yet received | | `PAID` | Invoice fully paid | | `PARTIALLY_PAID` | Partial payment received; remaining balance outstanding | | `OVERDUE` | Past due date without full payment | | `VOID` | Invoice cancelled | ## Example Usage ```javascript theme={null} const response = await fetch('https://api.fyatu.com/api/v3/account/invoices?page=1&perPage=10', { headers: { 'Authorization': `Bearer ${accessToken}` } }); const { data } = await response.json(); console.log(`Total invoices: ${data.pagination.totalItems}`); data.invoices.forEach(inv => { const outstanding = inv.totalAmount - inv.paidAmount; console.log(`Invoice ${inv.invoiceId} (${inv.period}): $${inv.totalAmount} — ${inv.status}`); if (outstanding > 0) { console.log(` Outstanding: $${outstanding.toFixed(2)}`); } }); ``` ## Use Cases 1. **Billing overview**: See all invoices and their payment status 2. **Outstanding balance**: Find unpaid or partially paid invoices 3. **Payment history**: Confirm when invoices were settled # Get Pricing Source: https://docs.fyatu.com/v3/api-reference/account/pricing v3/openapi.json GET /account/pricing Retrieve current Fyatu API pricing and fee schedule — card issuance fees, funding fees, and transaction costs. GET /account/pricing. ## Overview Retrieve the fees applicable to your business account. The fees returned depend on your app type: | App Type | Fee Categories | | ------------ | ---------------------------- | | `collection` | Collection fees, Payout fees | | `issuing` | Card fees | ## Fee Structure Each fee in the response includes: | Field | Type | Description | | ---------- | ------- | ----------------------------------------------------- | | `code` | string | Unique fee identifier | | `name` | string | Human-readable fee name | | `type` | string | `FIXED` or `PERCENTAGE` | | `rate` | number | Fee amount (USD for FIXED, percentage for PERCENTAGE) | | `minFee` | number | Minimum fee (for percentage-based fees) | | `maxFee` | number | Maximum fee cap (if applicable) | | `isWaived` | boolean | Whether this fee is currently waived for your account | ## Collection App Fees | Fee Code | Description | | ---------------- | ------------------------------ | | `COLLECTION_FEE` | Fee per collection transaction | | `PAYOUT_FEE` | Fee per payout/disbursement | ## Issuing App Fees | Fee Code | Description | | ---------------------- | ------------------------------------------ | | `CARD_ISSUANCE_FEE` | One-time fee when creating a card | | `CARD_FUNDING_FEE` | Fee when adding funds to a card | | `CARD_UNLOADING_FEE` | Fee when withdrawing from a card | | `CARD_REPLACEMENT_FEE` | Fee when replacing a card | | `CARD_MONTHLY_FEE` | Monthly maintenance fee | | `CARD_TRANSACTION_FEE` | Fee applied to card transactions | | `DECLINE_FEE` | Fee charged on a declined card transaction | ## Example Usage ```javascript theme={null} const response = await fetch('https://api.fyatu.com/api/v3/account/pricing', { headers: { 'Authorization': `Bearer ${accessToken}` } }); const { data } = await response.json(); console.log(`App Type: ${data.appType}`); data.fees.forEach(fee => { if (fee.type === 'FIXED') { console.log(`${fee.name}: $${fee.rate}`); } else { console.log(`${fee.name}: ${fee.rate}%`); } }); ``` Check `isWaived` to determine if a fee is currently waived for your account, often as part of promotional offers or custom pricing arrangements. # Get Statement Source: https://docs.fyatu.com/v3/api-reference/account/statement v3/openapi.json GET /account/statement Get the last 100 ledger entries for your business account with running balance snapshots. GET /account/statement. ## Overview Retrieve the last 100 ledger entries for your business account. Each entry includes the account balance **before** and **after** the operation, derived from the immutable double-entry ledger. This endpoint is designed for accounting and reconciliation use cases where you need to verify the exact balance impact of each operation. For a standard transaction list (category, fee, status, reference), use `GET /account/transactions` instead. The statement endpoint focuses on balance movements and does not include fee or status details. ## Response Fields | Field | Type | Description | | --------- | ------ | ----------------------------------------------- | | `entries` | array | Array of up to 100 ledger entries, newest first | | `count` | number | Number of entries returned | ### Entry Fields | Field | Type | Description | | --------------- | ------ | ------------------------------------------------------------------------- | | `transactionId` | string | Transaction batch ID (matches `transactionId` in `/account/transactions`) | | `type` | string | `CREDIT` or `DEBIT` | | `amount` | number | Amount in USD | | `currency` | string | Always `USD` | | `description` | string | Ledger description of the operation | | `balanceBefore` | number | Account balance before this entry (USD) | | `balanceAfter` | number | Account balance after this entry (USD) | | `createdAt` | string | ISO 8601 timestamp | ## Example Usage ```javascript theme={null} const response = await fetch('https://api.fyatu.com/api/v3/account/statement', { headers: { 'Authorization': `Bearer ${accessToken}` } }); const { data } = await response.json(); console.log(`Statement entries: ${data.count}`); data.entries.forEach(entry => { console.log(`${entry.type}: $${entry.amount} — ${entry.description}`); console.log(` Balance: $${entry.balanceBefore} → $${entry.balanceAfter}`); }); ``` ## Use Cases 1. **Reconciliation**: Verify the exact balance before and after each operation 2. **Audit trail**: Confirm that every debit and credit is accounted for 3. **Balance verification**: Cross-check your current balance against the last ledger entry's `balanceAfter` # Get Transaction Source: https://docs.fyatu.com/v3/api-reference/account/transaction v3/openapi.json GET /account/transactions/{transactionId} Get details of a specific business wallet transaction by ID — category, amount, fee, status, and description. GET /account/transactions/{id}. ## Overview Retrieve details of a specific transaction from your business account by its transaction ID. ## Path Parameters | Parameter | Type | Description | | --------------- | ------ | ------------------------------------------------------------------ | | `transactionId` | string | Transaction batch ID (e.g. `FYB69F984AA38EFB`, `DEP69FE561280613`) | ## Response Fields | Field | Type | Description | | --------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `transactionId` | string | Unique transaction batch ID | | `reference` | string | Card ID, payment reference, or on-chain hash | | `type` | string | `CREDIT` or `DEBIT` | | `category` | string | Raw ledger category, e.g. `DEPOSIT`, `WITHDRAW`, `CARD` (card issuance, funding, and unloading), `TERMINATION`, `TRANSFER`, `COLLECTION`, `PAYOUT`, `SUBSCRIPTION`, `AIRTIME`, `ESIM` | | `amount` | number | Transaction amount | | `fee` | number | Fee charged | | `currency` | string | `USD` or `USDT` for crypto deposits | | `status` | string | `PENDING`, `COMPLETED`, `FAILED`, or `PROCESSING` | | `description` | string | Cardholder name, counterparty, or asset name | | `createdAt` | string | ISO 8601 timestamp | | `updatedAt` | string | ISO 8601 timestamp of last status change | ### Withdrawal Object For transactions with `category` = `WITHDRAW`, the response also includes a nested `withdrawal` object: | Field | Type | Description | | ----------- | -------------- | --------------------------------------------------------------------- | | `address` | string | Destination wallet address | | `network` | string | Network the payout was sent on (defaults to `TRC20` when unspecified) | | `netAmount` | number | Amount sent after the withdrawal fee | | `txHash` | string \| null | On-chain transaction hash once settled, otherwise `null` | ## Example Usage ```javascript theme={null} const response = await fetch('https://api.fyatu.com/api/v3/account/transactions/FYB69E67E1B227D5', { headers: { 'Authorization': `Bearer ${accessToken}` } }); const { data } = await response.json(); console.log(`Transaction: ${data.transactionId}`); console.log(`Type: ${data.type} (${data.category})`); console.log(`Amount: $${data.amount}, Fee: $${data.fee}`); console.log(`Status: ${data.status}`); console.log(`Description: ${data.description}`); ``` ## Use Cases 1. **Verify a credit**: Confirm a deposit or collection payment landed in your wallet 2. **Card funding confirmation**: Verify that a card funding operation was settled 3. **Audit trail**: Inspect fee and counterparty details for any transaction # Get Transactions Source: https://docs.fyatu.com/v3/api-reference/account/transactions v3/openapi.json GET /account/transactions List all business wallet transactions with category, status, and fee data. GET /account/transactions. ## Overview Retrieve a paginated list of all transactions on your business account. This includes card issuance, funding, unloading, termination, transfers, collections, payouts, and deposits. ## Filtering | Parameter | Description | | --------- | ---------------------------------------------------------------- | | `type` | Filter by transaction direction: `CREDIT` or `DEBIT` | | `status` | Filter by status: `PENDING`, `COMPLETED`, `FAILED`, `PROCESSING` | ## Pagination | Parameter | Default | Max | | --------- | ------- | --- | | `page` | 1 | - | | `perPage` | 50 | 100 | ## Response Fields | Field | Type | Description | | --------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `transactionId` | string | Unique transaction batch ID | | `reference` | string | Card ID, payment reference, or on-chain hash depending on category | | `type` | string | `CREDIT` or `DEBIT` | | `category` | string | Raw ledger category, e.g. `DEPOSIT`, `WITHDRAW`, `CARD` (card issuance, funding, and unloading), `TERMINATION`, `TRANSFER`, `COLLECTION`, `PAYOUT`, `SUBSCRIPTION`, `AIRTIME`, `ESIM` | | `amount` | number | Transaction amount | | `fee` | number | Fee charged for this transaction | | `currency` | string | `USD` or `USDT` for crypto deposits | | `status` | string | `PENDING`, `COMPLETED`, `FAILED`, or `PROCESSING` | | `description` | string | Cardholder name, counterparty, or asset name | | `createdAt` | string | ISO 8601 timestamp | | `updatedAt` | string | ISO 8601 timestamp of last status change | ## Example Usage ```javascript theme={null} const response = await fetch('https://api.fyatu.com/api/v3/account/transactions?page=1&perPage=50', { headers: { 'Authorization': `Bearer ${accessToken}` } }); const { data } = await response.json(); console.log(`Total transactions: ${data.pagination.totalItems}`); data.transactions.forEach(tx => { console.log(`${tx.type} (${tx.category}): $${tx.amount} — ${tx.description}`); console.log(` Status: ${tx.status}, Fee: $${tx.fee}`); }); ``` # Get Wallet Source: https://docs.fyatu.com/v3/api-reference/account/wallet v3/openapi.json GET /account/wallet Get your Fyatu business wallet balance and currency details. GET /account/wallet. ## Overview Retrieve your business wallet information including: * **Balance**: Available, hold, and total amounts in USD * **Deposit Addresses**: TRC20 and ERC20 addresses for receiving crypto deposits * **Withdrawal Address**: Your configured USDT withdrawal destination ## Response Details ### Balance Object | Field | Type | Description | | ----------- | ------ | ----------------------------------------------- | | `available` | number | Balance available for use (total - hold) | | `hold` | number | Amount currently on hold for pending operations | | `total` | number | Total account balance | | `currency` | string | Always `USD` | The response also includes a top-level `totalCardBalance` (number, USD) — the total available balance loaded across your business's spendable cards. This is distinct from the wallet balance above, which is the unloaded balance available to fund cards. ### Deposit Addresses The deposit object contains: * **TRC20 Address**: For deposits on TRON network * **ERC20 Address**: For deposits on ETH, BSC, Polygon, Arbitrum, Avalanche networks Both address types accept USDT and USDC. If you don't have a deposit address yet, use the [Generate Deposit Address](/v3/api-reference/account/deposit-address) endpoint to create one. ### Withdrawal Address Returns `null` if no withdrawal address is configured. Otherwise includes: | Field | Type | Description | | ------------ | ---------------- | --------------------------------------------------------------------------------------------------- | | `address` | string | The withdrawal wallet address | | `currency` | string | `USDT` or `USDC` | | `network` | string | The chain the address is registered on: `TRON`, `ETH`, `BSC`, `POLYGON`, `ARBITRUM`, or `AVALANCHE` | | `isVerified` | boolean | Whether the address is verified for withdrawals | | `addedAt` | datetime \| null | When the address was registered (currently returned as `null`) | ## Example Usage ```javascript theme={null} const response = await fetch('https://api.fyatu.com/api/v3/account/wallet', { headers: { 'Authorization': `Bearer ${accessToken}` } }); const { data } = await response.json(); console.log(`Available: $${data.balance.available}`); console.log(`TRC20 Address: ${data.deposit.addresses.TRC20?.address}`); ``` # Request Withdrawal Source: https://docs.fyatu.com/v3/api-reference/account/withdraw v3/openapi.json POST /account/withdraw Request a withdrawal from your Fyatu business wallet to a registered address. POST /account/withdraw. ## Overview Request a withdrawal to your registered withdrawal address. The withdrawal currency (USDT or USDC) and network are determined by your registered withdrawal address. The funds (amount + fee) are moved from your available balance to a hold balance pending confirmation, which can take up to 24 hours. ## Supported Currencies & Networks Withdrawals support USDT and USDC on multiple networks: | Currency | Networks | | -------- | -------------------------------------------- | | `USDT` | TRON, ETH, BSC, Polygon, Arbitrum, Avalanche | | `USDC` | TRON, ETH, BSC, Polygon, Arbitrum, Avalanche | The currency and network are set when you register your withdrawal address. ## Prerequisites Before requesting a withdrawal, you must: 1. **Register a withdrawal address** via `POST /account/withdrawal-address` (specify currency and network) 2. **Have your withdrawal address verified** (automatic for valid addresses) 3. **Have sufficient available balance** to cover amount + withdrawal fee ## Request Body | Field | Type | Required | Description | | ---------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `amount` | number | Yes | Amount to withdraw (before fee) | | `currency` | string | No | `USDT` (default) or `USDC`. Validated for accepted values; the effective withdrawal currency is taken from your registered withdrawal address. | ## How It Works 1. **Validation**: System validates your withdrawal address is registered and verified 2. **Fee Calculation**: Withdrawal fee is calculated based on your business fee configuration 3. **Balance Check**: Ensures available balance >= amount + fee 4. **Hold Funds**: Moves total amount (amount + fee) from available balance to hold balance 5. **Create Transaction**: Creates a PENDING withdrawal transaction record ## Withdrawal Statuses | Status | Description | | ------------ | ------------------------------------------------------ | | `PENDING` | Withdrawal request submitted, awaiting processing | | `PROCESSING` | Withdrawal is being processed on the blockchain | | `COMPLETED` | Withdrawal completed successfully, funds sent | | `FAILED` | Withdrawal failed, funds returned to available balance | ## Example Usage ```javascript theme={null} const response = await fetch('https://api.fyatu.com/api/v3/account/withdraw', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` }, body: JSON.stringify({ amount: 100.00 }) }); const { data } = await response.json(); console.log(`Withdrawal ID: ${data.withdrawalId}`); console.log(`Amount: ${data.amount} ${data.currency}`); console.log(`Fee: ${data.fee}`); console.log(`Total Deducted: ${data.totalAmount}`); console.log(`To Address: ${data.address} (${data.network})`); console.log(`Status: ${data.status}`); console.log(`Est. Completion: ${data.estimatedCompletion}`); ``` ## Error Responses | Error Code | Description | | ------------------------------- | -------------------------------------------------- | | `WITHDRAWAL_ADDRESS_MISSING` | No withdrawal address registered | | `WITHDRAWAL_ADDRESS_UNVERIFIED` | Withdrawal address not yet verified | | `INSUFFICIENT_BALANCE` | Not enough available balance to cover amount + fee | | `HOLD_FAILED` | Funds could not be placed on hold | # Register Withdrawal Address Source: https://docs.fyatu.com/v3/api-reference/account/withdrawal-address v3/openapi.json POST /account/withdrawal-address Register a withdrawal destination address for your Fyatu business wallet. POST /account/withdrawal-address. ## Overview Register a withdrawal address for receiving USDT or USDC payouts on supported networks. Only one withdrawal address can be active at a time. If an address already exists, returns the existing address. ## Supported Currencies | Currency | Description | | -------- | -------------------- | | `USDT` | Tether USD (default) | | `USDC` | USD Coin | ## Supported Networks | Network | Address Format | Description | | ----------- | ------------------------------- | ----------------------------- | | `TRON` | Starts with `T`, 34 characters | TRC20 - Low fees, recommended | | `ETH` | Starts with `0x`, 42 characters | Ethereum mainnet | | `BSC` | Starts with `0x`, 42 characters | BNB Smart Chain | | `POLYGON` | Starts with `0x`, 42 characters | Polygon network | | `ARBITRUM` | Starts with `0x`, 42 characters | Arbitrum One | | `AVALANCHE` | Starts with `0x`, 42 characters | Avalanche C-Chain | ### Request Body | Field | Type | Required | Description | | ---------- | ------ | -------- | --------------------------------------------------------------------- | | `address` | string | Yes | Crypto wallet address | | `currency` | string | No | `USDT` (default) or `USDC` | | `network` | string | No | `TRON` (default), `ETH`, `BSC`, `POLYGON`, `ARBITRUM`, or `AVALANCHE` | ### Example Request ```javascript theme={null} const response = await fetch('https://api.fyatu.com/api/v3/account/withdrawal-address', { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ address: 'TYourTRC20AddressHere...', currency: 'USDT', network: 'TRON' }) }); ``` If a withdrawal address already exists, this endpoint returns the existing address. To set a new address, you must first delete the current one. ## Deleting Withdrawal Address To change your withdrawal address, use `DELETE /account/withdrawal-address` to remove the current one first. ```javascript theme={null} const response = await fetch('https://api.fyatu.com/api/v3/account/withdrawal-address', { method: 'DELETE', headers: { 'Authorization': `Bearer ${accessToken}` } }); ``` Pending withdrawals will fail if you delete your withdrawal address. Ensure all withdrawals are complete before changing addresses. ## Verification Status New withdrawal addresses start as unverified (`isVerified: false`). Verification may be required before processing large withdrawals. # Refresh Token Source: https://docs.fyatu.com/v3/api-reference/auth/refresh v3/openapi.json POST /auth/refresh Refresh an expiring Fyatu API access token without re-authenticating. POST /auth/refresh. ## Overview Refresh an existing JWT token to obtain a new one without re-authenticating with credentials. Tokens can be refreshed up to **5 minutes after expiry**. ## When to Refresh * Token is about to expire (less than 5 minutes remaining) * Token just expired (within 5-minute grace period) * You want to extend an active session If your token expired more than 5 minutes ago, you must obtain a new token using the [Generate Token](/v3/api-reference/auth/token) endpoint. ## Token Refresh Strategy Implement automatic token refresh in your application: ```javascript theme={null} class TokenManager { constructor(appId, secretKey) { this.appId = appId; this.secretKey = secretKey; this.token = null; this.expiresAt = null; } async getToken() { if (!this.token || this.isExpiringSoon()) { await this.refreshOrAuthenticate(); } return this.token; } isExpiringSoon() { if (!this.expiresAt) return true; const fiveMinutes = 5 * 60 * 1000; return (new Date(this.expiresAt) - new Date()) < fiveMinutes; } async refreshOrAuthenticate() { if (this.token && this.canRefresh()) { try { const res = await fetch('https://api.fyatu.com/api/v3/auth/refresh', { method: 'POST', headers: { 'Authorization': `Bearer ${this.token}` } }); const data = await res.json(); if (data.success) { this.token = data.data.accessToken; this.expiresAt = data.data.expiresAt; return; } } catch (e) { /* Fall through to authenticate */ } } // Get fresh token const res = await fetch('https://api.fyatu.com/api/v3/auth/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ appId: this.appId, secretKey: this.secretKey, grantType: 'client_credentials' }) }); const data = await res.json(); if (data.success) { this.token = data.data.accessToken; this.expiresAt = data.data.expiresAt; } } } ``` ## Error Codes | Code | Description | | --------------------------- | -------------------------------------------------------------------- | | `AUTH_TOKEN_MISSING` | No Authorization (Bearer) header provided | | `AUTH_TOKEN_REFRESH_FAILED` | Token too old (>5 min after expiry), malformed, or otherwise invalid | Refresh tokens proactively before they expire to ensure uninterrupted API access. # Revoke Token Source: https://docs.fyatu.com/v3/api-reference/auth/revoke v3/openapi.json POST /auth/revoke Revoke an active Fyatu API access token to invalidate a session. POST /auth/revoke. ## Overview Invalidate an access token before it naturally expires. Use this when: * User logs out of your application * You detect suspicious activity * Credentials may have been compromised * Token is no longer needed ## When to Revoke Tokens When a user explicitly logs out, revoke their token to prevent unauthorized access. ```javascript theme={null} async function logout() { await fetch('https://api.fyatu.com/api/v3/auth/revoke', { method: 'POST', headers: { 'Authorization': `Bearer ${currentToken}` } }); localStorage.removeItem('fyatu_token'); } ``` If you suspect a token has been compromised, revoke it immediately. ```javascript theme={null} async function handleSecurityIncident(compromisedToken) { await fetch('https://api.fyatu.com/api/v3/auth/revoke', { method: 'POST', headers: { 'Authorization': `Bearer ${compromisedToken}` } }); // Get a fresh token with new credentials } ``` When rotating API credentials, revoke existing tokens first. ## Error Codes | Code | Description | | -------------------- | ------------------------------------- | | `AUTH_TOKEN_MISSING` | No Authorization header provided | | `AUTH_TOKEN_INVALID` | Token is malformed or already expired | Once a token is revoked, it cannot be used for any API requests. Any in-flight requests using the revoked token may fail. After revoking a token, immediately clear it from your application's storage to prevent accidental reuse. # Generate Token Source: https://docs.fyatu.com/v3/api-reference/auth/token v3/openapi.json POST /auth/token Generate a JWT access token for Fyatu API v3 using client credentials. POST /auth/token with appId and secretKey. **Getting Your Credentials:** Login to [FYATU Dashboard](https://web.fyatu.com) → Business Console → Select App → Settings → API Keys & Credentials ## Overview Exchange your app credentials (`appId` and `secretKey`) for a JWT access token. This token is required to authenticate all other V3 API requests. ## Token Details | Property | Value | | -------------- | ---------------------------- | | Token Type | JWT (HS256) | | Expiry | 24 hours (86400 seconds) | | Refresh Window | Up to 5 minutes after expiry | ## Scopes by App Type | Scope | Description | | --------------- | ------------------------------------------ | | `collect:write` | Create checkout sessions, process payments | | `collect:read` | View collection transactions | | `payout:write` | Send payouts | | `payout:read` | View payout transactions | | Scope | Description | | ------------------- | ---------------------------------- | | `cards:write` | Create cards, fund, freeze | | `cards:read` | View card details and transactions | | `cardholders:write` | Create and update cardholders | | `cardholders:read` | View cardholder details | ## Using the Token Once you have an access token, include it in the `Authorization` header for all API requests: ```bash theme={null} curl -X GET https://api.fyatu.com/api/v3/collections \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ``` ## Error Codes | Code | Description | | -------------------------- | ------------------------------------- | | `VALIDATION_ERROR` | Missing or invalid request parameters | | `AUTH_INVALID_CREDENTIALS` | App not found or secret key mismatch | | `AUTH_APP_INACTIVE` | App is suspended or archived | Store tokens securely and track the `expiresAt` timestamp. Refresh tokens proactively before they expire to ensure uninterrupted API access. # Create Cardholder Source: https://docs.fyatu.com/v3/api-reference/cardholders/create v3/openapi.json POST /cardholders Create a new cardholder for virtual card issuing. Submit personal details and start issuing cards immediately. POST /cardholders. ## Overview Create a new cardholder for your card issuing program. The cardholder will be created with `ACTIVE` status. You can issue cards to the cardholder immediately after creation. ## Required Fields | Field | Type | Description | | ------------- | ------ | --------------------------------------------------- | | `firstName` | string | Cardholder's first name (1-100 chars) | | `lastName` | string | Cardholder's last name (1-100 chars) | | `email` | string | Email address (unique per business) | | `phone` | string | Phone number in E.164 format (e.g. `+15550001234`) | | `dateOfBirth` | string | Date of birth (`YYYY-MM-DD`, must be 18+ years old) | | `gender` | string | Gender (`MALE` or `FEMALE`) | | `country` | string | ISO 3166-1 alpha-2 country code (e.g. `US`) | ## Address Fields | Field | Type | Required | Description | | --------- | ------ | -------- | --------------------------------- | | `address` | string | Yes | Street address (max 255 chars) | | `city` | string | Yes | City (max 100 chars) | | `state` | string | Yes | State or province (max 100 chars) | | `zipCode` | string | Yes | Postal/ZIP code (max 20 chars) | ## Optional Fields | Field | Type | Description | | ------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `middleName` | string | Optional middle name (max 15 chars). Card issuers cap the number of active cards per *identical first + last name*, so set a `middleName` to distinguish two cardholders who share the same first and last name — when present it is sent to the card network on card creation so they count as separate holders. Unlike `firstName`/`lastName`, `middleName` stays editable after KYC approval. | | `externalId` | string | Your platform's cardholder ID (unique per business, max 100 chars) | | `metadata` | object | Arbitrary key-value pairs to store custom data about the cardholder | ## KYC Object (Shared KYC — Enabled Businesses Only) Cardholder KYC behaviour depends on your business's [KYC mode](/v3/documentation/concepts/cardholders/kyc): * **Managed** (default) — the new cardholder starts at `UNSUBMITTED`; verify them afterwards (self-service session or document submission). * **Shared** — you may include a `kyc` object here to submit the documents you already hold. The cardholder is set to `PENDING` and FYATU runs a **background verification** on them; a `cardholder.kyc_approved` webhook fires once verified. * **Minimal (No-KYC)** — the cardholder is created as `WAIVED` and can be issued a card immediately; no `kyc` object is needed (if sent, it is accepted but not required). If your business does **not** have Shared KYC enabled, the `kyc` object is silently ignored regardless of what is sent. | Field | Type | Required | Description | | ------------------ | ------ | -------- | ---------------------------------------------- | | `kyc.idFrontUrl` | string | Yes | URL to the front image of the ID document | | `kyc.selfieUrl` | string | Yes | URL to a selfie photo of the cardholder | | `kyc.idBackUrl` | string | No | URL to the back image of the ID document | | `kyc.documentType` | string | No | `PASSPORT`, `NATIONAL_ID`, or `DRIVER_LICENSE` | For standard identity verification (cardholder completes verification themselves), use [Initiate KYC Verification](/v3/api-reference/cardholders/kyc-session) after creating the cardholder. ## Metadata Object (Optional) The `metadata` field accepts any flat JSON object. Use it to store your own data alongside the cardholder — for example, department, employee ID, or tier level. ```json theme={null} { "metadata": { "department": "Engineering", "employee_id": "EMP-1234", "tier": "VIP", "cost_center": "CC-500" } } ``` Metadata is returned in all cardholder responses and displayed in the business panel. ## Example Usage ```php PHP theme={null} 'EXT-0001', 'firstName' => 'Alice', 'lastName' => 'Example', 'email' => 'alice@example.com', 'phone' => '+15550001234', 'dateOfBirth' => '1990-01-01', 'gender' => 'MALE', 'address' => '123 Main Street, Apt 4B', 'city' => 'Newark', 'state' => 'Delaware', 'country' => 'US', 'zipCode' => '000000', 'metadata' => [ 'department' => 'Engineering', 'employee_id' => 'EMP-1234', ], ]; $response = file_get_contents( 'https://api.fyatu.com/api/v3/cardholders', false, stream_context_create([ 'http' => [ 'method' => 'POST', 'header' => [ 'Authorization: Bearer ' . $accessToken, 'Content-Type: application/json' ], 'content' => json_encode($data) ] ]) ); $result = json_decode($response, true); echo "Created cardholder: " . $result['data']['id'] . "\n"; ``` ```javascript Node.js theme={null} const data = { externalId: 'EXT-0001', firstName: 'Alice', lastName: 'Example', email: 'alice@example.com', phone: '+15550001234', dateOfBirth: '1990-01-01', gender: 'MALE', address: '123 Main Street, Apt 4B', city: 'Newark', state: 'Delaware', country: 'US', zipCode: '000000', metadata: { department: 'Engineering', employee_id: 'EMP-1234', }, }; const response = await fetch('https://api.fyatu.com/api/v3/cardholders', { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); const result = await response.json(); console.log('Created cardholder:', result.data.id); ``` ## Example Response ```json theme={null} { "success": true, "status": 201, "message": "Cardholder created successfully", "data": { "id": "CH1a2b3c4d5e6f", "externalId": "EXT-0001", "firstName": "Alice", "lastName": "Example", "email": "alice@example.com", "phone": "+15550001234", "dateOfBirth": "1990-01-01", "gender": "MALE", "address": { "line1": "123 Main Street, Apt 4B", "city": "Newark", "state": "Delaware", "country": "US", "zipCode": "000000" }, "document": { "type": null, "number": null }, "kyc": { "status": "UNSUBMITTED", "idFrontUrl": null, "idBackUrl": null, "selfieUrl": null }, "metadata": { "department": "Engineering", "employee_id": "EMP-1234" }, "status": "ACTIVE", "cardsCount": 0, "createdAt": "2026-01-15T21:31:39Z", "updatedAt": null }, "meta": { "requestId": "req_2f7fb4227a1007418773122f", "timestamp": "2026-01-15T21:31:39+00:00" } } ``` ## Next Steps After creating a cardholder, you can: 1. **Verify identity** — Use [Initiate KYC Verification](/v3/api-reference/cardholders/kyc-session) to let the cardholder verify themselves, or [Submit KYC Documents](/v3/api-reference/cardholders/kyc) to submit documents on their behalf. Required under **Managed** and **Shared**; not needed under **Minimal**. 2. **Issue a card** — Use [Create Card](/v3/api-reference/cards/create). Immediate under **Minimal** (`WAIVED`); under **Managed / Shared** the cardholder must first reach `VERIFIED`. Whether KYC is required before issuance depends on your [KYC mode](/v3/documentation/concepts/cardholders/kyc): under **Managed** and **Shared** a cardholder must reach `VERIFIED` before a card can be issued; under **Minimal (No-KYC)** cardholders are created `WAIVED` and can be issued a card immediately. ## Error Responses ### Duplicate Email (409) ```json theme={null} { "success": false, "status": 409, "message": "A cardholder with this email already exists", "error": { "code": "CONFLICT" } } ``` ### Duplicate External ID (409) ```json theme={null} { "success": false, "status": 409, "message": "A cardholder with this externalId already exists", "error": { "code": "CONFLICT" } } ``` ### Underage Cardholder (400) ```json theme={null} { "success": false, "status": 400, "message": "Validation failed", "error": { "code": "VALIDATION_ERROR", "details": [ { "field": "dateOfBirth", "message": "Cardholder must be at least 18 years old." } ] } } ``` ### Validation Error (400) ```json theme={null} { "success": false, "status": 400, "message": "Validation failed", "error": { "code": "VALIDATION_ERROR", "details": [ { "field": "gender", "message": "The gender field is required." } ] } } ``` Email addresses and external IDs must be unique within your business. If you try to create a cardholder with an email or externalId that already exists, you'll receive a 409 Conflict error. Use the `externalId` field to store your platform's cardholder/user ID. This makes it easy to link FYATU cardholders to users in your own system. # Delete Cardholder Source: https://docs.fyatu.com/v3/api-reference/cardholders/delete v3/openapi.json DELETE /cardholders/{id} Delete a cardholder and terminate all associated cards. This action is permanent. DELETE /cardholders/{id}. ## Overview Delete a cardholder from your application. Note that cardholders with active (non-terminated) cards cannot be deleted. You must terminate all associated cards first. ## Path Parameters | Parameter | Type | Description | | --------- | ------ | ---------------------------- | | `id` | string | Unique cardholder identifier | ## Example Usage ```php PHP theme={null} [ 'method' => 'DELETE', 'header' => 'Authorization: Bearer ' . $accessToken ] ]) ); $result = json_decode($response, true); if ($result['success']) { echo "Cardholder deleted successfully\n"; } ``` ```javascript Node.js theme={null} const cardholderId = 'CH1a2b3c4d5e6f'; const response = await fetch( `https://api.fyatu.com/api/v3/cardholders/${cardholderId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${accessToken}` } } ); const result = await response.json(); if (result.success) { console.log('Cardholder deleted successfully'); } ``` ## Example Response ```json theme={null} { "success": true, "status": 200, "message": "Cardholder deleted successfully", "data": { "id": "CH1a2b3c4d5e6f", "deleted": true, "cardsDeleted": 0, "kycDocumentsDeleted": false }, "meta": { "requestId": "req_delete123abc", "timestamp": "2026-01-08T16:00:00+00:00" } } ``` ## Error Response - Active Cards If the cardholder has active cards, you'll receive a 409 Conflict error: ```json theme={null} { "success": false, "status": 409, "message": "Cannot delete cardholder with 2 active card(s). Terminate all active cards first.", "error": { "code": "CARDHOLDER_HAS_ACTIVE_CARDS" }, "meta": { "requestId": "req_delete123abc", "timestamp": "2026-01-08T16:00:00+00:00" } } ``` ## Deletion Requirements Before deleting a cardholder, ensure: 1. **All cards are terminated** - The cardholder must have no active cards 2. **Outstanding transactions are settled** - Ensure all pending transactions are completed This action is irreversible. Once a cardholder is deleted, all associated data is permanently removed. Instead of deleting a cardholder, consider setting their status to `INACTIVE` or `SUSPENDED` if you may need to reference them later. # Get Cardholder Source: https://docs.fyatu.com/v3/api-reference/cardholders/get v3/openapi.json GET /cardholders/{id} Get cardholder details including KYC status, personal information, and associated cards. GET /cardholders/{id}. ## Overview Retrieve detailed information about a specific cardholder, including personal details, address, document information, and KYC status. ## Path Parameters | Parameter | Type | Description | | --------- | ------ | ---------------------------- | | `id` | string | Unique cardholder identifier | ## Response Fields | Field | Type | Description | | ------------- | ------- | -------------------------------- | | `id` | string | Cardholder ID | | `externalId` | string | Your system's identifier | | `firstName` | string | First name | | `lastName` | string | Last name | | `email` | string | Email address | | `phone` | string | Phone number | | `dateOfBirth` | string | Date of birth | | `gender` | string | Gender | | `address` | object | Address details | | `document` | object | Document details | | `kyc` | object | KYC status and documents | | `metadata` | object | Custom key-value data (nullable) | | `status` | string | Cardholder status | | `cardsCount` | integer | Number of active cards | | `createdAt` | string | Creation timestamp | | `updatedAt` | string | Last update timestamp | `middleName` is also returned when one is set on the cardholder. ## Example Usage ```php PHP theme={null} [ 'method' => 'GET', 'header' => 'Authorization: Bearer ' . $accessToken ] ]) ); $result = json_decode($response, true); $ch = $result['data']; echo "Name: {$ch['firstName']} {$ch['lastName']}\n"; echo "Email: {$ch['email']}\n"; echo "KYC Status: {$ch['kyc']['status']}\n"; echo "Cards: {$ch['cardsCount']}\n"; ``` ```javascript Node.js theme={null} const cardholderId = 'ch_a1b2c3d4e5f6'; const response = await fetch( `https://api.fyatu.com/api/v3/cardholders/${cardholderId}`, { headers: { 'Authorization': `Bearer ${accessToken}` } } ); const result = await response.json(); const ch = result.data; console.log(`Name: ${ch.firstName} ${ch.lastName}`); console.log(`Email: ${ch.email}`); console.log(`KYC Status: ${ch.kyc.status}`); console.log(`Cards: ${ch.cardsCount}`); ``` ## Example Response ```json theme={null} { "success": true, "status": 200, "message": "Cardholder retrieved successfully", "data": { "id": "ch_a1b2c3d4e5f6", "externalId": "EXT-0001", "firstName": "Alice", "lastName": "Example", "email": "alice@example.com", "phone": "+15550001234", "dateOfBirth": "1990-01-01", "gender": "FEMALE", "address": { "line1": "123 Example Street", "city": "Springfield", "state": "Illinois", "country": "US", "zipCode": "62701" }, "document": { "type": "PASSPORT", "number": "XX0000000" }, "kyc": { "status": "VERIFIED", "idFrontUrl": "https://cdn.fyatu.com/example/id-front.jpg", "idBackUrl": "https://cdn.fyatu.com/example/id-back.jpg", "selfieUrl": "https://cdn.fyatu.com/example/selfie.jpg" }, "metadata": { "department": "Engineering", "employee_id": "EMP-1234" }, "status": "ACTIVE", "cardsCount": 1, "createdAt": "2026-01-10T14:30:00Z", "updatedAt": "2026-01-15T09:45:00Z" }, "meta": { "requestId": "req_d4e5f6g7h8i9", "timestamp": "2026-01-17T10:00:00+00:00" } } ``` ## KYC Status Values | Status | Description | | ------------- | ---------------------------------------------------------- | | `UNSUBMITTED` | No KYC documents have been submitted | | `PENDING` | Documents submitted / session in progress, awaiting result | | `VERIFIED` | KYC verification completed successfully | | `REJECTED` | KYC verification failed | | `WAIVED` | KYC not required (No-KYC / Minimal business) | The `cardsCount` field shows the number of active (non-terminated) cards associated with this cardholder. # Submit KYC Documents Source: https://docs.fyatu.com/v3/api-reference/cardholders/kyc v3/openapi.json POST /cardholders/{id}/kyc Submit pre-verified KYC documents for a cardholder via Shared KYC. Available only for businesses with Shared KYC enabled. POST /cardholders/{id}/kyc. ## Overview Submit identity documents for a cardholder on their behalf using **Shared KYC**. This endpoint is only available to businesses that have completed FYATU's Shared KYC onboarding process. The endpoint responds immediately with `kycStatus: PENDING`. Documents are processed in the background and a `cardholder.kyc_approved` webhook is dispatched when complete. This endpoint returns `403 SHARED_KYC_NOT_ENABLED` if your business does not have Shared KYC enabled. To apply, reach out to FYATU through your dedicated Slack channel. Enabling Shared KYC requires a due diligence review and the use of a recognised KYC provider (e.g. Sumsub, Persona, Onfido). ## Endpoint ``` POST /api/v3/cardholders/{cardholderId}/kyc ``` **Scope required:** `cardholders:write` ## Path Parameters | Parameter | Type | Required | Description | | -------------- | ------ | -------- | ---------------------------- | | `cardholderId` | string | Yes | Unique cardholder identifier | ## Request Fields | Field | Type | Required | Description | | -------------- | ------ | -------- | ---------------------------------------------- | | `idFrontUrl` | string | Yes | URL to the front image of the ID document | | `selfieUrl` | string | Yes | URL to a selfie photo of the cardholder | | `idBackUrl` | string | No | URL to the back image of the ID document | | `documentType` | string | No | `PASSPORT`, `NATIONAL_ID`, or `DRIVER_LICENSE` | ## Image Requirements * **Format**: JPEG, PNG, GIF, or WebP * **Maximum size**: 5MB per image * **Accessibility**: URLs must be publicly accessible ## How It Works ```mermaid theme={null} sequenceDiagram participant App as Your App participant FYATU as FYATU API App->>FYATU: POST /cardholders/{id}/kyc (document URLs) FYATU-->>App: { kycStatus: "PENDING" } (immediate) FYATU->>FYATU: Process documents (background) FYATU->>App: Webhook: cardholder.kyc_approved ``` 1. **Your app** sends document URLs to this endpoint 2. **FYATU** responds immediately with `PENDING` status 3. FYATU processes the documents in the background 4. On success, `kycStatus` is set to `VERIFIED` and a `cardholder.kyc_approved` webhook is dispatched with the final document URLs 5. On failure, `kycStatus` is set to `REJECTED` — you can re-submit ## Prerequisites * Business must have **Shared KYC enabled** (contact FYATU via your dedicated Slack channel) * Cardholder `kycStatus` must be `UNSUBMITTED` or `REJECTED` * All image URLs must be publicly accessible ## Example Usage ```php PHP theme={null} 'https://storage.example.com/docs/id-front.jpg', 'idBackUrl' => 'https://storage.example.com/docs/id-back.jpg', 'selfieUrl' => 'https://storage.example.com/docs/selfie.jpg', 'documentType' => 'NATIONAL_ID', ]; $response = file_get_contents( "https://api.fyatu.com/api/v3/cardholders/{$cardholderId}/kyc", false, stream_context_create([ 'http' => [ 'method' => 'POST', 'header' => [ 'Authorization: Bearer ' . $accessToken, 'Content-Type: application/json' ], 'content' => json_encode($data) ] ]) ); $result = json_decode($response, true); echo "KYC status: " . $result['data']['kycStatus'] . "\n"; // Output: KYC status: PENDING ``` ```javascript Node.js theme={null} const cardholderId = 'CH1a2b3c4d5e6f'; const data = { idFrontUrl: 'https://storage.example.com/docs/id-front.jpg', idBackUrl: 'https://storage.example.com/docs/id-back.jpg', selfieUrl: 'https://storage.example.com/docs/selfie.jpg', documentType: 'NATIONAL_ID', }; const response = await fetch( `https://api.fyatu.com/api/v3/cardholders/${cardholderId}/kyc`, { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify(data) } ); const result = await response.json(); console.log('KYC status:', result.data.kycStatus); // Output: KYC status: PENDING ``` ## Example Response ### Success (200) ```json theme={null} { "success": true, "status": 200, "message": "Documents submitted. The verification result will be available shortly.", "data": { "id": "CH1a2b3c4d5e6f", "kycStatus": "PENDING" }, "meta": { "requestId": "req_kyc123abc", "timestamp": "2026-01-08T17:00:00+00:00" } } ``` The final approved state arrives via webhook once background processing completes: ```json theme={null} { "event": "cardholder.kyc_approved", "version": "3.0", "eventId": "77d958cb-128d-4927-bd2c-c351a153fb39", "sign": "e5902a90747d0a43dd74498dedaaf09c40e1a51cb99ba651d5a3fdd9847d901e", "data": { "cardholderId": "CH1a2b3c4d5e6f", "firstName": "Alice", "lastName": "Example", "kycStatus": "VERIFIED", "kycLevel": "VERIFIED", "idFrontUrl": "https://cdn.fyatu.com/user/kyc/front_1234567890.jpg", "idBackUrl": "https://cdn.fyatu.com/user/kyc/back_1234567890.jpg", "idSelfieUrl": "https://cdn.fyatu.com/user/kyc/selfie_1234567890.jpg", "appId": "D0H6R7Z6R1C2N5O5", "timestamp": "2026-01-08T17:05:00Z" } } ``` The document image URLs (`idFrontUrl`, `idBackUrl`, `idSelfieUrl`) are flat keys in `data` and appear only for the slots that were uploaded. `idBackUrl` is omitted when no back image was submitted. ## Error Responses ### Shared KYC Not Enabled (403) ```json theme={null} { "success": false, "status": 403, "message": "Shared KYC is not enabled for this business", "error": { "code": "SHARED_KYC_NOT_ENABLED" } } ``` ### Already Accepted (409) ```json theme={null} { "success": false, "status": 409, "message": "KYC has already been accepted for this cardholder", "error": { "code": "CONFLICT" } } ``` ### Validation Error (400) ```json theme={null} { "success": false, "status": 400, "message": "Validation failed", "error": { "code": "VALIDATION_ERROR", "details": [ { "field": "selfieUrl", "message": "The selfieUrl field is required." } ] } } ``` You can also submit KYC documents at cardholder creation time by including a `kyc` object in the `POST /cardholders` request body. See the [Create Cardholder](/v3/api-reference/cardholders/create) endpoint for details. # Initiate KYC Verification Source: https://docs.fyatu.com/v3/api-reference/cardholders/kyc-session v3/openapi.json POST /cardholders/{id}/kyc/session Start identity verification for a cardholder via automated ID + liveness check. Returns a verification URL. POST /cardholders/{id}/kyc/session. ## Overview Initiate an optional automated KYC (Know Your Customer) verification session for a cardholder. This creates a secure verification session where the cardholder completes identity document capture and liveness verification. KYC verification is **not required** for card issuance — you can issue cards to cardholders without completing KYC. Use this endpoint when you need to verify a cardholder's identity for compliance or enhanced trust. This is the **self-service** KYC path where the cardholder completes verification themselves. If you already have the cardholder's ID documents and want to submit them on their behalf, use [Submit KYC Documents](/v3/api-reference/cardholders/kyc) instead. The verification result is delivered asynchronously via webhook (`cardholder.kyc_approved` or `cardholder.kyc_rejected`). ## Endpoint ``` POST /api/v3/cardholders/{cardholderId}/kyc/session ``` **Scope required:** `cardholders:write` ## Path Parameters | Parameter | Type | Required | Description | | -------------- | ------ | -------- | ---------------------------- | | `cardholderId` | string | Yes | Unique cardholder identifier | ## Request Body No request body required. ## How It Works ```mermaid theme={null} sequenceDiagram participant App as Your App participant FYATU as FYATU API participant Didit as Verification Provider participant User as Cardholder App->>FYATU: POST /cardholders/{id}/kyc/session FYATU-->>App: { verificationUrl, sessionId } App->>User: Redirect to verificationUrl User->>Didit: Complete ID scan + liveness check Didit->>FYATU: Verification result FYATU->>App: Webhook: cardholder.kyc_approved or cardholder.kyc_rejected ``` 1. **Your app** calls this endpoint to get a verification URL 2. **Redirect** the cardholder to the `verificationUrl` 3. The cardholder **completes** ID document capture and liveness verification 4. **FYATU sends a webhook** to your app with the result (`cardholder.kyc_approved` or `cardholder.kyc_rejected`) ## Verification Fee A fee is charged per successful verification, based on your plan: | Plan | Fee per verification | | ---------- | -------------------- | | Startup | \$1.20 | | Enterprise | \$0.80 | | Premium | \$0.40 | * The fee is **not charged upfront** - no wallet hold or deduction when initiating verification * **Added to your invoice** only when verification is approved (successful) * **Not charged** when verification is declined, abandoned, or expires * The fee appears as a line item on your next monthly invoice ## Prerequisites * Cardholder `kycStatus` must be `UNSUBMITTED` or `REJECTED` ## Example Usage ```php PHP theme={null} [ 'method' => 'POST', 'header' => [ 'Authorization: Bearer ' . $accessToken, 'Content-Type: application/json' ] ] ]) ); $result = json_decode($response, true); // Redirect cardholder to the verification URL $verificationUrl = $result['data']['verificationUrl']; echo "Redirect cardholder to: {$verificationUrl}\n"; ``` ```javascript Node.js theme={null} const cardholderId = 'CH1a2b3c4d5e6f'; const response = await fetch( `https://api.fyatu.com/api/v3/cardholders/${cardholderId}/kyc/session`, { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' } } ); const result = await response.json(); // Redirect cardholder to the verification URL const { verificationUrl } = result.data; console.log('Redirect cardholder to:', verificationUrl); ``` ```python Python theme={null} import requests cardholder_id = 'CH1a2b3c4d5e6f' response = requests.post( f'https://api.fyatu.com/api/v3/cardholders/{cardholder_id}/kyc/session', headers={ 'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json' } ) result = response.json() verification_url = result['data']['verificationUrl'] print(f'Redirect cardholder to: {verification_url}') ``` ## Example Response ### Success (201) ```json theme={null} { "success": true, "status": 201, "message": "KYC verification session created", "data": { "cardholderId": "CH1a2b3c4d5e6f", "sessionId": "ses_abc123def456", "verificationUrl": "https://verify.didit.me/session/ses_abc123def456", "kycStatus": "PENDING", "fee": 0.60 }, "meta": { "requestId": "req_kyc789xyz", "timestamp": "2026-04-02T10:30:00+00:00" } } ``` ### Session Already In Progress (200) If a verification session is already active, the existing session is returned: ```json theme={null} { "success": true, "status": 200, "message": "KYC session already in progress", "data": { "cardholderId": "CH1a2b3c4d5e6f", "sessionId": "ses_abc123def456", "verificationUrl": "https://verify.didit.me/session/ses_abc123def456", "kycStatus": "PENDING" }, "meta": { "requestId": "req_kyc790xyz", "timestamp": "2026-04-02T10:35:00+00:00" } } ``` ### No-KYC / Minimal Business (200) If the business runs in **Minimal (No-KYC)** mode, no verification session is created — the cardholder is waived instead: ```json theme={null} { "success": true, "status": 200, "message": "KYC is waived for this business — no verification required", "data": { "cardholderId": "CH1a2b3c4d5e6f", "kycStatus": "WAIVED" }, "meta": { "requestId": "req_kyc791xyz", "timestamp": "2026-04-02T10:40:00+00:00" } } ``` ## KYC Status Flow ``` UNSUBMITTED ──> PENDING (session initiated) ──> VERIFIED (verification approved) ^ | | ├──> REJECTED (verification failed) ──> UNSUBMITTED (can retry) | | | └──> UNSUBMITTED (session abandoned/expired, can retry) | └── REJECTED ──> PENDING (new session initiated) ``` | Status | Description | Can Initiate Session | | ------------- | ------------------------ | ----------------------------- | | `UNSUBMITTED` | No verification started | Yes | | `PENDING` | Verification in progress | No (returns existing session) | | `VERIFIED` | Verification approved | No | | `REJECTED` | Verification failed | Yes (retry allowed) | ## Webhook Events After the cardholder completes (or abandons) verification, you'll receive one of these webhooks: ### `cardholder.kyc_approved` ```json theme={null} { "event": "cardholder.kyc_approved", "version": "3.0", "eventId": "77d958cb-128d-4927-bd2c-c351a153fb39", "sign": "e5902a90747d0a43dd74498dedaaf09c40e1a51cb99ba651d5a3fdd9847d901e", "data": { "cardholderId": "CH1a2b3c4d5e6f", "firstName": "John", "lastName": "Smith", "kycStatus": "VERIFIED", "kycLevel": "VERIFIED", "idFrontUrl": "https://cdn.fyatu.com/user/kyc/front_1234567890.jpg", "idBackUrl": "https://cdn.fyatu.com/user/kyc/back_1234567890.jpg", "idSelfieUrl": "https://cdn.fyatu.com/user/kyc/selfie_1234567890.jpg", "appId": "D0H6R7Z6R1C2N5O5", "timestamp": "2026-04-02T10:45:00Z" } } ``` ### `cardholder.kyc_rejected` ```json theme={null} { "event": "cardholder.kyc_rejected", "version": "3.0", "eventId": "77d958cb-128d-4927-bd2c-c351a153fb39", "sign": "e5902a90747d0a43dd74498dedaaf09c40e1a51cb99ba651d5a3fdd9847d901e", "data": { "cardholderId": "CH1a2b3c4d5e6f", "firstName": "John", "lastName": "Smith", "status": "REJECTED", "reason": "{\"feature\":\"DOCUMENT\",\"risk\":\"EXPIRED_DOCUMENT\",\"short_description\":\"Document expired\"}", "appId": "D0H6R7Z6R1C2N5O5", "timestamp": "2026-04-02T10:45:00Z" } } ``` The rejection status is reported under the `status` key (not `kycStatus`), and `reason` is a JSON-encoded string from the KYC provider — parse it for the structured detail. ## Error Responses ### Already Accepted (409) ```json theme={null} { "success": false, "status": 409, "message": "KYC has already been accepted for this cardholder", "error": { "code": "CONFLICT" } } ``` ### Provider Error (503) ```json theme={null} { "success": false, "status": 503, "message": "Failed to create verification session. Please try again.", "error": { "code": "PROVIDER_ERROR" } } ``` ### KYC Service Not Configured (503) ```json theme={null} { "success": false, "status": 503, "message": "KYC session service is not configured", "error": { "code": "SERVICE_UNAVAILABLE" } } ``` Store the `verificationUrl` and provide it to the cardholder. If the cardholder doesn't complete verification, you can call this endpoint again to get a new session after the previous one expires. The verification fee is only charged on successful verification and added to your next monthly invoice. If the cardholder abandons the session or verification fails, no fee is charged. # List Cardholders Source: https://docs.fyatu.com/v3/api-reference/cardholders/list v3/openapi.json GET /cardholders List all cardholders in your Fyatu card program with pagination and KYC status filtering. GET /cardholders. ## Overview Retrieve a paginated list of cardholders associated with your application. Use filters to narrow down results by status, KYC status, or search terms. ## Query Parameters | Parameter | Type | Description | | ----------- | ------- | ----------------------------------------------------------------------- | | `page` | integer | Page number (default: 1) | | `limit` | integer | Items per page (default: 20, max: 100) | | `status` | string | Filter by status (ACTIVE, INACTIVE, SUSPENDED) | | `kycStatus` | string | Filter by KYC status (UNSUBMITTED, PENDING, VERIFIED, REJECTED, WAIVED) | | `search` | string | Search by name or email (partial match) | ## Response | Field | Type | Description | | ------------- | ------ | -------------------------- | | `cardholders` | array | List of cardholder objects | | `pagination` | object | Pagination info | ### Cardholder Object (Summary) | Field | Type | Description | | ------------ | ------- | ----------------------------------- | | `id` | string | Unique cardholder identifier | | `externalId` | string | Your system's identifier (nullable) | | `firstName` | string | First name | | `lastName` | string | Last name | | `email` | string | Email address | | `phone` | string | Phone number | | `status` | string | Cardholder status | | `kycStatus` | string | KYC verification status | | `cardsCount` | integer | Number of active cards | | `createdAt` | string | Creation timestamp | ## Example Usage ```php PHP theme={null} 'ACTIVE', 'kycStatus' => 'VERIFIED', 'limit' => 50 ]); $response = file_get_contents( "https://api.fyatu.com/api/v3/cardholders?{$params}", false, stream_context_create([ 'http' => [ 'method' => 'GET', 'header' => 'Authorization: Bearer ' . $accessToken ] ]) ); $result = json_decode($response, true); foreach ($result['data']['cardholders'] as $ch) { echo "{$ch['firstName']} {$ch['lastName']} - {$ch['email']} ({$ch['kycStatus']})\n"; } // Pagination info $pagination = $result['data']['pagination']; echo "Page {$pagination['currentPage']} of {$pagination['totalPages']}\n"; ``` ```javascript Node.js theme={null} // Get active cardholders with accepted KYC const params = new URLSearchParams({ status: 'ACTIVE', kycStatus: 'VERIFIED', limit: '50' }); const response = await fetch( `https://api.fyatu.com/api/v3/cardholders?${params}`, { headers: { 'Authorization': `Bearer ${accessToken}` } } ); const result = await response.json(); for (const ch of result.data.cardholders) { console.log(`${ch.firstName} ${ch.lastName} - ${ch.email} (${ch.kycStatus})`); } // Pagination info const { currentPage, totalPages } = result.data.pagination; console.log(`Page ${currentPage} of ${totalPages}`); ``` ## Example Response ```json theme={null} { "success": true, "status": 200, "message": "Cardholders retrieved successfully", "data": { "cardholders": [ { "id": "ch_a1b2c3d4e5f6", "externalId": "EXT-0001", "firstName": "Alice", "lastName": "Example", "email": "alice@example.com", "phone": "+15550001234", "status": "ACTIVE", "kycStatus": "VERIFIED", "cardsCount": 1, "createdAt": "2026-01-10T14:30:00Z" } ], "pagination": { "currentPage": 1, "itemsPerPage": 20, "totalItems": 47, "totalPages": 3 } }, "meta": { "requestId": "req_a1b2c3d4e5f6", "timestamp": "2026-01-17T10:00:00+00:00" } } ``` ## Filtering Tips ### By KYC Status ``` GET /cardholders?kycStatus=VERIFIED ``` ### Search by Name or Email ``` GET /cardholders?search=john ``` ### Combine Filters ``` GET /cardholders?status=ACTIVE&kycStatus=VERIFIED&limit=100 ``` Use the `search` parameter to quickly find cardholders by name or email. The search is case-insensitive and matches partial strings. # Update Cardholder Source: https://docs.fyatu.com/v3/api-reference/cardholders/update v3/openapi.json PATCH /cardholders/{id} Update cardholder personal information, metadata, or status. PATCH /cardholders/{id}. ## Overview Update cardholder information. Only the fields you provide will be updated. Note that the email address cannot be changed after creation. ## Path Parameters | Parameter | Type | Description | | --------- | ------ | ---------------------------- | | `id` | string | Unique cardholder identifier | ## Updatable Fields | Field | Type | Description | | ---------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `firstName` | string | First name (locked after KYC verification) | | `middleName` | string | Optional middle name (max 15 chars). **Editable even after KYC verification**, unlike first/last name. Send `""` to clear it. Distinguishes cardholders with identical first + last names for the card network's per-holder card limit. | | `lastName` | string | Last name (locked after KYC verification) | | `phone` | string | Phone number in E.164 format | | `dateOfBirth` | string | Date of birth (`YYYY-MM-DD`, must be 18+) | | `gender` | string | Gender (`MALE` or `FEMALE`) | | `address` | string | Street address | | `city` | string | City | | `state` | string | State or province | | `country` | string | ISO country code (2 chars) | | `zipCode` | string | Postal/ZIP code | | `documentType` | string | Document type (`PASSPORT`, `NATIONAL_ID`, `DRIVER_LICENSE`) | | `documentNumber` | string | Document number | | `externalId` | string | Your system's identifier | | `status` | string | Status (`ACTIVE`, `INACTIVE`, `SUSPENDED`) | | `metadata` | object | Custom key-value data (merge semantics, see below) | ## Metadata (Merge Semantics) The `metadata` field uses **merge semantics** on update: * **Add a key**: include the key with a value * **Update a key**: include the key with a new value * **Delete a key**: set the key to `null` or `""` * Keys you don't include are left unchanged ```json theme={null} // Existing metadata: { "department": "Engineering", "tier": "VIP" } // This request: { "metadata": { "tier": "Premium", "cost_center": "CC-500", "department": "" } } // Results in: { "tier": "Premium", "cost_center": "CC-500" } // "tier" was updated, "cost_center" was added, "department" was deleted ``` To clear all metadata, pass an empty object with every key set to `null`. ## Example Usage ```php PHP theme={null} '+1987654321', 'address' => '456 Oak Avenue', 'city' => 'Los Angeles', 'state' => 'CA', 'metadata' => [ 'department' => 'Sales', // update existing key 'badge_number' => 'B-789', // add new key ], ]; $response = file_get_contents( "https://api.fyatu.com/api/v3/cardholders/{$cardholderId}", false, stream_context_create([ 'http' => [ 'method' => 'PATCH', 'header' => [ 'Authorization: Bearer ' . $accessToken, 'Content-Type: application/json' ], 'content' => json_encode($data) ] ]) ); $result = json_decode($response, true); echo "Updated cardholder: " . $result['data']['id'] . "\n"; ``` ```javascript Node.js theme={null} const cardholderId = 'CH1a2b3c4d5e6f'; const data = { phone: '+1987654321', address: '456 Oak Avenue', city: 'Los Angeles', state: 'CA', metadata: { department: 'Sales', // update existing key badge_number: 'B-789', // add new key }, }; const response = await fetch( `https://api.fyatu.com/api/v3/cardholders/${cardholderId}`, { method: 'PATCH', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify(data) } ); const result = await response.json(); console.log('Updated cardholder:', result.data.id); ``` ## Example Response The response returns the complete updated cardholder data: ```json theme={null} { "success": true, "status": 200, "message": "Cardholder updated successfully", "data": { "id": "ch_a1b2c3d4e5f6", "externalId": "EXT-0001", "firstName": "Alice", "lastName": "Example", "email": "alice@example.com", "phone": "+15550009876", "dateOfBirth": "1990-01-01", "gender": "FEMALE", "address": { "line1": "456 Oak Avenue", "city": "Los Angeles", "state": "CA", "country": "US", "zipCode": "90001" }, "document": { "type": "PASSPORT", "number": "AB1234567" }, "kyc": { "status": "UNSUBMITTED", "idFrontUrl": null, "idBackUrl": null, "selfieUrl": null }, "status": "ACTIVE", "metadata": { "department": "Sales", "badge_number": "B-789" }, "createdAt": "2026-01-15T21:31:39Z", "updatedAt": "2026-01-16T00:37:34Z" }, "meta": { "requestId": "req_7d30cd59796bbcf0e903b5fd", "timestamp": "2026-01-16T00:37:34+00:00" } } ``` ## Suspend/Reactivate Cardholder You can change the cardholder's status to temporarily suspend or reactivate them: ```json theme={null} // Suspend a cardholder { "status": "SUSPENDED" } // Reactivate a cardholder { "status": "ACTIVE" } // Mark as inactive { "status": "INACTIVE" } ``` Suspending a cardholder does not automatically suspend their cards. You should manage card statuses separately if needed. The `email` field cannot be updated after cardholder creation. If you need to change the email, you must delete the cardholder and create a new one. Once the cardholder's KYC status is `VERIFIED`, the identity fields `firstName`, `lastName`, `dateOfBirth`, `documentType`, and `documentNumber` are locked and any attempt to change them returns `400 KYC_PROTECTED`. `middleName`, `phone`, address fields, `status`, and `metadata` remain editable. # Create Card Source: https://docs.fyatu.com/v3/api-reference/cards/create v3/openapi.json POST /cards Issue a new virtual Mastercard or Visa prepaid card programmatically. Specify cardholder, amount, and product. POST /cards. ## Overview Issue a new virtual card to a cardholder. The cardholder must have verified KYC status and your business wallet must have sufficient balance for the card amount plus fees. **Card creation is asynchronous.** A successful response means the request was **accepted**; the card may be returned with status `CREATING` and no PAN yet, and is **confirmed only when its status becomes `ACTIVE`**. If the response is delayed, the card is still being provisioned — poll [Get Card](/v3/api-reference/cards/get) until `status` is `ACTIVE` rather than treating the immediate response as final. A card that fails to provision does not activate and any held balance is released. ## Prerequisites 1. **Verified Cardholder**: The cardholder must exist and have `status: ACTIVE` 2. **Sufficient Balance**: Wallet must cover: `amount (in USD) + issuanceFee` 3. **Active Application**: Your app must be in `ACTIVE` status ## Request Body | Field | Type | Required | Description | | --------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------------------- | | `cardholderId` | string | Yes | ID of the cardholder to issue card to | | `amount` | number | Yes | Initial funding amount in product currency (minimum \$5 or €5) | | `name` | string | No | Name on card (defaults to cardholder name) | | `productId` | string | No | Card product to issue (from [List Products](/v3/api-reference/cards/products)). Defaults to the product marked `isDefault: true` | | `spendingLimit` | integer | No | **Deprecated** — Use `productId` instead. Monthly spending limit: `5000` or `10000` (default: 5000) | **Deprecation Notice**: The `spendingLimit` field is deprecated and will be removed in a future version. Use `productId` to select the card product, which determines the spending limit, brand, and currency automatically. If both `productId` and `spendingLimit` are provided, `productId` takes precedence. ## Example Usage ```php PHP theme={null} 'ch_a1b2c3d4e5f6', 'amount' => 100.00, 'name' => 'ALICE EXAMPLE', 'productId' => 'MCUSD1' ]; $ch = curl_init('https://api.fyatu.com/api/v3/cards'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . $accessToken, 'Content-Type: application/json' ], CURLOPT_POSTFIELDS => json_encode($data) ]); $response = curl_exec($ch); $result = json_decode($response, true); if ($result['success']) { echo "Card created: " . $result['data']['id'] . "\n"; echo "Brand: " . $result['data']['brand'] . "\n"; echo "Currency: " . $result['data']['currency'] . "\n"; echo "Balance: " . $result['data']['initialBalance'] . "\n"; } ``` ```javascript Node.js theme={null} // Recommended: use productId const response = await fetch('https://api.fyatu.com/api/v3/cards', { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ cardholderId: 'ch_a1b2c3d4e5f6', amount: 100.00, name: 'ALICE EXAMPLE', productId: 'MCUSD1' }) }); const result = await response.json(); if (result.success) { console.log('Card created:', result.data.id); console.log('Brand:', result.data.brand); console.log('Currency:', result.data.currency); } ``` ## EUR Card Example When issuing a EUR-denominated card, the `amount` is specified in EUR. Your wallet (USD) is debited the equivalent in USD at the current exchange rate. ```javascript theme={null} const response = await fetch('https://api.fyatu.com/api/v3/cards', { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ cardholderId: 'ch_a1b2c3d4e5f6', amount: 100.00, // €100 EUR productId: 'MCEUR1' }) }); // Response: // { // "data": { // "id": "crd_...", // "brand": "MASTERCARD", // "currency": "EUR", // "initialBalance": 100.00 // €100 on card // } // } // Wallet debited: ~$108.70 USD (100 / EUR rate) + issuance fee ``` ## Response Fields | Field | Type | Description | | ---------------- | -------------- | ------------------------------------------------------------------------------------------------------------ | | `id` | string | Unique card identifier | | `cardholderId` | string | The cardholder this card belongs to | | `name` | string | Name printed on the card | | `last4` | string \| null | Last 4 digits of the card number. `null` while the card is still provisioning (`CREATING`/`PROCESSING`) | | `maskedNumber` | string \| null | Masked card number (e.g., `****4829`). `null` while the card is still provisioning (`CREATING`/`PROCESSING`) | | `expiryDate` | string \| null | Card expiration date (MM/YY). `null` while the card is still provisioning (`CREATING`/`PROCESSING`) | | `brand` | string | Card brand: `MASTERCARD` or `VISA` | | `currency` | string | Card currency: `USD` or `EUR` | | `status` | string | Card status: `ACTIVE`, `CREATING`, or `PROCESSING` (see below) | | `initialBalance` | number | Initial funding amount in card currency | | `createdAt` | string | Card creation timestamp | ## Card Status on Creation Cards are provisioned asynchronously by the card provider. When a card is not ready in the create response, the API returns immediately with `status: CREATING` (or `PROCESSING` on legacy products) and card details (`last4`, `maskedNumber`, `expiryDate`) will be `null`. Provisioning usually completes within seconds but can take up to \~1 hour. Once the card is ready, its `status` becomes `ACTIVE` and the full card details (`last4`, `maskedNumber`, `expiryDate`) are populated. If provisioning fails, the card does not activate and any held balance is released. | Status | Meaning | | ------------ | ------------------------------------------------------------------------------------- | | `ACTIVE` | Card is ready to use immediately | | `CREATING` | Card is being provisioned by the provider — poll `GET /cards/{cardId}` until `ACTIVE` | | `PROCESSING` | Legacy async status (same meaning as `CREATING`) — poll `GET /cards/{cardId}` | Card creation is confirmed by the `status` field, not by a webhook — poll [Get Card](/v3/api-reference/cards/get) until `status` is `ACTIVE` (provisioning usually completes within seconds). Funding, unloading, and the rest of the card lifecycle each have their own webhook events. ## Error Responses | Error Code | Description | | -------------------------------- | -------------------------------------------------------- | | `APP_INACTIVE` | Application is not active | | `CARDHOLDER_INACTIVE` | Cardholder is not active (KYC not verified) | | `INSUFFICIENT_BALANCE` | Business wallet balance is too low | | `PRODUCT_NOT_FOUND` | Card product not found | | `PRODUCT_INACTIVE` | Card product is currently inactive | | `PRODUCT_UNAVAILABLE` | Card product is temporarily unavailable for new issuance | | `RATE_UNAVAILABLE` | Unable to fetch exchange rate for EUR products | | `HOLD_FAILED` | Failed to hold balance from business wallet | | `PROVIDER_NOT_CONFIGURED` | Card provider account not configured | | `CARD_CREATION_FAILED` | Failed to create card at the bank partner | | `CARDHOLDER_REGISTRATION_FAILED` | Failed to register cardholder with card provider | | `CARDHOLDER_RESTRICTED` | Cardholder has been restricted by the card provider | | `BUSINESS_KYB_REQUIRED` | Business KYB verification incomplete | Use the [List Products](/v3/api-reference/cards/products) endpoint to discover available card products and their fees before creating a card. # Terminate Card Source: https://docs.fyatu.com/v3/api-reference/cards/delete v3/openapi.json DELETE /cards/{cardId} Permanently terminate a virtual card. Remaining balance is returned to your wallet. DELETE /cards/{cardId}. ## Overview Permanently terminate a card. Any remaining balance will be automatically returned to your business wallet. This action cannot be undone. Terminating a card closes it immediately, but returning its remaining balance can take from seconds to several hours depending on the card network. The `refundedBalance` in this response is the amount we **expect** to return, not money that has moved. Two webhooks follow, and they mean different things: * **`card.terminated`** — a notification. The card is closed and unusable. Update your records; do not move money. * **`card.termination_refund`** — your cue to act. The balance is back in your account; now settle with your customer. This holds for every card in your account regardless of which network issued it. ## Path Parameters | Parameter | Type | Description | | --------- | ------ | -------------------------- | | `cardId` | string | The unique card identifier | ## Request Body (Optional) | Field | Type | Required | Description | | ----------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------- | | `reference` | string | No | Your unique reference for this operation. Defaults to cardId if not provided. Returned in webhooks for easy reconciliation. | ## Response Fields | Field | Type | Description | | ----------------- | ------ | -------------------------------------------------------------------------------------------------------- | | `id` | string | The terminated card's identifier | | `status` | string | Always `TERMINATED` | | `reason` | string | Termination reason (defaults to `Terminated by the user`) | | `refundedBalance` | number | Balance we **expect** to return to your wallet in USD (settled separately via `card.termination_refund`) | | `terminatedAt` | string | Termination timestamp (ISO 8601) | | `reference` | string | Your reference (or the cardId if none supplied) | ## Example Usage ```php PHP theme={null} 'cancel-card-abc123' // Optional: your unique reference ]; $ch = curl_init('https://api.fyatu.com/api/v3/cards/' . $cardId); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => 'DELETE', CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . $accessToken, 'Content-Type: application/json' ], CURLOPT_POSTFIELDS => json_encode($data) ]); $response = curl_exec($ch); $result = json_decode($response, true); if ($result['success']) { echo "Card terminated at: " . $result['data']['terminatedAt'] . "\n"; echo "Reference: " . $result['data']['reference'] . "\n"; } ``` ```javascript Node.js theme={null} const cardId = 'crd_8f3a2b1c4d5e6f7890abcdef12345678'; const response = await fetch(`https://api.fyatu.com/api/v3/cards/${cardId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ reference: 'cancel-card-abc123' // Optional: your unique reference }) }); const result = await response.json(); if (result.success) { console.log('Card terminated at:', result.data.terminatedAt); console.log('Reference:', result.data.reference); } ``` ## Error Responses | Error Code | Description | | -------------------- | -------------------------- | | `ALREADY_TERMINATED` | Card is already terminated | If you want to temporarily disable a card without losing the balance, use the [Freeze Card](/v3/api-reference/cards/freeze) endpoint instead. # Freeze Card Source: https://docs.fyatu.com/v3/api-reference/cards/freeze v3/openapi.json POST /cards/{cardId}/freeze Temporarily freeze a card to block all transactions. Card can be unfrozen later. POST /cards/{cardId}/freeze. ## Overview Temporarily freeze a card to prevent any transactions. The card can be unfrozen later using the [Unfreeze Card](/v3/api-reference/cards/unfreeze) endpoint. The card balance is preserved while frozen. ## Path Parameters | Parameter | Type | Description | | --------- | ------ | -------------------------- | | `cardId` | string | The unique card identifier | ## Example Usage ```php PHP theme={null} true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . $accessToken ] ]); $response = curl_exec($ch); $result = json_decode($response, true); if ($result['success']) { echo "Card status: " . $result['data']['status'] . "\n"; } ``` ```javascript Node.js theme={null} const cardId = 'crd_8f3a2b1c4d5e6f7890abcdef12345678'; const response = await fetch(`https://api.fyatu.com/api/v3/cards/${cardId}/freeze`, { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}` } }); const result = await response.json(); if (result.success) { console.log('Card status:', result.data.status); } ``` ## Error Responses | Status | Error Code | Description | | ------ | ----------------- | ------------------------------------------------ | | 400 | `CARD_TERMINATED` | Cannot freeze a terminated card | | 409 | `ALREADY_FROZEN` | Card is already frozen (auto-syncs local status) | | 500 | `FREEZE_FAILED` | Failed to freeze card at the bank partner | Use card freezing for temporary security concerns. If a card is compromised or lost, freeze it immediately while you investigate. # Fund Card Source: https://docs.fyatu.com/v3/api-reference/cards/fund v3/openapi.json POST /cards/{cardId}/fund Add funds to an existing virtual card. Minimum $5, fee applies per pricing. POST /cards/{cardId}/fund. ## Overview Add funds to a card from your business wallet. A funding fee may apply based on your pricing configuration. **Funding is asynchronous.** A successful (`2xx`) response means the request was **accepted and is pending confirmation from the card provider** — it does **not** mean the card has been funded, and the provider may still **reject** it. Do not treat the response as final. Use the [`card.funded`](/v3/webhooks/webhook-events/card-funded) (confirmed — balance added) and [`card.funding_failed`](/v3/webhooks/webhook-events/card-funding-failed) (rejected — no balance added) webhooks as the source of truth. Supply a unique `reference` to reconcile the webhook with your request. ## Path Parameters | Parameter | Type | Description | | --------- | ------ | -------------------------- | | `cardId` | string | The unique card identifier | ## Request Body | Field | Type | Required | Description | | ----------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------- | | `amount` | number | Yes | Amount to fund in USD (must be greater than 0) | | `reference` | string | No | Your unique reference for this operation. Defaults to cardId if not provided. Returned in webhooks for easy reconciliation. | ## Idempotency When you supply a `reference`, it is **single-use**. Retrying a fund with the same `reference` returns the **original** result instead of funding the card again, so a client retry or accidental double-submit can never double-charge. **A `reference` is final once it resolves** — both success and `FAILED` are terminal. If a fund resolves to **`FAILED`**, sending the **same** `reference` again does **not** re-run it; it returns `409 REFERENCE_ALREADY_FAILED`. To genuinely retry a failed fund, send a **new** `reference`. This stops one `reference` from ending up with two operations in two different states. ## Example Usage ```php PHP theme={null} 50.00, 'reference' => 'my-order-12345' // Optional: your unique reference ]; $ch = curl_init("https://api.fyatu.com/api/v3/cards/{$cardId}/fund"); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . $accessToken, 'Content-Type: application/json' ], CURLOPT_POSTFIELDS => json_encode($data) ]); $response = curl_exec($ch); $result = json_decode($response, true); if ($result['success']) { // Request accepted — NOT final. The card is funded only when the // card.funded webhook arrives; card.funding_failed means it was rejected. echo "Funding submitted: $" . $result['data']['amount'] . "\n"; echo "Reference: " . $result['data']['reference'] . "\n"; echo "Status: " . $result['data']['status'] . "\n"; // PENDING } ``` ```javascript Node.js theme={null} const cardId = 'crd_8f3a2b1c4d5e6f7890abcdef12345678'; const response = await fetch(`https://api.fyatu.com/api/v3/cards/${cardId}/fund`, { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: 50.00, reference: 'my-order-12345' // Optional: your unique reference }) }); const result = await response.json(); if (result.success) { // Request accepted — NOT final. The card is funded only when the // card.funded webhook arrives; card.funding_failed means it was rejected. console.log('Funding submitted: $' + result.data.amount); console.log('Reference: ' + result.data.reference); console.log('Status: ' + result.data.status); // PENDING } ``` ## Error Responses | Status | Error Code | Description | | ------ | -------------------------- | ------------------------------------------------------------------------------------------------- | | 400 | `CARD_NOT_ACTIVE` | Card is not active (frozen, suspended, or terminated) | | 400 | `INSUFFICIENT_BALANCE` | Business wallet balance is too low | | 400 | `CARD_FROZEN` | Card is frozen at bank partner (auto-syncs local status) | | 400 | `CARD_TERMINATED` | Card has been terminated (auto-syncs local status) | | 409 | `REFERENCE_ALREADY_FAILED` | This `reference` already resolved to `FAILED` and is single-use — send a new `reference` to retry | | 500 | `DEBIT_FAILED` | Failed to debit from business wallet | | 500 | `FUNDING_FAILED` | Failed to fund card at the bank partner | A funding fee (`CARD_FUNDING_FEE`) may apply. Use the [Get Pricing](/v3/api-reference/account/pricing) endpoint to retrieve your current rates. # Get Card Details Source: https://docs.fyatu.com/v3/api-reference/cards/get v3/openapi.json GET /cards/{cardId} Get full card details — masked number, expiry, brand, balance, status, and spending limits. GET /cards/{cardId}. ## Overview Retrieve full card details including the card number, CVV, and expiration date. This endpoint returns sensitive PCI data that should be handled securely. **Security Notice**: This endpoint returns full card details (card number, CVV). Never expose this data in client-side code, logs, or analytics. ## Path Parameters | Parameter | Type | Description | | --------- | ------ | -------------------------- | | `cardId` | string | The unique card identifier | ## Example Usage ```php PHP theme={null} [ 'method' => 'GET', 'header' => 'Authorization: Bearer ' . $accessToken ] ]) ); $result = json_decode($response, true); $card = $result['data']; echo "Card Number: " . $card['cardNumber'] . "\n"; echo "CVV: " . $card['cvv'] . "\n"; echo "Expiry: " . $card['expiryMonth'] . '/' . $card['expiryYear'] . "\n"; echo "Balance: $" . $card['balance']['available'] . "\n"; ``` ```javascript Node.js theme={null} const cardId = 'crd_8f3a2b1c4d5e6f7890abcdef12345678'; const response = await fetch(`https://api.fyatu.com/api/v3/cards/${cardId}`, { method: 'GET', headers: { 'Authorization': `Bearer ${accessToken}` } }); const result = await response.json(); const card = result.data; console.log('Card Number:', card.cardNumber); console.log('CVV:', card.cvv); console.log('Expiry:', `${card.expiryMonth}/${card.expiryYear}`); console.log('Balance: $' + card.balance.available); ``` ## Response Fields | Field | Type | Description | | -------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `id` | string | Unique card identifier | | `cardholderId` | string | The cardholder this card belongs to | | `name` | string | Name printed on the card | | `last4` | string | Last 4 digits of the card number | | `maskedNumber` | string | Masked card number (e.g. `****4829`) | | `cardNumber` | string | Full 16-digit card number (PCI sensitive) | | `cvv` | string | 3-digit security code (PCI sensitive) | | `expiryMonth` | string | Card expiration month (MM format) | | `expiryYear` | string | Card expiration year (YYYY format) | | `expiration` | string | Short expiration date (MM/YY format, e.g. `09/28`) | | `brand` | string | Card brand: `MASTERCARD` or `VISA` | | `status` | string | Card status: `CREATING` (still provisioning — no card number yet), `ACTIVE`, `FROZEN`, `SUSPENDED`, `TERMINATED` | | `suspendedReason` | string\|null | Reason for suspension, or `null` | | `isReloadable` | boolean | Whether the card can be funded | | `balance.available` | number | Available (spendable) balance in USD, net of any pending authorization holds | | `balance.holding` | number | Total value of pending authorization holds (liens) currently on the card. Reserved but not yet settled — `balance.available` already excludes it | | `pendingAuthorizations` | array | Individual pending authorization holds making up `balance.holding` (see below). Empty when there are no active holds | | `pendingAuthorizations[].merchantName` | string\|null | Merchant that placed the hold | | `pendingAuthorizations[].mcc` | string\|null | Merchant category code | | `pendingAuthorizations[].amount` | number | Held amount in USD | | `pendingAuthorizations[].currency` | string | Currency of the hold (always `USD`) | | `pendingAuthorizations[].authorizedAt` | string\|null | When the authorization was placed (provider timestamp) | | `needsReissue` | boolean | `true` when the provider flags the card for reissue (expired/compromised) and a replacement should be requested | | `spendingLimit` | number | Spending limit amount in USD (0 if no limit) | | `spendingPeriod` | string | Period the spending limit applies over (e.g. `DAILY`, `MONTHLY`), from the card product | | `invoiceDebt` | number | Outstanding unpaid fee debt for this card in USD (already deducted from `balance.available`) | | `declineCount` | integer | Number of declined transactions (insufficient funds) on this card | | `billingAddress` | object | Card billing address | | `billingAddress.line1` | string | Street address line | | `billingAddress.city` | string | City | | `billingAddress.state` | string | State/region | | `billingAddress.country` | string | Country code | | `billingAddress.zipCode` | string | Postal/ZIP code | | `createdAt` | string | Card creation timestamp (`YYYY-MM-DD HH:MM:SS`) | | `suspendedAt` | string\|null | When the card was suspended, or `null` | | `terminatedAt` | string\|null | When the card was terminated, or `null` | **Decline Count**: The number of consecutive insufficient-funds declines before automatic suspension depends on the card product — it can be 3, 15, or unlimited depending on the product's configuration. Monitor `declineCount` and notify cardholders before reaching their product's limit. # List Cards Source: https://docs.fyatu.com/v3/api-reference/cards/list v3/openapi.json GET /cards List all issued virtual and prepaid cards with pagination — filter by status, cardholder, or product. GET /cards. ## Overview Retrieve a paginated list of all virtual cards issued under your application. You can filter by cardholder ID, card status, or search by card name. ## Query Parameters | Parameter | Type | Description | | -------------- | ------- | --------------------------------------------------------------- | | `cardholderId` | string | Filter cards by cardholder ID | | `status` | string | Filter by status: `ACTIVE`, `FROZEN`, `SUSPENDED`, `TERMINATED` | | `scheme` | string | Filter by card scheme: `VISA`, `MASTERCARD` | | `search` | string | Search by card name, last 4 digits, or card ID | | `page` | integer | Page number (default: 1) | | `limit` | integer | Items per page (default: 20, max: 100) | ## Example Usage ```php PHP theme={null} 'ACTIVE', 'scheme' => 'MASTERCARD', 'page' => 1, 'limit' => 20 ]); $response = file_get_contents( 'https://api.fyatu.com/api/v3/cards?' . $queryParams, false, stream_context_create([ 'http' => [ 'method' => 'GET', 'header' => 'Authorization: Bearer ' . $accessToken ] ]) ); $result = json_decode($response, true); foreach ($result['data']['cards'] as $card) { echo $card['name'] . ' - ****' . $card['last4'] . ' - ' . $card['status'] . "\n"; } ``` ```javascript Node.js theme={null} const params = new URLSearchParams({ status: 'ACTIVE', scheme: 'MASTERCARD', page: '1', limit: '20' }); const response = await fetch(`https://api.fyatu.com/api/v3/cards?${params}`, { method: 'GET', headers: { 'Authorization': `Bearer ${accessToken}` } }); const result = await response.json(); result.data.cards.forEach(card => { console.log(`${card.name} - ****${card.last4} - ${card.status}`); }); ``` ## Response Fields | Field | Type | Description | | ------------------ | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | Unique card identifier | | `cardholderId` | string | The cardholder this card belongs to | | `name` | string | Name printed on the card | | `last4` | string | Last 4 digits of the card number | | `maskedNumber` | string | Masked card number (e.g., `****4829`) | | `expiryDate` | string | Card expiration date (MM/YY format) | | `brand` | string | Card brand: `MASTERCARD` or `VISA` | | `status` | string | Card status: `ACTIVE`, `FROZEN`, `SUSPENDED`, `TERMINATED` | | `isReloadable` | boolean | Whether the card can be funded | | `availableBalance` | number | Available (spendable) balance in USD, from the materialized per-card figure (no provider call). Present for cards on the active issuer only; omitted for cards on deprecated providers | | `createdAt` | string | Card creation timestamp | Use the `cardholderId` filter to get all cards belonging to a specific cardholder. Use the `search` parameter to find cards by name or last 4 digits. # List Card Products Source: https://docs.fyatu.com/v3/api-reference/cards/products v3/openapi.json GET /cards/products List available card products — returns productId, brand, currency, fees, and availability flags. Use productId when creating a card. GET /cards/products. ## Overview Retrieve the list of available card products. Each product defines the card brand, currency, fees, features, and availability. Use the `productId` when creating a card to specify which product to issue. Products include availability flags (`canIssue`, `canFund`, `canUnload`) so you know exactly what operations are supported for each product. The `isDefault` flag indicates which product is used as the automatic fallback during [card replacement](/v3/api-reference/cards/replace) when the original card type is unavailable for issuance. ## Example Usage ```php PHP theme={null} [ 'method' => 'GET', 'header' => 'Authorization: Bearer ' . $accessToken ] ]) ); $result = json_decode($response, true); foreach ($result['data']['products'] as $product) { $status = $product['canIssue'] ? '✓' : '✗'; echo "[{$status}] {$product['name']} ({$product['currency']}) - Fee: \${$product['issuanceFee']}"; if ($product['isDefault']) echo ' [DEFAULT]'; echo "\n"; } ``` ```javascript Node.js theme={null} const response = await fetch('https://api.fyatu.com/api/v3/cards/products', { method: 'GET', headers: { 'Authorization': `Bearer ${accessToken}` } }); const result = await response.json(); result.data.products.forEach(product => { const status = product.canIssue ? '✓' : '✗'; const def = product.isDefault ? ' [DEFAULT]' : ''; console.log(`[${status}] ${product.name} (${product.currency}) - Fee: $${product.issuanceFee}${def}`); }); ``` ## Response Fields | Field | Type | Description | | --------------------- | ------- | ----------------------------------------------------------------------------- | | `productId` | string | Product identifier to use in card creation | | `name` | string | Human-readable product name | | `brand` | string | Card brand: `MASTERCARD` or `VISA` | | `currency` | string | Card currency: `USD` or `EUR` | | `type` | string | Card type (e.g., `PREPAID`, `DEBIT`) | | `issuanceFee` | number | Fee charged when issuing this card (in USD) | | `minimumFunding` | number | Minimum initial funding amount (in product currency) | | `spendingLimit` | number | Maximum spending limit for the card | | `spendingPeriod` | string | Time period for the spending limit: `DAILY`, `MONTHLY`, or `YEARLY` | | `features.applePay` | boolean | Whether the card supports Apple Pay | | `features.googlePay` | boolean | Whether the card supports Google Pay | | `features.3dSecure` | boolean | Whether the card supports 3D Secure | | `features.reloadable` | boolean | Whether the card can be funded after creation | | `isTokenized` | boolean | Whether the card supports digital wallet tokenization (Apple Pay, Google Pay) | | `canIssue` | boolean | Whether this product is currently available for new card issuance | | `canFund` | boolean | Whether cards of this product can receive funding (loading money) | | `canUnload` | boolean | Whether cards of this product support unloading (withdrawing money) | | `isDefault` | boolean | Whether this is the default fallback product used during card replacement | ## Example Response ```json theme={null} { "success": true, "message": "Card products retrieved successfully", "data": { "products": [ { "productId": "MCUSD1", "name": "Mastercard World USD", "brand": "MASTERCARD", "currency": "USD", "type": "PREPAID", "issuanceFee": 5.00, "minimumFunding": 5.00, "spendingLimit": 25000, "spendingPeriod": "DAILY", "features": { "applePay": false, "googlePay": false, "3dSecure": true, "reloadable": true }, "isTokenized": false, "canIssue": true, "canFund": true, "canUnload": true, "isDefault": true } ] } } ``` Use the `productId` value from this response as the `productId` field when [creating a card](/v3/api-reference/cards/create). Only products with `canIssue: true` can be used for new card creation. **Default Product & Card Replacement**: When a card is [replaced](/v3/api-reference/cards/replace), the system first checks if the original card's product type is still available for issuance (`canIssue: true`). If it is, the replacement card will be the same type. If not, the product marked as `isDefault: true` is used automatically as a fallback — no action is needed from your side. **EUR Products**: For EUR-denominated cards, the `amount` you provide in the create card request is in EUR. Your business wallet (which is USD-denominated) will be debited the equivalent amount in USD based on the current exchange rate, plus the issuance fee. # Replace Card Source: https://docs.fyatu.com/v3/api-reference/cards/replace v3/openapi.json POST /cards/{cardId}/replace Replace a card with a new one — same cardholder, new card number. POST /cards/{cardId}/replace. ## Overview Replace an existing card with a brand new one. The `cardId` remains the same for your records, but the card details (number, expiry, CVV) are replaced with a completely new card. Any remaining balance is automatically transferred to the new card. The old card will be immediately terminated and cannot be used after replacement. All future transactions must use the new card details. ## Use Cases * **Card Compromised**: When a cardholder reports unauthorized use or data exposure * **Card Lost**: When a physical card is lost and needs to be replaced * **Card Damaged**: When the card details are no longer accessible ## Path Parameters | Parameter | Type | Description | | --------- | ------ | -------------------------- | | `cardId` | string | The unique card identifier | ## Request Body (Optional) | Field | Type | Required | Description | | ----------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------- | | `reason` | string | No | Reason for replacing the card (e.g., "Card compromised", "Card lost") | | `reference` | string | No | Your unique reference for this operation. Defaults to cardId if not provided. Returned in webhooks for easy reconciliation. | ## Example Usage ```php PHP theme={null} 'Card compromised', 'reference' => 'replace-card-abc123' // Optional: your unique reference ]; $ch = curl_init("https://api.fyatu.com/api/v3/cards/{$cardId}/replace"); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . $accessToken, 'Content-Type: application/json' ], CURLOPT_POSTFIELDS => json_encode($data) ]); $response = curl_exec($ch); $result = json_decode($response, true); if ($result['success']) { echo "Card replaced successfully!\n"; echo "New Last 4: " . $result['data']['last4'] . "\n"; echo "New Expiry: " . $result['data']['expiryDate'] . "\n"; echo "Balance Transferred: $" . $result['data']['balance'] . "\n"; echo "Reference: " . $result['data']['reference'] . "\n"; } ``` ```javascript Node.js theme={null} const cardId = 'crd_8f3a2b1c4d5e6f7890abcdef12345678'; const response = await fetch(`https://api.fyatu.com/api/v3/cards/${cardId}/replace`, { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ reason: 'Card compromised', reference: 'replace-card-abc123' // Optional: your unique reference }) }); const result = await response.json(); if (result.success) { console.log('Card replaced successfully!'); console.log('New Last 4:', result.data.last4); console.log('New Expiry:', result.data.expiryDate); console.log('Balance Transferred: $' + result.data.balance); console.log('Reference:', result.data.reference); } ``` ## Response Fields | Field | Type | Description | | -------------- | ------ | ------------------------------------------------ | | `id` | string | The card identifier (unchanged from before) | | `cardholderId` | string | The cardholder identifier | | `name` | string | Name on the card | | `last4` | string | Last 4 digits of the **new** card number | | `maskedNumber` | string | Masked **new** card number | | `expiryDate` | string | **New** card expiry date (MM/YYYY) | | `brand` | string | Card brand (VISA or MASTERCARD) | | `status` | string | Card status (always ACTIVE for new cards) | | `balance` | number | Current balance transferred from old card | | `reference` | string | Your reference for this operation | | `replacedAt` | string | ISO 8601 timestamp of when the card was replaced | ## Error Responses | Status | Error Code | Description | | ------ | ---------------------- | ------------------------------------------------------------ | | 400 | `CARD_TERMINATED` | Cannot replace a terminated card | | 400 | `INSUFFICIENT_BALANCE` | Business wallet has insufficient balance for replacement fee | | 404 | `CARD_NOT_FOUND` | Card not found or doesn't belong to your app | | 500 | `REPLACE_FAILED` | Failed to replace card at the bank partner | The `cardId` stays the same after replacement, so you don't need to update your database references. Only the card details (number, expiry, CVV) change. **Product Fallback**: When replacing a card, the system first tries to issue the same card product type. If that product is no longer available for issuance (i.e., `canIssue: false` in the [products list](/v3/api-reference/cards/products)), the default product (`isDefault: true`) is automatically used instead. The replacement is seamless — the balance is transferred regardless of which product is used. A card replacement fee may apply based on your pricing configuration. Use the [Get Pricing](/v3/api-reference/account/pricing) endpoint to check current fees. # Download Card Statement Source: https://docs.fyatu.com/v3/api-reference/cards/statement v3/openapi.json GET /cards/{cardId}/statement Download a PDF statement for a card with transaction history, business branding, and card details. GET /cards/{id}/statement. ## Overview Generate a PDF statement for a card. The statement is branded with your company name and includes a cardholder information panel, card details, and the card's transaction history for the requested period. The endpoint returns the PDF **directly** in the response body (`Content-Type: application/pdf`, `Content-Disposition: attachment`) with HTTP `200`. Save the response bytes to a file. ## Endpoint ``` GET /api/v3/cards/{cardId}/statement ``` **Scope required:** `cards:read` ## Path Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ---------------------- | | `cardId` | string | Yes | Unique card identifier | ## Query Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ---------------------------------------------------------- | | `from` | string | No | Start date (`YYYY-MM-DD`). Defaults to card creation date. | | `to` | string | No | End date (`YYYY-MM-DD`). Defaults to today. | ## Response Responds with HTTP `200` and the PDF file itself in the body (`Content-Type: application/pdf`, `Content-Disposition: attachment; filename="statement___.pdf"`). Write the response bytes straight to a file. ## Statement Contents The generated PDF includes: * **Header** — your business name and a "CARD STATEMENT" label on a branded band * **Statement Period** — the `from`–`to` date range covered * **Cardholder Information** — cardholder name and billing address * **Card Details** — masked card number, card brand, card status, currency * **Transaction History** — a table of transactions in the period with **Date**, **Description**, **Debit (USD)** and **Credit (USD)** columns * **Footer** — an automated-statement disclaimer ## Example Usage ```php PHP theme={null} [ 'method' => 'GET', 'header' => 'Authorization: Bearer ' . $accessToken, ] ])); // Save to file file_put_contents("card_statement_{$cardId}.pdf", $response); ``` ```javascript Node.js theme={null} const cardId = 'CRD678A3B4C5D6E7'; const response = await fetch( `https://api.fyatu.com/api/v3/cards/${cardId}/statement?from=2026-03-01&to=2026-03-31`, { headers: { 'Authorization': `Bearer ${accessToken}`, } } ); const buffer = await response.arrayBuffer(); fs.writeFileSync(`card_statement_${cardId}.pdf`, Buffer.from(buffer)); ``` ## Error Responses ### Card Not Found (404) ```json theme={null} { "success": false, "status": 404, "message": "Card not found", "error": { "code": "NOT_FOUND" } } ``` ### Card Still Provisioning (202) ```json theme={null} { "success": false, "status": 202, "message": "Card is still being provisioned", "error": { "code": "CARD_PROVISIONING" } } ``` The statement uses your business branding from Business Settings (logo, colors, company name and address). Make sure to configure your branding for a professional white-labeled statement. If no date range is specified, the statement covers the entire card lifetime from creation date to today. # Get Card Transaction Source: https://docs.fyatu.com/v3/api-reference/cards/transaction v3/openapi.json GET /cards/{cardId}/transactions/{txnId} Retrieve a single card transaction by id. GET /cards/{cardId}/transactions/{txnId}. ## Overview Returns one card transaction by its id, scoped to a card your business owns. Use it when you already hold a transaction id — from a webhook, or from [Get Card Transactions](/v3/api-reference/cards/transactions) — and want its detail without paging the list. The object returned is **identical in shape to one entry of that list**, so code that renders a transaction works with either endpoint. The transaction must belong to the card in the path. A transaction id that exists but sits on a different card returns `404` — you cannot read another card's transactions through a card you own. ## Path Parameters | Parameter | Type | Description | | --------- | ------ | -------------------------- | | `cardId` | string | The unique card identifier | | `txnId` | string | The transaction identifier | ## Example Usage ```bash cURL theme={null} curl https://api.fyatu.com/api/v3/cards/{cardId}/transactions/{txnId} \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` ```php PHP theme={null} true, CURLOPT_HTTPHEADER => ["Authorization: Bearer {$accessToken}"], ]); $transaction = json_decode(curl_exec($ch), true)['data']; curl_close($ch); ``` ```javascript Node.js theme={null} const res = await fetch( `https://api.fyatu.com/api/v3/cards/${cardId}/transactions/${txnId}`, { headers: { Authorization: `Bearer ${accessToken}` } } ); const { data: transaction } = await res.json(); ``` ```python Python theme={null} import requests r = requests.get( f"https://api.fyatu.com/api/v3/cards/{card_id}/transactions/{txn_id}", headers={"Authorization": f"Bearer {access_token}"}, ) transaction = r.json()["data"] ``` ## Response ```json theme={null} { "success": true, "status": 200, "message": "Transaction retrieved successfully", "data": { "id": "TXN8H2K9L4M6N1", "reference": "FND6A8CB4646CFC0", "type": "DEBIT", "amount": 12.50, "currency": "USD", "merchant": "Meta Platforms Ireland", "logo": null, "status": "COMPLETED", "description": "Meta Ads", "category": "Card Charge", "createdAt": "2026-01-15 10:30:00", "references": { "authorizationCode": "0eaf73d7-ee43-4fc1-9f5b-dabcf2044259", "rrn": "623015440030", "merchantId": "RC6HIN7BS6WLIBF", "network": "MASTERCARD", "maskedPan": "XXXXXXXXXXXX5740", "authorizationAmount": 12.50, "authorizationCurrency": "USD" } } } ``` ## Fields | Field | Type | Description | | ------------- | ------ | --------------------------------------------------------------------------------------------------------------- | | `id` | string | Transaction identifier — the same id webhooks carry as `reference` | | `reference` | string | Provider reference the operation carried on the card (its `clientReference`) | | `type` | string | `DEBIT` for spend and fees, `CREDIT` for funding, refunds and reversals | | `amount` | number | Transaction amount | | `currency` | string | Currency of the transaction | | `merchant` | string | Merchant name, or a description of the operation for funding and fees | | `status` | string | `COMPLETED`, `PENDING`, `FAILED`, `REVERSED` or `DECLINED` | | `description` | string | Narration from the merchant or the network | | `category` | string | What kind of transaction it is, e.g. `Card Charge`, `Cross-border Fee` | | `createdAt` | string | When the transaction occurred | | `references` | object | Network identifiers for tracing the payment — see [References](/v3/api-reference/cards/transactions#references) | ## Errors | Status | Meaning | | ------ | ----------------------------------------------------------------------------- | | `401` | The access token is missing or invalid | | `404` | The card does not exist, is not yours, or the transaction is not on that card | | `503` | Card services are temporarily unavailable — retry shortly | # Get Card Transactions Source: https://docs.fyatu.com/v3/api-reference/cards/transactions v3/openapi.json GET /cards/{cardId}/transactions Get transaction history for a specific card — merchant name, amount, status, and timestamps. GET /cards/{cardId}/transactions. ## Overview Retrieve a paginated list of all transactions made with a specific card, including purchases, refunds, and funding operations. ## Path Parameters | Parameter | Type | Description | | --------- | ------ | -------------------------- | | `cardId` | string | The unique card identifier | ## Query Parameters | Parameter | Type | Description | | ----------- | ------- | ------------------------------------------------------------ | | `page` | integer | Page number (default: 1) | | `reference` | string | Return only transactions carrying this reference — see below | ## Confirming an operation reached the card Pass `reference` with the `transactionReference` returned by a fund or unload call to see the transactions that operation produced on the card. This is the authoritative check. The card provider's transaction history is the record of what actually happened to the card, and it is independent of the status we report on the operation itself. If a funding call reports a failure but the money did reach the card, this lookup shows it — which is exactly the case worth verifying before re-funding a card. ```bash theme={null} curl "https://api.fyatu.com/api/v3/cards/{cardId}/transactions?reference=FND6A8CB4646CFC0" -H "Authorization: Bearer $ACCESS_TOKEN" ``` An empty `transactions` array means no transaction with that reference exists on the card. One or more entries means the operation reached it — a single reference can return more than one row when an operation also attracted a fee. The lookup searches the card's history rather than a single page, so `page` is ignored when `reference` is set. `reference` matches the **`transactionReference`** from the fund/unload response, not the `reference` you supplied for idempotency. The two are different identifiers: yours keys the request, `transactionReference` identifies the operation on the card. ## Example Usage ```php PHP theme={null} true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . $accessToken ] ]); $response = curl_exec($ch); $result = json_decode($response, true); if ($result['success']) { foreach ($result['data']['transactions'] as $txn) { $sign = $txn['type'] === 'CREDIT' ? '+' : '-'; $cat = $txn['category'] ? "[{$txn['category']}] " : ''; echo "{$cat}{$txn['merchant']}: {$sign}\${$txn['amount']} ({$txn['status']})\n"; // e.g. "[Cross-border Fee] FACEBK *TQSR DUBLIN: -$0.50 (COMPLETED)" } if ($result['data']['pagination']['hasMore']) { echo "Page {$result['data']['pagination']['currentPage']} of {$result['data']['pagination']['totalPages']}\n"; } } ``` ```javascript Node.js theme={null} const cardId = 'crd_8f3a2b1c4d5e6f7890abcdef12345678'; const response = await fetch( `https://api.fyatu.com/api/v3/cards/${cardId}/transactions?page=1`, { headers: { 'Authorization': `Bearer ${accessToken}` } } ); const result = await response.json(); if (result.success) { result.data.transactions.forEach(txn => { const sign = txn.type === 'CREDIT' ? '+' : '-'; const cat = txn.category ? `[${txn.category}] ` : ''; console.log(`${cat}${txn.merchant}: ${sign}$${txn.amount} (${txn.status})`); // e.g. "[Cross-border Fee] FACEBK *TQSR DUBLIN: -$0.50 (COMPLETED)" }); const { currentPage, totalPages, hasMore } = result.data.pagination; if (hasMore) console.log(`Page ${currentPage} of ${totalPages}`); } ``` ## Response Fields | Field | Type | Description | | ------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | Unique transaction identifier | | `reference` | string | Provider reference the operation carried on the card (the `clientReference`); filter on it with the `reference` query param | | `type` | string | `DEBIT` (purchases, fees) or `CREDIT` (funding, refunds) | | `amount` | number | Transaction amount in USD | | `currency` | string | Transaction currency (usually `USD`) | | `merchant` | string | Merchant name | | `logo` | string | Merchant logo URL (nullable) | | `status` | string | `COMPLETED`, `PENDING`, `DECLINED`, `REFUNDED`, `REVERSED` | | `description` | string | Network narration or payment code (e.g. `"Account verification"`) | | `category` | string | Human-readable transaction category (e.g. `"Card Charge"`, `"Cross-border Fee"`, `"Card Funding"`). Use this to distinguish the actual payment from associated fees. | | `createdAt` | string | Transaction timestamp (`YYYY-MM-DD HH:MM:SS` UTC) | | `references` | object | Network identifiers for tracing the payment — see [References](#references) | ## References `references` carries the identifiers a merchant, a cardholder's bank or our support team needs to trace a card payment — for example when a cardholder asks a merchant for a refund. It is always present; funding and withdrawals carry no network identifiers, so for them it is an empty object. Each field appears only when the card network supplied it. | Field | Type | Description | | ----------------------- | ------ | ------------------------------------------------------------------------------------------------------------- | | `authorizationCode` | string | Authorization identifier issued when the payment was approved | | `rrn` | string | Retrieval Reference Number assigned by the card network (12 digits) | | `arn` | string | Acquirer Reference Number — see the note below | | `merchantId` | string | Merchant identifier at the network | | `network` | string | Card network, e.g. `MASTERCARD` | | `maskedPan` | string | Masked card number the payment was made with | | `authorizationAmount` | number | Amount the merchant requested, in the merchant's currency. Differs from `amount` on foreign-currency payments | | `authorizationCurrency` | string | Currency of `authorizationAmount` | | `relatedTransactionId` | string | On a fee (`Cross-border Fee`, decline fees): the `id` of the transaction it was charged on | The **ARN** is assigned by the merchant's acquirer when it submits the payment for clearing, so an authorization that has not cleared never has one. When `arn` is absent on a cleared payment, contact support with the `rrn` and `authorizationCode` and we will obtain it. A refund request to the merchant does not need the ARN — the merchant identifies the payment from its own order. ## Category Values | Value | Meaning | | ----------------------------- | --------------------------------------------------------- | | `Card Charge` | The actual merchant payment | | `Cross-border Fee` | International surcharge attached to a cross-border charge | | `Card Funding` | Balance top-up | | `Card Withdrawal` | Balance unload | | `Refund` | Merchant refund | | `Reversal` | Transaction reversal | | `Declined` | Declined authorisation attempt | | `Decline Fee (Domestic)` | Fee for a declined domestic authorisation | | `Decline Fee (International)` | Fee for a declined international authorisation | ## Pagination | Field | Type | Description | | ------------- | ------- | -------------------------------- | | `currentPage` | integer | Current page number | | `totalItems` | integer | Total number of transactions | | `totalPages` | integer | Total number of pages | | `hasMore` | boolean | Whether more pages are available | When a single card payment generates two DEBIT entries with the same merchant (e.g. a Facebook charge of $1.00 followed by a $0.50 entry), the `category` field tells them apart: `"Card Charge"` is the actual payment and `"Cross-border Fee"` is the international surcharge. Always check `category` before rendering transaction labels in your UI. # Unfreeze Card Source: https://docs.fyatu.com/v3/api-reference/cards/unfreeze v3/openapi.json POST /cards/{cardId}/unfreeze Reactivate a previously frozen card to resume transactions. POST /cards/{cardId}/unfreeze. ## Overview Reactivate a frozen card to allow transactions again. The card returns to `ACTIVE` status and can be used immediately. ## Path Parameters | Parameter | Type | Description | | --------- | ------ | -------------------------- | | `cardId` | string | The unique card identifier | ## Example Usage ```php PHP theme={null} true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . $accessToken ] ]); $response = curl_exec($ch); $result = json_decode($response, true); if ($result['success']) { echo "Card status: " . $result['data']['status'] . "\n"; } ``` ```javascript Node.js theme={null} const cardId = 'crd_8f3a2b1c4d5e6f7890abcdef12345678'; const response = await fetch(`https://api.fyatu.com/api/v3/cards/${cardId}/unfreeze`, { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}` } }); const result = await response.json(); if (result.success) { console.log('Card status:', result.data.status); } ``` ## Error Responses | Status | Error Code | Description | | ------ | ----------------- | ------------------------------------------------ | | 400 | `CARD_TERMINATED` | Cannot unfreeze a terminated card | | 409 | `ALREADY_ACTIVE` | Card is already active (auto-syncs local status) | | 500 | `UNFREEZE_FAILED` | Failed to unfreeze card at the bank partner | After unfreezing, the card can be used immediately for transactions. The spending limit counter continues from where it was before freezing. # Unload Card Source: https://docs.fyatu.com/v3/api-reference/cards/unload v3/openapi.json POST /cards/{cardId}/unload Withdraw remaining balance from a card back to your business wallet. POST /cards/{cardId}/unload. ## Overview Withdraw funds from a card back to your business wallet. An unloading fee may apply based on your pricing configuration. **Unloading is asynchronous.** A successful (`2xx`) response means the request was **accepted and is pending confirmation from the card provider** — it does **not** mean the funds have been returned, and the provider may still **reject** the unload. Do not treat the response as final. Use the [`card.unloaded`](/v3/api-reference/webhooks/events) (confirmed — funds credited) and [`card.unloading_failed`](/v3/api-reference/webhooks/events) (rejected — no funds credited) webhooks as the source of truth. ## Path Parameters | Parameter | Type | Description | | --------- | ------ | -------------------------- | | `cardId` | string | The unique card identifier | ## Request Body | Field | Type | Required | Description | | ----------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------- | | `amount` | number | Yes | Amount to unload in USD | | `reference` | string | No | Your unique reference for this operation. Defaults to cardId if not provided. Returned in webhooks for easy reconciliation. | **A `reference` is single-use and final once it resolves.** Retrying the same `reference` returns the original result rather than unloading again. If an unload resolves to **`FAILED`**, the same `reference` returns `409 REFERENCE_ALREADY_FAILED` — send a **new** `reference` to genuinely retry. ## Example Usage ```php PHP theme={null} 25.00, 'reference' => 'withdraw-67890' // Optional: your unique reference ]; $ch = curl_init("https://api.fyatu.com/api/v3/cards/{$cardId}/unload"); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . $accessToken, 'Content-Type: application/json' ], CURLOPT_POSTFIELDS => json_encode($data) ]); $response = curl_exec($ch); $result = json_decode($response, true); if ($result['success']) { // Request accepted — NOT final. Funds are credited only when the // card.unloaded webhook arrives; card.unloading_failed means it was rejected. echo "Unload request submitted: $" . $result['data']['amountUnloaded'] . "\n"; echo "Reference: " . $result['data']['reference'] . "\n"; } ``` ```javascript Node.js theme={null} const cardId = 'crd_8f3a2b1c4d5e6f7890abcdef12345678'; const response = await fetch(`https://api.fyatu.com/api/v3/cards/${cardId}/unload`, { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: 25.00, reference: 'withdraw-67890' // Optional: your unique reference }) }); const result = await response.json(); if (result.success) { // Request accepted — NOT final. Funds are credited only when the // card.unloaded webhook arrives; card.unloading_failed means it was rejected. console.log('Unload request submitted: $' + result.data.amountUnloaded); console.log('Reference: ' + result.data.reference); } ``` ## Asynchronous Processing Card unloading is processed asynchronously by the card provider. A `2xx` response confirms only that the request was **accepted and submitted** — the funds are **not yet credited**, and the operation can still be rejected downstream. * When the provider **confirms** the unload, a [`card.unloaded`](/v3/api-reference/webhooks/events) webhook is sent and the net amount is credited to your business wallet. * If the provider **rejects** the unload, a [`card.unloading_failed`](/v3/api-reference/webhooks/events) webhook is sent and **no funds are credited**. Match webhooks to your request using the `reference` you supplied (it is returned in both the response and the webhook). Treat the immediate response as **"request accepted"**, never as "completed". Use the `card.unloaded` / `card.unloading_failed` webhooks — not the synchronous response — as the source of truth for the final status. ## Error Responses A `2xx` response is an *acceptance*, not a settlement (see [Asynchronous Processing](#asynchronous-processing)). These errors are returned synchronously when the request cannot even be submitted: | Status | Error Code | Description | | ------ | --------------------------- | ------------------------------------------------------------------------------------------------- | | 400 | `CARD_NOT_ACTIVE` | Card is not active (frozen, suspended, or terminated) | | 400 | `INSUFFICIENT_CARD_BALANCE` | Card balance is lower than the requested unload amount | | 400 | `CARD_FROZEN` | Card is frozen at bank partner (auto-syncs local status) | | 400 | `CARD_TERMINATED` | Card has been terminated (auto-syncs local status) | | 409 | `UNLOAD_IN_PROGRESS` | A previous unload with the same reference is still pending | | 409 | `REFERENCE_ALREADY_FAILED` | This `reference` already resolved to `FAILED` and is single-use — send a new `reference` to retry | | 500 | `BALANCE_CHECK_FAILED` | Failed to verify card balance | | 500 | `UNLOAD_FAILED` | The provider rejected the request at submission time | Use the [Get Pricing](/v3/api-reference/account/pricing) endpoint to check if unloading fees apply. Funds are credited to your business wallet **only once the card provider confirms** the unload (see [Asynchronous Processing](#asynchronous-processing) above). # API Reference Source: https://docs.fyatu.com/v3/api-reference/overview Complete REST API reference for Fyatu v3 — card issuing, cardholders, cards, account, and webhook configuration endpoints. This section provides detailed documentation for every V3 endpoint, including request/response schemas, parameters, and examples. ## Base URL ``` https://api.fyatu.com/api/v3 ``` ## Authentication V3 uses JWT Bearer tokens. First obtain a token, then include it in all requests: ```bash theme={null} # Step 1: Get token curl -X POST https://api.fyatu.com/api/v3/auth/token \ -H "Content-Type: application/json" \ -d '{"appId": "YOUR_APP_ID", "secretKey": "YOUR_SECRET", "grantType": "client_credentials"}' # Step 2: Use token curl -X GET https://api.fyatu.com/api/v3/cards \ -H "Authorization: Bearer YOUR_JWT_TOKEN" ``` ## Request Format * All requests use **JSON** body format * Set `Content-Type: application/json` for POST/PUT/PATCH requests * All timestamps are in **ISO 8601** format ## Response Format All V3 responses follow this structure: ```json Success (2xx) theme={null} { "success": true, "status": 200, "message": "Operation completed successfully", "data": { ... }, "meta": { "requestId": "req_xxxxxxxxxxxx", "timestamp": "2026-01-05T10:30:00+00:00" } } ``` ```json Error (4xx/5xx) theme={null} { "success": false, "status": 400, "message": "Error description", "error": { "code": "ERROR_CODE", "details": [...] }, "meta": { "requestId": "req_xxxxxxxxxxxx", "timestamp": "2026-01-05T10:30:00+00:00" } } ``` ## Available Endpoints ### Authentication Obtain and manage JWT access tokens. | Method | Endpoint | Description | | ------ | --------------- | ---------------------- | | POST | `/auth/token` | Generate access token | | POST | `/auth/refresh` | Refresh existing token | | POST | `/auth/revoke` | Revoke a token | ### Account Manage your business account, wallet, and fees. | Method | Endpoint | Description | | ------ | ----------------------------- | --------------------------- | | GET | `/account/pricing` | Get applicable fees | | GET | `/account/wallet` | Get wallet balances | | GET | `/account/transactions` | List account transactions | | GET | `/account/withdrawals` | Get withdrawal history | | POST | `/account/deposit-address` | Generate deposit address | | POST | `/account/withdrawal-address` | Register withdrawal address | ### Cardholders Manage cardholders and their KYC. | Method | Endpoint | Description | | ------ | ------------------- | ---------------------- | | GET | `/cardholders` | List cardholders | | POST | `/cardholders` | Create cardholder | | GET | `/cardholders/{id}` | Get cardholder details | | PATCH | `/cardholders/{id}` | Update cardholder | | DELETE | `/cardholders/{id}` | Delete cardholder | ### Cards Issue and manage virtual cards. | Method | Endpoint | Description | | ------ | ---------------------- | ------------------ | | GET | `/cards/products` | List card products | | POST | `/cards` | Issue a card | | GET | `/cards` | List cards | | GET | `/cards/{id}` | Get card details | | POST | `/cards/{id}/fund` | Fund a card | | POST | `/cards/{id}/unload` | Unload a card | | POST | `/cards/{id}/freeze` | Freeze a card | | POST | `/cards/{id}/unfreeze` | Unfreeze a card | ## Error Codes | Code | HTTP | Description | | -------------------------- | ---- | ----------------------------- | | `AUTH_TOKEN_MISSING` | 401 | No token provided | | `AUTH_TOKEN_INVALID` | 401 | Token is malformed or expired | | `AUTH_TOKEN_EXPIRED` | 401 | Token has expired | | `AUTH_INVALID_CREDENTIALS` | 401 | Invalid appId or secretKey | | `AUTH_APP_INACTIVE` | 401 | App is not active | | `AUTH_SCOPE_DENIED` | 403 | Token lacks required scope | | `VALIDATION_ERROR` | 400 | Request validation failed | | `RESOURCE_NOT_FOUND` | 404 | Resource not found | | `INSUFFICIENT_BALANCE` | 402 | Wallet balance too low | | `DUPLICATE_REFERENCE` | 409 | Reference already used | | `RATE_LIMIT_EXCEEDED` | 429 | Too many requests | | `INTERNAL_ERROR` | 500 | Server error | ## Rate Limits | Endpoint Category | Rate Limit | | ----------------- | ----------- | | Authentication | 10 req/min | | Read operations | 100 req/min | | Write operations | 30 req/min | Exceeding rate limits returns `429 Too Many Requests`. Implement exponential backoff in retry logic. # Authentication Source: https://docs.fyatu.com/v3/documentation/authentication JWT-based authentication for Fyatu API v3. Generate access tokens with your appId and secretKey. Tokens valid for 24 hours with refresh and revoke support. # Authentication FYATU API v3 uses JWT (JSON Web Tokens) for secure, stateless authentication. Exchange your app credentials for a short-lived access token, then use that token to authenticate all subsequent requests. ## Overview ```mermaid theme={null} sequenceDiagram participant App as Your App participant Auth as FYATU Auth participant API as FYATU API App->>Auth: POST /v3/auth/token (appId + secretKey) Auth-->>App: JWT Access Token App->>API: Request with Bearer Token API-->>App: API Response ``` ## Getting Your Credentials Go to [FYATU Dashboard](https://web.fyatu.com) and login to your account Navigate to the **Business Console** from your dashboard Click on your **Collection App** or **Issuing App** depending on which APIs you need Go to **Settings** tab, then click **API Keys & Credentials** ## App Types & Scopes Your access token's scopes depend on the app type: | App Type | Available APIs | Scopes | | ------------------ | -------------------- | -------------------------------------------------------------------- | | **Collection App** | Collections, Payouts | `collect:write`, `collect:read`, `payout:write`, `payout:read` | | **Issuing App** | Cards, Cardholders | `cards:write`, `cards:read`, `cardholders:write`, `cardholders:read` | ## Token Lifecycle | Property | Value | | -------------- | ---------------------------- | | Token Type | JWT (HS256) | | Token Expiry | 24 hours | | Refresh Window | Up to 5 minutes after expiry | | Token Format | Bearer token | ## Step 1: Obtain Access Token Exchange your app credentials for a JWT access token: ```bash cURL theme={null} curl -X POST https://api.fyatu.com/api/v3/auth/token \ -H "Content-Type: application/json" \ -d '{ "appId": "DD123FR45446CECES", "secretKey": "your_secret_key_here", "grantType": "client_credentials" }' ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.fyatu.com/api/v3/auth/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ appId: 'DD123FR45446CECES', secretKey: 'your_secret_key_here', grantType: 'client_credentials' }) }); const data = await response.json(); const accessToken = data.data.accessToken; ``` ```php PHP theme={null} $ch = curl_init('https://api.fyatu.com/api/v3/auth/token'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode([ 'appId' => 'DD123FR45446CECES', 'secretKey' => 'your_secret_key_here', 'grantType' => 'client_credentials' ]) ]); $response = json_decode(curl_exec($ch), true); $accessToken = $response['data']['accessToken']; ``` ```python Python theme={null} import requests response = requests.post( 'https://api.fyatu.com/api/v3/auth/token', json={ 'appId': 'DD123FR45446CECES', 'secretKey': 'your_secret_key_here', 'grantType': 'client_credentials' } ) data = response.json() access_token = data['data']['accessToken'] ``` ### Response ```json theme={null} { "success": true, "status": 200, "message": "Token generated successfully", "data": { "accessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJERDEyM0ZSNDQ0NjZDRUNFUyIsImJ1cyI6Ik4xUzBXM1E4UDBWMUU1TTZRNFIzRDhaOSIsInR5cGUiOiJjb2xsZWN0aW9uIiwic2NvcGVzIjpbImNvbGxlY3Q6d3JpdGUiLCJjb2xsZWN0OnJlYWQiLCJwYXlvdXQ6d3JpdGUiLCJwYXlvdXQ6cmVhZCJdLCJpYXQiOjE3MzYwNzU4MDAsImV4cCI6MTczNjE2MjIwMCwianRpIjoiand0XzdhZjRkMmI4ZTkxYzM1ZmE0YjIxODkwZSJ9.x2kPqR7mN5vL8wT3fA9sD6gH1jK4cB0eW7yU2iO3pVn", "tokenType": "Bearer", "expiresIn": 86400, "expiresAt": "2026-01-06T10:30:00+00:00", "appType": "collection", "scopes": ["collect:write", "collect:read", "payout:write", "payout:read"] }, "meta": { "requestId": "req_7af4d2b8e91c35fa4b21890e", "timestamp": "2026-01-05T10:30:00+00:00" } } ``` ## Step 2: Use Token in Requests Include the access token in the `Authorization` header for all API requests: ```bash theme={null} curl -X GET https://api.fyatu.com/api/v3/collections \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ``` ## Step 3: Refresh Token (Optional) Before your token expires, you can refresh it to get a new token. Refresh is allowed up to 5 minutes after expiry. ```bash theme={null} curl -X POST https://api.fyatu.com/api/v3/auth/refresh \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ``` ### Response ```json theme={null} { "success": true, "status": 200, "message": "Token refreshed successfully", "data": { "accessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJERDEyM0ZSNDQ0NjZDRUNFUyIsImJ1cyI6Ik4xUzBXM1E4UDBWMUU1TTZRNFIzRDhaOSIsInR5cGUiOiJjb2xsZWN0aW9uIiwic2NvcGVzIjpbImNvbGxlY3Q6d3JpdGUiLCJjb2xsZWN0OnJlYWQiLCJwYXlvdXQ6d3JpdGUiLCJwYXlvdXQ6cmVhZCJdLCJpYXQiOjE3MzYxNjIyMDAsImV4cCI6MTczNjI0ODYwMCwianRpIjoiand0XzhjZTk1YTRmMWIzZDY3ZWEyYzA5NDU4ZiJ9.m8nK3pW6rY1qL5vJ0hS9dF2gB4eA7xC3wU8tZ0iV5oR", "tokenType": "Bearer", "expiresIn": 86400, "expiresAt": "2026-01-07T10:30:00+00:00", "appType": "collection", "scopes": ["collect:write", "collect:read", "payout:write", "payout:read"] }, "meta": { "requestId": "req_8ce95a4f1b3d67ea2c09458f", "timestamp": "2026-01-06T10:30:00+00:00" } } ``` ## Error Responses ### Invalid Credentials ```json theme={null} { "success": false, "status": 401, "message": "Invalid credentials. Secret key mismatch.", "error": { "code": "AUTH_INVALID_CREDENTIALS" }, "meta": { "requestId": "req_abc123def456", "timestamp": "2026-01-05T10:30:00+00:00" } } ``` ### Token Expired ```json theme={null} { "success": false, "status": 401, "message": "Invalid or expired token", "error": { "code": "AUTH_TOKEN_INVALID" }, "meta": { "requestId": "req_abc123def456", "timestamp": "2026-01-05T10:30:00+00:00" } } ``` ### Insufficient Scope ```json theme={null} { "success": false, "status": 403, "message": "Access denied. Required scope: cards:write", "error": { "code": "AUTH_SCOPE_DENIED" }, "meta": { "requestId": "req_abc123def456", "timestamp": "2026-01-05T10:30:00+00:00" } } ``` ## JWT Payload Structure When decoded, the JWT token contains: ```json theme={null} { "sub": "DD123FR45446CECES", "bus": "BUS_xxxxxxxxxxxx", "type": "collection", "scopes": ["collect:write", "collect:read", "payout:write", "payout:read"], "iat": 1704451800, "exp": 1704455400, "jti": "jwt_xxxxxxxxxxxxxxxxxxxx" } ``` | Claim | Description | | -------- | ------------------------------------ | | `sub` | App ID (subject) | | `bus` | Business ID | | `type` | App type (`collection` or `issuing`) | | `scopes` | Array of granted permissions | | `iat` | Issued at timestamp | | `exp` | Expiration timestamp | | `jti` | Unique token identifier | ## Best Practices * Store tokens securely in memory or encrypted storage * Never expose tokens in client-side code or logs * Implement automatic token refresh before expiry * Check token expiry before each request * Refresh when less than 5 minutes remain * Handle refresh failures by re-authenticating * Catch 401 errors and re-authenticate * Catch 403 errors and check required scopes * Log request IDs for debugging with FYATU support ## Rate Limits Authentication endpoints have the following rate limits: | Endpoint | Rate Limit | | ----------------------- | ---------------------- | | `POST /v3/auth/token` | 10 requests per minute | | `POST /v3/auth/refresh` | 30 requests per minute | Exceeding rate limits will result in a `429 Too Many Requests` response. Implement exponential backoff in your retry logic. # KYC Verification Source: https://docs.fyatu.com/v3/documentation/concepts/cardholders/kyc How cardholder identity verification works. Behaviour depends on your business's KYC mode: Managed (default), Shared, or Minimal (No-KYC). # KYC Verification How a cardholder's identity is verified — and whether FYATU verifies it at all — is determined by your business's **KYC mode**. Every business is assigned one of three modes: | Mode | Who verifies the cardholder | Cardholder `kycStatus` outcome | Availability | | ----------------------- | ------------------------------------------------------------------------------ | ---------------------------------------- | ---------------------------------- | | **Managed** *(default)* | FYATU verifies each cardholder | `ACCEPTED` after verification | All businesses | | **Shared** | You supply the documents; **FYATU runs a background verification** on each one | `ACCEPTED` after background verification | After onboarding & due diligence | | **Minimal (No-KYC)** | **You** verify every cardholder yourself; FYATU waives its own KYC | `WAIVED` — frictionless issuance | Paid add-on (Enterprise / Premium) | **Compliance eligibility for Shared and Minimal.** Both modes move KYC responsibility onto you, so they are gated behind a due-diligence review. To qualify, your business **must perform full identity verification (KYC) on every cardholder** — either in-house or through a recognised KYC provider such as **Onfido, Sumsub, Didit, or Persona** — and be able to evidence it on request. Contact FYATU through your dedicated Slack channel to apply. ## Managed (default) FYATU verifies every cardholder before a card can be issued — the cardholder is not usable for issuance until `kycStatus` reaches `ACCEPTED`. There are two ways to complete it: * **Self-service** — the cardholder verifies themselves via a hosted link (`POST /cardholders/{id}/kyc/session`). Best for consumer-facing products where the end user is present. * **You submit documents** — send the cardholder's document URLs (`POST /cardholders/{id}/kyc`, or a `kyc` object at creation) and FYATU verifies them. ```mermaid theme={null} sequenceDiagram participant App as Your App participant FYATU as FYATU API participant V as Verification Provider participant User as Cardholder App->>FYATU: POST /cardholders/{id}/kyc/session FYATU-->>App: { verificationUrl, sessionId } App->>User: Redirect to verificationUrl User->>V: ID scan + liveness check V->>FYATU: Verification result FYATU->>App: Webhook: cardholder.kyc_approved Note over App: kycStatus: ACCEPTED → can now issue cards ``` ## Shared For businesses that already KYC their own cardholders and want to reuse that work. You submit the document URLs you already hold and **FYATU runs a background verification on each submission** — cardholders are **not** pre-approved; every one is checked before it reaches `ACCEPTED`. Requires completing FYATU's Shared KYC onboarding (see the compliance eligibility note above). A **one-time activation fee of \$950** enables the mode for your business. ```mermaid theme={null} sequenceDiagram participant App as Your App participant FYATU as FYATU API App->>FYATU: POST /cardholders/{id}/kyc (document URLs) FYATU-->>App: { kycStatus: "PENDING" } FYATU->>FYATU: Background verification FYATU->>App: Webhook: cardholder.kyc_approved Note over App: kycStatus: ACCEPTED → can now issue cards ``` You can also submit documents at cardholder creation by including a `kyc` object in `POST /cardholders`. ## Minimal (No-KYC) FYATU **waives its own KYC** for your cardholders: every cardholder you create is set to `kycStatus: WAIVED` and can be **issued a card immediately** — no verification session, no document submission, no FYATU review. In exchange, **you are fully responsible for KYC and AML on every cardholder** (in-house or via a KYC provider — see the compliance eligibility note above). Minimal is a paid add-on on Enterprise / Premium plans, billed as a **recurring monthly fee of \$950**. Under Minimal you never call the KYC session or document endpoints — cardholders are usable the moment they are created. If you send KYC documents for a Minimal business they are accepted but not required. ## KYC status flow | kycStatus | Description | Can issue cards | Can initiate verification | | ------------- | ---------------------------------------------- | --------------- | ----------------------------- | | `UNSUBMITTED` | No verification started (Managed/Shared) | No | Yes | | `PENDING` | Verification in progress | No | No (returns existing session) | | `ACCEPTED` | Identity verified by FYATU (Managed/Shared) | **Yes** | No | | `WAIVED` | KYC waived — you verify off-platform (Minimal) | **Yes** | No | | `REJECTED` | Verification failed (Managed/Shared) | No | Yes (retry allowed) | ## Fees | Mode | Fee | | ----------- | ----------------------------------------------------------- | | **Managed** | Per successful verification — **rate depends on your plan** | | **Shared** | **\$950** one-time activation fee | | **Minimal** | **\$950 / month** (recurring add-on) | * **Managed** — charged only when a verification **succeeds**; added to your monthly invoice and never charged on failure, abandonment, or expiry. The per-verification rate is set by your plan. * **Shared** — a single activation fee to enable the mode for your business. * **Minimal** — a recurring monthly add-on on Enterprise / Premium plans. ## Webhook Events | Event | Description | | ------------------------- | --------------------------------------------------- | | `cardholder.kyc_approved` | KYC verification approved — cards can now be issued | | `cardholder.kyc_rejected` | KYC verification failed — retry is allowed | ## Endpoints * `POST /cardholders/{id}/kyc/session` — [Initiate self-service KYC session](/v3/api-reference/cardholders/kyc-session) * `POST /cardholders/{id}/kyc` — [Submit documents (Shared KYC)](/v3/api-reference/cardholders/kyc) # Cardholders Source: https://docs.fyatu.com/v3/documentation/concepts/cardholders/overview Create cardholders, complete KYC verification, and issue virtual cards. A cardholder must be KYC-verified before cards can be issued. # Cardholders A cardholder represents a person who will receive and use virtual cards issued through your Issuing app. After creation the cardholder is active but **unverified** — KYC verification is required before any card can be issued. ``` Create Cardholder → Verify Identity (KYC) → Issue Card(s) ``` ## What a Cardholder Has | Field | Description | | ------------- | ---------------------------------------------------------------- | | `id` | Unique cardholder ID (e.g. `CH1a2b3c4d5e6f`) | | `externalId` | Your own ID for this person — map to your user database | | Personal info | First name, last name, email, phone, date of birth, gender | | Address | Street, city, state, country, postal code | | `status` | Account status: `ACTIVE`, `INACTIVE`, or `SUSPENDED` | | `kycStatus` | Identity verification status — must be `ACCEPTED` to issue cards | | `metadata` | Custom key-value data from your platform | ## Lifecycle ```mermaid theme={null} stateDiagram-v2 [*] --> Unsubmitted: POST /cardholders\n(status: ACTIVE, kycStatus: UNSUBMITTED) Unsubmitted --> Pending: Initiate KYC Pending --> Accepted: Verification approved Pending --> Rejected: Verification failed Rejected --> Pending: Retry verification note right of Accepted: Cards can be issued Accepted --> Suspended: Manual suspension Unsubmitted --> Suspended: Manual suspension Suspended --> Unsubmitted: Reactivation ``` ## Statuses ### Account Status | Status | Description | | ----------- | ---------------------- | | `ACTIVE` | Normal operating state | | `INACTIVE` | Temporarily disabled | | `SUSPENDED` | Blocked due to policy | Suspending a cardholder does **not** automatically freeze their cards. Manage card statuses separately if needed. ### KYC Status | kycStatus | Description | Can Issue Cards | | ------------- | ----------------------------------- | --------------- | | `UNSUBMITTED` | No verification started | No | | `PENDING` | Verification in progress | No | | `ACCEPTED` | Identity verified | **Yes** | | `REJECTED` | Verification failed — retry allowed | No | Cards can only be issued when `status: ACTIVE` **and** `kycStatus: ACCEPTED`. ## Quick Start ```javascript theme={null} // 1. Create cardholder const cardholder = await createCardholder({ firstName: 'Alice', lastName: 'Example', email: 'john@example.com', phone: '+15550001234', dateOfBirth: '1990-01-01', gender: 'MALE', country: 'US' }); // cardholder.kycStatus === 'UNSUBMITTED' — cannot issue cards yet // 2. Initiate KYC and redirect cardholder const kyc = await initiateKycSession(cardholder.id); redirectTo(kyc.verificationUrl); // 3. Listen for webhook, then issue card // Webhook: cardholder.kyc_approved const card = await createCard({ cardholderId: cardholder.id }); ``` See [KYC Verification](/v3/documentation/concepts/cardholders/kyc) for details on the verification paths. ## Integration Patterns ### External ID Mapping Link cardholders to your existing user database using `externalId`: ```javascript theme={null} await createCardholder({ firstName: 'Alice', lastName: 'Doe', email: 'john@example.com', phone: '+15550001234', dateOfBirth: '1985-03-20', gender: 'MALE', country: 'US', externalId: 'user_12345' // your database ID }); ``` ### Suspend / Reactivate ```javascript theme={null} // Suspend a cardholder PATCH /cardholders/{id} { "status": "SUSPENDED" } // Reactivate PATCH /cardholders/{id} { "status": "ACTIVE" } ``` ## Best Practices Gather all required fields at registration: `firstName`, `lastName`, `email`, `phone`, `dateOfBirth`, `gender`, `country`. Validate email format and use E.164 phone format (+country code + number). Trigger the KYC flow immediately after cardholder creation so verification completes before the cardholder expects to use a card. Delaying KYC is the most common reason card issuance fails. Always set `externalId` to your internal user ID. It makes cardholder lookups and reconciliation straightforward without storing our cardholder IDs in your system. ## Error Codes | Code | Cause | Resolution | | ----------------------------- | ------------------------------- | ---------------------------------------------- | | `DUPLICATE_EMAIL` | Email already registered | Use existing cardholder or a different email | | `DUPLICATE_EXTERNAL_ID` | External ID already used | Check existing cardholders first | | `CARDHOLDER_NOT_VERIFIED` | KYC not accepted | Complete KYC verification before issuing cards | | `CARDHOLDER_HAS_ACTIVE_CARDS` | Cannot delete with active cards | Terminate all cards before deleting | ## Webhook Events | Event | Description | | ---------------------- | ------------------------- | | `cardholder.created` | New cardholder registered | | `cardholder.updated` | Cardholder info modified | | `cardholder.suspended` | Cardholder suspended | | `cardholder.activated` | Cardholder reactivated | ## Endpoints * `GET /cardholders` — [List cardholders](/v3/api-reference/cardholders/list) * `POST /cardholders` — [Create cardholder](/v3/api-reference/cardholders/create) * `GET /cardholders/{id}` — [Get cardholder](/v3/api-reference/cardholders/get) * `PATCH /cardholders/{id}` — [Update cardholder](/v3/api-reference/cardholders/update) * `DELETE /cardholders/{id}` — [Delete cardholder](/v3/api-reference/cardholders/delete) # Card Fees Source: https://docs.fyatu.com/v3/documentation/concepts/cards/fees Fee structure for card issuing — issuance, maintenance, funding, unloading, and per-transaction fees. Retrieve your current rates via the pricing endpoint. # Card Fees Fees are configured per business and can be customised. Use [GET /account/pricing](/v3/api-reference/account/pricing) to retrieve your current rates. Businesses with approved KYB may receive reduced rates. ## Issuance & Maintenance | Fee Code | Description | Type | When Applied | | ------------------------------- | --------------------------------------- | ----- | ------------------------------- | | `CARD_ISSUANCE_FEE_UNTOKENIZED` | Standard card creation fee | Fixed | When a standard card is issued | | `CARD_ISSUANCE_FEE_TOKENIZED` | Tokenized card creation fee | Fixed | When a tokenized card is issued | | `CARD_MONTHLY_FEE_UNTOKENIZED` | Monthly maintenance for standard cards | Fixed | Monthly | | `CARD_MONTHLY_FEE_TOKENIZED` | Monthly maintenance for tokenized cards | Fixed | Monthly | | `CARD_REPLACEMENT_FEE` | Replacement card fee | Fixed | When replacing a card | | `CARD_DELETION_FEE` | Card termination fee | Fixed | When terminating a card | | `CARD_PLASTIC_FEE` | Physical plastic card surcharge | Fixed | For physical cards | | `CARD_METAL_FEE` | Physical metal card surcharge | Fixed | For metal cards | ## Funding & Unloading | Fee Code | Description | Type | When Applied | | -------------------- | ------------------------------------- | ---------- | --------------------- | | `CARD_FUNDING_FEE` | Fee for loading funds onto a card | Percentage | When funding a card | | `CARD_UNLOADING_FEE` | Fee for withdrawing funds from a card | Percentage | When unloading a card | ## Transaction Fees | Fee Code | Description | Type | When Applied | | ------------------------ | --------------------------------- | ---------- | ----------------------------------------- | | `CARD_AUTHORIZATION_FEE` | Fee per transaction authorization | Fixed | Per authorized transaction | | `CARD_SETTLEMENT_FEE` | Fee per settled transaction | Fixed | Per settled transaction | | `CARD_CROSSBORDER_FEE` | Cross-border transaction fee | Percentage | When card is used in a different currency | | `DECLINE_FEE` | Declined transaction fee | Fixed | Per declined transaction | ## Endpoint * `GET /account/pricing` — [Get your current fee rates](/v3/api-reference/account/pricing) # Card Operations Source: https://docs.fyatu.com/v3/documentation/concepts/cards/operations Create, fund, freeze, terminate, and replace cards. Retrieve card transactions and handle webhook events for the full card lifecycle. # Card Operations ## Creating a Card **Two prerequisites**: your business must have completed KYB verification, and the target cardholder must have `kycStatus: ACCEPTED` before a card can be issued to them. ```mermaid theme={null} sequenceDiagram participant App as Your App participant FYATU as FYATU API participant Provider as Card Provider App->>FYATU: POST /cards Note over FYATU: Validate cardholder is active Note over FYATU: Check business wallet balance FYATU->>FYATU: Debit business wallet FYATU->>Provider: Create card Provider-->>FYATU: Card details FYATU-->>App: Card created ``` ```javascript theme={null} POST /cards { "cardholderId": "CH1a2b3c4d5e6f", "amount": 100.00, "name": "JOHN DOE", // optional, defaults to cardholder name "productId": "MCUSD1" // optional, defaults to default product } ``` *** ## Fund Add money to a card from your business wallet: ```javascript theme={null} POST /cards/{cardId}/fund { "amount": 50.00 } ``` * A funding fee (percentage-based) may apply — see [Card Fees](/v3/documentation/concepts/cards/fees) * Full amount is added to the card balance; fee is debited from your wallet separately * Can fund a frozen card ## Unload Withdraw balance from a card back to your wallet: ```javascript theme={null} POST /cards/{cardId}/unload { "amount": 25.00 } ``` * Cannot unload more than the available card balance * An unloading fee may apply based on your pricing ## Freeze / Unfreeze Temporarily disable a card without losing its balance: ```javascript theme={null} POST /cards/{cardId}/freeze // disable transactions POST /cards/{cardId}/unfreeze // re-enable transactions ``` * Frozen cards cannot make purchases but retain their balance * You can still fund or unload a frozen card ## Terminate Permanently close a card: ```javascript theme={null} DELETE /cards/{cardId} { "reason": "Customer requested closure", // optional "reference": "REF123456" // optional } ``` **Balance refund**: any remaining card balance is automatically returned to your business wallet on termination — no fee is charged. ```json theme={null} { "status": true, "message": "Card terminated successfully", "data": { "id": "CRD678E4F2A1B3C9", "status": "TERMINATED", "reason": "Customer requested closure", "refundedBalance": 25.00, "terminatedAt": "2026-01-15T10:30:00+00:00", "reference": "REF123456" } } ``` * Card cannot be reactivated after termination * Cardholder can still have other active cards * Provide a `reason` for record-keeping (visible in webhooks and logs) ## Replace Issue a replacement card for a terminated card. The balance is transferred automatically: ```javascript theme={null} POST /cards/{cardId}/replace ``` If the original product has `canIssue: false`, the default product is used as a fallback. *** ## Transactions ### Transaction Types | Type | Description | | --------- | ---------------------------------------- | | `debit` | Purchase or payment (money leaves card) | | `credit` | Refund or cashback (money added to card) | | `funding` | Card funding operation | | `unload` | Card unloading operation | ### Transaction Statuses | Status | Description | | ----------- | -------------------- | | `PENDING` | Authorization hold | | `COMPLETED` | Successfully settled | | `DECLINED` | Transaction rejected | | `REVERSED` | Refunded or reversed | ```javascript theme={null} GET /cards/{cardId}/transactions?page=1 ``` *** ## Webhook Events ### Card Lifecycle | Event | Trigger | | ------------------------- | ----------------------------------------------------------------------- | | `card.funded` | Funds added | | `card.funding_failed` | Funding was rejected; the debited amount is returned | | `card.unloaded` | Funds withdrawn | | `card.unloading_failed` | Unload was rejected; the card keeps the balance | | `card.frozen` | Card frozen | | `card.unfrozen` | Card unfrozen | | `card.terminated` | Card permanently closed. Delivered twice: `PROCESSING` then `COMPLETED` | | `card.termination_refund` | The balance left on a terminated card has been returned | | `card.negative_balance` | Card settled below zero and owes a balance | ### Transaction Events | Event | Trigger | | -------------------------------------------- | ---------------------------------------------- | | `card.transaction.approved` | Transaction approved | | `card.transaction.declined` | Transaction declined | | `card.transaction.reversed` | Transaction reversed | | `card.transaction.cross_border_fee` | A cross-border fee was charged | | `card.transaction.decline_fee_domestic` | A fee was charged for a domestic decline | | `card.transaction.decline_fee_international` | A fee was charged for an international decline | *** ## Error Codes | Code | Cause | Resolution | | --------------------------- | ---------------------------------------- | ----------------------------------------------------- | | `INSUFFICIENT_BALANCE` | Wallet balance too low | Top up business wallet | | `INSUFFICIENT_CARD_BALANCE` | Card balance too low | Fund the card | | `CARD_NOT_ACTIVE` | Card is frozen, suspended, or terminated | Check status; unfreeze if frozen | | `CARD_INACTIVE` | Physical card not yet activated | Activate with activation code | | `CARD_SUSPENDED` | System suspension | Fund the card (decline suspension) or contact support | | `CARD_TERMINATED` | Card permanently closed | Issue a new card | | `CARDHOLDER_INACTIVE` | Cardholder not active | Activate the cardholder first | | `BUSINESS_KYB_REQUIRED` | Business verification incomplete | Complete KYB verification | | `ALREADY_FROZEN` | Card already frozen | No action needed | | `ALREADY_ACTIVE` | Card already active | No action needed | | `DECLINE_LIMIT_REACHED` | 15 insufficient funds declines | Fund the card to reset and reactivate | *** ## Endpoints * `POST /cards` — [Create card](/v3/api-reference/cards/create) * `GET /cards` — [List cards](/v3/api-reference/cards/list) * `GET /cards/{id}` — [Get card details](/v3/api-reference/cards/get) * `DELETE /cards/{id}` — [Terminate card](/v3/api-reference/cards/delete) * `POST /cards/{id}/fund` — [Fund card](/v3/api-reference/cards/fund) * `POST /cards/{id}/unload` — [Unload card](/v3/api-reference/cards/unload) * `POST /cards/{id}/freeze` — [Freeze card](/v3/api-reference/cards/freeze) * `POST /cards/{id}/unfreeze` — [Unfreeze card](/v3/api-reference/cards/unfreeze) * `POST /cards/{id}/replace` — [Replace card](/v3/api-reference/cards/replace) * `GET /cards/{id}/transactions` — [Get transactions](/v3/api-reference/cards/transactions) # Cards Overview Source: https://docs.fyatu.com/v3/documentation/concepts/cards/overview Virtual Mastercard and Visa prepaid cards — types, lifecycle, statuses, and how decline count and auto-suspension work. # Cards Overview A card represents a set of payment identifiers: the PAN (16-digit number), expiry date, and CVV. When a card is terminated and a replacement is issued, an entirely new card is created with a new ID, CVV, and expiry — old cards remain in your history as terminated. ## Card Types ### Virtual Cards Digital cards for online transactions. Issued instantly and **active by default**. | Feature | Standard | Tokenized | | ---------------------- | ---------------- | --------- | | Issuance | Instant | Instant | | Initial status | Active | Active | | Networks | Mastercard, Visa | Visa | | Apple Pay / Google Pay | No | Yes | | 3D Secure | Yes | Yes | Tokenized cards support provisioning to digital wallets (Apple Pay, Google Pay) for contactless in-store payments. Use [List Products](/v3/api-reference/cards/products) to see which products have `isTokenized: true`. ### Physical Cards *(Coming Soon)* Plastic or metal cards shipped to the cardholder's address. **Inactive by default** — require activation with a code before use. | Feature | Plastic | Metal | | -------------- | ------------------------------ | ------------------------------ | | Delivery | 7–14 business days | 7–14 business days | | Initial status | Inactive (requires activation) | Inactive (requires activation) | | Contactless | Yes | Yes | | ATM | Yes | Yes | ### Crypto Cards *(Coming Soon)* Cards funded directly from cryptocurrency balances with automatic conversion. *** ## Card Lifecycle ```mermaid theme={null} stateDiagram-v2 [*] --> Inactive: Create physical card [*] --> Active: Create virtual card Inactive --> Active: Activate with code Active --> Frozen: Freeze Active --> Suspended: System suspension Frozen --> Active: Unfreeze Suspended --> Active: Resolve suspension Active --> Terminated: Revoke / Delete Frozen --> Terminated: Revoke / Delete Suspended --> Terminated: Revoke / Delete Terminated --> [*] ``` ## Card Statuses | Status | Description | Can Transact | Can Fund | | ------------ | -------------------------------------------------------- | ------------ | -------- | | `INACTIVE` | Issued but not yet activated (physical only) | No | No | | `ACTIVE` | Usable for all transactions | Yes | Yes | | `FROZEN` | Temporarily disabled by cardholder or business | No | Yes | | `SUSPENDED` | Disabled by system (fraud, compliance, or decline limit) | No | Yes\* | | `TERMINATED` | Permanently closed — cannot be reactivated | No | No | **FROZEN vs SUSPENDED**: A `FROZEN` card is disabled by you and can be unfrozen via API. A `SUSPENDED` card is disabled by the system and requires either funding (for decline suspensions) or support intervention to resolve. ## Status Transitions | Action | From | To | | ----------------------------------------- | --------------------- | ------------ | | Create virtual card | — | `ACTIVE` | | Create physical card | — | `INACTIVE` | | Activate with code | `INACTIVE` | `ACTIVE` | | Freeze | `ACTIVE` | `FROZEN` | | Unfreeze | `FROZEN` | `ACTIVE` | | Fund card (resets decline count) | `SUSPENDED` (decline) | `ACTIVE` | | System detects fraud / compliance issue | `ACTIVE` / `FROZEN` | `SUSPENDED` | | 15 insufficient funds declines | `ACTIVE` | `SUSPENDED` | | Support resolves suspension | `SUSPENDED` | `ACTIVE` | | Revoke / Delete / Lost / Stolen / Expired | Any non-terminated | `TERMINATED` | ## Decline Count & Auto-Suspension Cards track consecutive declined transactions due to insufficient funds: * **Limit**: 15 consecutive insufficient funds declines triggers automatic suspension * **Reset**: funding the card resets the decline count and reactivates the card (if suspended due to declines only) * **Prevention**: monitor `declineCount` in the card details response to warn cardholders before suspension # Card Products Source: https://docs.fyatu.com/v3/documentation/concepts/cards/products Each card is issued from a product that determines its brand, currency, spending limits, and features. Learn how to discover and select the right product. # Card Products Every card is issued from a **product** — a template that defines the card's brand, currency, spending limits, tokenization support, and available features. Use [List Products](/v3/api-reference/cards/products) to discover what's available for your business. ## Selecting a Product Pass `productId` when [creating a card](/v3/api-reference/cards/create): ```javascript theme={null} POST /cards { "cardholderId": "CH1a2b3c4d5e6f", "amount": 100.00, "productId": "MCUSD1" // specific product } ``` If `productId` is omitted, the product marked `isDefault: true` is used automatically. ## Retired Products Products are occasionally consolidated — several older products replaced by one. When that happens the old `productId` keeps working: it resolves to the product that replaced it, and the card is issued on the replacement's terms. | Retired `productId` | Issues as | | ------------------- | ------------- | | `MCSILVER` | `MCELITE` | | `MCWORLD` | `MCELITE` | | `VISAPLATINUM` | `VISACLASSIC` | The replacement is a real product with its own price, BIN and limits, so a retired `productId` is charged and issued at the replacement's terms — not the withdrawn product's. Check [List Products](/v3/api-reference/cards/products) for current pricing, and update your integration to the new `productId` when convenient; cards already issued keep working either way. ## Availability Flags Each product exposes flags that indicate what's currently supported: | Flag | Description | | ----------- | ----------------------------------------------------------------------------------------------------------------- | | `canIssue` | Whether new cards can be created from this product. Only `canIssue: true` products are accepted at card creation. | | `canFund` | Whether cards of this product can receive funding. | | `canUnload` | Whether cards of this product support unloading (withdrawing balance). | | `isDefault` | Fallback product used during card replacement when the original product is unavailable. | **Replacement fallback**: when replacing a card, the system first tries to issue the same product. If that product has `canIssue: false`, the `isDefault: true` product is used instead. The card balance is transferred seamlessly regardless. ## Spending Limits Each product defines a `spendingLimit` and `spendingPeriod` that cap how much can be spent within a time window. For example, `spendingLimit: 25000` with `spendingPeriod: DAILY` allows up to \$25,000 in purchases per day. Limits apply to purchase transactions only — funding and unloading are not affected. ## Multi-Currency Cards are available in multiple currencies (USD, EUR). For non-USD cards, `amount` is specified in the card's currency and your USD wallet is debited the equivalent at the current exchange rate: | Card Currency | Wallet Debit | Card Balance | | ------------- | ------------------------ | ------------- | | USD | Direct (1:1) | Amount in USD | | EUR | Converted at market rate | Amount in EUR | ## Endpoint * `GET /cards/products` — [List available card products](/v3/api-reference/cards/products) # Error Handling Source: https://docs.fyatu.com/v3/documentation/errors Fyatu API error codes and HTTP status codes. Handle validation errors, insufficient balance, invalid tokens, and provider failures in your card issuing integration. # Error Handling The FYATU API uses conventional HTTP response codes and returns detailed error messages with machine-readable error codes to help you handle errors programmatically. ## Response Format All API responses follow a consistent format: ```json Success Response theme={null} { "success": true, "status": 200, "message": "Operation completed successfully", "data": { // Response data here }, "meta": { "requestId": "req_7af4d2b8e91c35fa", "timestamp": "2026-01-05T10:30:00+00:00" } } ``` ```json Error Response theme={null} { "success": false, "status": 400, "message": "Validation failed", "error": { "code": "VALIDATION_ERROR", "details": [ { "field": "amount", "message": "Amount is required" } ] }, "meta": { "requestId": "req_7af4d2b8e91c35fa", "timestamp": "2026-01-05T10:30:00+00:00" } } ``` Always include the `requestId` from the `meta` object when contacting support — it helps us trace your request through our systems. ## HTTP Status Codes | Code | Status | Description | | ----- | -------------------- | ---------------------------------------------------- | | `200` | OK | Request succeeded | | `201` | Created | Resource created successfully | | `400` | Bad Request | Invalid request parameters or validation failed | | `401` | Unauthorized | Authentication failed or token expired | | `402` | Payment Required | Insufficient wallet balance | | `403` | Forbidden | Access denied (scope mismatch or resource not owned) | | `404` | Not Found | Resource doesn't exist | | `409` | Conflict | Duplicate reference or conflicting operation | | `422` | Unprocessable Entity | Request understood but cannot be processed | | `429` | Too Many Requests | Rate limit exceeded | | `500` | Server Error | Something went wrong on our end | ## Error Codes Each error response includes a machine-readable `code` field: | Code | HTTP | Description | | -------------------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------- | | `AUTH_TOKEN_MISSING` | 401 | No authorization token provided | | `AUTH_TOKEN_INVALID` | 401 | Token is malformed, expired, or revoked | | `AUTH_INVALID_CREDENTIALS` | 401 | App ID or secret key is incorrect | | `AUTH_SCOPE_DENIED` | 403 | Token doesn't have the required scope for this endpoint | | `VALIDATION_ERROR` | 400 | One or more request fields failed validation | | `RESOURCE_NOT_FOUND` | 404 | The requested resource does not exist | | `INSUFFICIENT_BALANCE` | 402 | Wallet balance is too low for this operation | | `DUPLICATE_REFERENCE` | 409 | The `externalReference` has already been used | | `REFERENCE_ALREADY_FAILED` | 409 | A card fund/unload `reference` already resolved to `FAILED`. References are single-use — send a **new** `reference` to retry | | `RATE_LIMIT_EXCEEDED` | 429 | Too many requests — slow down | | `INTERNAL_ERROR` | 500 | Unexpected server error | ## Common Error Scenarios ### Authentication Errors (401) ```json Missing Token theme={null} { "success": false, "status": 401, "message": "Authorization token is required", "error": { "code": "AUTH_TOKEN_MISSING" } } ``` **Solution**: Include the `Authorization: Bearer {token}` header in your request. ```json Expired Token theme={null} { "success": false, "status": 401, "message": "Invalid or expired token", "error": { "code": "AUTH_TOKEN_INVALID" } } ``` **Solution**: Refresh your token via `POST /v3/auth/refresh` or request a new one via `POST /v3/auth/token`. ```json Insufficient Scope theme={null} { "success": false, "status": 403, "message": "Access denied. Required scope: cards:write", "error": { "code": "AUTH_SCOPE_DENIED" } } ``` **Solution**: Your app type doesn't have access to this endpoint. A Collection App cannot access Card endpoints and vice versa. ### Validation Errors (400) ```json theme={null} { "success": false, "status": 400, "message": "Validation failed", "error": { "code": "VALIDATION_ERROR", "details": [ { "field": "amount", "message": "Amount must be greater than 0" }, { "field": "email", "message": "The email field must contain a valid email address" } ] } } ``` **Solution**: Check the `details` array for specific field requirements and fix the request payload. ### Insufficient Balance (402) ```json theme={null} { "success": false, "status": 402, "message": "Insufficient balance. Required: $105.00, Available: $50.00", "error": { "code": "INSUFFICIENT_BALANCE" } } ``` **Solution**: Fund your business wallet before retrying. The required amount includes applicable fees. ### Resource Not Found (404) ```json theme={null} { "success": false, "status": 404, "message": "Card not found", "error": { "code": "RESOURCE_NOT_FOUND" } } ``` **Solution**: Verify the resource ID exists and belongs to your app. ### Duplicate Reference (409) ```json theme={null} { "success": false, "status": 409, "message": "A collection with this external reference already exists", "error": { "code": "DUPLICATE_REFERENCE" } } ``` **Solution**: Use a unique `externalReference` for each new operation. ### Rate Limiting (429) ```json theme={null} { "success": false, "status": 429, "message": "Rate limit exceeded. Try again in 60 seconds.", "error": { "code": "RATE_LIMIT_EXCEEDED" } } ``` **Solution**: Implement exponential backoff in your retry logic. ## Error Handling Best Practices ### 1. Always Check the Response ```javascript theme={null} const response = await fetch('https://api.fyatu.com/api/v3/cards', { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify(cardData) }); const result = await response.json(); if (!result.success) { console.error(`API Error [${result.error?.code}]: ${result.message}`); console.error('Request ID:', result.meta?.requestId); return; } // Process successful response console.log('Card created:', result.data.cardId); ``` ### 2. Handle Specific Error Codes ```javascript theme={null} async function createCollection(data) { const result = await fyatuRequest('POST', '/v3/collections', data); if (result.success) { return { ok: true, collection: result.data }; } switch (result.error?.code) { case 'AUTH_TOKEN_INVALID': // Token expired — refresh and retry await refreshToken(); return createCollection(data); case 'VALIDATION_ERROR': return { ok: false, fields: result.error.details }; case 'INSUFFICIENT_BALANCE': return { ok: false, error: 'Please fund your wallet' }; case 'DUPLICATE_REFERENCE': return { ok: false, error: 'This order was already submitted' }; case 'RATE_LIMIT_EXCEEDED': await sleep(60000); return createCollection(data); default: return { ok: false, error: result.message }; } } ``` ### 3. Implement Retry with Backoff ```javascript theme={null} async function requestWithRetry(method, endpoint, data, maxRetries = 3) { for (let attempt = 1; attempt <= maxRetries; attempt++) { const result = await fyatuRequest(method, endpoint, data); if (result.success) return result; // Only retry on rate limits and server errors if (result.status === 429 || result.status >= 500) { const waitMs = Math.pow(2, attempt) * 1000; console.log(`Retry ${attempt}/${maxRetries} in ${waitMs}ms...`); await new Promise(r => setTimeout(r, waitMs)); continue; } // Don't retry client errors (400, 401, 403, 404, 409) return result; } throw new Error(`Failed after ${maxRetries} retries`); } ``` ### 4. Log Errors for Debugging ```javascript theme={null} function logApiError(endpoint, result) { console.error({ timestamp: new Date().toISOString(), endpoint, status: result.status, code: result.error?.code, message: result.message, requestId: result.meta?.requestId, // Never log tokens, card numbers, or secrets }); } ``` ## Idempotency For operations that create resources, use the `externalReference` field to prevent duplicates: ```javascript theme={null} // Use a deterministic reference for each operation const reference = `order_${orderId}_${customerId}`; const result = await fyatuRequest('POST', '/v3/collections', { amount: 25.00, currency: 'USD', externalReference: reference, description: 'Premium subscription' }); // If you retry with the same externalReference, // the API returns 409 instead of creating a duplicate ``` This prevents duplicate charges if your request times out and you retry. ## Rate Limits | Endpoint Category | Rate Limit | | --------------------------------- | ----------------------- | | Authentication (`/v3/auth/*`) | 10 requests per minute | | Read endpoints (GET) | 120 requests per minute | | Write endpoints (POST/PUT/DELETE) | 60 requests per minute | Exceeding rate limits will result in `429 Too Many Requests`. Implement exponential backoff in your retry logic. # Introduction Source: https://docs.fyatu.com/v3/documentation/introduction Fyatu API v3 — issue virtual prepaid cards, collect payments, and send payouts programmatically via REST. # Fyatu API The Fyatu API lets you embed financial infrastructure directly into your product. Issue virtual Mastercard prepaid cards, accept payments from Fyatu users, and send payouts — all through a single REST API. Make your first API call in minutes JWT tokens, scopes, and best practices Browse all endpoints and schemas Receive real-time event notifications ## What You Can Build Issue Mastercard prepaid virtual cards to your cardholders programmatically. Fund them on creation, add balance at any time, freeze or unfreeze, and terminate when no longer needed. Full card details (PAN, CVV, expiry) are available via API. Create checkout sessions and accept payments from Fyatu users. Customers are redirected to a hosted checkout page and you receive a webhook notification on completion. Refunds are also supported. Send payouts to verified external accounts. Ideal for vendor payments, affiliate disbursements, and salary payouts. Create and manage the individuals associated with your issued cards. Collect KYC documents or initiate a hosted KYC verification session before issuing cards. ## Base URL ``` https://api.fyatu.com/api/v3 ``` ## App-Based Access Every API call is scoped to an **App** you create in your Business Console. The app type determines which APIs are available: | App Type | APIs Available | Scopes Granted | | ------------------ | ----------------------------- | -------------------------------------------------------------------- | | **Collection App** | Collections, Payouts, Refunds | `collect:write`, `collect:read`, `payout:write`, `payout:read` | | **Issuing App** | Cards, Cardholders | `cards:write`, `cards:read`, `cardholders:write`, `cardholders:read` | All app types also have access to the **Account** endpoints (wallet balance, transactions, statement, invoices, pricing). ## Getting Started Sign up at [web.fyatu.com](https://web.fyatu.com/auth/register) and complete identity verification (KYC). Upgrade your account to Business from the dashboard to access the Business Console. In the Business Console, create a **Collection App** or an **Issuing App** depending on what you want to build. From your app's **Settings → API Keys & Credentials**, copy your `appId` and `secretKey`. Exchange your credentials for a JWT access token and include it as a `Bearer` token on every request. Follow the quickstart guide to make your first API call ## Key Concepts | Concept | Description | | ------------------- | ----------------------------------------------------------------------------------------------------------- | | **JWT tokens** | Short-lived access tokens (24 h) scoped to your app type. Refresh up to 5 minutes after expiry. | | **Scopes** | Each token carries the permissions granted to its app type. Calls outside the scope return `403`. | | **requestId** | Every response includes a `meta.requestId` — use it when contacting support. | | **Webhooks** | HMAC-SHA256 signed callbacks for every significant event. Verify the signature before trusting the payload. | | **Business wallet** | Your USD balance used to fund card issuance, payouts, and fees. Replenish via USDT deposit. | ## Need Help? Click the chat icon in the bottom right corner to talk to our support team, or reach us at [support@fyatu.com](mailto:support@fyatu.com). # Quick Start Source: https://docs.fyatu.com/v3/documentation/quickstart Get started with Fyatu's card issuing API in under 5 minutes. Authenticate, create a cardholder, issue your first virtual Mastercard, and set up webhooks. # Quick Start Guide This guide will walk you through making your first FYATU API call. By the end, you'll have: 1. Authenticated with the API using JWT 2. Created a cardholder (Issuing App) or a collection (Collection App) ## Prerequisites Sign up for a free FYATU account at [web.fyatu.com/auth/register](https://web.fyatu.com/auth/register) Submit your identity documents to verify your account Upgrade your account to Business from the dashboard Create a **Collection App** or **Issuing App** from your Business Console Deposit USDT to your business wallet (minimum \$10 to get started) ## Step 1: Get Your Credentials Log into your [Business Console](https://web.fyatu.com/business/apps), select your app, and go to **Settings > API Keys & Credentials**: You'll need two credentials for authentication: | Credential | Description | Example | | -------------- | ---------------------------- | ----------------------- | | **App ID** | Your app's unique identifier | `DD123FR45446CECES` | | **Secret Key** | Your app's secret key | `sk_live_xxxxxxxxxxxxx` | Keep your secret key safe! It's only shown once when generated. Never expose it in client-side code or public repositories. ## Step 2: Get an Access Token Exchange your credentials for a JWT access token: ```bash cURL theme={null} curl -X POST "https://api.fyatu.com/api/v3/auth/token" \ -H "Content-Type: application/json" \ -d '{ "appId": "DD123FR45446CECES", "secretKey": "your_secret_key_here", "grantType": "client_credentials" }' ``` ```javascript Node.js theme={null} const response = await fetch('https://api.fyatu.com/api/v3/auth/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ appId: 'DD123FR45446CECES', secretKey: 'your_secret_key_here', grantType: 'client_credentials' }) }); const { data } = await response.json(); const accessToken = data.accessToken; ``` ```python Python theme={null} import requests response = requests.post( 'https://api.fyatu.com/api/v3/auth/token', json={ 'appId': 'DD123FR45446CECES', 'secretKey': 'your_secret_key_here', 'grantType': 'client_credentials' } ) data = response.json() access_token = data['data']['accessToken'] ``` ```php PHP theme={null} $ch = curl_init('https://api.fyatu.com/api/v3/auth/token'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode([ 'appId' => 'DD123FR45446CECES', 'secretKey' => 'your_secret_key_here', 'grantType' => 'client_credentials' ]) ]); $response = json_decode(curl_exec($ch), true); $accessToken = $response['data']['accessToken']; ``` You should receive a response like: ```json theme={null} { "success": true, "status": 200, "data": { "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "tokenType": "Bearer", "expiresIn": 86400, "expiresAt": "2026-01-16T21:31:39+00:00" } } ``` Your token is valid for **24 hours**. Use it in the `Authorization` header for all subsequent requests: ```bash theme={null} Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... ``` ## Step 3: Make Your First API Call Use the access token to make authenticated requests. Here are examples for each app type: ### Create a Cardholder ```bash cURL theme={null} curl -X POST "https://api.fyatu.com/api/v3/cardholders" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "externalId": "EXT-0001", "firstName": "Alice", "lastName": "Example", "email": "alice@example.com", "phone": "+15550001234", "dateOfBirth": "1990-01-01", "gender: 'FEMALE'", "address": "123 Main Street, Apt 4B", "city": "Newark", "state": "Delaware", "country": "US", "zipCode": "000000" }' ``` ```javascript Node.js theme={null} const response = await fetch('https://api.fyatu.com/api/v3/cardholders', { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ externalId: 'EXT-0001', firstName: 'Alice', lastName: 'Example', email: 'alice@example.com', phone: '+15550001234', dateOfBirth: '1990-01-01', gender: 'FEMALE'', address: '123 Main Street, Apt 4B', city: 'Newark', state: 'Delaware', country: 'US', zipCode: '000000' }) }); const result = await response.json(); console.log('Cardholder ID:', result.data.id); ``` ```php PHP theme={null} $data = [ 'externalId' => 'EXT-0001', 'firstName' => 'Alice', 'lastName' => 'Example', 'email' => 'alice@example.com', 'phone' => '+15550001234', 'dateOfBirth' => '1990-01-01', 'gender: 'FEMALE'', 'address' => '123 Main Street, Apt 4B', 'city' => 'Newark', 'state' => 'Delaware', 'country' => 'US', 'zipCode' => '000000' ]; $ch = curl_init('https://api.fyatu.com/api/v3/cardholders'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . $accessToken, 'Content-Type: application/json' ], CURLOPT_POSTFIELDS => json_encode($data) ]); $result = json_decode(curl_exec($ch), true); echo 'Cardholder ID: ' . $result['data']['id']; ``` ```json Response (201) theme={null} { "success": true, "status": 201, "message": "Cardholder created successfully", "data": { "id": "CH1a2b3c4d5e6f", "externalId": "EXT-0001", "firstName": "Alice", "lastName": "Example", "email": "alice@example.com", "phone": "+15550001234", "dateOfBirth": "1990-01-01", "gender: 'FEMALE'", "address": { "line1": "123 Main Street, Apt 4B", "city": "Newark", "state": "Delaware", "country": "US", "zipCode": "000000" }, "status": "ACTIVE", "kycStatus": "UNSUBMITTED", "createdAt": "2026-01-15T21:31:39+00:00" }, "meta": { "requestId": "req_2f7fb4227a1007418773122f", "timestamp": "2026-01-15T21:31:39+00:00" } } ``` Save the cardholder `id` — you'll need it to create a card. ### Issue a Virtual Card ```bash cURL theme={null} curl -X POST "https://api.fyatu.com/api/v3/cards" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "cardholderId": "CH1a2b3c4d5e6f", "amount": 100.00, "name": "JOHN SMITH", "productId": "MCUSD1" }' ``` ```javascript Node.js theme={null} const response = await fetch('https://api.fyatu.com/api/v3/cards', { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ cardholderId: 'CH1a2b3c4d5e6f', amount: 100.00, name: 'JOHN SMITH', productId: 'MCUSD1' }) }); const result = await response.json(); console.log('Card ID:', result.data.id); console.log('Last 4:', result.data.last4); ``` ```php PHP theme={null} $ch = curl_init('https://api.fyatu.com/api/v3/cards'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . $accessToken, 'Content-Type: application/json' ], CURLOPT_POSTFIELDS => json_encode([ 'cardholderId' => 'CH1a2b3c4d5e6f', 'amount' => 100.00, 'name' => 'JOHN SMITH', 'productId' => 'MCUSD1' ]) ]); $result = json_decode(curl_exec($ch), true); echo 'Card ID: ' . $result['data']['id']; ``` ```json Response (201) theme={null} { "success": true, "status": 201, "message": "Card created successfully", "data": { "id": "crd_9x8y7z6w5v4u3t2s", "cardholderId": "CH1a2b3c4d5e6f", "name": "JOHN SMITH", "last4": "4829", "maskedNumber": "****4829", "expiryDate": "03/2029", "brand": "MASTERCARD", "currency": "USD", "status": "ACTIVE", "initialBalance": 100.00, "createdAt": "2026-01-15T21:35:00+00:00" }, "meta": { "requestId": "req_8ce95a4f1b3d67ea2c09458f", "timestamp": "2026-01-15T21:35:00+00:00" } } ``` Card details (full number, CVV) are returned only on the [Get Card](/v3/api-reference/cards/get) endpoint. Always handle them securely and never log or expose them. ### Create a Collection (Payment) ```bash cURL theme={null} curl -X POST "https://api.fyatu.com/api/v3/collections" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "amount": 25.00, "orderId": "INV-001", "description": "Premium Subscription", "callbackUrl": "https://yoursite.com/payment/complete", "webhookUrl": "https://yoursite.com/webhook/fyatu", "metadata": { "userId": "12345", "plan": "premium" } }' ``` ```javascript Node.js theme={null} const response = await fetch('https://api.fyatu.com/api/v3/collections', { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: 25.00, orderId: `INV-${Date.now()}`, description: 'Premium Subscription', callbackUrl: 'https://yoursite.com/payment/complete', webhookUrl: 'https://yoursite.com/webhook/fyatu', metadata: { userId: '12345', plan: 'premium' } }) }); const result = await response.json(); if (result.success) { // Redirect your customer to the checkout page console.log('Checkout URL:', result.data.checkoutUrl); } ``` ```php PHP theme={null} $ch = curl_init('https://api.fyatu.com/api/v3/collections'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . $accessToken, 'Content-Type: application/json' ], CURLOPT_POSTFIELDS => json_encode([ 'amount' => 25.00, 'orderId' => 'INV-' . time(), 'description' => 'Premium Subscription', 'callbackUrl' => 'https://yoursite.com/payment/complete', 'webhookUrl' => 'https://yoursite.com/webhook/fyatu', 'metadata' => [ 'userId' => '12345', 'plan' => 'premium' ] ]) ]); $result = json_decode(curl_exec($ch), true); if ($result['success']) { // Redirect customer to checkout header('Location: ' . $result['data']['checkoutUrl']); exit; } ``` ```json Response (201) theme={null} { "success": true, "status": 201, "message": "Checkout session created successfully", "data": { "collectionId": "col_a1b2c3d4e5f6", "reference": "A2B4C6D8E0F2", "orderId": "ORD-0001", "amount": 25.00, "fee": 0.75, "netAmount": 24.25, "currency": "USD", "status": "PENDING", "checkoutUrl": "https://checkout.fyatu.com/sci/checkout?batch=col_a1b2c3d4e5f6", "expiresAt": "2026-01-08T12:30:00+00:00" }, "meta": { "requestId": "req_abc123def456", "timestamp": "2026-01-08T11:30:00+00:00" } } ``` Redirect your customer to the `checkoutUrl` to complete payment. The session expires in **60 minutes**. After payment, the customer is redirected to your `callbackUrl`: ``` https://yoursite.com/payment/complete?batch=col_a1b2c3d4e5f6&status=COMPLETED ``` And your `webhookUrl` receives a server-to-server notification: ```json theme={null} { "event": "collection.received", "version": "3.0", "sign": "hmac_sha256_signature", "data": { "reference": "A2B4C6D8E0F2", "externalReference": "INV-001", "amount": 25.00, "charge": 0.75, "netAmount": 24.25, "currency": "USD", "paymentMethod": "WALLET", "customerName": "Alice Example", "status": "SUCCESS", "appId": "DD123FR45446CECES", "timestamp": "2026-01-08T11:35:00+00:00" } } ``` ## Step 4: Set Up Webhooks Configure a webhook URL to receive real-time notifications for all events (payments received, cards funded, etc.): ```bash cURL theme={null} curl -X PUT "https://api.fyatu.com/api/v3/webhooks" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "webhookUrl": "https://yoursite.com/webhooks/fyatu" }' ``` ```javascript Node.js theme={null} const response = await fetch('https://api.fyatu.com/api/v3/webhooks', { method: 'PUT', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ webhookUrl: 'https://yoursite.com/webhooks/fyatu' }) }); const result = await response.json(); // Store the webhook secret securely — it's only shown once! console.log('Webhook Secret:', result.data.webhookSecret); ``` ```json Response (200) theme={null} { "success": true, "status": 200, "message": "Webhook URL updated successfully", "data": { "webhookUrl": "https://yoursite.com/webhooks/fyatu", "hasWebhookSecret": true, "isConfigured": true, "webhookSecret": "whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", "secretNote": "This is your webhook secret. Store it securely - it will not be shown again." }, "meta": { "requestId": "req_abc123xyz789", "timestamp": "2026-01-15T10:30:00+00:00" } } ``` Your `webhookSecret` is only shown once when first generated. Store it securely — you'll need it to verify webhook signatures. See the [Webhooks guide](/v3/concepts/webhooks) for the full list of events and payload structures. ## What's Next? Token refresh, scopes, and best practices Accept payments from Fyatu users Learn about card lifecycle and operations Handle API errors gracefully # Get Webhook Configuration Source: https://docs.fyatu.com/v3/webhooks/get v3/openapi.json GET /webhooks Retrieve your current webhook configuration — endpoint URL, enabled events, and signing secret status. GET /webhooks. # Get Webhook Configuration Returns the current webhook configuration for your app, including the webhook URL and whether a webhook secret is configured. ## Request ```bash theme={null} curl -X GET https://api.fyatu.com/api/v3/webhooks \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ## Response Whether the request was successful The configured webhook URL, or an empty string if not set Whether a webhook secret is configured Whether webhooks are fully configured (URL is set) The app type, uppercased: `ISSUING` or `COLLECTION` ISO 8601 timestamp when the app was created ISO 8601 timestamp of the last configuration change ```json 200 theme={null} { "success": true, "status": 200, "message": "Webhook configuration retrieved", "data": { "webhookUrl": "https://example.com/webhooks/fyatu", "hasWebhookSecret": true, "isConfigured": true, "appType": "ISSUING", "createdAt": "2026-01-10T08:15:00+00:00", "updatedAt": "2026-01-15T10:30:00+00:00" }, "meta": { "requestId": "req_abc123xyz789", "timestamp": "2026-01-15T10:30:00+00:00" } } ``` ```json 200 (Not Configured) theme={null} { "success": true, "status": 200, "message": "Webhook configuration retrieved", "data": { "webhookUrl": "", "hasWebhookSecret": false, "isConfigured": false, "appType": "ISSUING", "createdAt": "2026-01-10T08:15:00+00:00", "updatedAt": "2026-01-10T08:15:00+00:00" }, "meta": { "requestId": "req_def456uvw123", "timestamp": "2026-01-15T10:30:00+00:00" } } ``` ## Use Cases * Check if webhooks are configured before testing * Verify webhook URL in your integration dashboard * Debug webhook delivery issues # Regenerate Webhook Secret Source: https://docs.fyatu.com/v3/webhooks/regenerate-secret v3/openapi.json POST /webhooks/secret/regenerate Generate a new HMAC-SHA256 webhook signing secret. Previous secret is immediately invalidated. POST /webhooks/regenerate-secret. # Regenerate Webhook Secret Generate a new webhook secret for signing webhook payloads. This immediately invalidates your previous secret. After regenerating your secret, you must update your webhook handler with the new secret. Any webhooks sent after regeneration will be signed with the new secret, and verification using the old secret will fail. ## Request ```bash theme={null} curl -X POST https://api.fyatu.com/api/v3/webhooks/secret/regenerate \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` No request body is required. ## Response Whether the request was successful Your new webhook secret. **Store this securely!** ISO 8601 timestamp of when the secret was regenerated A reminder about updating your webhook handler ```json 200 theme={null} { "success": true, "status": 200, "message": "Webhook secret regenerated successfully", "data": { "webhookSecret": "whsec_x1y2z3a4b5c6d7e8f9g0h1i2j3k4l5m6", "regeneratedAt": "2026-01-15T10:30:00+00:00", "note": "Update your server with this new secret. The old secret is now invalid." }, "meta": { "requestId": "req_abc123xyz789", "timestamp": "2026-01-15T10:30:00+00:00" } } ``` ## When to Regenerate You should regenerate your webhook secret if: * Your secret was accidentally exposed * An employee with access to the secret has left your organization * You want to rotate secrets as a security best practice * You suspect unauthorized access to your webhooks ## After Regenerating 1. **Copy the new secret** from the response immediately 2. **Update your webhook handler** with the new secret 3. **Test webhook delivery** using the [Test Webhook](/v3/api-reference/webhooks/test) endpoint 4. **Monitor your logs** to ensure webhooks are being verified correctly The old secret is invalidated immediately. There is no grace period. Make sure you're ready to update your handler before regenerating. # Webhook Signature Verification Source: https://docs.fyatu.com/v3/webhooks/signature-verification How to verify that webhook events are genuinely from Fyatu using HMAC-SHA256 signatures. Examples in Node.js, PHP, Python, Go, Ruby, Java, and C#. ## Overview Every webhook event Fyatu delivers includes a `sign` field in the JSON body. This is an HMAC-SHA256 signature you must verify before processing the event. ```json theme={null} { "event": "card.funded", "version": "3.0", "eventId": "112dff51-8275-4d60-9cd4-ad9aeb930478", "sign": "c580cd5259a8d2289a22ca6f97af56ed5ebd8a7a783bf56636761ef9d59b1830", "data": { "cardId": "c78041e26160072b02e04e855ae8d6e5b5dedfe5b3c9edc9cd", "cardholderId": "2d35aecc059dc46b68bdee8b3d009fe789a0", "reference": "333550a7-aea3-4cfd-b250-6eacd18828fa", "amount": 5, "fee": 0, "currency": "USD", "appId": "F3R0Q8D1Z5B8O6F8", "timestamp": "2026-05-10T23:18:45+00:00" } } ``` The `sign` is computed over **the raw `data` value only** — not the full envelope. Your endpoint must recompute the same HMAC and compare it against `sign` before trusting anything in the payload. Your `webhookSecret` is generated when you call [`POST /webhooks/secret/regenerate`](/v3/api-reference/webhooks/regenerate-secret). It is shown **once** and never returned again. Store it securely in an environment variable — never in code or version control. *** ## Signature Algorithm ``` sign = HMAC-SHA256( key = webhookSecret, message = raw JSON bytes of the "data" value // exactly as received — do not re-serialize ) ``` **Critical details:** * Sign only the `data` value — not `event`, `version`, `eventId`, or `sign` itself * Use the **exact bytes from the HTTP body** for the `data` value — do not parse and re-serialize it * Always use **constant-time comparison** — never `===` or `==` **Do not re-serialize `data` through a dictionary or map.** Most JSON libraries sort map keys when encoding, which produces different bytes than the original and causes signature mismatch. The examples below all preserve the raw bytes. *** ## Test Your Implementation Use these known-good values to verify your implementation before going live. | Field | Value | | ------------------- | ------------------------------------------------------------------ | | **Webhook secret** | `975127f2e7165836d99f54cf9c298da5b8bd43060bc0634e8cb3774e8bd6db4c` | | **Expected `sign`** | `c580cd5259a8d2289a22ca6f97af56ed5ebd8a7a783bf56636761ef9d59b1830` | **Full payload to feed into your handler:** ```json theme={null} {"event":"card.funded","version":"3.0","eventId":"112dff51-8275-4d60-9cd4-ad9aeb930478","sign":"c580cd5259a8d2289a22ca6f97af56ed5ebd8a7a783bf56636761ef9d59b1830","data":{"cardId":"c78041e26160072b02e04e855ae8d6e5b5dedfe5b3c9edc9cd","cardholderId":"2d35aecc059dc46b68bdee8b3d009fe789a0","reference":"333550a7-aea3-4cfd-b250-6eacd18828fa","amount":5,"fee":0,"currency":"USD","appId":"F3R0Q8D1Z5B8O6F8","timestamp":"2026-05-10T23:18:45+00:00"}} ``` Your `verifySignature` function should return `true` when given this payload and secret. If it returns `false`, your implementation has a bug — the most common cause is re-serializing `data` instead of using the raw bytes. *** ## Verification Examples ```javascript Node.js (Express) theme={null} const crypto = require('crypto'); const express = require('express'); const app = express(); // Use raw body middleware — required to extract the exact data bytes app.use(express.raw({ type: 'application/json' })); function verifySignature(rawBody, secret) { // Parse the envelope fields but sign the raw data bytes from the original body const body = rawBody.toString('utf8'); const payload = JSON.parse(body); const { sign, data } = payload; if (!sign || !data) return { valid: false, payload: null }; // Extract the raw "data" value as it appears in the body — key order preserved const dataStart = body.indexOf('"data"'); const dataJson = body.slice(body.indexOf('{', dataStart)); // Simpler: re-stringify works in Node.js since V8 preserves key order after JSON.parse const rawData = JSON.stringify(data); const expected = crypto .createHmac('sha256', secret) .update(rawData) .digest('hex'); const valid = crypto.timingSafeEqual( Buffer.from(sign, 'hex'), Buffer.from(expected, 'hex') ); return { valid, payload }; } app.post('/webhooks/fyatu', (req, res) => { const secret = process.env.FYATU_WEBHOOK_SECRET; const { valid, payload } = verifySignature(req.body, secret); if (!valid) { return res.status(401).json({ error: 'Invalid signature' }); } const { event, data } = payload; switch (event) { case 'card.funded': // handle funded card break; case 'card.transaction.approved': // handle approved transaction break; // ... other events } res.status(200).json({ received: true }); }); ``` ```php PHP theme={null} 'Missing required fields']); exit; } if (!verifySignature($data, $sign, $secret)) { http_response_code(401); echo json_encode(['error' => 'Invalid signature']); exit; } // Signature valid — process the event switch ($event) { case 'card.funded': // handle funded card break; case 'card.transaction.approved': // handle approved transaction break; // ... other events } http_response_code(200); echo json_encode(['received' => true]); ``` ```python Python (Flask) theme={null} import hashlib import hmac import json import os from flask import Flask, request, jsonify, abort app = Flask(__name__) def verify_signature(data: dict, sign: str, secret: str) -> bool: # ensure_ascii=False + separators with no spaces matches the server encoding # Python 3.7+ dict preserves insertion order — key order is kept after json.loads message = json.dumps(data, ensure_ascii=False, separators=(',', ':')) expected = hmac.new( secret.encode('utf-8'), message.encode('utf-8'), hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, sign) @app.route('/webhooks/fyatu', methods=['POST']) def handle_webhook(): payload = request.get_json() sign = payload.get('sign', '') data = payload.get('data') event = payload.get('event', '') secret = os.environ['FYATU_WEBHOOK_SECRET'] if not sign or not isinstance(data, dict): abort(400, description='Missing required fields') if not verify_signature(data, sign, secret): abort(401, description='Invalid signature') if event == 'card.funded': pass # handle funded card elif event == 'card.transaction.approved': pass # handle approved transaction # ... other events return jsonify({'received': True}), 200 ``` ```go Go theme={null} package main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "io" "net/http" "os" ) // WebhookPayload uses json.RawMessage for Data so the original bytes are preserved. // Do NOT use map[string]interface{} here — json.Marshal on a map sorts keys // alphabetically, producing different bytes than the original and breaking verification. type WebhookPayload struct { Event string `json:"event"` Version string `json:"version"` EventID string `json:"eventId"` Sign string `json:"sign"` Data json.RawMessage `json:"data"` // raw bytes — key order preserved } func verifySignature(rawData []byte, sign, secret string) bool { signBytes, err := hex.DecodeString(sign) if err != nil { return false } mac := hmac.New(sha256.New, []byte(secret)) mac.Write(rawData) return hmac.Equal(mac.Sum(nil), signBytes) } func webhookHandler(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, `{"error":"Failed to read body"}`, http.StatusBadRequest) return } var payload WebhookPayload if err := json.Unmarshal(body, &payload); err != nil { http.Error(w, `{"error":"Invalid JSON"}`, http.StatusBadRequest) return } secret := os.Getenv("FYATU_WEBHOOK_SECRET") if payload.Sign == "" || payload.Data == nil { http.Error(w, `{"error":"Missing required fields"}`, http.StatusBadRequest) return } // payload.Data contains the exact bytes from the body — no re-serialization if !verifySignature(payload.Data, payload.Sign, secret) { http.Error(w, `{"error":"Invalid signature"}`, http.StatusUnauthorized) return } // Signature valid — process the event switch payload.Event { case "card.funded": // handle funded card case "card.transaction.approved": // handle approved transaction // ... other events } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(`{"received":true}`)) } func main() { http.HandleFunc("/webhooks/fyatu", webhookHandler) http.ListenAndServe(":8080", nil) } ``` ```ruby Ruby (Sinatra) theme={null} require 'sinatra' require 'openssl' require 'json' def verify_signature(data, sign, secret) # Ruby Hash preserves insertion order — to_json keeps the original key order message = data.to_json expected = OpenSSL::HMAC.hexdigest('sha256', secret, message) Rack::Utils.secure_compare(expected, sign) end post '/webhooks/fyatu' do payload = JSON.parse(request.body.read) sign = payload['sign'] || '' data = payload['data'] event = payload['event'] || '' secret = ENV['FYATU_WEBHOOK_SECRET'] halt 400, { error: 'Missing required fields' }.to_json if sign.empty? || !data.is_a?(Hash) halt 401, { error: 'Invalid signature' }.to_json unless verify_signature(data, sign, secret) case event when 'card.funded' # handle funded card when 'card.transaction.approved' # handle approved transaction end content_type :json { received: true }.to_json end ``` ```java Java (Spring Boot) theme={null} import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.web.bind.annotation.*; import org.springframework.http.*; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.security.MessageDigest; import java.util.HexFormat; import java.util.LinkedHashMap; import java.util.Map; @RestController public class WebhookController { private final String webhookSecret = System.getenv("FYATU_WEBHOOK_SECRET"); // Disable sorting — Jackson uses LinkedHashMap by default which preserves key order private final ObjectMapper mapper = new ObjectMapper(); private boolean verifySignature(Map data, String sign) throws Exception { // Jackson preserves key order when deserializing into Map (uses LinkedHashMap) String message = mapper.writeValueAsString(data); Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(webhookSecret.getBytes("UTF-8"), "HmacSHA256")); byte[] expected = mac.doFinal(message.getBytes("UTF-8")); byte[] received = HexFormat.of().parseHex(sign); return MessageDigest.isEqual(expected, received); } @PostMapping(value = "/webhooks/fyatu", consumes = "application/json") public ResponseEntity handleWebhook(@RequestBody Map payload) { try { String sign = (String) payload.get("sign"); @SuppressWarnings("unchecked") Map data = (Map) payload.get("data"); String event = (String) payload.get("event"); if (sign == null || data == null) { return ResponseEntity.badRequest().body("{\"error\":\"Missing required fields\"}"); } if (!verifySignature(data, sign)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body("{\"error\":\"Invalid signature\"}"); } switch (event != null ? event : "") { case "card.funded": // handle funded card break; case "card.transaction.approved": // handle approved transaction break; } return ResponseEntity.ok("{\"received\":true}"); } catch (Exception e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body("{\"error\":\"Verification failed\"}"); } } } ``` ```csharp C# (.NET) theme={null} using System.Security.Cryptography; using System.Text; using System.Text.Json; using Microsoft.AspNetCore.Mvc; [ApiController] [Route("webhooks")] public class WebhookController : ControllerBase { private readonly string _webhookSecret = Environment.GetEnvironmentVariable("FYATU_WEBHOOK_SECRET")!; private bool VerifySignature(JsonElement data, string sign) { // JsonElement serializes with original key order preserved var message = JsonSerializer.Serialize(data); using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_webhookSecret)); var expected = hmac.ComputeHash(Encoding.UTF8.GetBytes(message)); var received = Convert.FromHexString(sign); return CryptographicOperations.FixedTimeEquals(expected, received); } [HttpPost("fyatu")] public IActionResult HandleWebhook([FromBody] JsonElement payload) { if (!payload.TryGetProperty("sign", out var signEl) || !payload.TryGetProperty("data", out var dataEl) || !payload.TryGetProperty("event", out var eventEl)) { return BadRequest(new { error = "Missing required fields" }); } var sign = signEl.GetString() ?? ""; if (!VerifySignature(dataEl, sign)) return Unauthorized(new { error = "Invalid signature" }); var eventType = eventEl.GetString(); switch (eventType) { case "card.funded": // handle funded card break; case "card.transaction.approved": // handle approved transaction break; // ... other events } return Ok(new { received = true }); } } ``` *** ## Best Practices Return `200` within **10 seconds**. Acknowledge first and process asynchronously if needed. Fyatu retries timed-out deliveries. The same event may be delivered more than once. Use `eventId` or `reference` to deduplicate — store processed event identifiers in your database. Always use timing-safe functions (`timingSafeEqual`, `hash_equals`, `hmac.Equal`). Variable-time `===` comparisons are vulnerable to timing attacks. Sign the raw `data` bytes as received. Re-encoding through a dictionary can change key order, producing a different HMAC. The Go example uses `json.RawMessage` to avoid this. *** ## Retry Behavior If your endpoint returns a non-`2xx` status or doesn't respond within 10 seconds, Fyatu retries with exponential backoff: | Attempt | Delay | | --------- | ---------- | | 1st retry | 1 minute | | 2nd retry | 5 minutes | | 3rd retry | 30 minutes | After 3 failed attempts the event is marked as undelivered. Use the [Test Webhook](/v3/api-reference/webhooks/test) endpoint to replay events during development. *** ## Rotating Your Secret If your `webhookSecret` is compromised, regenerate it immediately: ```bash theme={null} curl -X POST https://api.fyatu.com/api/v3/webhooks/secret/regenerate \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` The new secret is returned once in the response and takes effect immediately. Update your environment variable before the old secret is invalidated. # Test Webhook Source: https://docs.fyatu.com/v3/webhooks/test v3/openapi.json POST /webhooks/test Send a test webhook event to verify your endpoint is receiving and processing notifications correctly. POST /webhooks/test. # Test Webhook Send a test webhook event to your configured endpoint. This is useful for: * Verifying your webhook handler is working correctly * Testing signature verification * Developing and debugging your integration **All test data is simulated** and marked with `_test: true` in the payload `data`. These are not real transactions, cards, or cardholders. The only real values are: * Your `appId` * The webhook signature (signed with your real webhook secret) ## Request The event type to simulate. Use [List Events](/v3/api-reference/webhooks/events) to see available events for your app type. Optional — if omitted, defaults to `card.funded` for issuing apps and `collection.initiated` for collection apps. ```bash theme={null} curl -X POST https://api.fyatu.com/api/v3/webhooks/test \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "event": "card.funded" }' ``` ## Response Whether the request was successful The event type that was sent The URL where the webhook was sent Whether the webhook was delivered successfully (HTTP 200 response) The exact payload that was sent to your webhook endpoint Reminder that this is a simulated test event The HMAC-SHA256 signature (hex) computed over the `testData` payload with your webhook secret. Use it to verify your signature-checking code. ```json 200 (Delivered Successfully) theme={null} { "success": true, "status": 200, "message": "Test webhook sent", "data": { "event": "card.funded", "webhookUrl": "https://example.com/webhooks/fyatu", "delivered": true, "testData": { "appId": "D0H6R7Z6R1C2N5O5", "timestamp": "2026-01-15T10:30:00+00:00", "_test": true, "cardId": "CRD8a7b6c5d4e3f2a1", "cardholderId": "CH4a3b2c1d5e6f", "type": "VIRTUAL", "brand": "MASTERCARD", "currency": "USD", "last4": "4242", "status": "ACTIVE" }, "note": "This is a simulated test event. Verify using the signature below.", "signature": "c580cd5259a8d2289a22ca6f97af56ed5ebd8a7a783bf56636761ef9d59b1830" }, "meta": { "requestId": "req_abc123xyz789", "timestamp": "2026-01-15T10:30:00+00:00" } } ``` ```json 200 (Delivery Failed) theme={null} { "success": true, "status": 200, "message": "Test webhook sent", "data": { "event": "card.funded", "webhookUrl": "https://example.com/webhooks/fyatu", "delivered": false, "testData": { "appId": "D0H6R7Z6R1C2N5O5", "timestamp": "2026-01-15T10:30:00+00:00", "_test": true, "cardId": "CRD8a7b6c5d4e3f2a1", "cardholderId": "CH4a3b2c1d5e6f", "type": "VIRTUAL", "brand": "MASTERCARD", "currency": "USD", "last4": "4242", "status": "ACTIVE" }, "note": "This is a simulated test event. Verify using the signature below.", "signature": "c580cd5259a8d2289a22ca6f97af56ed5ebd8a7a783bf56636761ef9d59b1830" }, "meta": { "requestId": "req_def456uvw123", "timestamp": "2026-01-15T10:30:00+00:00" } } ``` ```json 400 (Webhook Not Configured) theme={null} { "success": false, "status": 400, "message": "No webhook URL configured. Update your webhook URL first.", "error": { "code": "WEBHOOK_NOT_CONFIGURED" }, "meta": { "requestId": "req_ghi789rst456", "timestamp": "2026-01-15T10:30:00+00:00" } } ``` ## Test Payload Structure All test webhooks follow the standard webhook format: ```json theme={null} { "event": "card.funded", "version": "3.0", "eventId": "112dff51-8275-4d60-9cd4-ad9aeb930478", "sign": "hmac_sha256_signature_here", "data": { "appId": "YOUR_REAL_APP_ID", "timestamp": "2026-01-15T10:30:00+00:00", "_test": true, // ... event-specific test data } } ``` ## Identifying Test Webhooks Test webhooks can be identified by the `_test: true` field in the `data` payload. Your webhook handler should check for `_test: true` if you want to handle test webhooks differently in production. ## Available Test Events ### Issuing App Events ```bash theme={null} # Card events card.funded card.funded card.unloaded card.frozen card.unfrozen card.terminated card.terminated card.maintenance_fee_paid # Card transaction events card.transaction.approved card.transaction.declined card.transaction.reversed card.transaction.cross_border_fee card.transaction.decline_fee_domestic card.transaction.decline_fee_international # Cardholder events cardholder.created cardholder.updated cardholder.kyc_submitted cardholder.kyc_approved cardholder.kyc_rejected cardholder.suspended cardholder.activated ``` ### Collection App Events ```bash theme={null} # Collection events collection.initiated collection.received collection.failed collection.expired # Payout events payout.initiated payout.completed payout.failed # Refund events refund.initiated refund.completed refund.failed ``` ## Verifying the Signature Even test webhooks are signed with your real webhook secret. Use this to verify your signature verification code: ```javascript theme={null} const crypto = require('crypto'); function verifyWebhook(payload, signature, secret) { const expectedSignature = crypto .createHmac('sha256', secret) .update(JSON.stringify(payload.data)) .digest('hex'); return signature === expectedSignature; } // In your webhook handler app.post('/webhooks/fyatu', (req, res) => { const { event, sign, data } = req.body; if (!verifyWebhook(req.body, sign, process.env.WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); } // Check if it's a test webhook if (data._test) { console.log('Received test webhook:', event); return res.status(200).send('Test webhook received'); } // Process real webhook... res.status(200).send('OK'); }); ``` Your webhook endpoint must return HTTP 200 within 10 seconds for the delivery to be considered successful. # Update Webhook URL Source: https://docs.fyatu.com/v3/webhooks/update v3/openapi.json PUT /webhooks Set or update your webhook endpoint URL for receiving real-time event notifications. PUT /webhooks. # Update Webhook URL Set or update the webhook URL where FYATU will send event notifications. The URL is required and must use HTTPS. If this is the first time setting a webhook URL, a webhook secret will be automatically generated and returned in the response. **Store this secret securely** - it will not be shown again. ## Request The HTTPS URL where webhooks will be sent. Required — an empty or missing value is rejected with a validation error. ```bash theme={null} curl -X PUT https://api.fyatu.com/api/v3/webhooks \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "webhookUrl": "https://example.com/webhooks/fyatu" }' ``` ## Response Whether the request was successful The new webhook URL Whether a webhook secret is configured Whether webhooks are fully configured The webhook secret (only returned when newly generated). **Store this securely!** A reminder to store the secret securely ```json 200 (New Configuration) theme={null} { "success": true, "status": 200, "message": "Webhook URL updated successfully", "data": { "webhookUrl": "https://example.com/webhooks/fyatu", "hasWebhookSecret": true, "isConfigured": true, "webhookSecret": "whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", "secretNote": "Store this secret securely — it will not be shown again. Use it to verify webhook signatures." }, "meta": { "requestId": "req_abc123xyz789", "timestamp": "2026-01-15T10:30:00+00:00" } } ``` ```json 200 (Update Existing) theme={null} { "success": true, "status": 200, "message": "Webhook URL updated successfully", "data": { "webhookUrl": "https://new-endpoint.example.com/webhooks", "hasWebhookSecret": true, "isConfigured": true }, "meta": { "requestId": "req_def456uvw123", "timestamp": "2026-01-15T10:30:00+00:00" } } ``` ```json 400 (Invalid URL) theme={null} { "success": false, "status": 400, "message": "Validation failed", "error": { "code": "VALIDATION_ERROR", "details": [ { "field": "webhookUrl", "message": "webhookUrl must use HTTPS" } ] }, "meta": { "requestId": "req_ghi789rst456", "timestamp": "2026-01-15T10:30:00+00:00" } } ``` ## Validation Rules | Rule | Description | | -------------- | ------------------------------------------ | | Required | `webhookUrl` must be present and non-empty | | HTTPS Required | Webhook URL must start with `https://` | When you first configure a webhook URL, a secret will be generated. **Copy and store it immediately** as it will not be displayed again. You'll need this secret to verify webhook signatures. # Card 3DS OTP Source: https://docs.fyatu.com/v3/webhooks/webhook-events/card-3ds-otp v3/openapi.json webhook card.3ds_otp Sent when a 3DS authentication code is generated during an online purchase. Forward this code to the cardholder immediately — it expires within minutes. ```json Webhook Payload theme={null} { "event": "card.3ds_otp", "version": "3.0", "eventId": "d6e7f8a9-b0c1-2345-9012-def012345678", "sign": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "data": { "cardId": "a4e8f2b6c9d1e3f7a2b5c8d0e4f1a3b6c9d2e5f8a1b4c7d0e3", "cardholderId": "8f4e2a1b3c5d7e9f0a2b4c6d8e0f1a3b5c7d", "last4": "9720", "code": "719384", "reference": "brc_db_a4e8f2b6_20260510143200", "amount": 49.99, "currency": "USD", "merchant": "AMAZON *MARKETPLACE", "appId": "A1B2C3D4E5F6G7H8", "timestamp": "2026-05-10T14:32:00Z" } } ``` ```json 200 theme={null} {} ``` # Card Delete Warning Source: https://docs.fyatu.com/v3/webhooks/webhook-events/card-delete-warning v3/openapi.json webhook card.delete_warning Sent when a card is scheduled for deletion due to inactivity. Making a transaction before the deletion date will cancel it. ```json Webhook Payload theme={null} { "event": "card.delete_warning", "version": "3.0", "eventId": "a3b4c5d6-e7f8-9012-6789-abcdef012345", "sign": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "data": { "cardId": "a4e8f2b6c9d1e3f7a2b5c8d0e4f1a3b6c9d2e5f8a1b4c7d0e3", "cardholderId": "8f4e2a1b3c5d7e9f0a2b4c6d8e0f1a3b5c7d", "potentialDeleteDate": "2026-06-10T00:00:00Z", "appId": "A1B2C3D4E5F6G7H8", "timestamp": "2026-05-10T14:32:00Z" } } ``` ```json 200 theme={null} {} ``` # Card Frozen Source: https://docs.fyatu.com/v3/webhooks/webhook-events/card-frozen v3/openapi.json webhook card.frozen Sent when a card is frozen, either via API or automatically by the provider (fraud, inactivity). ```json Webhook Payload theme={null} { "event": "card.frozen", "version": "3.0", "eventId": "00a733b1-0536-40f2-b271-b128f83077e7", "sign": "4dc7d46c2fb499974b8ecd25aee6257cd75d988d183fba95d09a00a74e5c2b42", "data": { "cardId": "a4e8f2b6c9d1e3f7a2b5c8d0e4f1a3b6c9d2e5f8a1b4c7d0e3", "cardholderId": "8f4e2a1b3c5d7e9f0a2b4c6d8e0f1a3b5c7d", "cardName": "JOHN DOE", "last4": "9720", "appId": "A1B2C3D4E5F6G7H8", "timestamp": "2026-05-09T23:57:58Z" } } ``` ```json 200 theme={null} {} ``` # Card Funded Source: https://docs.fyatu.com/v3/webhooks/webhook-events/card-funded v3/openapi.json webhook card.funded Sent when funds are successfully added to a card. ```json Webhook Payload theme={null} { "event": "card.funded", "version": "3.0", "eventId": "b62c8aa1-8c08-4143-8ce7-3c4b5635c866", "sign": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "data": { "cardId": "a4e8f2b6c9d1e3f7a2b5c8d0e4f1a3b6c9d2e5f8a1b4c7d0e3", "cardholderId": "8f4e2a1b3c5d7e9f0a2b4c6d8e0f1a3b5c7d", "amount": 5, "currency": "USD", "reference": "e65710b5-9b48-4067-9e36-800e84cc63de", "clientReference": "order-8842", "fee": 0, "appId": "A1B2C3D4E5F6G7H8", "timestamp": "2026-05-09T23:51:03Z" } } ``` ```json 200 theme={null} {} ``` # Card Funding Failed Source: https://docs.fyatu.com/v3/webhooks/webhook-events/card-funding-failed v3/openapi.json webhook card.funding_failed Sent when a card funding attempt fails. Any held amount is released automatically — no funds are deducted. ```json Webhook Payload theme={null} { "event": "card.funding_failed", "version": "3.0", "eventId": "a7b8c9d0-e1f2-3456-0123-456789abcdef", "sign": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "data": { "cardId": "a4e8f2b6c9d1e3f7a2b5c8d0e4f1a3b6c9d2e5f8a1b4c7d0e3", "cardholderId": "8f4e2a1b3c5d7e9f0a2b4c6d8e0f1a3b5c7d", "reference": "FND_20260510143200_c3d4e5", "amount": 50, "currency": "USD", "reason": "Card provider failed to fund card", "appId": "A1B2C3D4E5F6G7H8", "timestamp": "2026-05-10T14:32:00Z" } } ``` ```json 200 theme={null} {} ``` # Card Maintenance Fee Paid Source: https://docs.fyatu.com/v3/webhooks/webhook-events/card-maintenance-fee-paid v3/openapi.json webhook card.maintenance_fee_paid Sent when the monthly card maintenance fee is automatically debited from a card. ```json Webhook Payload theme={null} { "event": "card.maintenance_fee_paid", "version": "3.0", "eventId": "f2a3b4c5-d6e7-8901-5678-9abcdef01234", "sign": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "data": { "cardId": "a4e8f2b6c9d1e3f7a2b5c8d0e4f1a3b6c9d2e5f8a1b4c7d0e3", "cardholderId": "8f4e2a1b3c5d7e9f0a2b4c6d8e0f1a3b5c7d", "amount": 1, "currency": "USD", "appId": "A1B2C3D4E5F6G7H8", "timestamp": "2026-05-10T00:00:00Z" } } ``` ```json 200 theme={null} {} ``` # Card Negative Balance Source: https://docs.fyatu.com/v3/webhooks/webhook-events/card-negative-balance v3/openapi.json webhook card.negative_balance Sent when a card's balance drops below zero. The outstanding amount is recovered from your business wallet. ```json Webhook Payload theme={null} { "event": "card.negative_balance", "version": "3.0", "eventId": "b4c5d6e7-f8a9-0123-7890-bcdef0123456", "sign": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "data": { "cardId": "a4e8f2b6c9d1e3f7a2b5c8d0e4f1a3b6c9d2e5f8a1b4c7d0e3", "cardholderId": "8f4e2a1b3c5d7e9f0a2b4c6d8e0f1a3b5c7d", "amount": 3.50, "currency": "USD", "appId": "A1B2C3D4E5F6G7H8", "timestamp": "2026-05-10T14:32:00Z" } } ``` ```json 200 theme={null} {} ``` # Card Post-Refund Charge Source: https://docs.fyatu.com/v3/webhooks/webhook-events/card-post-refund-charge v3/openapi.json webhook card.post_refund_charge Sent when a card termination refund is corrected and the amount is charged back to your business wallet. Fired when an amount previously refunded to your wallet on card termination is charged back, because what the issuer actually returned did not stand behind what was credited. This is the charge side of [`card.termination_refund`](/v3/webhooks/webhook-events/card-termination-refund). A termination refund returns what a card held at the moment it closed; where that figure later proves not to have been backed by returned float, the difference is corrected with this event rather than left in your wallet. Reasons you may receive it: * a settlement landed against the card after its balance had already been refunded, * the issuer reconciled the closed card and returned less than was credited, or * the card was never terminated at the issuer, so no funds were ever returned for it. Your wallet balance decreases when this event fires. The `amount` is always positive — the event name carries the direction, exactly as it does for the refund it corrects. Always deduplicate on `reference`. ```json Webhook Payload theme={null} { "event": "card.post_refund_charge", "version": "3.0", "eventId": "6b2c9a1d-7e4f-40a8-b3c2-1f9e8d7c6b5a", "sign": "8c4768fa5a3e8a36d69942bc066f2c236e920701fa5a4348e3075aa7ead3588", "data": { "cardId": "a4e8f2b6c9d1e3f7a2b5c8d0e4f1a3b6c9d2e5f8a1b4c7d0e3", "cardholderId": "8f4e2a1b3c5d7e9f0a2b4c6d8e0f1a3b5c7d", "amount": 12.50, "currency": "USD", "reference": "a1b2c3d4e5f6a7b8c9d0e1f2", "reason": "REFUND_NOT_BACKED_BY_RETURNED_FLOAT", "appId": "A1B2C3D4E5F6G7H8", "timestamp": "2026-07-08T14:30:00Z" } } ``` ```json 200 theme={null} {} ``` ## Payload Fields | Field | Type | Description | | -------------- | ------ | -------------------------------------------------------------- | | `cardId` | string | The card whose termination refund is being corrected | | `cardholderId` | string | The cardholder the card belonged to | | `amount` | number | Amount charged back, debited from your wallet. Always positive | | `currency` | string | ISO-4217 currency of the charge | | `reference` | string | Unique per charge — use it as your idempotency key | | `reason` | string | Why the correction was raised. See below | | `appId` | string | The app the card belongs to | | `timestamp` | string | ISO 8601 time the charge was applied | ## Reasons | Value | Meaning | | ------------------------------------- | --------------------------------------------------------------------- | | `REFUND_NOT_BACKED_BY_RETURNED_FLOAT` | The refund credited exceeded what the issuer returned for the card | | `POST_TERMINATION_SETTLEMENT` | A transaction settled against the card after its balance was refunded | A card that receives this event may still be active at the issuer — a refund raised against a card that was never terminated is one of the cases this corrects, and the card continues to work normally. # Card Terminated Source: https://docs.fyatu.com/v3/webhooks/webhook-events/card-terminated v3/openapi.json webhook card.terminated Sent when a card is permanently terminated. Any remaining balance is automatically refunded to your business wallet. ```json Webhook Payload theme={null} { "event": "card.terminated", "version": "3.0", "eventId": "447b7b5d-bcbf-40b3-907b-e7c0ec390377", "sign": "36e920701fa5a4348e3075aa7ead358818c4768fa5a3e8a36d69942bc066f2c2", "data": { "cardId": "a4e8f2b6c9d1e3f7a2b5c8d0e4f1a3b6c9d2e5f8a1b4c7d0e3", "cardholderId": "8f4e2a1b3c5d7e9f0a2b4c6d8e0f1a3b5c7d", "cardName": "JOHN DOE", "last4": "9720", "reason": "Terminated by the user", "reference": "DEL_20260509235853_a4e8f2b6", "refundedBalance": 0, "refundPending": false, "appId": "A1B2C3D4E5F6G7H8", "timestamp": "2026-05-09T23:58:53Z" } } ``` ```json 200 theme={null} {} ``` # Card Termination Refund Source: https://docs.fyatu.com/v3/webhooks/webhook-events/card-termination-refund v3/openapi.json webhook card.termination_refund Sent when a terminated card's remaining balance is refunded to your business wallet. Fired once when a card is terminated and its remaining balance is returned to your business wallet. The `reference` field is the provider transaction id — it is unique per refund, so use it as your idempotency key to avoid processing the same refund twice. ```json Webhook Payload theme={null} { "event": "card.termination_refund", "version": "3.0", "eventId": "447b7b5d-bcbf-40b3-907b-e7c0ec390377", "sign": "36e920701fa5a4348e3075aa7ead358818c4768fa5a3e8a36d69942bc066f2c2", "data": { "cardId": "a4e8f2b6c9d1e3f7a2b5c8d0e4f1a3b6c9d2e5f8a1b4c7d0e3", "cardholderId": "8f4e2a1b3c5d7e9f0a2b4c6d8e0f1a3b5c7d", "amount": 12.50, "currency": "USD", "reference": "9f1e6b31f6fd010a6e8bdd317e8579", "appId": "A1B2C3D4E5F6G7H8", "timestamp": "2026-07-08T09:00:00Z" } } ``` ```json 200 theme={null} {} ``` # Card Post-Termination Refund Source: https://docs.fyatu.com/v3/webhooks/webhook-events/card-termination-refund-post v3/openapi.json webhook card.termination_refund_post Sent when funds arrive on an already-terminated card and are refunded to your business wallet. Fired when money lands on an **already-terminated** card and is returned to your business wallet. This happens for: * a later reconciliation/discrepancy refund from the provider, or * a merchant refund that posts to a card after it was terminated. Unlike [`card.termination_refund`](/v3/webhooks/webhook-events/card-termination-refund), this event can fire **multiple times** for the same card. Each delivery carries a distinct `reference` (the provider transaction id) and is credited exactly once — always deduplicate on `reference`. ```json Webhook Payload theme={null} { "event": "card.termination_refund_post", "version": "3.0", "eventId": "6b2c9a1d-7e4f-40a8-b3c2-1f9e8d7c6b5a", "sign": "8c4768fa5a3e8a36d69942bc066f2c236e920701fa5a4348e3075aa7ead3588", "data": { "cardId": "a4e8f2b6c9d1e3f7a2b5c8d0e4f1a3b6c9d2e5f8a1b4c7d0e3", "cardholderId": "8f4e2a1b3c5d7e9f0a2b4c6d8e0f1a3b5c7d", "amount": 4.25, "currency": "USD", "reference": "a1b2c3d4e5f6a7b8c9d0e1f2", "appId": "A1B2C3D4E5F6G7H8", "timestamp": "2026-07-08T14:30:00Z" } } ``` ```json 200 theme={null} {} ``` # Card Tokenization OTP Source: https://docs.fyatu.com/v3/webhooks/webhook-events/card-tokenization-otp v3/openapi.json webhook card.tokenization_otp Sent when a one-time code is generated for adding a card to a digital wallet (Apple Pay, Google Pay, Samsung Pay). Display this code to the cardholder immediately — it expires in minutes. ```json Webhook Payload theme={null} { "event": "card.tokenization_otp", "version": "3.0", "eventId": "c5d6e7f8-a9b0-1234-8901-cdef01234567", "sign": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "data": { "cardId": "a4e8f2b6c9d1e3f7a2b5c8d0e4f1a3b6c9d2e5f8a1b4c7d0e3", "cardholderId": "8f4e2a1b3c5d7e9f0a2b4c6d8e0f1a3b5c7d", "last4": "9720", "code": "483921", "reference": "", "amount": 0, "currency": "USD", "merchant": "", "appId": "A1B2C3D4E5F6G7H8", "timestamp": "2026-05-10T14:32:00Z" } } ``` ```json 200 theme={null} {} ``` # Card Transaction Approved Source: https://docs.fyatu.com/v3/webhooks/webhook-events/card-transaction-approved v3/openapi.json webhook card.transaction.approved Sent when a card charge is approved at the point of sale or online. Fee charges (cross-border, decline fees) are delivered as their own dedicated events. ```json Webhook Payload (Card Charge — Pending) theme={null} { "event": "card.transaction.approved", "version": "3.0", "eventId": "c9d0e1f2-a3b4-5678-2345-6789abcdef01", "sign": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "data": { "cardId": "a4e8f2b6c9d1e3f7a2b5c8d0e4f1a3b6c9d2e5f8a1b4c7d0e3", "cardholderId": "8f4e2a1b3c5d7e9f0a2b4c6d8e0f1a3b5c7d", "reference": "hos_tx_a4e8f2b6_20260510143200", "type": "DEBIT", "amount": 49.99, "currency": "USD", "merchant": { "name": "AMAZON MARKETPLACE", "country": "US", "mcc": "5999" }, "category": "Card Charge", "network": "VISA", "authorizationCode": "AUTH123", "status": "PENDING", "appId": "A1B2C3D4E5F6G7H8", "timestamp": "2026-05-10T14:32:00Z" } } ``` ```json Webhook Payload (Card Charge — Approved) theme={null} { "event": "card.transaction.approved", "version": "3.0", "eventId": "d1e2f3a4-b5c6-7890-3456-789abcdef012", "sign": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "data": { "cardId": "a4e8f2b6c9d1e3f7a2b5c8d0e4f1a3b6c9d2e5f8a1b4c7d0e3", "cardholderId": "8f4e2a1b3c5d7e9f0a2b4c6d8e0f1a3b5c7d", "reference": "hos_tx_a4e8f2b6_20260510143200", "type": "DEBIT", "amount": 49.99, "currency": "USD", "merchant": { "name": "AMAZON MARKETPLACE", "country": "US", "mcc": "5999" }, "category": "Card Charge", "network": "VISA", "authorizationCode": "AUTH123", "status": "APPROVED", "appId": "A1B2C3D4E5F6G7H8", "timestamp": "2026-05-10T14:35:00Z" } } ``` ```json 200 theme={null} {} ``` ## Event Flow Card charge events follow a two-phase pattern: 1. **PENDING** — sent immediately when the authorization is captured (before settlement). `status` is `"PENDING"`. 2. **APPROVED** — sent again when the charge settles. `status` is `"APPROVED"`. If the original authorization is reversed at settlement, the second event has `status: "REVERSED"` (see `card.transaction.reversed` for a standalone reversal). Fee charges are delivered as their own dedicated events — [`card.transaction.cross_border_fee`](/v3/webhooks/webhook-events/card-transaction-cross-border-fee), [`card.transaction.decline_fee_domestic`](/v3/webhooks/webhook-events/card-transaction-decline-fee-domestic), and [`card.transaction.decline_fee_international`](/v3/webhooks/webhook-events/card-transaction-decline-fee-international) — not through `card.transaction.approved`. ## Payload Fields | Field | Type | Description | | ------------------- | -------------- | --------------------------------------------- | | `cardId` | string | Your card identifier | | `cardholderId` | string | Cardholder identifier | | `reference` | string \| null | Unique transaction reference | | `type` | string | Always `"DEBIT"` | | `amount` | number | Transaction amount | | `currency` | string | Currency code (e.g., `"USD"`) | | `merchant.name` | string | Merchant name | | `merchant.country` | string \| null | Merchant country code | | `merchant.mcc` | string \| null | Merchant category code | | `category` | string | Always `"Card Charge"` | | `network` | string \| null | Card network (e.g., `"VISA"`, `"MASTERCARD"`) | | `authorizationCode` | string \| null | Authorization code from the network | | `status` | string | `"PENDING"`, `"APPROVED"`, or `"REVERSED"` | # Card Transaction Cross-border Fee Source: https://docs.fyatu.com/v3/webhooks/webhook-events/card-transaction-cross-border-fee v3/openapi.json webhook card.transaction.cross_border_fee Sent when a cross-border fee is charged on a card transaction made in a foreign currency or at an international merchant. ```json Webhook Payload theme={null} { "event": "card.transaction.cross_border_fee", "version": "3.0", "eventId": "f3a4b5c6-d7e8-9012-5678-abcdef012345", "sign": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "data": { "cardId": "a4e8f2b6c9d1e3f7a2b5c8d0e4f1a3b6c9d2e5f8a1b4c7d0e3", "cardholderId": "8f4e2a1b3c5d7e9f0a2b4c6d8e0f1a3b5c7d", "reference": "hos_fee_a4e8f2b6_20260510143300", "originalReference": "hos_tx_a4e8f2b6_20260510143200", "type": "DEBIT", "amount": 0.50, "currency": "USD", "merchant": { "name": "Gulf Health Counsel Riyadh", "country": "SA", "mcc": "9399" }, "category": "Cross-border Fee", "network": null, "authorizationCode": "955834", "status": "APPROVED", "appId": "A1B2C3D4E5F6G7H8", "timestamp": "2026-05-10T14:33:00Z" } } ``` ```json 200 theme={null} {} ``` ## Payload Fields | Field | Type | Description | | ------------------- | -------------- | -------------------------------------------------------- | | `cardId` | string | Your card identifier | | `cardholderId` | string | Cardholder identifier | | `reference` | string \| null | Unique fee transaction reference | | `originalReference` | string \| null | Reference of the original charge that triggered this fee | | `type` | string | Always `"DEBIT"` | | `amount` | number | Fee amount charged | | `currency` | string | Currency code (e.g., `"USD"`) | | `merchant.name` | string | Merchant name from the original charge | | `merchant.country` | string \| null | Merchant country code | | `merchant.mcc` | string \| null | Merchant category code | | `category` | string | Always `"Cross-border Fee"` | | `network` | string \| null | Card network | | `authorizationCode` | string \| null | Authorization code from the original charge | | `status` | string | Always `"APPROVED"` | # Card Transaction Decline Fee (Domestic) Source: https://docs.fyatu.com/v3/webhooks/webhook-events/card-transaction-decline-fee-domestic v3/openapi.json webhook card.transaction.decline_fee_domestic Sent when a domestic decline fee is charged after a card transaction is declined at a domestic merchant. ```json Webhook Payload theme={null} { "event": "card.transaction.decline_fee_domestic", "version": "3.0", "eventId": "g4b5c6d7-e8f9-0123-6789-bcdef0123456", "sign": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "data": { "cardId": "a4e8f2b6c9d1e3f7a2b5c8d0e4f1a3b6c9d2e5f8a1b4c7d0e3", "cardholderId": "8f4e2a1b3c5d7e9f0a2b4c6d8e0f1a3b5c7d", "reference": "hos_fee_a4e8f2b6_20260510143400", "originalReference": "hos_dc_a4e8f2b6_20260510143201", "type": "DEBIT", "amount": 0.40, "currency": "USD", "merchant": { "name": "GOOGLE Lightroom", "country": "US", "mcc": "5816" }, "category": "Decline Fee (Domestic)", "network": "VISA", "authorizationCode": null, "status": "APPROVED", "appId": "A1B2C3D4E5F6G7H8", "timestamp": "2026-05-10T14:34:00Z" } } ``` ```json 200 theme={null} {} ``` ## Payload Fields | Field | Type | Description | | ------------------- | -------------- | ------------------------------------------------------------- | | `cardId` | string | Your card identifier | | `cardholderId` | string | Cardholder identifier | | `reference` | string \| null | Unique fee transaction reference | | `originalReference` | string \| null | Reference of the declined transaction that triggered this fee | | `type` | string | Always `"DEBIT"` | | `amount` | number | Fee amount charged | | `currency` | string | Currency code (e.g., `"USD"`) | | `merchant.name` | string | Merchant name from the declined transaction | | `merchant.country` | string \| null | Merchant country code | | `merchant.mcc` | string \| null | Merchant category code | | `category` | string | Always `"Decline Fee (Domestic)"` | | `network` | string \| null | Card network (e.g., `"VISA"`, `"MASTERCARD"`) | | `authorizationCode` | string \| null | Authorization code if available | | `status` | string | Always `"APPROVED"` | # Card Transaction Decline Fee (International) Source: https://docs.fyatu.com/v3/webhooks/webhook-events/card-transaction-decline-fee-international v3/openapi.json webhook card.transaction.decline_fee_international Sent when an international decline fee is charged after a card transaction is declined at an international merchant. ```json Webhook Payload theme={null} { "event": "card.transaction.decline_fee_international", "version": "3.0", "eventId": "h5c6d7e8-f9a0-1234-7890-cdef01234567", "sign": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "data": { "cardId": "a4e8f2b6c9d1e3f7a2b5c8d0e4f1a3b6c9d2e5f8a1b4c7d0e3", "cardholderId": "8f4e2a1b3c5d7e9f0a2b4c6d8e0f1a3b5c7d", "reference": "hos_fee_a4e8f2b6_20260510143500", "originalReference": "hos_dc_a4e8f2b6_20260510143202", "type": "DEBIT", "amount": 0.40, "currency": "USD", "merchant": { "name": "Gulf Health Counsel Riyadh", "country": "SA", "mcc": "9399" }, "category": "Decline Fee (International)", "network": "MASTERCARD", "authorizationCode": null, "status": "APPROVED", "appId": "A1B2C3D4E5F6G7H8", "timestamp": "2026-05-10T14:35:00Z" } } ``` ```json 200 theme={null} {} ``` ## Payload Fields | Field | Type | Description | | ------------------- | -------------- | ------------------------------------------------------------- | | `cardId` | string | Your card identifier | | `cardholderId` | string | Cardholder identifier | | `reference` | string \| null | Unique fee transaction reference | | `originalReference` | string \| null | Reference of the declined transaction that triggered this fee | | `type` | string | Always `"DEBIT"` | | `amount` | number | Fee amount charged | | `currency` | string | Currency code (e.g., `"USD"`) | | `merchant.name` | string | Merchant name from the declined transaction | | `merchant.country` | string \| null | Merchant country code | | `merchant.mcc` | string \| null | Merchant category code | | `category` | string | Always `"Decline Fee (International)"` | | `network` | string \| null | Card network (e.g., `"VISA"`, `"MASTERCARD"`) | | `authorizationCode` | string \| null | Authorization code if available | | `status` | string | Always `"APPROVED"` | # Card Transaction Declined Source: https://docs.fyatu.com/v3/webhooks/webhook-events/card-transaction-declined v3/openapi.json webhook card.transaction.declined Sent when a card purchase is declined at the point of sale or online. ```json Webhook Payload theme={null} { "event": "card.transaction.declined", "version": "3.0", "eventId": "d0e1f2a3-b4c5-6789-3456-789abcdef012", "sign": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "data": { "cardId": "a4e8f2b6c9d1e3f7a2b5c8d0e4f1a3b6c9d2e5f8a1b4c7d0e3", "cardholderId": "8f4e2a1b3c5d7e9f0a2b4c6d8e0f1a3b5c7d", "reference": "hos_dc_a4e8f2b6_20260510143201", "type": "DEBIT", "amount": 299.99, "currency": "USD", "merchant": { "name": "BESTBUY", "country": "US", "mcc": "5732" }, "category": "Declined", "reason": "Insufficient funds", "network": "MASTERCARD", "status": "DECLINED", "appId": "A1B2C3D4E5F6G7H8", "timestamp": "2026-05-10T14:32:01Z" } } ``` ```json 200 theme={null} {} ``` ## Payload Fields | Field | Type | Description | | ------------------ | -------------- | ---------------------------------------------- | | `cardId` | string | Your card identifier | | `cardholderId` | string | Cardholder identifier | | `reference` | string \| null | Unique transaction reference | | `type` | string | Always `"DEBIT"` | | `amount` | number | Attempted transaction amount | | `currency` | string | Currency code (e.g., `"USD"`) | | `merchant.name` | string | Merchant name | | `merchant.country` | string \| null | Merchant country code | | `merchant.mcc` | string \| null | Merchant category code | | `category` | string | Always `"Declined"` | | `reason` | string | Human-readable decline reason from the network | | `network` | string \| null | Card network (e.g., `"VISA"`, `"MASTERCARD"`) | | `status` | string | Always `"DECLINED"` | # Card Transaction Reversed Source: https://docs.fyatu.com/v3/webhooks/webhook-events/card-transaction-reversed v3/openapi.json webhook card.transaction.reversed Sent when a previously approved card authorization is voided or reversed by the merchant. ```json Webhook Payload theme={null} { "event": "card.transaction.reversed", "version": "3.0", "eventId": "e1f2a3b4-c5d6-7890-4567-89abcdef0123", "sign": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "data": { "cardId": "a4e8f2b6c9d1e3f7a2b5c8d0e4f1a3b6c9d2e5f8a1b4c7d0e3", "cardholderId": "8f4e2a1b3c5d7e9f0a2b4c6d8e0f1a3b5c7d", "reference": "hos_rv_a4e8f2b6_20260510143202", "originalReference": "hos_tx_a4e8f2b6_20260510143200", "type": "CREDIT", "amount": 49.99, "currency": "USD", "merchant": { "name": "AMAZON MARKETPLACE", "country": "US", "mcc": "5999" }, "category": "Reversal", "network": "VISA", "authorizationCode": "AUTH123", "status": "REVERSED", "appId": "A1B2C3D4E5F6G7H8", "timestamp": "2026-05-10T14:32:02Z" } } ``` ```json 200 theme={null} {} ``` ## Payload Fields | Field | Type | Description | | ------------------- | -------------- | ----------------------------------------------- | | `cardId` | string | Your card identifier | | `cardholderId` | string | Cardholder identifier | | `reference` | string \| null | Unique reversal transaction reference | | `originalReference` | string \| null | Reference of the original charge being reversed | | `type` | string | Always `"CREDIT"` (funds returned to the card) | | `amount` | number | Reversed amount | | `currency` | string | Currency code (e.g., `"USD"`) | | `merchant.name` | string | Merchant name | | `merchant.country` | string \| null | Merchant country code | | `merchant.mcc` | string \| null | Merchant category code | | `category` | string | Always `"Reversal"` | | `network` | string \| null | Card network (e.g., `"VISA"`, `"MASTERCARD"`) | | `authorizationCode` | string \| null | Authorization code from the original charge | | `status` | string | Always `"REVERSED"` | # Card Unfrozen Source: https://docs.fyatu.com/v3/webhooks/webhook-events/card-unfrozen v3/openapi.json webhook card.unfrozen Sent when a frozen card is reactivated and becomes usable again. ```json Webhook Payload theme={null} { "event": "card.unfrozen", "version": "3.0", "eventId": "3c8d9703-ba5f-4873-9de5-bda8335d2e7d", "sign": "c0ca49ab02cd8eda8cca4fb553f24923cc0381f76df48fa149975509df7de38e", "data": { "cardId": "a4e8f2b6c9d1e3f7a2b5c8d0e4f1a3b6c9d2e5f8a1b4c7d0e3", "cardholderId": "8f4e2a1b3c5d7e9f0a2b4c6d8e0f1a3b5c7d", "cardName": "JOHN DOE", "last4": "9720", "appId": "A1B2C3D4E5F6G7H8", "timestamp": "2026-05-09T23:58:24Z" } } ``` ```json 200 theme={null} {} ``` # Card Unloaded Source: https://docs.fyatu.com/v3/webhooks/webhook-events/card-unloaded v3/openapi.json webhook card.unloaded Sent when funds are successfully withdrawn from a card back to your business wallet. ```json Webhook Payload theme={null} { "event": "card.unloaded", "version": "3.0", "eventId": "06d3acb3-3544-40c3-af44-7e7dafc7403d", "sign": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "data": { "cardId": "a4e8f2b6c9d1e3f7a2b5c8d0e4f1a3b6c9d2e5f8a1b4c7d0e3", "cardholderId": "8f4e2a1b3c5d7e9f0a2b4c6d8e0f1a3b5c7d", "reference": "UNL_20260510143200_d4e5f6", "amount": 5, "fee": 0, "currency": "USD", "appId": "A1B2C3D4E5F6G7H8", "timestamp": "2026-05-09T23:56:19Z" } } ``` ```json 200 theme={null} {} ``` # Card Unloading Failed Source: https://docs.fyatu.com/v3/webhooks/webhook-events/card-unloading-failed v3/openapi.json webhook card.unloading_failed Sent when a card unloading attempt fails. The card balance remains unchanged and no funds are credited to your wallet. ```json Webhook Payload theme={null} { "event": "card.unloading_failed", "version": "3.0", "eventId": "b8c9d0e1-f2a3-4567-1234-56789abcdef0", "sign": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "data": { "cardId": "a4e8f2b6c9d1e3f7a2b5c8d0e4f1a3b6c9d2e5f8a1b4c7d0e3", "cardholderId": "8f4e2a1b3c5d7e9f0a2b4c6d8e0f1a3b5c7d", "reference": "UNL_20260510143200_e5f6a7", "amount": 50, "currency": "USD", "reason": "Card provider failed to process withdrawal", "appId": "A1B2C3D4E5F6G7H8", "timestamp": "2026-05-10T14:32:00Z" } } ``` ```json 200 theme={null} {} ``` # Cardholder Created Source: https://docs.fyatu.com/v3/webhooks/webhook-events/cardholder-created v3/openapi.json webhook cardholder.created Sent when a new cardholder is created in your app. ```json Webhook Payload theme={null} { "event": "cardholder.created", "version": "3.0", "eventId": "77d958cb-128d-4927-bd2c-c351a153fb39", "sign": "3a5b8c2d1e4f9a0b7c6d5e4f3a2b1c0d", "data": { "cardholderId": "ch_a1b2c3d4e5f6", "firstName": "Alice", "lastName": "Example", "email": "alice@example.com", "phone": "+15550001234", "status": "ACTIVE", "appId": "D0H6R7Z6R1C2N5O5", "timestamp": "2026-01-12T18:30:00Z" } } ``` ```json 200 theme={null} {} ``` # Cardholder KYC Approved Source: https://docs.fyatu.com/v3/webhooks/webhook-events/cardholder-kyc-approved v3/openapi.json webhook cardholder.kyc_approved Sent when a cardholder's KYC verification is approved. ```json Webhook Payload theme={null} { "event": "cardholder.kyc_approved", "version": "3.0", "eventId": "77d958cb-128d-4927-bd2c-c351a153fb39", "sign": "3a5b8c2d1e4f9a0b7c6d5e4f3a2b1c0d", "data": { "cardholderId": "CH1a2b3c4d5e6f", "firstName": "John", "lastName": "Smith", "kycStatus": "VERIFIED", "kycLevel": "VERIFIED", "idFrontUrl": "https://cdn.fyatu.com/user/kyc/front_1234567890.jpg", "idBackUrl": "https://cdn.fyatu.com/user/kyc/back_1234567890.jpg", "idSelfieUrl": "https://cdn.fyatu.com/user/kyc/selfie_1234567890.jpg", "appId": "D0H6R7Z6R1C2N5O5", "timestamp": "2026-01-12T18:30:00Z" } } ``` ```json 200 theme={null} {} ``` The approved status is reported as `"VERIFIED"` under both `kycStatus` and `kycLevel`. The document image URLs (`idFrontUrl`, `idBackUrl`, `idSelfieUrl`) are flat keys on `data` and are included only for the slots on file — `idBackUrl` is omitted when no back image was captured. # Cardholder KYC Rejected Source: https://docs.fyatu.com/v3/webhooks/webhook-events/cardholder-kyc-rejected v3/openapi.json webhook cardholder.kyc_rejected Sent when a cardholder's KYC verification is rejected. ```json Webhook Payload theme={null} { "event": "cardholder.kyc_rejected", "version": "3.0", "eventId": "77d958cb-128d-4927-bd2c-c351a153fb39", "sign": "e5902a90747d0a43dd74498dedaaf09c40e1a51cb99ba651d5a3fdd9847d901e", "data": { "cardholderId": "6ab2697da91d1ec94b1ce2b7535e47db2e6c", "firstName": "Rachel", "lastName": "MOLO", "reason": "{\"feature\":\"LIVENESS\",\"risk\":\"DUPLICATED_FACE\",\"short_description\":\"Duplicated face from other approved session\",\"long_description\":\"The system identified a duplicated face from another approved session, requiring further investigation.\",\"node_id\":\"feature_liveness\",\"log_type\":\"information\",\"additional_data\":{\"api_service\":null,\"duplicated_session_id\":\"4c448606-9968-4008-837f-a294362fef5e\",\"duplicated_session_number\":4012}}", "status": "REJECTED", "appId": "D6J6X8V3Z5D7G5S0", "timestamp": "2026-05-12T13:07:30+00:00" } } ``` ```json 200 theme={null} {} ``` ## Payload Fields | Field | Type | Description | | -------------- | ------ | --------------------------------------------------------------- | | `cardholderId` | string | Cardholder identifier | | `firstName` | string | Cardholder first name | | `lastName` | string | Cardholder last name | | `reason` | string | JSON-encoded rejection detail from the KYC provider (see below) | | `status` | string | Always `"REJECTED"` | ## Reason Field The `reason` field is a **JSON-encoded string** from the KYC provider. Parse it to access structured rejection data: ```javascript theme={null} const detail = JSON.parse(data.reason); // detail.short_description — human-readable summary // detail.long_description — full explanation // detail.risk — risk code (e.g. "DUPLICATED_FACE", "LIVENESS_FAILED") // detail.feature — KYC check that failed (e.g. "LIVENESS", "DOCUMENT") ``` The `short_description` field contains a human-readable summary suitable for display. Use `risk` for programmatic handling. # Cardholder KYC Submitted Source: https://docs.fyatu.com/v3/webhooks/webhook-events/cardholder-kyc-submitted v3/openapi.json webhook cardholder.kyc_submitted Sent when a cardholder submits KYC documents for verification. ```json Webhook Payload theme={null} { "event": "cardholder.kyc_submitted", "version": "3.0", "eventId": "77d958cb-128d-4927-bd2c-c351a153fb39", "sign": "3a5b8c2d1e4f9a0b7c6d5e4f3a2b1c0d", "data": { "cardholderId": "CH1a2b3c4d5e6f", "firstName": "John", "lastName": "Smith", "kycStatus": "PENDING", "appId": "D0H6R7Z6R1C2N5O5", "timestamp": "2026-01-12T18:30:00Z" } } ``` ```json 200 theme={null} {} ```