Coinland PayDocs

Payouts and refunds

Sending money to a Coinland customer, refunding a payment, and the separate key class both require

Coinland Pay moves money in two directions. Taking a payment is a checkout session the customer confirms. Sending money is a payout: you name a recipient and an amount, and coin leaves your business wallet for their Coinland balance.

A refund is the same operation aimed at a payment you already took, and it answers with the same object. The difference is that a refund takes its recipient and its currency from the payment record rather than from you, and that it is free.

A payout is synchronous. 201 means the money is gone

There is no pending state, no approval step and nothing to poll. By the time you read the response, your wallet has been debited and the recipient has been credited. Treat a timeout on this endpoint the way you would treat a timeout on a wire transfer: resend the identical request with the same reference_id and let idempotency tell you what happened.

The payout key

Payouts need their own key class. Your checkout key cannot call them, and a payout key cannot create sessions.

clpay_live_<64 hex>      the CHECKOUT class -- sessions, payments, receipts
clpay_payout_<64 hex>    the PAYOUT class   -- payouts, refunds

Mint one in your business console, choosing the payout class. As with a checkout key, it is shown once and stored only as a hash, and you may hold up to 5 enabled keys of each class.

A payout key can also read GET /payments, GET /payments/{id} and GET /me, because reconciling what you sent against what you took needs both sides. It can do nothing else on the checkout surface.

Using the wrong class is PAY_WRONG_KEY_KIND (403) rather than an authentication failure -- the key is valid, it is simply the other one you want.

Why the split is worth the second credential

A checkout key is the one that ends up in more places: in the service that creates sessions, in a staging environment, in a deployment pipeline. Splitting the classes means the widely-copied key cannot move money out of your wallet, and revoking it does not stop you taking payments.

Before your first payout

Payouts are switched off until Coinland turns them on for your business, and turning them on means two things, not one:

  1. Payouts are enabled for your account.
  2. Both of your USD limits are set -- a maximum per payout, and a ceiling on the trailing 24 hours.

Limits are not optional and there is no unlimited setting. An account with payouts enabled but no limits configured is not armed, and it refuses every payout: money leaving is never open-ended by omission.

Until all of that is in place, every write answers MERCHANT_PAYOUTS_DISABLED (403). The same code covers a suspended business and a rail Coinland has paused globally, because the remedy is the same one: talk to us. It is not a code to retry through.

Paying a customer who has paid you

Every payment object carries a payer_id: an opaque handle for the customer who paid, scoped to your business.

{
  "id": "b92e4d17-6c38-4a05-9f2b-1e7d3c8a5049",
  "reference_id": "order-10492",
  "currency": "usdt",
  "amount": "24.90",
  "payer_id": "7c1e5b90-3f42-4a86-9d05-2b8e4c1f6a37",
  "paid_at": "2026-08-11T08:12:44.000Z"
}

That handle is stable: the same customer paying you again next month carries the same one. It is not a Coinland user id, it means nothing to any other business, and it reveals nothing about the person. It is also the entire recipient argument for a payout -- store it against your own customer record and you never need an email address to pay them.

Handles exist for every payment ever taken on this rail, including ones taken before payouts existed.

curl -X POST https://my.coinlandexchange.com/api/pay/v1/payouts \
  -H "Authorization: Bearer $COINLAND_PAY_PAYOUT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "reference_id": "payout-2291",
    "currency": "usdt",
    "amount": "25.00",
    "payer_id": "7c1e5b90-3f42-4a86-9d05-2b8e4c1f6a37",
    "comment": "Cashback for order 10492"
  }'
201 Created
{
  "id": "9e3c7a41-0b52-4f18-8d6a-3c7e1f9b40d5",
  "reference_id": "payout-2291",
  "kind": "payout",
  "status": "completed",
  "currency": "usdt",
  "amount": "25.00",
  "debited_amount": "25.125",
  "fee_amount": "0.125",
  "fee_percent": "0.5",
  "usd_value": "25.00",
  "payer_id": "7c1e5b90-3f42-4a86-9d05-2b8e4c1f6a37",
  "payment_id": null,
  "comment": "Cashback for order 10492",
  "created_at": "2026-08-11T09:31:04.000Z",
  "settled_at": "2026-08-11T09:31:04.000Z"
}

The currency does not have to be one you accept at checkout. Any enabled coin your business wallet holds can be paid out, which matters if you convert your takings into a stablecoin. Toman is refused: this rail is crypto only, in both directions.

Paying someone by email

For a recipient who has never paid you, there is no handle to use, so there is a second path: look the address up, show a human the name that comes back, and spend the resulting token.

This path is off by default. Coinland enables it per business, and where it is not enabled the lookup answers MERCHANT_PAYOUTS_DISABLED -- the same answer an unpayable address gets, so the endpoint cannot be used to test whether the feature is on either.

Look up the address.

curl -X POST https://my.coinlandexchange.com/api/pay/v1/payouts/recipients/lookup \
  -H "Authorization: Bearer $COINLAND_PAY_PAYOUT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "customer@example.com"}'
200 OK
{
  "recipient_token": "v1.eyJtZXJjaGFudElkIjo0Miwi….9f3c1d60ab72",
  "masked_name": "A**** B****",
  "expires_at": "2026-08-11T09:41:22.000Z"
}

The token is valid for ten minutes, works only for the business that requested it, and carries the resolved recipient inside its own signature. The email address never travels on the payout request.

Show masked_name to a person and have them confirm it.

The mask is enough to recognise someone you already meant to pay and not enough to identify a stranger. That is the whole point of it: it catches a mistyped address before the money moves, without turning our customer directory into something you can read.

Create the payout, echoing the masked name back verbatim.

curl -X POST https://my.coinlandexchange.com/api/pay/v1/payouts \
  -H "Authorization: Bearer $COINLAND_PAY_PAYOUT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "reference_id": "payout-2292",
    "currency": "usdt",
    "amount": "40.50",
    "recipient_token": "v1.eyJtZXJjaGFudElkIjo0Miwi….9f3c1d60ab72",
    "recipient_confirm": "A**** B****"
  }'

recipient_confirm must match the masked_name character for character. A mismatch is refused, which is what makes step 2 a real check rather than a screen someone clicks through.

The response carries a payer_id for the recipient, so the next payout to the same person can skip all of this and use the handle.

Every recipient failure is one code

An address with no Coinland account, a disabled account, an account that has not finished identity verification, an expired token, a token minted for another business, a confirm that does not match -- all of them answer PAY_RECIPIENT_INVALID (422). The endpoint deliberately will not tell you which, because a lookup that distinguished them would be a way to find out who banks with us.

Lookups are also metered per business. Exceeding the budget is PAY_LOOKUP_THROTTLED (429), which affects only lookups -- payouts by handle keep working.

Refunds

A refund returns coin to the customer who paid you. Address it by payment id or by receipt number, and send only an amount:

curl -X POST https://my.coinlandexchange.com/api/pay/v1/payments/CLP-10492-8F3A/refund \
  -H "Authorization: Bearer $COINLAND_PAY_PAYOUT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "reference_id": "refund-10492-1",
    "amount": "10.00",
    "comment": "One item returned"
  }'

There is no recipient field and no currency field, on purpose: both come from the payment, so a refund can only travel back the way the money came.

  • Partial refunds are allowed, and you can issue several against one payment.
  • The cap is cumulative, at the payment's charged_amount. A refund that would take the running total past it is PAY_REFUND_EXCEEDS_PAYMENT (422), and the error reports how much has already been refunded.
  • Refunds are free. fee_amount is "0" and debited_amount equals amount, so a refund costs you the coin and nothing more.

Coinland keeps the fee it charged on the original payment and takes no new fee on the way back. One flow is charged once: you are not billed twice for a sale that unwound, and the original fee is not returned either.

The result reads as a payout with kind: "refund" and the refunded payment's id in payment_id.

Fees

Payouts are charged at the same tier rate as your payments, and you always bear it. The recipient receives exactly the amount you named -- there is no version of this where they are shown one figure and credited another.

recipient receives   amount
your wallet pays     debited_amount  ==  amount + fee_amount

debited_amount - amount == fee_amount holds exactly, on every payout. Reconcile your wallet against debited_amount and what you promised the recipient against amount; using one figure for both is what makes books drift.

Payout volume counts toward the 30-day USD volume that decides your fee tier, so money you send helps you reach a better rate. The payment count thresholds still count payments only.

Limits

Two limits apply, both in USD, and both are set by Coinland rather than by you.

LimitApplies toOver it
Per-payout maximumOne payoutPAY_PAYOUT_LIMIT (422), details.limit is per-payout
Trailing 24-hour ceilingThe sum of the last 24 hoursPAY_PAYOUT_LIMIT (422), details.limit is daily

A refund is exempt from the per-payout maximum -- a limit lower than a payment must not make that payment unrefundable -- but it still counts toward the daily ceiling.

Both are measured in USD, which means a payout can only be sent if the coin can be valued right now. If no live rate is available the payout is refused with PAY_RATE_UNAVAILABLE (503) rather than sent unmetered. The request is fine; retry it.

If your business wallet does not cover amount + fee, the answer is INSUFFICIENT_BALANCE (422). Move funds from your spot balance into the business wallet in your console and retry.

Idempotency

reference_id is your idempotency key, and it works exactly as it does on sessions:

  • The same reference_id with an identical payload returns the original payout, with 201. No second transfer happens.
  • The same reference_id with a different payload is PAY_DUPLICATE_REFERENCE (409).
// A timeout tells you nothing about whether the payout settled. Resend the
// identical request -- never a fresh reference_id, which is how one payout
// becomes two.
async function payOut(body) {
  for (let attempt = 0; attempt < 3; attempt++) {
    try {
      return await post("/api/pay/v1/payouts", body);
    } catch (err) {
      if (!isTimeout(err)) throw err;
      await sleep(2 ** attempt * 1000);
    }
  }
  // Still unsure? Read it back by your own id.
  return get(`/api/pay/v1/payouts/${body.reference_id}`);
}

GET /payouts/{id} accepts either the payout id or your reference_id, which makes that last line a reliable way to settle the question after a network failure.

Knowing it happened

Since a payout settles inside the request, the response is already authoritative and you rarely need anything else. A payout.completed webhook is delivered as well, for the case where the payout was created somewhere other than your own code -- from the business console, for instance:

payout.completed
{
  "event_id": "a17b3e50-9d24-4c81-b6f3-5e0a2c7d1948",
  "type": "payout.completed",
  "payout_id": "9e3c7a41-0b52-4f18-8d6a-3c7e1f9b40d5",
  "reference_id": "payout-2291",
  "kind": "payout",
  "status": "completed"
}

Same signature scheme, same five-minute window, same deduplication rule, and the same warning: the payload is a hint. It carries no amounts, and anything you act on should come from GET /payouts/{id}. See webhooks for the verification code.

Note that kind distinguishes a payout from a refund on this event, so a handler that credits a customer's loyalty balance on payouts should branch on it rather than assume.

Errors

CodeHTTPMeaningWhat to do
MERCHANT_PAYOUTS_DISABLED403Payouts are not armed for your business, or the email path is not enabled for youContact Coinland. Not retryable
PAY_WRONG_KEY_KIND403A checkout key on a payout route, or the reverseUse the key of the other class
PAY_RECIPIENT_INVALID422The recipient cannot be paid, or you sent both recipient paths, or neitherCheck the handle or redo the lookup. One code covers every reason on purpose
PAY_PAYOUT_LIMIT422Over the per-payout maximum or the 24-hour ceilingRead details.limit. Split the payout, wait out the window, or ask Coinland to raise it
PAY_REFUND_EXCEEDS_PAYMENT422Cumulative refunds would exceed the payment's charged_amountRefund the remainder instead; details.already_refunded says how much is gone
PAY_LOOKUP_THROTTLED429The lookup budget for the minute or the day is spentBack off. Payouts by handle are unaffected
INSUFFICIENT_BALANCE422The business wallet does not cover amount + feeTop the wallet up from your spot balance and retry
PAY_RATE_UNAVAILABLE503The coin has no live USD rate, so the limits cannot be enforcedRetry; the request itself is fine
PAY_DUPLICATE_REFERENCE409The same reference_id with a different payloadResend the original payload, or use a new id for a genuinely new payout

The full catalog, including the codes shared with checkout, is on the errors page.

Checklist

  • Payout key minted separately, stored separately from the checkout key.
  • payer_id persisted against your own customer records at payment time.
  • reference_id derived from something stable in your system, never a random value per attempt.
  • Timeouts retried with the identical request, never a fresh id.
  • amount and debited_amount both stored, as decimal strings.
  • A human confirms masked_name before any email-path payout.
  • PAY_PAYOUT_LIMIT handled as a business condition, not a bug -- someone should be told.

On this page