Skip to content
FFastPay Docs

Crypto

Price in US dollars or naira, get paid in coin, and withdraw your balance to your own wallet.

Before you start#

  • Crypto opens per business. Until it is on for yours, crypto calls are refused.
  • A test key works in the test book, a live key in the live book. Nothing in test moves on chain.
  • Coin amounts are integers in base units of 8 decimals, with the same amount as decimal text beside them: available_units: 12500000000 is available: "125".
  • A coin is named by its code on its network, such as USDTTRC. List the coins on offer.

Take a payment#

Send the price in US cents and the coin. fee_bearer decides who pays the platform fee: customer adds it to what the payer sends, merchant takes it from what you are credited. Leave it out for your business default.

POST/api/v1/crypto/payments
curl -X POST "https://api.vervapay.com/api/v1/crypto/payments" \
  -H "Authorization: Bearer sk_test_…" \
  -H "Idempotency-Key: order-1001" \
  -H "Content-Type: application/json" \
  -d '{
    "amount_minor": 2500,
    "asset": "USDTTRC",
    "email": "ada@example.com",
    "reference": "order-1001",
    "fee_bearer": "customer"
  }'
201 Created
{
  "data": {
    "id": "0192f3a1-5d0b-7f7e-9a51-3c7c2f0d8e11",
    "status": "pending",
    "amount_minor": 2500,
    "currency": "USD",
    "reference": "order-1001",
    "created_at": "2026-11-09T09:30:00Z",
    "crypto": {
      "asset": "USDTTRC",
      "network": "Tron (TRC-20)",
      "fee_bearer": "customer",
      "fee_units": 37500000,
      "fee": "0.375",
      "held": false,
      "address": "TQ5nV3uXk7pZbW9eYhM2rC4sD6fG8hJ1kL",
      "pay_amount": "25.375",
      "pay_units": 2537500000,
      "expires_at": "2026-11-09T09:50:00Z"
    }
  }
}

Show the payer address, the exact pay_amount, the network in words and expires_at. The address is returned while it is valid; once the payment settles or expires it is left out. Fetch the payment to see amount_received, tx_hash and credited.

In the test book, test_case picks the outcome: success, common, failed or partially_paid.

Naira prices#

Send currency: "NGN" and the price in kobo. The coin amount is the price at the platform's naira rate for the coin, rounded up to the coin's precision, and fixed on the payment when the address is issued. A hosted checkout priced in naira offers crypto beside its other methods.

POST/api/v1/crypto/payments
curl -X POST "https://api.vervapay.com/api/v1/crypto/payments" \
  -H "Authorization: Bearer sk_test_…" \
  -H "Idempotency-Key: order-1002" \
  -H "Content-Type: application/json" \
  -d '{
    "amount_minor": 15000000,
    "currency": "NGN",
    "asset": "USDTTRC",
    "email": "ada@example.com",
    "reference": "order-1002"
  }'
201 Created
{
  "data": {
    "id": "0192f3a1-5d0b-7f7e-9a51-3c7c2f0d8e11",
    "status": "pending",
    "amount_minor": 15000000,
    "currency": "NGN",
    "reference": "order-1002",
    "created_at": "2026-11-09T09:30:00Z",
    "crypto": {
      "asset": "USDTTRC",
      "network": "Tron (TRC-20)",
      "fee_bearer": "merchant",
      "fee_units": 150000000,
      "fee": "1.5",
      "held": false,
      "rate_minor": 150000,
      "price_units": 10000000000,
      "price": "100",
      "address": "TQ5nV3uXk7pZbW9eYhM2rC4sD6fG8hJ1kL",
      "pay_amount": "100",
      "pay_units": 10000000000,
      "expires_at": "2026-11-09T09:50:00Z"
    }
  }
}
  • rate_minor: naira per coin, in kobo, as fixed on this payment.
  • price_units: the price in the coin at that rate, before any fee the payer bears.
  • You are credited the coin, never naira. The US$2,000 cap applies to it.

Balances#

GET/api/v1/crypto/balances
curl "https://api.vervapay.com/api/v1/crypto/balances" \
  -H "Authorization: Bearer sk_test_…"
200 OK
{
  "data": [
    {
      "asset": "USDTTRC",
      "decimals": 8,
      "available_units": 12500000000,
      "available": "125",
      "on_hold_units": 0,
      "on_hold": "0",
      "in_transit_units": 2500000000,
      "in_transit": "25"
    }
  ]
}
  • available: what you can withdraw.
  • on_hold: held while a dispute is open.
  • in_transit: withdrawn and not yet confirmed sent.

Turn on API withdrawals#

Your secret key can read balances, addresses and withdrawals at any time. To add or remove an address, withdraw, or change an automatic withdrawal through the API, a business admin first turns on API withdrawals in the dashboard and confirms with a code sent by email. Until then those calls are refused with 403 permission_denied.

Withdrawal addresses#

A withdrawal goes only to an address you have verified, one per coin.

POST/api/v1/crypto/addresses
curl -X POST "https://api.vervapay.com/api/v1/crypto/addresses" \
  -H "Authorization: Bearer sk_test_…" \
  -H "Content-Type: application/json" \
  -d '{
    "asset": "USDTTRC",
    "address": "TQ5nV3uXk7pZbW9eYhM2rC4sD6fG8hJ1kL",
    "label": "Treasury"
  }'
201 Created
{
  "data": {
    "id": "0192f3a2-7c1e-7a40-8d1b-4b2c9e0f1a22",
    "asset": "USDTTRC",
    "mode": "test",
    "address": "TQ5nV3uXk7pZbW9eYhM2rC4sD6fG8hJ1kL",
    "label": "Treasury",
    "status": "pending_code",
    "verified_at": null,
    "usable_from": null,
    "usable": false,
    "added_by_staff": false,
    "code_sent_to": "a***@example.com",
    "created_at": "2026-11-09T09:30:00Z"
  }
}
  1. The address is checked for the coin, and a code is emailed to the business admin. Every owner and admin is told.
  2. Verify with that code: {"code": "482913"}. Ask for a new code if it expired.
  3. After a cool-off (24 hours by default) the address is usable; usable_from says when.

Removing an address stops any automatic withdrawal that used it.

Withdraw#

Price it first: the platform fee and the network fee are taken from the amount, and send is what arrives.

POST/api/v1/crypto/withdrawal-quotes
curl -X POST "https://api.vervapay.com/api/v1/crypto/withdrawal-quotes" \
  -H "Authorization: Bearer sk_test_…" \
  -H "Content-Type: application/json" \
  -d '{
    "asset": "USDTTRC",
    "amount_units": 10000000000
  }'
200 OK
{
  "data": {
    "asset": "USDTTRC",
    "requested_units": 10000000000,
    "platform_fee_units": 100000000,
    "network_fee_units": 100000000,
    "send_units": 9800000000,
    "send": "98",
    "debit_units": 10000000000
  }
}

Request it with the same body and an idempotency key. A code is emailed to the business admin; nothing moves yet.

POST/api/v1/crypto/withdrawals
curl -X POST "https://api.vervapay.com/api/v1/crypto/withdrawals" \
  -H "Authorization: Bearer sk_test_…" \
  -H "Idempotency-Key: withdrawal-0001" \
  -H "Content-Type: application/json" \
  -d '{
    "asset": "USDTTRC",
    "amount_units": 10000000000
  }'
201 Created
{
  "data": {
    "id": "0192f3a3-1a9b-7c55-9e2d-5c3dae1f2b33",
    "asset": "USDTTRC",
    "mode": "test",
    "address_id": "0192f3a2-7c1e-7a40-8d1b-4b2c9e0f1a22",
    "address": "TQ5nV3uXk7pZbW9eYhM2rC4sD6fG8hJ1kL",
    "origin": "manual",
    "requested_units": 10000000000,
    "debit_units": 10000000000,
    "platform_fee_units": 100000000,
    "network_fee_quote_units": 100000000,
    "send_units": 9800000000,
    "send": "98",
    "status": "awaiting_code",
    "code_sent_to": "a***@example.com",
    "created_at": "2026-11-09T10:00:00Z",
    "completed_at": null
  }
}

Confirm with the code. The amount leaves your balance and the withdrawal moves through queued, submitted and released to paid, with its tx_hash. If it cannot be sent it is returned and the amount is back in your balance.

While it waits for its code you can ask for a new code or cancel it. List withdrawals by status, asset and date, and follow next_cursor for more.

Automatic withdrawals#

Once a Lagos day, at run_at, your whole available balance of a coin is withdrawn to a verified address when it is at or above minimum_units.

PUT/api/v1/crypto/auto-withdrawals/{asset}
curl -X PUT "https://api.vervapay.com/api/v1/crypto/auto-withdrawals/USDTTRC" \
  -H "Authorization: Bearer sk_test_…" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "address_id": "0192f3a2-7c1e-7a40-8d1b-4b2c9e0f1a22",
    "minimum_units": 5000000000,
    "run_at": "09:00"
  }'

Turning it on without a code returns 202 and emails one to the business admin; nothing changes. Send the same body again with code to turn it on. Turning it off needs no code.

Webhooks#

  • crypto.payment.receivedCoin arrived and is waiting for confirmations.
  • crypto.payment.confirmedThe payment is confirmed and credited. payment.succeeded is sent too.
  • crypto.payment.heldSomething did not match, such as the amount or the timing, so it is held for review. hold_reason says why.
  • crypto.withdrawal.requestedA confirmed withdrawal left your balance and is queued.
  • crypto.withdrawal.sentThe coin was sent. tx_hash is the transaction on chain.
  • crypto.withdrawal.failedIt could not be sent, and the amount is back in your balance.
crypto.payment.confirmed
{
  "id": "0192f3b0-2b6c-7d10-8a3e-6d4e0f2a3c44",
  "type": "crypto.payment.confirmed",
  "version": "2026-01",
  "created_at": "2026-11-09T09:41:00Z",
  "data": {
    "payment_id": "0192f3a1-5d0b-7f7e-9a51-3c7c2f0d8e11",
    "status": "succeeded",
    "amount_minor": 2500,
    "currency": "USD",
    "reference": "order-1001",
    "metadata": {
      "cart_id": "c_42"
    },
    "crypto": {
      "asset": "USDTTRC",
      "amount_received": "25.375",
      "tx_hash": "9f2c4e…a1b7",
      "credited_units": 2500000000,
      "credited": "25"
    }
  }
}
crypto.withdrawal.sent
{
  "id": "0192f3b1-4d8e-7f21-9b4f-7e5f1a3b4d55",
  "type": "crypto.withdrawal.sent",
  "version": "2026-01",
  "created_at": "2026-11-09T12:05:00Z",
  "data": {
    "withdrawal_id": "0192f3a3-1a9b-7c55-9e2d-5c3dae1f2b33",
    "status": "paid",
    "asset": "USDTTRC",
    "address": "TQ5nV3uXk7pZbW9eYhM2rC4sD6fG8hJ1kL",
    "amount_units": 9800000000,
    "amount": "98",
    "debit_units": 10000000000,
    "origin": "manual",
    "tx_hash": "4be1d0…c93f"
  }
}

Every event is signed and retried like the others: see events and signatures.

Errors#

  • 403 permission_denied: API withdrawals are off, or live crypto is not open for your business.
  • 400 validation_failed: the request does not match the API; the message names the field.
  • 422 validation_failed: a wrong or expired code, an address that cannot take the coin, or an amount below the smallest or above the largest allowed. A withdrawal below the smallest carries details.minimum_units and details.asset.
  • 409 conflict: the address is not verified or still cooling off, the amount is more than available, or crypto is not available right now.
  • 409 idempotency_key_reused: the key was used for a different request.
  • 429 rate_limited: too many codes asked for, or too many wrong codes; codes lock for a day.
  • 503 provider_unavailable: nothing was sent or charged. details.reason says why: not_set_up (crypto is not set up for this mode yet), refused (access was refused upstream; try later) or unreachable (try again).
  • 409 conflict with details.reason: declined: the request was turned down upstream. Nothing was sent or charged.