REST API · v1

API Reference

Accept crypto payments, manage balances, issue refunds, run batch payouts, and listen to webhooks — all over a single, predictable JSON API.

Base URL
https://api.cryptoix.io/v1
Auth
Bearer token (header only)
Format
JSON · UTF-8 · UTC. Use 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...
Important: Passing the key as ?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_allowed otherwise)
  • 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.

ScopeAllows
transactions:readList & inspect payments
balances:readRead merchant wallet balances
withdrawals:readList/inspect withdrawals
withdrawals:writeRequest a new withdrawal
refunds:readList/inspect refunds
refunds:writeIssue a refund
links:readList payment links
links:writeCreate payment links
invoices:readList/inspect invoices
invoices:writeCreate & send invoices
webhooks:readList webhook delivery attempts
webhooks:writeRe-queue (replay) failed deliveries
payouts:readList/inspect batch payouts
payouts:writeCreate, submit & cancel batch payouts
payments:readLegacy: read /api/v1/payment/*
payments:writeLegacy: create /api/v1/payment/create
rates:readPublic 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

HTTPcodeMeaning
400api_key_in_queryKey sent in ?api_key= instead of a header
401missing_api_keyNo Authorization / X-API-Key header
401invalid_api_keyHeader present but key not found / revoked
403ip_not_allowedRequest IP not in the key’s allow-list
403account_not_approvedOwner account is not approved
403missing_scopeKey lacks the scope this route requires
403legacy_key_no_scopesLegacy bypass disabled; create a scoped key
404not_foundResource does not exist or is not yours
404feature_disabledRoute is gated by a feature flag turned off
409state_changedResource changed concurrently — retry
422validation_failedBody or query failed validation
429rate_limitedSliding-window limit exceeded; honour Retry-After
500internal_errorUnexpected 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.

GET /api/v1/transactions transactions:read

Filters: 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"
GET /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.

GET /api/v1/balances balances:read
curl -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.

GET/api/v1/withdrawals withdrawals:read

Filters: status, currency_id.

GET/api/v1/withdrawals/{uuid} withdrawals:read
POST/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.

GET/api/v1/refundsrefunds:read
GET/api/v1/refunds/{uuid}refunds:read
POST/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.

GET/api/v1/payoutspayouts:read
GET/api/v1/payouts/{uuid}payouts:read
POST/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.

POST/api/v1/payouts/{uuid}/submitpayouts:write
POST/api/v1/payouts/{uuid}/cancelpayouts:write

Invoices flag · invoices_v2

Create line-itemed invoices, then email them. Customers pay at https://Cryptoix.io/i/{number}.

GET/api/v1/invoicesinvoices:read
GET/api/v1/invoices/{uuid}invoices:read
POST/api/v1/invoicesinvoices:write
POST/api/v1/invoices/{uuid}/sendinvoices:write

Webhook deliveries

Inspect every outbound webhook attempt and re-queue failed ones (replay requires webhooks_v2).

GET/api/v1/webhook-deliverieswebhooks:read

Filters: success (0/1), event_type, transaction_uuid.

POST/api/v1/webhook-deliveries/{id}/replaywebhooks:write

Legacy 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.

POST/api/v1/payment/createpayments:write
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",
    "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.

GET/api/v1/payment/{uuid}/checkpayments:read
GET/api/v1/ratesrates:read

Outbound 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.

AttemptWait before retry
11 minute
25 minutes
330 minutes
42 hours
56 hours
612 hours
724 hours
8give 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 the X-Api-Version: 1 response 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

  1. Create an account API key from Dashboard → API Keys.
  2. Pick the scopes you need (e.g. transactions:read, payments:write).
  3. 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.