How to automate stablecoins or crypto payouts with a crypto payout API

August 28, 2026

How to automate stablecoins or crypto payouts with a crypto payout API

Paying one contractor in crypto is a manual task that takes two minutes. Paying four hundred of them every second Friday is an engineering problem. Somewhere between those two volumes, most finance teams stop copying addresses into a dashboard and ask a developer (or coding agent) to wire the payouts into the system that already knows who is owed what.

A crypto payout API is the component that does that work. This guide covers the six endpoints EukaPay exposes for payouts, the order you should call them in, and the one header that stops a retry from paying somebody twice.

In this guide, you'll learn:

  • Which payout endpoints exist and what each one is actually for

  • How to check your available balance and estimate network fees before you send

  • How to use idempotency keys so a timeout never turns into a duplicate payment

  • How to track payout status through webhooks instead of polling

What a crypto payout API does

A payout API moves value in the opposite direction from a payment gateway. Instead of collecting funds from a customer, you are sending funds to a recipient: a contractor, an affiliate, a player withdrawing a balance, or a vendor invoice you have approved for payment.

EukaPay exposes six endpoints for this work, all under

/crypto_payouts

:

Endpoint

What it does

GET /crypto_payouts/balance

Returns the balance available to pay out

POST /crypto_payouts/estimate

Returns a fee and amount estimate before you commit

POST /crypto_payouts

Creates a single payout

GET /crypto_payouts

Lists payouts, with cursor pagination

GET /crypto_payouts/{code}

Retrieves one payout by its code

PUT /crypto_payouts/{code}

Updates a payout

One point to be clear about, because it shapes how you design the integration:

POST /crypto_payouts

creates

one

payout per call. There is no bulk endpoint that accepts an array of recipients. Paying four hundred contractors means four hundred calls, which is a solved problem once your queue and your idempotency keys are correct. If you would rather not build that loop, EukaPay also accepts a CSV upload in the merchant dashboard for batch runs. The API route and the CSV route reach the same payout rails.

Before your first payout

API keys - one per environment

Generate keys in the merchant dashboard under Settings > Integrations > API keys. Keys carry an environment prefix, so

sk_test_

keys work against staging and

sk_live_

keys work against production. The secret is displayed once at creation, so store it in your secret manager at that moment.

Authentication is a single custom header. There is no bearer token and no request signing on outbound calls:

curl "https://api.eukapay.com/crypto_payouts/balance" \
  -H "x-api-key: sk_live_YOUR_KEY"

Two details that catch developers out. The production host is

https://api.eukapay.com

and staging is

https://api-stg.eukapay.com

. And there is no version prefix in the path: the endpoint is

/crypto_payouts

, not

/v1/crypto_payouts

, even though the reference is labelled v1.0.

Check the balance before you build the batch

Every payout run should start by asking what is actually available.

GET /crypto_payouts/balance

answers that, and calling it first turns a mid-batch failure into a pre-batch decision.

curl "https://api.eukapay.com/crypto_payouts/balance" \
  -H "x-api-key: sk_live_YOUR_KEY"

The response is keyed by fiat currency, and it distinguishes your total from what you can actually spend:

{
  "CAD": {
    "amount": 123.45,
    "availableAmount": 111.11
  }
}

Read

availableAmount

, not

amount

. The two differ when funds are committed but not yet cleared, and a batch sized against

amount

will fail partway through. If the available balance will not cover the run, top up from your settlement balance using

POST /balance/transfer

before you begin, rather than discovering the shortfall on payout two hundred and seventeen.

Estimate the cost - POST /crypto_payouts/estimate

Network fees on a payout are real money and they vary by chain. The estimate endpoint prices a payout before you commit to it, which matters when the fee on one network is a rounding error and on another it is a meaningful percentage of a small payment.

curl -X POST "https://api.eukapay.com/crypto_payouts/estimate" \
  -H "x-api-key: sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cryptocurrencySymbol": "BTC",
    "blockchainNetwork": "Bitcoin",
    "sourceAmount": 1000
  }'
{
  "sourceAmount": 1000,
  "destinationAmount": 0.0061364,
  "sourceCurrency": { "name": "Canadian Dollar", "unit": "CAD", "sign": "CA$" },
  "destinationCurrency": { "name": "Bitcoin", "unit": "BTC", "network": "Bitcoin" }
}

The estimate endpoint is narrower than EukaPay's full currency list, and the constraint is worth designing around rather than discovering in production. It accepts

cryptocurrencySymbol

values of

USDC

,

USDT

,

ETH

and

BTC

only, and

blockchainNetwork

values of

Ethereum

,

Tron

and

Bitcoin

only.

Note also that estimate is the one create-shaped endpoint that does not accept

x-idempotent-key

. It does not move money, so a repeated call costs you nothing beyond a request.

In practice this means USDT payouts on Tron are the cheapest high-volume option available, which is why most contractor and affiliate programmes settle on USDT over Tron once volume grows.

Creating a payout

A payout is a single

POST /crypto_payouts

call. One detail catches almost everyone: this endpoint takes

multipart/form-data

, not JSON. It is the only create endpoint in the API that does, because recipients in high-risk countries require a supporting document as a file part.

curl -X POST "https://api.eukapay.com/crypto_payouts" \
  -H "x-api-key: sk_live_YOUR_KEY" \
  -H "x-idempotent-key: 8f14e45f-ceea-467a-9f4a-1c2e7d5a9b30" \
  -F "cryptocurrencySymbol=USDC" \
  -F "blockchainNetwork=Ethereum" \
  -F "destinationAmount=200" \
  -F "walletAddress=0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B" \
  -F "firstName=Satoshi" \
  -F "lastName=Nakamoto" \
  -F "address1=123 ABC St." \
  -F "city=Toronto" \
  -F "state=Ontario" \
  -F "country=Canada" \
  -F "zip=M5B 2H1" \
  -F "email=recipient@example.com" \
  -F "purposeOfFunds=invoice_payment"

The response comes back as JSON:

{
  "code": "cpo_pGmNtGRwtOM7464dsmqZZnHPVt",
  "sourceAmount": 200,
  "destinationAmount": 0,
  "walletAddress": "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B",
  "firstName": "Satoshi",
  "lastName": "Nakamoto",
  "city": "Toronto",
  "state": "Ontario",
  "country": "Canada",
  "zip": "M5B 2H1",
  "email": "recipient@example.com",
  "purposeOfFunds": "invoice_payment",
  "purposeDescription": "",
  "status": "Processing",
  "createdAt": "2024-03-08T14:23:04.450Z",
  "updatedAt": "2024-03-08T14:23:04.450Z",
  "sourceCurrency": { "name": "Canadian Dollar", "unit": "CAD", "sign": "CA$" },
  "destinationCurrency": { "name": "USD Coin", "unit": "USDC", "network": "Ethereum" },
  "requiresComplianceReview": false
}

Four field rules worth knowing before you write the loop.

cryptocurrencySymbol

and

blockchainNetwork

are the only two formally required fields. You specify either

sourceAmount

, which is the fiat amount to send, or

destinationAmount

, which is the crypto amount, and never both. Minimum destination amounts are 0.001 BTC, 0.02 ETH, 100 USDT and 100 USDC. And

firstName

and

lastName

are required unless you pass a

customerCode

for a saved recipient, which is the cleaner pattern for anyone you pay repeatedly.

purposeOfFunds

accepts

contract_payment

,

employee_payment

,

invoice_payment

,

professional_service_payments

or

other

, and

purposeDescription

becomes required when you choose

other

. Recipients in grey-list countries require

email

, and recipients in Russia or Ukraine require both a

purposeOfFunds

value and a

supportingDocument

file part of up to 5 MB as PDF, JPG or PNG.

Idempotency keys - the header that prevents double payments

This is the section most payout tutorials skip, and it is the one that costs real money when it is missing.

A payout request can time out after the server has already accepted it. Your HTTP client sees a failure, your retry logic fires, and without protection the recipient is paid twice. Crypto payouts are irreversible, so there is no chargeback to fall back on.

EukaPay accepts an

x-idempotent-key

header to close that gap. You generate the value, the documentation recommends a V4 UUID, it can be up to 255 characters, and keys expire after 24 hours. If a request arrives carrying a key EukaPay has already seen, it is recognised as a repeat rather than executed as a new payout.

Three rules make idempotency actually work:

  1. Derive the key from the payout, not from the request.

    A key generated fresh on every retry protects nothing. Derive it from something stable, such as the payroll run identifier plus the recipient identifier, so every retry of that specific payout carries the same key.

  2. Persist the key before you send.

    Write it to your database in the same transaction that marks the payout as attempted. If your process dies between generating the key and sending the request, the next run needs to find the same key.

  3. Respect the 24-hour expiry.

    A retry attempted more than a day later will not be recognised as a duplicate. For anything older, reconcile against

    GET /crypto_payouts

    before resending.

If the API returns an

idempotency_error

, treat it as a signal to stop and reconcile against

GET /crypto_payouts

rather than to retry harder with a fresh key. EukaPay documents the three error types,

api_error

,

idempotency_error

and

invalid_request_error

, alongside a 409 Conflict status for requests that conflict with another request.

Tracking payout status

A created payout is not a completed payout. EukaPay publishes six status values for a crypto payout:

Status

What it means

Pending

Accepted, not yet actioned

Reviewing

Under compliance review before release

Processing

Being sent to the network

Sent

Broadcast to the recipient address

Error

Failed

Revoked

Cancelled before sending

Reviewing

deserves a note, because it does not appear in the status list of unlicensed payout providers. EukaPay is registered with FINTRAC in Canada and FinCEN in the United States, and a registered processor screens outbound transfers. Build for it: a payout sitting in

Reviewing

is working as designed, not stuck, and your internal status display should say so rather than showing your finance team a failure.

Webhooks for payouts

Polling

GET /crypto_payouts/{code}

in a loop works, and it wastes your rate limit. Webhooks are the better pattern.

Configure an endpoint under Settings > Integrations > Webhooks. Four of EukaPay's eight webhook events cover payouts:

  • cryptoPayoutCreated
  • cryptoPayoutSent
  • cryptoPayoutError
  • cryptoPayoutRevoked

Verifying the signature - HMAC SHA-512

Every webhook arrives with an

x-eukapay-signature

header containing an HMAC of the payload, keyed to the secret for that webhook. The algorithm is

SHA-512

, not the SHA-256 most gateways use. If a reviewer or a linter suggests correcting it to SHA-256, the correction is wrong.

const crypto = require('crypto');

function verifyEukaPaySignature(rawBody, signatureHeader, secret) {
  const digest = crypto.createHmac('sha512', secret).update(rawBody).digest('hex');
  if (digest.length !== signatureHeader.length) {
    return false;
  }
  return crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(signatureHeader));
}

Use

timingSafeEqual

rather than

===

. A plain string comparison returns as soon as it finds a differing character, which leaks information about how much of a forged signature was correct.

One detail to settle against your own account before you rely on it. EukaPay's documentation gives two instructions that do not agree: the written steps say to use the payload "as it is without any alteration or converting to json," while the accompanying code sample calls

JSON.stringify

on an already-parsed body. Those two inputs produce different bytes whenever key order or whitespace differs, so they cannot both be correct. Capture the raw body as your framework delivers it, verify one real webhook against it, and if verification fails, try the re-serialised form before assuming your HMAC code is wrong.

Retries - plan for repeats

EukaPay retries a failed webhook delivery every 20 minutes for up to two hours. That schedule has a direct consequence: your handler will receive the same event more than once, and it must be safe to process twice. Record the

webhookId

you have already handled and return early on a repeat.

Verify the signature, record the event, return a 2xx quickly, and do the slow work asynchronously. A handler that runs a database migration before responding will time out and trigger the retry schedule you were trying to avoid.

Paying many recipients

With the pieces above, a payout run at volume is a queue rather than a bulk API call:

  1. Check

    GET /crypto_payouts/balance

    once at the start of the run.

  2. For each recipient, derive and persist a stable idempotency key.

  3. Enqueue one

    POST /crypto_payouts

    job per recipient.

  4. Back off on a 429. EukaPay documents rate limiting and recommends exponential backoff, without publishing a specific requests-per-second figure, so treat the 429 itself as your signal rather than hard-coding a limit.

  5. Let webhooks drive status. Reconcile anything that has not reported within your expected window against

    GET /crypto_payouts

    .

The pattern scales because each payout is independent. A single failure retries on its own key without touching the other three hundred and ninety-nine.

One platform underneath

Payouts and pay-ins run on the same EukaPay account and the same infrastructure: instant crypto-to-fiat conversion at a locked exchange rate to remove all crypto volatility, protection against chargebacks, support for a wide range of cryptocurrencies, and settlement in USD, EUR, GBP, CAD to your bank account. EukaPay supports most major cryptocurrencies like BTC, ETH, LTC, SOL, USDC, USDT.

Crypto pay-ins and crypto payouts are peers rather than separate products. The balance a customer payment settles into is the balance a contractor payout draws from, which means one integration and one reconciliation surface instead of two.

Get started with EukaPay

Create an account at

app.eukapay.com/signup

, complete verification, provide your legal business information, then generate an API key.

The full payout reference, including request and response schemas for all six endpoints, is at

docs.eukapay.com

. Generate a

sk_test_

key and run the flow against staging before you touch production. If you want the product context first, the

crypto payouts product page

covers what settles where.

Frequently asked questions

Does EukaPay have a bulk crypto payout endpoint?

No.

POST /crypto_payouts

creates one payout per call. For batch runs without writing a loop, EukaPay accepts a CSV upload in the merchant dashboard, which uses the same payout rails as the API.

How do I stop a retry from paying a recipient twice?

Send an

x-idempotent-key

header with every payout request, derived from something stable like the payroll run plus the recipient identifier. Keys expire after 24 hours, so reconcile anything older against

GET /crypto_payouts

.

What does the Reviewing payout status mean?

The payout is under compliance review before release. EukaPay is registered with FINTRAC and FinCEN, and a registered processor screens outbound transfers. A payout in

Reviewing

is progressing normally.

Which currencies and networks can I estimate payouts for?

POST /crypto_payouts/estimate

accepts

USDC

,

USDT

,

ETH

and

BTC

, on

Ethereum

,

Tron

and

Bitcoin

.

What hashing algorithm does EukaPay use for webhook signatures?

HMAC SHA-512, sent in the

x-eukapay-signature

header. Compute your comparison HMAC over the raw request body.

How often does EukaPay retry a failed webhook?

Every 20 minutes for up to two hours. Your handler should be idempotent, because it will receive repeats.

Can I pay out in fiat instead of crypto?

Yes. EukaPay settles to your bank account in USD, EUR, GBP, CAD, and Canadian merchants can also take Interac withdrawals.