API Reference
Programmatic virtual card issuance, funding, and transaction monitoring for partners.
https://api.uncard.cc/v1Version 1 · April 2026Introduction
The UnCard API lets you issue virtual cards, move funds, and monitor transactions through a single REST interface. Whether you're building an expense management tool, a payouts system, or a card-as-a-service product, the API handles the card network complexity so you can focus on your product.
Authentication
Every protected request must include an API key in the X-API-Key header.
Required headers
| Header | Required | Description |
|---|---|---|
| X-API-Key | Yes | Plaintext API key (format: uncard_sk_<token>). |
| Content-Type | Yes (POST) | Must be application/json on POST routes. |
Authentication errors
| Status | Error |
|---|---|
| 401 | Missing X-API-Key header |
| 401 | Invalid or expired API key |
Response Envelope
All JSON responses use a consistent envelope shape, regardless of route or success state.
{ "success": true, "data": { ... } }
{ "success": false, "error": "message" }| Field | Type | When present |
|---|---|---|
| success | boolean | Always. |
| data | any | On success. |
| error | string | On failure. |
HTTP status codes
| Status | Meaning |
|---|---|
| 200 | OK. |
| 201 | Created (returned by POST /cards/issue). |
| 400 | Bad request — missing or invalid parameter. |
| 401 | Missing or invalid API key. |
| 404 | Resource not found. |
| 502 | Upstream error. |
Enumerations
Card status
| Value | Meaning |
|---|---|
| active | Card is live and can be used for authorizations and top-ups. |
| frozen | Temporarily paused — no authorizations allowed. |
| closed | Permanently terminated; cannot be re-opened. |
| processing | Card issuance in flight; not yet usable. |
Card type
| Value | Meaning |
|---|---|
| online | Card usable for online (card-not-present) transactions. |
| wallet | Card loaded into a mobile wallet (Apple Pay / Google Pay). |
Transaction status
| Value | Meaning |
|---|---|
| pending | Submitted, awaiting confirmation. |
| complete | Accepted and settled. |
| declined | Rejected; funds not moved. |
Health
/healthUnauthenticated liveness probe.
Response 200
{ "status": "ok", "timestamp": "2026-04-19T12:00:00.000Z" }Cards
List cards
/cardsList all cards owned by the authenticated user. Query params: none.
Response 200
{
"success": true,
"data": [
{
"id": "uuid",
"name": "Netflix",
"masked_number": "411111******1234",
"card_plan": "standard",
"balance": 2500,
"status": "active",
"type": "online",
"total_deposited": 5000,
"total_withdrawn": 2500
}
]
}List card plans
/cards/plansReturns the list of card plans. Query params: none.
Response 200
{
"success": true,
"data": [
{ "slug": "standard", "name": "Standard", "topup_limit": 10000, "creation_fee": 1 }
]
}| Field | Type | Description |
|---|---|---|
| slug | string | Identifier used when issuing a card. |
| name | string | Human-readable plan name. |
| topup_limit | number | Maximum top-up limit (USD). |
| creation_fee | number | One-time fee charged on issuance (USD). |
List card transactions
/cards/transactionsList card transactions for the authenticated user, newest first.
Query params
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
| card_id | uuid | No | all | Filter transactions to a single card (any card owned by user). |
| page | int | No | 1 | 1-based page number (>= 1). |
| limit | int | No | 20 | Page size (1–100). |
Response 200
{
"success": true,
"data": {
"data": [
{
"id": "uuid",
"card_id": "uuid",
"amount": 1000,
"currency": "USD",
"masked_number": "411111******1234",
"description": "Top-up",
"status": "complete",
"created_at": "2026-04-19T10:00:00Z"
}
],
"total": 42,
"page": 1,
"limit": 20
}
}Get a card
/cards/:idGet a single card by ID.
Path params
| Param | Type | Required | Description |
|---|---|---|---|
| id | uuid | Yes | Card ID owned by the caller. |
Response 200
{
"success": true,
"data": {
"id": "uuid",
"name": "Netflix",
"masked_number": "411111******1234",
"card_plan": "standard",
"balance": 2500,
"status": "active",
"type": "online",
"billing_address": { "...": "..." },
"total_deposited": 5000,
"total_withdrawn": 2500,
"minimum_topup": 5,
"min_balance": 0
}
}Errors: 404 if the card does not exist or is not owned by the caller.
Get sensitive card details
/cards/:id/detailsReturns sensitive card details (PAN, CVV, expiry). Treat the response as short-lived and never log it.
Path params
| Param | Type | Required | Description |
|---|---|---|---|
| id | uuid | Yes | Card ID owned by the caller. |
Response 200
{
"success": true,
"data": {
"id": "uuid",
"number": "4111111111111234",
"cvv": "123",
"expiry_date": "12/29"
}
}Errors: 400 if id is missing; 404 if the card is not owned by the caller.
Issue a card
/cards/issueIssue a new card for the authenticated user.
Body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| bin_id | uuid | Yes | — | Which BIN the card is issued on (ID from GET /bins). |
| plan_slug | string | Yes | — | Card plan to apply (slug from GET /cards/plans). |
| card_name | string | No | "New Card" | Display name for the card (any UTF-8 string). |
Request
{ "bin_id": "uuid", "plan_slug": "standard", "card_name": "Netflix" }Response 201
{ "success": true, "data": { "cardId": "uuid" } }Errors: 400 for invalid bin or plan.
Top up a card
/cards/topupLoad funds from the user's wallet onto a card. A top-up fee applies.
Body
| Field | Type | Required | Description |
|---|---|---|---|
| card_id | uuid | Yes | Target card. Must be owned by caller with status = active. |
| amount | number | Yes | Amount in USD to top up (> 0, up to the plan's topup_limit). |
Request
{ "card_id": "uuid", "amount": 100 }Response 200
{ "success": true, "data": { "success": true } }Errors: 400 when the wallet has insufficient balance or the amount is invalid; 502 when the top-up is declined.
Withdraw from a card
/cards/withdrawWithdraw funds from a card back to the user's wallet.
Body
| Field | Type | Required | Description |
|---|---|---|---|
| card_id | uuid | Yes | Source card. Must be owned by caller with status = active. |
| amount | number | Yes | Amount in USD to withdraw (> 0, <= card.balance). |
Request
{ "card_id": "uuid", "amount": 50 }Response 200
{ "success": true, "data": { "success": true } }Errors: 400 when amount exceeds the card balance; 502 when the withdrawal is declined.
Close a card
/cards/closePermanently close a card. Remaining balance is returned to the wallet. This action is irreversible — the card's status becomes closed.
Body
| Field | Type | Required | Description |
|---|---|---|---|
| card_id | uuid | Yes | Card to close. Must be owned by caller, not already closed. |
Request
{ "card_id": "uuid" }Response 200
{ "success": true, "data": { "success": true } }Errors: 400 if the card is already closed.
BINs
List BINs
/binsList enabled BINs available for issuing cards. The first 4 digits of bin are returned in the clear; the rest is masked with **. Query params: none.
Response 200
{
"success": true,
"data": [
{
"id": "uuid",
"bin": "4111**",
"type": "online",
"country": "US",
"initial_balance": 0,
"minimum_topup": 5,
"min_balance": 0
}
]
}| Field | Type | Description |
|---|---|---|
| id | uuid | BIN ID — pass as bin_id to POST /cards/issue. |
| bin | string | First 4 digits + ** (rest masked). |
| type | string | Card type that BINs of this series produce (same values as card type). |
| country | string | ISO-3166 alpha-2 country code. |
| initial_balance | number | Balance seeded at issuance (USD). |
| minimum_topup | number | Minimum single top-up amount (USD). |
| min_balance | number | Minimum balance that must remain on the card (USD). |
Wallet
Get wallet balance
/walletReturn the authenticated user's wallet balance. Query params: none.
Response 200
{ "success": true, "data": { "balance": 12500 } }Errors: 404 if the user has no wallet.
Webhooks
UnCard sends HTTP POST requests to your configured endpoint whenever an event occurs on one of your cards (e.g. a transaction). This lets you react to card activity in real time instead of polling.
Configuration
Webhooks are configured per-user from your dashboard.
| Field | Required | Description |
|---|---|---|
| webhook_url | Yes | HTTPS URL that receives event deliveries. |
| webhook_secret | No | Signing secret. If set, deliveries include an HMAC signature header. |
If no webhook_url is set, no events are delivered.
Delivery
| Property | Value |
|---|---|
| Method | POST |
| Content-Type | application/json |
| Timeout | 10 seconds |
Your endpoint should acknowledge receipt with a 2xx response as quickly as possible.
Request headers
| Header | Example | Notes |
|---|---|---|
| Content-Type | application/json | |
| User-Agent | UnCard-Webhook/1.0 | |
| X-UnCard-Event | transaction.created | The event type. |
| X-UnCard-Timestamp | 2026-07-29T12:34:56.000Z | ISO 8601 delivery time. |
| X-UnCard-Signature | sha256=<hex> | Present only if webhook_secret is configured. |
Payload
Every delivery has the same envelope:
{
"type": "transaction.created",
"data": { ... }
}Events
| Event type (type / X-UnCard-Event) | When it fires |
|---|---|
| transaction.created | A new card transaction is recorded — includes purchases, plus Transaction fee and Decline fee entries. |
transaction.created — data object
| Field | Type | Description |
|---|---|---|
| id | string | UnCard transaction UUID. |
| card_id | string | UnCard card UUID. |
| amount | number | Amount in major units (e.g. dollars, not cents). |
| currency | string | ISO currency code, e.g. USD. |
| masked_number | string | null | Masked card number. |
| description | string | null | Human-readable description (Transaction fee, Decline fee, or the merchant description). |
| status | string | complete, declined, reversed, or pending. |
| created_at | string | ISO 8601 timestamp. |
Example
{
"type": "transaction.created",
"data": {
"id": "a1b2c3d4-0000-4444-8888-abcdef012345",
"card_id": "f0e1d2c3-1111-4444-8888-abcdef543210",
"amount": 12.50,
"currency": "USD",
"masked_number": "411111******1111",
"description": "COFFEE SHOP",
"status": "complete",
"created_at": "2026-07-29T12:34:56.000Z"
}
}Note: fee transactions (Transaction fee, Decline fee) are delivered as separate transaction.created events, each with its own id.
Verifying signatures
If you set a webhook_secret, verify the X-UnCard-Signature header before trusting a payload. The signature is sha256= followed by the hex HMAC-SHA256 of the raw request body using your secret.
const crypto = require('crypto');
function verify(rawBody, signatureHeader, secret) {
const expected =
'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signatureHeader),
Buffer.from(expected)
);
}Compute the HMAC over the exact bytes received — the relay forwards the body byte-for-byte so the signature stays valid.