Quickstart
Three steps, about a minute.
- 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.
- Copy the key. Dashboard → Developers → API key. It looks like
cc_live_followed by 32 hex characters. - Grant the scopes you need. Same panel. Several are off by default, on purpose — see Scopes.
# 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.
Authorization: Bearer cc_live_4f2c9ab17e0d5836bb41c7a290e6fd18| Status | Error | When |
|---|---|---|
| 401 | unauthorized | The header is missing, or does not match Bearer cc_live_ + 32 hex characters. |
| 401 | invalid_key | Well-formed, but no account carries that key. |
| 403 | insufficient_scope | Valid 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
| Scope | Default | Grants |
|---|---|---|
account:read |
on | Read the email and account status |
Scopes for Money
| Scope | Default | Grants |
|---|---|---|
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
| Scope | Default | Grants |
|---|---|---|
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
| Scope | Default | Grants |
|---|---|---|
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
| Scope | Default | Grants |
|---|---|---|
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.
{
"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": "insufficient_scope",
"message": "This key does not carry the scope this endpoint requires.",
"required_scope": "cards:pan"
}| Status | Meaning |
|---|---|
| 200 | Success — or a refused top-up. See the note on POST /topups. |
| 201 | Created — a card or a deposit. |
| 204 | Deleted. Empty body. |
| 400 | The body or the parameters are not valid. |
| 401 | Missing, malformed or unknown key. |
| 402 | The balance does not cover the operation. |
| 403 | The key lacks the required scope. |
| 404 | No such resource on this account. |
| 405 | Wrong method for that path. |
| 409 | The resource is in a state that forbids it — frozen card, default card, resolved challenge, no slot left. |
| 429 | Rate limited. |
| 500 | Our 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
id | integer | Stable identifier. Use it in every /cards/{id} path. |
label | string | Your name for the card, up to 40 characters. |
type | string | virtual or physical, lower-case. |
last4 | string | Last four digits. Always present. |
expiry | string | MM/YY. |
status | string | active or frozen. Deleted cards are never returned. |
default | boolean | The account's default card. It cannot be deleted. |
monthly_limit | number | null | Monthly cap in USD, null when uncapped. |
spent_usd | number | Spent against the current cap. |
created_at | string | null | ISO-8601, UTC. |
number | string | Full PAN. Only from /cards/{id}/secure, with cards:pan. |
cvv | string | Three digits, derived on the fly and stored nowhere. Same endpoint, same scope. |
Top-up
reference | string | The deposit id. Use it in /topups/{reference}. |
status | string | pending until the network confirms, then confirmed. |
coin | string | The symbol you asked for. |
amount_usd | number | What you asked to credit, before the fee. |
fee_usd | number | 1% of amount_usd. |
credit_usd | number | What actually lands on the balance. |
deposit_address | string | Send here, and only for this deposit. |
deposit_amount | string | The exact amount of coin to send. A string, not a number — trailing precision matters and floats lose it. |
deposit_network | string | The chain, e.g. Monero, TRC-20. |
deposit_tag | string | null | Memo or tag. When it is not null, sending without it loses the deposit. |
tx_hash | string | null | Set once the payment is seen on-chain. |
created_at | string | ISO-8601, UTC. |
expires_at | string | null | After this, the address stops being watched. |
credited_at | string | null | Set when the balance moved. This is the field that means "done". |
Transaction
kind | string | topup, payment, refund, fee or yield. |
title | string | Short label, as shown in the dashboard. |
detail | string | null | Merchant, coin and amount, or the reason for a fee. |
coin | string | null | Set on top-ups. Always null on a yield movement — interest is credited in USD, never in a coin. |
amount_usd | number | Signed: negative when it leaves the balance. |
status | string | Where the movement stands. |
created_at | string | ISO-8601, UTC. |
Interest day
day | string | The UTC calendar day the interest was calculated for, YYYY-MM-DD. The credit itself is dated 00:00 UTC the following day. |
basis_usd | number | The closing balance used as the base. Because each payment joins the balance, this grows day over day if nothing is spent. |
amount_usd | number | What was credited, to the cent. 0 means the day’s interest stayed below one cent and was carried forward. |
3-D Secure challenge
id | string | The challenge reference. Use it in /3ds/{id}. |
status | string | pending, approved, declined or expired. |
merchant | string | Who is asking. |
amount_usd | number | The amount being authorised. |
card_id | integer | null | The card involved. |
channel | string | How the holder was notified. |
created_at | string | ISO-8601, UTC. |
expires_at | string | After this the challenge expires on its own and can no longer be resolved. |
resolved_at | string | null | When it was approved or declined. |
code | string | The 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.
/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
curl https://cryptocard.net/api/v1/account \
-H "Authorization: Bearer $CC_API_KEY"Response
{
"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.
/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
curl https://cryptocard.net/api/v1/balance \
-H "Authorization: Bearer $CC_API_KEY"Response
{
"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"
}
}/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
days | integer | Between 1 and 365. Defaults to 30. Values outside the range are clamped, not rejected. |
Example
curl https://cryptocard.net/api/v1/interest \
-H "Authorization: Bearer $CC_API_KEY"Response
{
"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
}
]
}/transactions
transactions:read
The ledger
Every movement, newest first: top-ups, card payments, refunds, fees and the daily yield credit.
Query parameters
limit | integer | Between 1 and 200. Defaults to 50. Values outside the range are clamped, not rejected. |
Example
curl https://cryptocard.net/api/v1/transactions \
-H "Authorization: Bearer $CC_API_KEY"Response
{
"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"
}
]
}/topups
topups:read
List deposits
The 50 most recent deposits, newest first, whatever their status.
Example
curl https://cryptocard.net/api/v1/topups \
-H "Authorization: Bearer $CC_API_KEY"Response
{ "data": [ /* Top-up objects */ ] }/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 |
number | What 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 |
string | One of BTC, XMR, ETH, USDT, USDTTRC, LTC, TRX. |
Example
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
{
"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_unsupported | The coin is not one of the supported symbols. The message lists them. |
| 400 | amount_too_low | Below the account minimum. The response carries minimum_usd. |
| 400 | amount_too_high | Above 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.
/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
reference | string | The deposit reference, e.g. 440062. |
Example
curl https://cryptocard.net/api/v1/topups/440062 \
-H "Authorization: Bearer $CC_API_KEY"Response
{ /* Top-up object */ }Errors
| 404 | not_found | No 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.
/cards
cards:read
List cards
Every card on the account that has not been deleted.
Example
curl https://cryptocard.net/api/v1/cards \
-H "Authorization: Bearer $CC_API_KEY"Response
{ "data": [ /* Card objects */ ] }/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 |
string | Up to 40 characters. Longer labels are truncated, an empty one becomes Virtual card. |
monthly_limit |
number | null | Monthly spend cap in USD. null means no cap. |
Example
curl -X POST https://cryptocard.net/api/v1/cards \
-H "Authorization: Bearer $CC_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "label": "Shopping" }'Response
{
"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_left | All 5 slots are in use. |
| 402 | insufficient_funds | The balance does not cover the card fee. Carries required_usd. |
/cards/{id}
cards:read
Retrieve a card
A single card, masked.
Path parameters
id | integer | The card id. |
Example
curl https://cryptocard.net/api/v1/cards/214 \
-H "Authorization: Bearer $CC_API_KEY"Response
{ /* Card object */ }Errors
| 404 | not_found | No such card on this account. |
/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
id | integer | The card id. |
Body
label |
string | New label, up to 40 characters. An empty string is ignored. |
monthly_limit |
number | null | New cap, or null to remove it. |
Example
curl -X PATCH https://cryptocard.net/api/v1/cards/214 \
-H "Authorization: Bearer $CC_API_KEY"Response
{ /* the updated Card object */ }/cards/{id}
cards:write
Delete a card
Frees the slot. Returns 204 No Content with an empty body.
Path parameters
id | integer | The card id. |
Example
curl -X DELETE https://cryptocard.net/api/v1/cards/214 \
-H "Authorization: Bearer $CC_API_KEY"Response
(204 — no body)Errors
| 409 | cannot_delete_default | The default card cannot be deleted. |
/cards/{id}/freeze
cards:write
Freeze
Declines every authorisation immediately. Reversible, and it does not consume a slot.
Path parameters
id | integer | The card id. |
Example
curl -X POST https://cryptocard.net/api/v1/cards/214/freeze \
-H "Authorization: Bearer $CC_API_KEY"Response
{ /* Card object, "status": "frozen" */ }/cards/{id}/unfreeze
cards:write
Unfreeze
Puts the card back to active.
Path parameters
id | integer | The card id. |
Example
curl -X POST https://cryptocard.net/api/v1/cards/214/unfreeze \
-H "Authorization: Bearer $CC_API_KEY"Response
{ /* Card object, "status": "active" */ }/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
id | integer | The card id. |
Example
curl https://cryptocard.net/api/v1/cards/214/secure \
-H "Authorization: Bearer $CC_API_KEY"Response
{
"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_frozen | Unfreeze 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.
/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
status | string | One of pending (default), approved, declined, expired, all. Anything else is a 400. |
Example
curl https://cryptocard.net/api/v1/3ds \
-H "Authorization: Bearer $CC_API_KEY"Response
{
"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"
}
]
}/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
id | string | The challenge id, e.g. 3ds_7Kq2mR. |
Example
curl https://cryptocard.net/api/v1/3ds/214 \
-H "Authorization: Bearer $CC_API_KEY"Response
{ /* Challenge object, with "code" while pending */ }Errors
| 404 | not_found | No such challenge on this account. |
/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
id | string | The challenge id. |
Example
curl -X POST https://cryptocard.net/api/v1/3ds/214/approve \
-H "Authorization: Bearer $CC_API_KEY"Response
{ /* Challenge object, "status": "approved" */ }Errors
| 409 | not_pending | Already resolved, or expired. |
| 404 | not_found | No such challenge. |
/3ds/{id}/decline
3ds:write
Decline a payment
Refuses the payment. Same rules as approve.
Path parameters
id | string | The challenge id. |
Example
curl -X POST https://cryptocard.net/api/v1/3ds/214/decline \
-H "Authorization: Bearer $CC_API_KEY"Response
{ /* Challenge object, "status": "declined" */ }Errors
| 409 | not_pending | Already resolved, or expired. |
| 404 | not_found | No such challenge. |
Webhooks
Register the endpoints you want notified. Ten per account, HTTPS only.
/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
curl https://cryptocard.net/api/v1/webhooks \
-H "Authorization: Bearer $CC_API_KEY"Response
{
"data": [
{ "id": 3, "url": "https://example.com/hooks/cc", "event": "topup.confirmed" }
]
}/webhooks
webhooks:write
Register an endpoint
Returns the full list after insertion, so you never need a second call to reconcile.
Body
url required |
string | Absolute HTTPS URL. Plain HTTP is refused. |
event required |
string | One of topup.confirmed, card.created, payment.settled, *. * means every event. |
Example
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
{ "data": [ /* every endpoint, including the new one */ ] }Errors
| 400 | not_https | Webhook endpoints must use HTTPS. |
| 400 | bad_url | Not a valid URL. |
| 400 | bad_event | Unknown event name. The message lists the valid ones. |
| 400 | duplicate | That URL is already registered for that event. |
| 400 | too_many | Ten endpoints already registered. |
/webhooks/{id}
webhooks:write
Remove an endpoint
Returns 204 No Content.
Path parameters
id | integer | The endpoint id from the list. |
Example
curl -X DELETE https://cryptocard.net/api/v1/webhooks/214 \
-H "Authorization: Bearer $CC_API_KEY"Response
(204 — no body)Errors
| 404 | not_found | No 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.
curl https://cryptocard.net/api/v1{
"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.


