Developers

The CryptoCard API for virtual cards

Everything the dashboard does, over a REST API that speaks JSON and nothing else. Issue cards, open deposit addresses, read the ledger — and pull a 3-D Secure code without a human in the loop.

REST · JSON Version v1 120 req/min Bearer key
Base URL https://cryptocard.net/api/v1
On this page

Quickstart

Three steps, about a minute.

  1. Activate a card. The key stays hidden until the account has at least one active card — it can issue cards and spend a balance, so handing that power to an account that has activated nothing makes no sense.
  2. Copy the key. Dashboard → Developers → API key. It looks like cc_live_ followed by 32 hex characters.
  3. Grant the scopes you need. Same panel. Several are off by default, on purpose — see Scopes.
first-call.sh
# Who am I, and what can this key do?
curl https://cryptocard.net/api/v1/account \
  -H "Authorization: Bearer $CC_API_KEY"

The key is a bearer credential: anyone holding it is the account. Keep it server-side, in an environment variable — never in a browser, a mobile binary, or a repository.

Not a developer question? The help center covers deposits, cards and 3-D Secure in plain language, and the product guide explains what the card is and what it costs.

Authentication

Every endpoint except the discovery root requires a key, passed as a bearer token. There are no cookies and no CSRF tokens: this is a server-to-server API.

auth.txt
Authorization: Bearer cc_live_4f2c9ab17e0d5836bb41c7a290e6fd18
StatusErrorWhen
401unauthorizedThe header is missing, or does not match Bearer cc_live_ + 32 hex characters.
401invalid_keyWell-formed, but no account carries that key.
403insufficient_scopeValid key, but the scope this endpoint needs is off. The response carries required_scope.

Rotating a key

Rotate from Dashboard → Developers. Rotation is immediate and there is no grace period: the previous key stops working on the next request, so deploy the new one first.

Scopes

A key carries the scopes you switch on, and nothing else. Endpoints check their scope on every call — these are not decorative toggles. The four marked sensitive are off by default: they read card numbers, read a payment confirmation code, or approve a payment.

Scopes for Account

ScopeDefaultGrants
account:read on Read the email and account status

Scopes for Money

ScopeDefaultGrants
balance:read on Read the spendable balance
transactions:read on Read the ledger — top-ups, payments, refunds, fees
topups:read on Read deposits and their confirmation status
topups:write off Open new deposit addresses

Scopes for Cards

ScopeDefaultGrants
cards:read on List cards, limits and spend — masked numbers only
cards:write on Issue, freeze, unfreeze, set limits, delete
cards:pan sensitive off Read the FULL card number, expiry and CVV

Scopes for 3-D Secure

ScopeDefaultGrants
3ds:read sensitive off Read the confirmation code of a pending online payment
3ds:write sensitive off Approve or decline a pending online payment

Scopes for Integration

ScopeDefaultGrants
webhooks:write off Register and remove webhook endpoints

Rate limits

120 requests per minute, per key. The window is a calendar minute in UTC, not a sliding one: the counter resets on the minute boundary. Going over returns 429 and costs nothing else — no ban, no backoff penalty.

429.json
{
  "error": "rate_limited",
  "message": "Too many requests. The limit is 120 per minute per key."
}

Polling /topups/{reference} once a second for ten minutes is 600 requests — comfortably inside the limit for one deposit, but not for twenty at once. Register a webhook instead of polling in a loop.

Errors

Every failure has the same shape: a machine-readable error and a human-readable message. Some carry an extra field that tells you what to do next — required_scope, minimum_usd, required_usd. No stack traces, no file paths.

error.json
{
  "error": "insufficient_scope",
  "message": "This key does not carry the scope this endpoint requires.",
  "required_scope": "cards:pan"
}
StatusMeaning
200Success — or a refused top-up. See the note on POST /topups.
201Created — a card or a deposit.
204Deleted. Empty body.
400The body or the parameters are not valid.
401Missing, malformed or unknown key.
402The balance does not cover the operation.
403The key lacks the required scope.
404No such resource on this account.
405Wrong method for that path.
409The resource is in a state that forbids it — frozen card, default card, resolved challenge, no slot left.
429Rate limited.
500Our fault. The body stays JSON.

Seeing failures on every endpoint at once, rather than on one? Check the service status before debugging your own code — it publishes live state and a full incident history.

Do not branch on the status code alone. A refused deposit answers 200 with an error field, because a 5xx body gets replaced by an HTML error page at our edge. Test for error first, always.

Objects

Four shapes cover the whole API. Every timestamp is ISO-8601 in UTC, or null when it has not happened yet. Every amount is a JSON number in USD unless the field name says otherwise.

Card

idintegerStable identifier. Use it in every /cards/{id} path.
labelstringYour name for the card, up to 40 characters.
typestringvirtual or physical, lower-case.
last4stringLast four digits. Always present.
expirystringMM/YY.
statusstringactive or frozen. Deleted cards are never returned.
defaultbooleanThe account's default card. It cannot be deleted.
monthly_limitnumber | nullMonthly cap in USD, null when uncapped.
spent_usdnumberSpent against the current cap.
created_atstring | nullISO-8601, UTC.
numberstringFull PAN. Only from /cards/{id}/secure, with cards:pan.
cvvstringThree digits, derived on the fly and stored nowhere. Same endpoint, same scope.

Top-up

referencestringThe deposit id. Use it in /topups/{reference}.
statusstringpending until the network confirms, then confirmed.
coinstringThe symbol you asked for.
amount_usdnumberWhat you asked to credit, before the fee.
fee_usdnumber1% of amount_usd.
credit_usdnumberWhat actually lands on the balance.
deposit_addressstringSend here, and only for this deposit.
deposit_amountstringThe exact amount of coin to send. A string, not a number — trailing precision matters and floats lose it.
deposit_networkstringThe chain, e.g. Monero, TRC-20.
deposit_tagstring | nullMemo or tag. When it is not null, sending without it loses the deposit.
tx_hashstring | nullSet once the payment is seen on-chain.
created_atstringISO-8601, UTC.
expires_atstring | nullAfter this, the address stops being watched.
credited_atstring | nullSet when the balance moved. This is the field that means "done".

Transaction

kindstringtopup, payment, refund, fee or yield.
titlestringShort label, as shown in the dashboard.
detailstring | nullMerchant, coin and amount, or the reason for a fee.
coinstring | nullSet on top-ups. Always null on a yield movement — interest is credited in USD, never in a coin.
amount_usdnumberSigned: negative when it leaves the balance.
statusstringWhere the movement stands.
created_atstringISO-8601, UTC.

Interest day

daystringThe UTC calendar day the interest was calculated for, YYYY-MM-DD. The credit itself is dated 00:00 UTC the following day.
basis_usdnumberThe closing balance used as the base. Because each payment joins the balance, this grows day over day if nothing is spent.
amount_usdnumberWhat was credited, to the cent. 0 means the day’s interest stayed below one cent and was carried forward.

3-D Secure challenge

idstringThe challenge reference. Use it in /3ds/{id}.
statusstringpending, approved, declined or expired.
merchantstringWho is asking.
amount_usdnumberThe amount being authorised.
card_idinteger | nullThe card involved.
channelstringHow the holder was notified.
created_atstringISO-8601, UTC.
expires_atstringAfter this the challenge expires on its own and can no longer be resolved.
resolved_atstring | nullWhen it was approved or declined.
codestringThe confirmation code. Present only while pending, and only with 3ds:read.

Account

One read-only endpoint that tells an integration who it is talking to, and exactly which scopes the key carries.

GET /account account:read

The account behind the key

Returns the email, the two-factor state, the spendable balance and the list of scopes this key actually carries. Useful as a health check at start-up: if granted_scopes is missing what your integration needs, fail loudly rather than at the first write.

Example

request.sh
curl https://cryptocard.net/api/v1/account \
  -H "Authorization: Bearer $CC_API_KEY"

Response

response.json
{
  "email": "you@proton.me",
  "two_factor": true,
  "balance_usd": 247.5,
  "created_at": "2026-08-14T09: 12: 31+00: 00",
  "granted_scopes": [
    "account:read",
    "balance:read",
    "transactions:read",
    "topups:read",
    "cards:read",
    "cards:write"
  ]
}

Money

The balance, the ledger behind it, and the deposits that feed it. A top-up is credited automatically after one network confirmation — you never have to confirm anything by hand.

GET /balance balance:read

Spendable balance

The amount that can be spent right now, fees already deducted. Always USD. The yield object describes what that balance is earning — the rate is a decimal fraction, not a percentage, so 0.04 means 4% a year.

Example

request.sh
curl https://cryptocard.net/api/v1/balance \
  -H "Authorization: Bearer $CC_API_KEY"

Response

response.json
{
  "balance_usd": 247.5,
  "currency": "USD",
  "yield": {
    "rate": 0.04,
    "apy": 0.040808,
    "basis": 365,
    "accruing_today": 0.027123,
    "paid_total_usd": 3.41,
    "next_payment_at": "2026-08-27T00: 00: 00+00: 00"
  }
}
GET /interest transactions:read

Daily interest, day by day

One row per day, newest first, with the closing balance that was used as the base. Useful for reconciling: every row here has a matching yield movement in /transactions. Days on which the accrued amount stayed below one cent are returned with amount_usd: 0 — the fraction was carried, not lost.

Query parameters

daysintegerBetween 1 and 365. Defaults to 30. Values outside the range are clamped, not rejected.

Example

request.sh
curl https://cryptocard.net/api/v1/interest \
  -H "Authorization: Bearer $CC_API_KEY"

Response

response.json
{
  "rate": 0.04,
  "apy": 0.040808,
  "basis": 365,
  "data": [
    {
      "day": "2026-08-25",
      "basis_usd": 247.5,
      "amount_usd": 0.03
    },
    {
      "day": "2026-08-24",
      "basis_usd": 247.47,
      "amount_usd": 0.03
    }
  ]
}
GET /transactions transactions:read

The ledger

Every movement, newest first: top-ups, card payments, refunds, fees and the daily yield credit.

Query parameters

limitintegerBetween 1 and 200. Defaults to 50. Values outside the range are clamped, not rejected.

Example

request.sh
curl https://cryptocard.net/api/v1/transactions \
  -H "Authorization: Bearer $CC_API_KEY"

Response

response.json
{
  "data": [
    {
      "kind": "topup",
      "title": "Top-up credited",
      "detail": "XMR · 1.47128394",
      "coin": "XMR",
      "amount_usd": 247.5,
      "status": "done",
      "created_at": "2026-08-26T10: 02: 44+00: 00"
    },
    {
      "kind": "fee",
      "title": "Virtual card issued",
      "detail": "Shopping · card fee",
      "coin": null,
      "amount_usd": -2,
      "status": "done",
      "created_at": "2026-08-26T10: 03: 10+00: 00"
    },
    {
      "kind": "yield",
      "title": "Interest",
      "detail": "4% a year on 247.50 held",
      "coin": null,
      "amount_usd": 0.03,
      "status": "done",
      "created_at": "2026-08-27T00: 00: 05+00: 00"
    }
  ]
}
GET /topups topups:read

List deposits

The 50 most recent deposits, newest first, whatever their status.

Example

request.sh
curl https://cryptocard.net/api/v1/topups \
  -H "Authorization: Bearer $CC_API_KEY"

Response

response.json
{ "data": [ /* Top-up objects */ ] }
POST /topups topups:write

Open a deposit address

Reserves a deposit address with the payment provider and returns the exact amount to send. The address belongs to this deposit only — send the exact amount shown in deposit_amount, and include deposit_tag when the chain requires one.

Body

amount_usd required numberWhat you want credited, in USD, before the 1% fee. Must be at least the account minimum (see minimum_usd in the error), and at most 100000.
coin required stringOne of BTC, XMR, ETH, USDT, USDTTRC, LTC, TRX.

Example

request.sh
curl -X POST https://cryptocard.net/api/v1/topups \
  -H "Authorization: Bearer $CC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "amount_usd": 250, "coin": "XMR" }'

Response

response.json
{
  "reference": "440062",
  "status": "pending",
  "coin": "XMR",
  "amount_usd": 250,
  "fee_usd": 2.5,
  "credit_usd": 247.5,
  "deposit_address": "48ju9c8k2wZpLXqYvKq3s7hV6mFbN2rTgW9dQ1xJ4eR8uC5aZ7yH3nP6kM0sD2fG",
  "deposit_amount": "1.47128394",
  "deposit_network": "Monero",
  "deposit_tag": null,
  "tx_hash": null,
  "created_at": "2026-08-26T10: 00: 00+00: 00",
  "expires_at": "2026-08-26T11: 00: 00+00: 00",
  "credited_at": null
}

Errors

400 coin_unsupportedThe coin is not one of the supported symbols. The message lists them.
400 amount_too_lowBelow the account minimum. The response carries minimum_usd.
400 amount_too_highAbove 100000 USD.

If the payment provider refuses or is unreachable, this endpoint answers 200 with an error field rather than a 5xx. That is deliberate: our edge replaces the body of a 5xx with an HTML error page, and a client expecting JSON would receive <!DOCTYPE instead of a reason it can act on. Always branch on the presence of error, not on the status alone.

GET /topups/{reference} topups:read

Poll one deposit

Look a deposit up by the reference returned at creation. Poll this until status becomes confirmed and credited_at is set — or register a topup.confirmed webhook instead.

Path parameters

referencestringThe deposit reference, e.g. 440062.

Example

request.sh
curl https://cryptocard.net/api/v1/topups/440062 \
  -H "Authorization: Bearer $CC_API_KEY"

Response

response.json
{ /* Top-up object */ }

Errors

404 not_foundNo deposit with that reference on this account.

Cards

Issue, rename, cap, freeze and delete cards. Card numbers stay masked unless the key carries the separate cards:pan scope.

GET /cards cards:read

List cards

Every card on the account that has not been deleted.

Example

request.sh
curl https://cryptocard.net/api/v1/cards \
  -H "Authorization: Bearer $CC_API_KEY"

Response

response.json
{ "data": [ /* Card objects */ ] }
POST /cards cards:write

Issue a virtual card

Creates a virtual card and charges the $2 card fee to the balance in the same transaction — if the balance does not cover it, nothing is created. An account holds at most 5 cards; delete one to free a slot.

Body

label stringUp to 40 characters. Longer labels are truncated, an empty one becomes Virtual card.
monthly_limit number | nullMonthly spend cap in USD. null means no cap.

Example

request.sh
curl -X POST https://cryptocard.net/api/v1/cards \
  -H "Authorization: Bearer $CC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "label": "Shopping" }'

Response

response.json
{
  "id": 214,
  "label": "Shopping",
  "type": "virtual",
  "last4": "4410",
  "expiry": "09/29",
  "status": "active",
  "default": false,
  "monthly_limit": 400,
  "spent_usd": 0,
  "created_at": "2026-08-26T10: 03: 10+00: 00"
}

Errors

409 no_slot_leftAll 5 slots are in use.
402 insufficient_fundsThe balance does not cover the card fee. Carries required_usd.
GET /cards/{id} cards:read

Retrieve a card

A single card, masked.

Path parameters

idintegerThe card id.

Example

request.sh
curl https://cryptocard.net/api/v1/cards/214 \
  -H "Authorization: Bearer $CC_API_KEY"

Response

response.json
{ /* Card object */ }

Errors

404 not_foundNo such card on this account.
PATCH /cards/{id} cards:write

Rename or re-cap

Send only the fields you want to change. Omitting a field leaves it untouched; sending monthly_limit: null removes the cap.

Path parameters

idintegerThe card id.

Body

label stringNew label, up to 40 characters. An empty string is ignored.
monthly_limit number | nullNew cap, or null to remove it.

Example

request.sh
curl -X PATCH https://cryptocard.net/api/v1/cards/214 \
  -H "Authorization: Bearer $CC_API_KEY"

Response

response.json
{ /* the updated Card object */ }
DELETE /cards/{id} cards:write

Delete a card

Frees the slot. Returns 204 No Content with an empty body.

Path parameters

idintegerThe card id.

Example

request.sh
curl -X DELETE https://cryptocard.net/api/v1/cards/214 \
  -H "Authorization: Bearer $CC_API_KEY"

Response

response.json
(204 — no body)

Errors

409 cannot_delete_defaultThe default card cannot be deleted.
POST /cards/{id}/freeze cards:write

Freeze

Declines every authorisation immediately. Reversible, and it does not consume a slot.

Path parameters

idintegerThe card id.

Example

request.sh
curl -X POST https://cryptocard.net/api/v1/cards/214/freeze \
  -H "Authorization: Bearer $CC_API_KEY"

Response

response.json
{ /* Card object, "status": "frozen" */ }
POST /cards/{id}/unfreeze cards:write

Unfreeze

Puts the card back to active.

Path parameters

idintegerThe card id.

Example

request.sh
curl -X POST https://cryptocard.net/api/v1/cards/214/unfreeze \
  -H "Authorization: Bearer $CC_API_KEY"

Response

response.json
{ /* Card object, "status": "active" */ }
GET /cards/{id}/secure cards:pan

Full number, expiry and CVV

The only endpoint that returns the full PAN. It needs its own scope, off by default — turn it on in Dashboard → Developers only for a key that genuinely needs it. The CVV is derived on the fly from the PAN and the application key: it is stored nowhere, so a dump of the database does not contain it.

Path parameters

idintegerThe card id.

Example

request.sh
curl https://cryptocard.net/api/v1/cards/214/secure \
  -H "Authorization: Bearer $CC_API_KEY"

Response

response.json
{
  "id": 214,
  "label": "Shopping",
  "type": "virtual",
  "last4": "4410",
  "expiry": "09/29",
  "status": "active",
  "default": false,
  "monthly_limit": 400,
  "spent_usd": 0,
  "created_at": "2026-08-26T10: 03: 10+00: 00",
  "number": "5412753488214410",
  "cvv": "417"
}

Errors

409 card_frozenUnfreeze the card before reading its number.

3-D Secure

When an online payment asks for 3-D Secure, the issuer opens a challenge on the account. A challenge is never created by you — you read it and you resolve it. This is what lets a bot check out without a human reading an inbox.

GET /3ds 3ds:read

List challenges

The 50 most recent challenges, newest first. The confirmation code is included only while a challenge is still pending — once approved, declined or expired it is never returned again.

Query parameters

statusstringOne of pending (default), approved, declined, expired, all. Anything else is a 400.

Example

request.sh
curl https://cryptocard.net/api/v1/3ds \
  -H "Authorization: Bearer $CC_API_KEY"

Response

response.json
{
  "data": [
    {
      "id": "3ds_7Kq2mR",
      "status": "pending",
      "merchant": "STEAMGAMES.COM",
      "amount_usd": 59.99,
      "card_id": 214,
      "channel": "email",
      "created_at": "2026-08-26T10: 41: 02+00: 00",
      "expires_at": "2026-08-26T10: 51: 02+00: 00",
      "resolved_at": null,
      "code": "884213"
    }
  ]
}
GET /3ds/{id} 3ds:read

Retrieve one challenge

Same object as the list. Poll this the moment your checkout hits a 3-D Secure step.

Path parameters

idstringThe challenge id, e.g. 3ds_7Kq2mR.

Example

request.sh
curl https://cryptocard.net/api/v1/3ds/214 \
  -H "Authorization: Bearer $CC_API_KEY"

Response

response.json
{ /* Challenge object, with "code" while pending */ }

Errors

404 not_foundNo such challenge on this account.
POST /3ds/{id}/approve 3ds:write

Approve a payment

Confirms the payment. Only a challenge that is still pending and not yet expired can be resolved; the resolution is atomic, so two concurrent calls cannot both win.

Path parameters

idstringThe challenge id.

Example

request.sh
curl -X POST https://cryptocard.net/api/v1/3ds/214/approve \
  -H "Authorization: Bearer $CC_API_KEY"

Response

response.json
{ /* Challenge object, "status": "approved" */ }

Errors

409 not_pendingAlready resolved, or expired.
404 not_foundNo such challenge.
POST /3ds/{id}/decline 3ds:write

Decline a payment

Refuses the payment. Same rules as approve.

Path parameters

idstringThe challenge id.

Example

request.sh
curl -X POST https://cryptocard.net/api/v1/3ds/214/decline \
  -H "Authorization: Bearer $CC_API_KEY"

Response

response.json
{ /* Challenge object, "status": "declined" */ }

Errors

409 not_pendingAlready resolved, or expired.
404 not_foundNo such challenge.

Webhooks

Register the endpoints you want notified. Ten per account, HTTPS only.

GET /webhooks webhooks:write

List endpoints

Reading the list needs the same scope as writing it — an endpoint URL is itself a piece of your infrastructure.

Example

request.sh
curl https://cryptocard.net/api/v1/webhooks \
  -H "Authorization: Bearer $CC_API_KEY"

Response

response.json
{
  "data": [
    { "id": 3, "url": "https://example.com/hooks/cc", "event": "topup.confirmed" }
  ]
}
POST /webhooks webhooks:write

Register an endpoint

Returns the full list after insertion, so you never need a second call to reconcile.

Body

url required stringAbsolute HTTPS URL. Plain HTTP is refused.
event required stringOne of topup.confirmed, card.created, payment.settled, *. * means every event.

Example

request.sh
curl -X POST https://cryptocard.net/api/v1/webhooks \
  -H "Authorization: Bearer $CC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/hooks/cc", "event": "topup.confirmed" }'

Response

response.json
{ "data": [ /* every endpoint, including the new one */ ] }

Errors

400 not_httpsWebhook endpoints must use HTTPS.
400 bad_urlNot a valid URL.
400 bad_eventUnknown event name. The message lists the valid ones.
400 duplicateThat URL is already registered for that event.
400 too_manyTen endpoints already registered.
DELETE /webhooks/{id} webhooks:write

Remove an endpoint

Returns 204 No Content.

Path parameters

idintegerThe endpoint id from the list.

Example

request.sh
curl -X DELETE https://cryptocard.net/api/v1/webhooks/214 \
  -H "Authorization: Bearer $CC_API_KEY"

Response

response.json
(204 — no body)

Errors

404 not_foundNo endpoint with that id on this account.

Delivery is not live yet. These three endpoints register, list and remove endpoints, and they work — but nothing dispatches to them at the moment. Until that ships, poll /topups/{reference} and /3ds?status=pending. This note goes away when delivery does.

Discovery

The root of the API answers without a key. It lists the version, the scopes and every route — handy in a health check to confirm you are talking to the right service. It exposes nothing about any account.

discovery.sh
curl https://cryptocard.net/api/v1
discovery.json
{
  "service": "CryptoCard API",
  "version": "v1",
  "auth": "Authorization: Bearer cc_live_…",
  "scopes": [ /* every scope with its description */ ],
  "endpoints": [ "GET    /api/v1/account", "…" ]
}

Ready to build?

Create an account, activate a card, and your key is one click away.