API Reference
Accept crypto payments, manage balances, issue refunds, run batch payouts, and listen to webhooks — all over a single, predictable JSON API.
https://api.cryptoix.io/v1 for all first-party integrations and official plugins.Authentication
Send your API key in one of the headers below — never both, never in the URL.
Authorization: Bearer ak_live_4f8a9c5e3d2b1a98...
# or
X-API-Key: ak_live_4f8a9c5e3d2b1a98... ?api_key=… in the query string is rejected with HTTP 400 api_key_in_query. Query strings leak into webserver logs, browser history, and the Referer header. Account API keys (recommended)
Created at Dashboard → API Keys. Keys are prefixed ak_live_ or ak_test_.
- Fine-grained scopes
- Optional IP allow-list (rejected with
403 ip_not_allowedotherwise) - Per-key usage tracking and one-click revocation
Legacy gateway keys
Each gateway exposes a live and a test key. They predate the scope system, so they receive an implicit grant on every scope unless an operator disables that fallback (returning 403 legacy_key_no_scopes). Every legacy-key call is recorded as api_legacy_key_used in the activity log.
Scopes
Account API keys grant access only to the scopes you select. A request without the required scope returns 403 missing_scope with the missing scope echoed in error.details.required_scope.
| Scope | Allows |
|---|---|
| transactions:read | List & inspect payments |
| balances:read | Read merchant wallet balances |
| withdrawals:read | List/inspect withdrawals |
| withdrawals:write | Request a new withdrawal |
| refunds:read | List/inspect refunds |
| refunds:write | Issue a refund |
| links:read | List payment links |
| links:write | Create payment links |
| invoices:read | List/inspect invoices |
| invoices:write | Create & send invoices |
| webhooks:read | List webhook delivery attempts |
| webhooks:write | Re-queue (replay) failed deliveries |
| payouts:read | List/inspect batch payouts |
| payouts:write | Create, submit & cancel batch payouts |
| payments:read | Legacy: read /api/v1/payment/* |
| payments:write | Legacy: create /api/v1/payment/create |
| rates:read | Public exchange rates endpoint |
Response envelope
Every JSON response has the same shape. Always inspect ok before reading data.
Success
{
"ok": true,
"data": { /* resource or array of resources */ },
"meta": {
"request_id": "req_a1b2c3d4e5f6",
"page": 1,
"per_page": 25,
"total": 137,
"total_pages": 6
}
} Error
{
"ok": false,
"error": {
"code": "validation_failed",
"message": "Human-readable, safe to display.",
"details": { "field": "amount" },
"request_id": "req_a1b2c3d4e5f6"
}
} Every response includes the X-Request-Id header with the same value as request_id. Log it on your side — support uses it to trace any single call end-to-end.
Common error codes
| HTTP | code | Meaning |
|---|---|---|
| 400 | api_key_in_query | Key sent in ?api_key= instead of a header |
| 401 | missing_api_key | No Authorization / X-API-Key header |
| 401 | invalid_api_key | Header present but key not found / revoked |
| 403 | ip_not_allowed | Request IP not in the key’s allow-list |
| 403 | account_not_approved | Owner account is not approved |
| 403 | missing_scope | Key lacks the scope this route requires |
| 403 | legacy_key_no_scopes | Legacy bypass disabled; create a scoped key |
| 404 | not_found | Resource does not exist or is not yours |
| 404 | feature_disabled | Route is gated by a feature flag turned off |
| 409 | state_changed | Resource changed concurrently — retry |
| 422 | validation_failed | Body or query failed validation |
| 429 | rate_limited | Sliding-window limit exceeded; honour Retry-After |
| 500 | internal_error | Unexpected error; request_id is in our logs |
Rate limits
Sliding window per client IP, 60 requests / minute by default. The following headers are present on every API call:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 47
X-Api-Version: 1
X-Request-Id: req_a1b2c3d4e5f6 When you exceed the limit you get HTTP 429 with Retry-After: 60. Back off and try again after the indicated number of seconds.
Pagination
List endpoints accept page (1-indexed, default 1) and per_page (default 25, max 100). Pagination details are returned in meta:
{
"ok": true,
"data": [ /* items */ ],
"meta": { "page": 2, "per_page": 25, "total": 137, "total_pages": 6, "request_id": "req_…" }
} Transactions
Read-only access to every payment processed by your gateways.
/api/v1/transactions transactions:readFilters: status, gateway_id, currency_id, from, to, page, per_page.
curl -sS "https://api.cryptoix.io/v1/transactions?status=completed&per_page=10" \
-H "Authorization: Bearer $API_KEY" /api/v1/transactions/{uuid} transactions:read{
"uuid": "p_8f...",
"status": "completed",
"merchant_order_id": "ORD-1029",
"amount_fiat": 49.90,
"amount_crypto": 0.001837,
"currency": { "id": 5, "symbol": "BTC", "network": "bitcoin" },
"wallet_address": "bc1q...",
"tx_hash": "0xabc...",
"confirmations": 2,
"callback_url": "https://merchant.example/webhook",
"paid_at": "2026-04-12T14:31:08Z",
"expires_at": "2026-04-12T15:01:08Z",
"created_at": "2026-04-12T14:01:08Z",
"updated_at": "2026-04-12T14:31:08Z"
} Balances
Per-currency available + frozen balance for the merchant's crypto wallets.
/api/v1/balances balances:readcurl -sS https://api.cryptoix.io/v1/balances \
-H "Authorization: Bearer $API_KEY"
[
{
"currency": { "id": 5, "symbol": "BTC", "network": "bitcoin", "name": "Bitcoin" },
"available": 0.0124,
"frozen": 0.0010,
"total_deposited": 1.230,
"total_withdrawn": 1.218
}
] Withdrawals
List, inspect, and request withdrawals to a verified address.
/api/v1/withdrawals withdrawals:readFilters: status, currency_id.
/api/v1/withdrawals/{uuid} withdrawals:read/api/v1/withdrawals withdrawals:write{
"currency_id": 5,
"amount": 0.005,
"withdrawal_address_id": 14
} withdrawal_address_id must reference a verified address from Wallets → Addresses. Newly verified addresses sit in a short cooldown (409 address_cooldown).
Possible failure codes: insufficient_balance, invalid_address, network_mismatch, kyc_per_tx_limit, kyc_daily_limit, kyc_monthly_limit, account_frozen, compliance_review, rate_unavailable.
Refunds flag · refund_v1
Issue partial or full refunds against a completed payment.
/api/v1/refundsrefunds:read/api/v1/refunds/{uuid}refunds:read/api/v1/refundsrefunds:write{
"transaction_uuid": "p_8f...",
"amount_fiat": 12.50,
"mode": "balance",
"reason": "duplicate charge",
"destination_address": "bc1q...",
"notes": "Customer email confirmation #4321"
} mode is balance (debits your platform balance) or external (records an off-platform refund — supply destination_address and later attach the on-chain tx_hash in the dashboard).
Batch payouts flag · batch_payouts_v1
Create a draft, submit it for admin approval, then track per-item delivery. Submitting a batch above your 2FA threshold requires a TOTP code.
/api/v1/payoutspayouts:read/api/v1/payouts/{uuid}payouts:read/api/v1/payoutspayouts:write{
"currency_id": 5,
"title": "April salaries",
"idempotency_key": "salary-2026-04",
"auto_submit": true,
"two_fa_code": "123456",
"items": [
{ "address": "bc1q...", "amount": 0.05, "memo_tag": "ABC", "reference": "EMP-001" },
{ "address": "bc1q...", "amount": 0.10, "reference": "EMP-002" }
]
} Pass idempotency_key to make retries safe — duplicates return the existing batch with meta.idempotent = true. Rejected rows are returned in meta.rejected.
/api/v1/payouts/{uuid}/submitpayouts:write/api/v1/payouts/{uuid}/cancelpayouts:writePayment links flag · payment_links_v1
Hosted checkout URLs (https://Cryptoix.io/l/{slug}) you can share with customers.
/api/v1/payment-linkslinks:read/api/v1/payment-linkslinks:write{
"title": "Pro plan — annual",
"gateway_id": 12,
"amount_mode": "fixed",
"amount_fiat": 199.00,
"fiat_currency": "USD",
"usage_type": "multi",
"max_uses": 100,
"accepted_currency_id": 5,
"success_url": "https://merchant.example/thanks",
"cancel_url": "https://merchant.example/cancel",
"expires_at": "2026-12-31T23:59:59Z",
"description": "Includes priority support."
} Invoices flag · invoices_v2
Create line-itemed invoices, then email them. Customers pay at https://Cryptoix.io/i/{number}.
/api/v1/invoicesinvoices:read/api/v1/invoices/{uuid}invoices:read/api/v1/invoicesinvoices:write/api/v1/invoices/{uuid}/sendinvoices:writeWebhook deliveries
Inspect every outbound webhook attempt and re-queue failed ones (replay requires webhooks_v2).
/api/v1/webhook-deliverieswebhooks:readFilters: success (0/1), event_type, transaction_uuid.
/api/v1/webhook-deliveries/{id}/replaywebhooks:writeLegacy payment API
Three older endpoints kept for backward compatibility. They use the { "success": true, "data": … } envelope. Prefer /api/v1/transactions/* for new integrations. currency_id and currency are optional: omit both to use the gateway's saved automatic/manual payment currency method.
/api/v1/payment/createpayments:writecurl -sS https://api.cryptoix.io/v1/payment/create \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 25,
"order_id": "ORD-42",
"customer_email": "[email protected]",
"callback_url": "https://merchant.example/webhook",
"return_url": "https://merchant.example/thanks"
}' In automatic mode Cryptoix creates the transaction with the first available active currency. In manual mode the response still includes payment_url, but it points to /pay/select/{uuid} so the customer can choose from currently available currencies before checkout. Send currency_id or currency only when you intentionally want to override the gateway default for that request.
/api/v1/payment/{uuid}/checkpayments:read/api/v1/ratesrates:readOutbound webhooks
When a transaction's status changes we POST a JSON body to your callback_url. Event types:
transaction.pending · transaction.confirming · transaction.completed · transaction.failed · transaction.expired
Headers
Content-Type: application/json
X-Gateway-Signature: 8c1f3a... (hex hmac-sha256)
X-Gateway-Signature-256: sha256=8c1f3a... Payload
{
"uuid": "p_8f...",
"status": "completed",
"merchant_order_id": "ORD-1029",
"amount_fiat": 49.90,
"amount_crypto": 0.001837,
"currency": "BTC",
"network": "bitcoin",
"tx_hash": "0xabc...",
"paid_at": "2026-04-12T14:31:08Z",
"timestamp": 1744467068
} Verifying signatures
Use the gateway's webhook_secret (rotate it from Dashboard → Gateways). HMAC the raw request body with SHA-256 and compare in constant time.
$body = file_get_contents('php://input');
$got = $_SERVER['HTTP_X_GATEWAY_SIGNATURE'] ?? '';
$want = hash_hmac('sha256', $body, $WEBHOOK_SECRET);
if (!hash_equals($want, $got)) {
http_response_code(401);
exit('bad signature');
}
$event = json_decode($body, true); // Node.js / Express
const crypto = require('crypto');
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const got = req.get('X-Gateway-Signature') || '';
const want = crypto.createHmac('sha256', WEBHOOK_SECRET).update(req.body).digest('hex');
if (got.length !== want.length || !crypto.timingSafeEqual(Buffer.from(got), Buffer.from(want))) {
return res.status(401).send('bad signature');
}
const event = JSON.parse(req.body.toString('utf8'));
res.sendStatus(200);
}); # Python / Flask
import hmac, hashlib
@app.post('/webhook')
def webhook():
body = request.get_data()
got = request.headers.get('X-Gateway-Signature', '')
want = hmac.new(WEBHOOK_SECRET.encode(), body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(got, want):
return 'bad signature', 401
event = request.get_json()
return '', 200 Retry & backoff
Your endpoint must return a 2xx. Anything else is treated as a failure and re-tried on the schedule below (with webhooks_v2 enabled — production default). Maximum 8 attempts.
| Attempt | Wait before retry |
|---|---|
| 1 | 1 minute |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 6 hours |
| 6 | 12 hours |
| 7 | 24 hours |
| 8 | give up |
Use the payload_id field (echoed in webhook_deliveries) to deduplicate when you receive the same event after a retry.
Idempotency
Batch payouts accept an idempotency_key body field. Replays return the existing batch with meta.idempotent = true.
All other write endpoints are not yet idempotent at the protocol level — guard against double-submission by tracking request_id server-side or by debouncing in your client.
Versioning policy
- URL-prefix versioning:
/api/v{N}/.... The current version is 1, reflected in theX-Api-Version: 1response header. - Non-breaking changes (new optional fields, new endpoints) ship under the same version.
- Breaking changes (removed/renamed fields, changed semantics, removed endpoints) require a new version path. They are never applied in-place.
Quick start
- Create an account API key from Dashboard → API Keys.
- Pick the scopes you need (e.g.
transactions:read,payments:write). - Drop the key in your secret store and start calling the API.
export API_KEY="ak_live_..."
# 1) Create a payment
curl -sS https://api.cryptoix.io/v1/payment/create \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"amount": 25, "order_id": "ORD-42"}'
# 2) List recent transactions
curl -sS "https://api.cryptoix.io/v1/transactions?per_page=10&status=completed" \
-H "Authorization: Bearer $API_KEY"
# 3) Check balances
curl -sS https://api.cryptoix.io/v1/balances \
-H "Authorization: Bearer $API_KEY" Every response carries X-Request-Id — log it on your side so you and support can trace any individual call end-to-end.