Documentation

Build withCryptocardium.

Issue cards, fund accounts, fire transactions, and watch the lifecycle — from cURL, from any SDK, from your AI agent. The whole platform is one stable REST API and one native MCP server.

Welcome

Cryptocardium is a no-KYC card programme funded with crypto. Every action you can take in the panel — opening an account, topping up, issuing a card, loading it, spending, watching transactions — is available through the same REST API and MCP server.

These docs cover everything: conventions (how the API talks), resources (what you can ask for), events (how you stay in sync), and integrations (Claude, ChatGPT, Cursor, any agent).

API keys are generated in the panel.

Sign up at /account, open API Settings, click "New key". The full plaintext is shown once — copy it into a secret manager.

Quickstart

From zero to a live virtual card in three calls. The shell below uses cURL; swap for any HTTP client.

1. Authenticate

Place your API key in the Authorization header. Every endpoint requires it.

export CCM_KEY="ccm_live_a1b2c3d4..."

2. Top up the treasury

Create a USDT deposit address. Send funds; we credit on confirmation.

curl https://api.cryptocardium.com/v1/topups \
  -H "Authorization: Bearer $CCM_KEY" \
  -H "Idempotency-Key: top_$(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "amount_usd": 500, "asset": "USDT", "chain": "tron" }'

3. Issue a card & spend

Pick a BIN (Visa Platinum for wallets, Visa Business for ads), load it, and you're live.

curl https://api.cryptocardium.com/v1/cards \
  -H "Authorization: Bearer $CCM_KEY" \
  -d '{
    "type": "virtual",
    "bin": "489517",
    "load_usd": 300,
    "wallet_provision": ["apple_pay", "google_pay"]
  }'

# → 201 Created · card is live, balance loaded, wallet-ready
That's it.

Forty-seven seconds median, signup to first auth. The remaining sections cover everything else — error handling, idempotency, webhooks, agent setup — that you'll need in production.

Authentication

Three credential types, picked by integration shape:

TypeFormatUse for
Session bearersess_…Interactive scripts, testing, the panel itself.
API keyccm_live_…Production servers, CI, scheduled jobs, long-lived.
OAuth 2.1 + DCReyJh… (JWT)Agents that enrol themselves at runtime, scoped grants.

Bearer header

All three pass through the same header:

Authorization: Bearer ccm_live_a1b2c3d4...

Generate an API key

Keys are generated only in the panel. We never accept a key creation request over the API without an authenticated session attached to the panel session itself.

  1. Sign in to your account.
  2. Open API Settings.
  3. Click New key, give it a recognisable name, save the plaintext (shown once).
  4. Store in a secret manager. Add to your environment as CCM_KEY.
Treat keys like passwords.

A leaked ccm_live_ key carries account-level permission. Rotate via /api-settings if you suspect exposure — revocation is instant.

Testing & sandbox

Today, top-ups under $200 are accepted as live but routed to a sandbox card processor, so you can integrate without burning real settlement. Production accounts get sandbox-mode toggles per key.

  • Top up $200+ → production rails, real settlement.
  • Top up <$200 → sandbox rails, simulated authorisations.
  • Cards issued from sandbox top-ups carry "mode": "sandbox".

Base URL & versioning

Production base URL:

https://api.cryptocardium.com/v1

The v1 prefix is part of the path. v1 is LTS-forever — no breaking changes will ever be shipped under it. Additive changes (new optional fields, new endpoints) ship transparently.

Breaking changes go under a new prefix (v2) with at least six months of overlap. Deprecations are announced via webhook (system.deprecation) and the changelog.

Requests

All requests are HTTPS-only (TLS 1.3). Request bodies are JSON; nested objects are first-class. Form-encoded bodies are not supported.

  • Content-Type: application/json for write endpoints.
  • <strong>Charset</strong>: UTF-8 always.
  • HTTP methods: GET (read), POST (create / action), PATCH (partial update), DELETE (remove).
  • Header limits: 8 KB total, 4 KB per value.
  • Body limit: 25 MB (100 MB for dispute evidence uploads).
  • Timeouts: 30 s connect, 60 s read. Long-running operations are async.

Responses

Every response is JSON. Success responses return 200/201/204 with the resource body. Errors return a structured error object — see Errors.

Standard envelope

{
  "id": "card_8f3a91b7c4d2",
  "object": "card",
  "created_at": "2026-05-19T07:30:00Z",
  "updated_at": "2026-05-19T07:30:00Z",
  ...
}

Timestamps

All timestamps are ISO 8601 in UTC, formatted as YYYY-MM-DDTHH:MM:SSZ. There are no other timezones in the API.

Monetary values

All amounts are USD-equivalent decimals in amount_usd fields. Two-decimal precision. Internally backed by USDT.

Pagination

List endpoints (/topups, /cards, /transactions, …) are cursor-paginated. No offset, no SQL LIMIT.

GET /v1/transactions?limit=50&cursor=tx_8f3a91b7c4d2
  • limit — page size, default 25, max 100.
  • cursor — pass back the next_cursor from the previous response.
  • Pagination follows the natural sort of each resource (typically <code>created_at</code> desc).
{
  "object": "list",
  "data": [ /* 50 items */ ],
  "has_more": true,
  "next_cursor": "tx_2bea88..."
}

Filtering & sorting

Every list endpoint supports filtering on its primary fields via query parameters:

GET /v1/transactions
  ?card_id=card_8f3a91b7c4d2
  &status=captured
  &created_after=2026-05-01T00:00:00Z
  &sort=-amount_usd

The sort parameter accepts a single field; prefix with - for descending. See each resource's endpoint reference for the supported filter keys.

Idempotency

Every write request (POST, PATCH, DELETE) accepts an Idempotency-Key header. Pass a unique value per logical operation. Retries with the same key return the original response.

POST /v1/cards
Idempotency-Key: card_create_b9f1a4...
Authorization: Bearer ccm_live_...
  • Keys are stored for 24 hours. After that, the same key can be reused for a fresh operation.
  • Recommended format: <operation>_<uuid-v4>.
  • Same key + different body → 409 Conflict with conflict_idempotency_key.
  • GET requests are always idempotent and don't need (or accept) the header.

Rate limits

Per API key:

WindowLimitNotes
1 s burst60 reqShort spikes tolerated.
1 min1 000 reqSustained ceiling.
1 day50 000 reqAggregate per key.

Every response carries:

X-RateLimit-Limit:     1000
X-RateLimit-Remaining: 943
X-RateLimit-Reset:     1718999999

When you exceed the limit, you'll get 429 Too Many Requests with a Retry-After header in seconds. Honour it; we exponential-backoff bad citizens.

Errors

Every error response uses the same shape:

{
  "object": "error",
  "type": "invalid_request_error",
  "code": "missing_required_field",
  "message": "Field 'bin' is required.",
  "param": "bin",
  "request_id": "req_a9f4b1..."
}

HTTP status mapping

StatusTypeMeaning
400invalid_request_errorMalformed payload, missing fields.
401authentication_errorMissing or invalid bearer.
403permission_errorBearer is valid, action not allowed (e.g., balance gate).
404not_found_errorResource doesn't exist (or isn't yours).
409conflict_errorState conflict (e.g., reused idempotency key).
419session_expiredPanel session timed out. Re-auth.
422validation_errorPayload valid JSON, business rule failed.
429rate_limit_errorSlow down. Retry-After in seconds.
500api_errorOur fault. We've already alerted.
503service_unavailableUpstream issuer down. Auto-retry recommended.

Always log the request_id. Support tickets reference it directly.

Accounts

The account is your root resource. Everything else (top-ups, cards, transactions) belongs to an account.

Create

POST /v1/accounts
{
  "email": "[email protected]",
  "password": "long-random-string"
}

Returns a session bearer immediately. No KYC, no document upload, no email verification step required to start using the API.

Retrieve current

GET /v1/accounts/me
{
  "id": "acc_8f3a91b7c4d2",
  "email": "[email protected]",
  "balance_usd": 487.20,
  "twofa_enabled": true,
  "created_at": "2026-05-18T20:14:00Z"
}

Balance & treasury

The account balance is the spendable USDT pool. Top-ups credit it, card loads and withdrawals debit it.

GET /v1/balance
{
  "object": "balance",
  "available_usd": 487.20,
  "pending_usd": 200.00,
  "updated_at": "2026-05-19T07:30:00Z"
}

pending_usd covers in-flight top-ups (not yet confirmed) and in-flight card loads.

Top-ups

A top-up is the asymmetric step where you commit crypto on-chain and we credit USDT after finality.

Create

POST /v1/topups
{
  "amount_usd": 500,
  "asset": "USDT",
  "chain": "tron"
}

Returns a deposit address, QR data URI, and an expiry (default 60 min). Send the exact amount to the address; we credit on confirmation.

{
  "id": "top_4e21a99c7b",
  "status": "pending",
  "amount_usd": 500,
  "deposit_address": "T9zFR...kQp",
  "qr_data_uri": "data:image/png;base64,...",
  "expires_at": "2026-05-19T08:30:00Z"
}

Status lifecycle

StatusMeaning
pendingAwaiting on-chain deposit.
completedFunds credited to balance.
expiredAddress window closed. Late deposits still auto-credit.
cancelledYou called POST /v1/topups/:id/cancel.
errorSettlement failed (rare). Auto-refunded.

Prefer the topup.confirmed webhook over polling — it fires once and saves you 30+ polls.

Cards

Cards come in two types — virtual (live in seconds) and physical (shipped in 5–9 days, locked to the Visa Gold BIN).

Issue

POST /v1/cards
{
  "type": "virtual",
  "bin": "489517",
  "load_usd": 300,
  "wallet_provision": ["apple_pay", "google_pay"]
}

BIN catalogue

BINNameBest forType
416842Visa BusinessAd spend (3-D Secure)Virtual
557213Mastercard WorldMulti-currency, premiumVirtual
489517Visa PlatinumApple & Google PayVirtual
472305Visa CorporateSaaS subscriptionsVirtual
448585Visa GoldPhysical only (3-D Secure)Physical

Reveal full PAN + CVV

The full card number, CVV and expiry are returned only on a dedicated, audited call:

GET /v1/cards/:id/pan
{
  "pan": "4895 1712 ●●●● 4218",
  "cvv": "347",
  "expires_at": "2029-08",
  "audit_id": "audit_8c2e3f..."
}

Every reveal is logged to the audit trail. Agents should reveal once per purchase, never store, and discard from memory after use.

Operations

  • POST /v1/cards/:id/freeze — stop authorisations.
  • POST /v1/cards/:id/unfreeze — resume.
  • POST /v1/cards/:id/load — add USDT to the card.
  • POST /v1/cards/:id/unload — move unspent balance back to treasury.
  • POST /v1/cards/:id/cancel — permanent retirement.
  • PATCH /v1/cards/:id/limits — per-card transaction / monthly ceilings.
  • PATCH /v1/cards/:id/mcc — MCC allow/deny lists.
  • PATCH /v1/cards/:id/geo — country allow-lists.

Card loads & balances

Card balance is denominated in USD-equivalent USDT. A load deducts from the treasury, applies the 2% rail fee, and credits the card.

POST /v1/cards/:id/load
{ "amount_usd": 200 }
{
  "card_id": "card_8f3a91b7c4d2",
  "loaded_usd": 200.00,
  "rail_fee_usd": 4.00,
  "new_card_balance_usd": 200.00,
  "new_treasury_balance_usd": 283.20
}

Loads are atomic — the call returns only after the funds have moved.

Transactions

Every authorisation, capture, refund, and decline is a transaction object. They're append-only and immutable.

GET /v1/cards/:id/transactions?status=captured&limit=50
{
  "object": "list",
  "data": [
    {
      "id": "tx_b1c2d3",
      "object": "transaction",
      "card_id": "card_8f3a91b7c4d2",
      "status": "captured",
      "amount_usd": 42.95,
      "merchant": { "name": "OpenAI", "mcc": "7372" },
      "auth_response_code": "00",
      "created_at": "2026-05-19T03:14:00Z"
    }
  ],
  "has_more": false
}

Filter on status, card_id, mcc, merchant_name, created_after, created_before, amount_min, amount_max.

Withdrawals

Send treasury USDT back to any external wallet you control.

POST /v1/withdrawals
{
  "amount_usd": 100,
  "chain": "tron",
  "address": "T9zFR...kQp"
}

Min $10, max your full balance. Supported chains: tron (cheapest), ethereum, bsc. Cross-network sends are unrecoverable — always verify the address.

Disputes

File a chargeback on any transaction:

POST /v1/disputes
{
  "transaction_id": "tx_b1c2d3",
  "reason": "duplicate",
  "description": "Merchant charged twice on 2026-05-19, same order #4921."
}

Reason codes: duplicate, not_received, fraud, not_as_described, cancelled_subscription, other.

Attach evidence (receipts, screenshots, correspondence) via POST /v1/disputes/:id/evidence. We file with the issuer on your behalf — no extra fee.

Webhooks · subscribing

Webhooks push events to your HTTPS endpoint as they happen. Avoid polling; use webhooks.

POST /v1/webhooks
{
  "url": "https://yourapp.example.com/webhooks/cryptocardium",
  "events": [
    "topup.confirmed",
    "card.issued",
    "transaction.captured",
    "transaction.declined"
  ],
  "description": "Production sync"
}

Response contains a signing_secret — store it; you'll need it to verify payloads. Subscribe to "*" to receive every event.

Webhooks · signature verification

Every webhook carries a HMAC-SHA256 signature in the Cryptocardium-Signature header:

Cryptocardium-Signature: t=1718999999,v1=4a8b2f0e6c9d...
Cryptocardium-Event-Id:  evt_a1b2c3d4
Cryptocardium-Delivery:  dlv_8f3a91...

Compute the expected signature over "{t}.{rawBody}" with your signing_secret. Compare using a constant-time function.

Node.js

import crypto from 'crypto';

export function verify(rawBody, sigHeader, secret) {
  const [t, v1] = sigHeader.split(',')
    .map(s => s.split('=')[1]);
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(v1), Buffer.from(expected)
  );
}

Python

import hmac, hashlib

def verify(raw_body: bytes, sig_header: str, secret: str) -> bool:
    parts = dict(p.split('=') for p in sig_header.split(','))
    t, v1 = parts['t'], parts['v1']
    expected = hmac.new(
        secret.encode(),
        f"{t}.{raw_body.decode()}".encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(v1, expected)

PHP

function verify($rawBody, $sigHeader, $secret): bool {
    $parts = [];
    foreach (explode(',', $sigHeader) as $p) {
        [$k, $v] = explode('=', $p, 2);
        $parts[$k] = $v;
    }
    $expected = hash_hmac('sha256',
        "{$parts['t']}.{$rawBody}", $secret);
    return hash_equals($expected, $parts['v1']);
}

Webhooks · retries & replays

At-least-once delivery. We retry on any non-2xx response or timeout (10 s).

AttemptDelay
1immediate
230 s
35 min
430 min
52 h
612 h
724 h (final)

Replay any event from the dashboard or via POST /v1/webhooks/:id/replay/:event_id. Use the Cryptocardium-Event-Id header for deduplication on your side.

Event catalog

Account account.created account.signed_in account.signed_out account.password_changed account.totp_enabled account.totp_disabled
Top-ups topup.created topup.confirmed topup.expired topup.cancelled topup.error
Cards card.issued card.loaded card.unloaded card.frozen card.unfrozen card.cancelled card.replaced
Tx transaction.authorized transaction.captured transaction.refunded transaction.declined transaction.reversed
Dispute dispute.opened dispute.responded dispute.won dispute.lost
Withdraw withdrawal.created withdrawal.broadcasted withdrawal.confirmed withdrawal.failed
System system.maintenance system.deprecation

MCP server

We host a Model Context Protocol server at https://mcp.cryptocardium.com/v1. It exposes 40+ tools mapping 1:1 to REST endpoints, with agent-friendly names (create_topup, issue_card, reveal_pan, etc.).

Transport: Streamable HTTP. Auth: OAuth 2.1 with Dynamic Client Registration — agents enrol themselves on first connect, no API key needs to be pasted into the agent's config.

MCP · client setup

Claude Desktop / Claude Code

// ~/.config/claude/claude_desktop_config.json
{
  "mcpServers": {
    "cryptocardium": {
      "url": "https://mcp.cryptocardium.com/v1",
      "transport": "http"
    }
  }
}

Cursor / Continue / mcp-cli

# Adds the server, kicks off OAuth DCR on first connect
mcp-cli add cryptocardium \
  --url https://mcp.cryptocardium.com/v1 \
  --auth oauth

After authorising in your browser, the agent has access to every tool in the catalog (or just the ones you granted, if you tightened the scope).

MCP · tools catalog

Tools mirror REST endpoints. A small sample (the full list is in /api):

Account create_account sign_in get_account enable_2fa
Treasury get_balance create_topup withdraw
Cards issue_card load_card reveal_pan freeze_card set_card_limits
Tx & logs list_transactions get_activity file_dispute

MCP · OAuth 2.1 + DCR

Agents register themselves dynamically via RFC 7591. Flow:

  1. Agent POST /oauth/register — receives client_id & client_secret.
  2. User is prompted in-browser to authorise (one-time).
  3. Agent receives access_token (scoped, time-limited).
  4. Subsequent requests carry the JWT bearer.

Per-tool scopes

Restrict an agent to read-only, to a specific card, or to a subset of tools by passing scope= at registration time. Examples:

scope=read           # list & get only, no writes
scope=cards:write    # manage cards but not withdraw
scope=card:card_8f3a91b7c4d2  # single card

SDKs & examples

Official SDKs ship next. Until then, the API is a flat REST surface — every modern HTTP client handles it.

Node.js (fetch)

const res = await fetch('https://api.cryptocardium.com/v1/cards', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.CCM_KEY}`,
    'Idempotency-Key': `card_${crypto.randomUUID()}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    type: 'virtual', bin: '489517', load_usd: 300
  })
});
const card = await res.json();

Python (requests)

import requests, os, uuid

r = requests.post(
    'https://api.cryptocardium.com/v1/cards',
    headers={
        'Authorization': f"Bearer {os.environ['CCM_KEY']}",
        'Idempotency-Key': f"card_{uuid.uuid4()}",
    },
    json={'type': 'virtual', 'bin': '489517', 'load_usd': 300}
)
card = r.json()

PHP (curl)

$ch = curl_init('https://api.cryptocardium.com/v1/cards');
curl_setopt_array($ch, [
    CURLOPT_POST => 1,
    CURLOPT_RETURNTRANSFER => 1,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . $_ENV['CCM_KEY'],
        'Idempotency-Key: card_' . bin2hex(random_bytes(16)),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'type' => 'virtual',
        'bin' => '489517',
        'load_usd' => 300,
    ]),
]);
$card = json_decode(curl_exec($ch), true);

Changelog

Additive changes ship transparently. Breaking changes go under a new version prefix.

  • 2026-05-19 · v1.4 Added

    POST /v1/cards/:id/load & /unload are now atomic. card.loaded webhook event added.

  • 2026-05-18 · v1.3 Added

    MCP server live at mcp.cryptocardium.com. OAuth 2.1 DCR. 40+ tools mapped from REST.

  • 2026-05-17 · v1.2 Added

    Physical Gold card programme (BIN 448585), 3-D Secure on Visa Business and Visa Gold.

  • 2026-05-15 · v1.1 Changed

    Top-up minimum lowered to $20 for sandbox routing, $200 for production rails.

  • 2026-05-12 · v1.0 Released

    v1 LTS. 50+ endpoints. Bearer auth + idempotency keys + HMAC webhooks.

Support

Stuck? Two paths:

Include the request_id from the failing response — it speeds resolution by 10×.

FAQ

Developer questions.

Everything people actually ask. Last updated .

Where is the OpenAPI specification?

OpenAPI 3.1 is at https://cryptocardium.com/openapi.json (JSON) and /openapi.yaml. Linked from the homepage via rel="service-desc".

How are requests authenticated?

Three options: (1) Bearer session from sign-in, (2) bearer API key created at /api-settings, (3) OAuth 2.1 access token (recommended for AI agents, supports Dynamic Client Registration). All three are sent in the Authorization header.

What is the rate limit?

600 requests per minute per API key, with a 100-request burst. Headers X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset are returned on every response. Higher ceilings on request via /contact.

How does idempotency work?

Pass an Idempotency-Key header (any UUID or 32-byte random string) on POST and PATCH. Identical retries within 24 hours return the cached response. Different payloads with the same key return 409 Conflict.

How are errors returned?

Every non-2xx response is { "error": "machine_readable_code", "message": "Human-readable", "request_id": "req_..." }. The error code is the canonical machine-readable identifier; map your client logic to that, not to the message.

Are there client SDKs?

Code samples in cURL, JavaScript (Node + Fetch) and Python are inline throughout the docs. Official SDKs ship through the GitHub organisation; community SDKs are linked from /api.

Is there a sandbox environment?

Yes. Use a test API key from /api-settings (prefixed sk_test_). All endpoints are mirrored; cards issued in sandbox do not clear on real card rails.