Coingate

Accept crypto with CoinGate

Accept crypto with confidence using everything you need in one platform.

Crypto Payments

Payout Links API: Creating and Managing Links Over REST

A developer guide to the CoinGate Payout Links API: the four REST endpoints, the required and optional create fields, the draft branch you have to handle, and which lifecycle callbacks actually fire today.
Payout Links API: Creating and Managing Links Over REST
Last updated: August 25, 2026 12 min read
VB
Vilius Barbaravičius

Paying somebody new over our payouts API is two calls, not one. First you create a beneficiary, which needs their country, the currency, the network, an actual wallet address and their identity details. Only then can you create the send request that points at it. And before either call, somebody has to have collected all of that from them, which in practice means a form you built or an email thread a human is managing.

The Payout Links API deletes that first half. One POST, and the only field in it about the person you are paying is their email address. They fill in the rest themselves, on a page we host and screen.

This guide covers the merchant side: creating links, listing and reading them, cancelling them, and what the callbacks actually do today. If you have not met the feature yet, the overview explains the concept, and the dashboard walkthrough shows the manual version of what you are about to automate.


Wiring payouts into your own system? Get an API key from us.


Two surfaces, and only one of them is yours

Worth getting straight before you write anything.

The merchant API is REST and follows the same conventions as the rest of api/v2. You create links, list them, read one, cancel one. That is the whole surface, and it is the part you integrate with.

The collection flow is not yours. The recipient lands on a hosted page keyed by the link’s UUID, and that page talks to a separate public endpoint you never call. This is the half of the integration you are not writing. You do not build a wallet form, you do not validate addresses against networks, you do not collect identity data, and you do not run the screening. Your job starts at creating the link and ends at reacting to what happens to it.

Before your first call

Authentication is the standard api/v2 token header, and the base URL is the usual one:

Authorization: Token YOUR_API_TOKEN
https://api.coingate.com/api/v2/payout_links

Three things to know before you debug anything.

  • All four endpoints 404 if Payout Links is not enabled on your account. That is a feature check, not a routing mistake, and it looks identical to a typo. If every path returns 404 on a token that works elsewhere, ask us to switch it on rather than rereading your URLs.
  • IP allowlisting applies. Same as the rest of the API. A call from an address you have not allowed does not fail in an obvious way, it just fails.
  • Currency and platform IDs are not portable between environments. Sandbox and live hand out different numbers, and both sets are valid integers, so a hardcoded ID does not error, it just pays the wrong thing. Resolve them from /v2/currencies at startup per environment. On live, EUR is 11.

Rate limiting is inherited rather than special: 200 requests per minute, and over the limit you get back a body naming RateLimitExceeded.

Creating a link

One POST to /api/v2/payout_links. Five fields are required and four are optional.

{
  "ledger_account_id": 3198,
  "input_amount": "250.00",
  "input_currency_id": 11,
  "purpose": "Q1 affiliate payout",
  "expires_in_days": 7,
  "recipient_email": "alex@example.com",
  "send_email": true,
  "external_id": "affiliate_invoice_4421",
  "callback_url": "https://merchant.com/coingate/callbacks"
}

The required five:

  • ledger_account_id is the balance the link is funded from. It has to be yours and it has to cover the amount plus fees.
  • input_amount and input_currency_id are the display amount, what the recipient sees they are being paid. The currency is an ID, not an ISO string. This is a separate thing from the balance funding it, and conflating the two is the most common mistake on this endpoint.
  • purpose is a free-text reason, 4 to 191 characters, and the recipient reads it.
  • expires_in_days is one to seven.
  • recipient_email is required, and it is the field most integrations get wrong because it reads like a delivery convenience. It is not. The recipient gets a one-time code at that address and cannot change it, so the email is what binds the link to a person. Blank fails with a validation error, and so does a malformed one. The address is also checked for a deliverable domain and rejected if it is a disposable one, so a throwaway inbox will not get a link created.

The optional four:

  • send_email decides whether we email the link over. Leave it off and you get the URL back to distribute yourself. The one-time code still goes to recipient_email either way.
  • external_id is your own reference, up to 50 characters, echoed back on every response and every callback. Use it. It is what ties a link to the invoice, payout run or partner record in your database.
  • callback_url is where we POST lifecycle events. Leave it blank and delivery is skipped quietly rather than erroring.

What comes back: the link’s id and status, your external_id, the display amount and currency, and the money side broken out as balance_debit_amount, balance_debit_currency and service_fee_amount. That fee figure is the whole charge, a flat component plus a percentage plus a conversion fee, and the conversion part is included on every link whether or not an exchange ends up running, because the collect currency is unknown at create time. Amounts are decimal strings, not floats. Currencies are objects rather than ISO strings, carrying an id, title, kind and symbol, so read the symbol rather than assuming the shape.

The balance debit happens the moment that call returns. Build for it. From your system’s point of view, creating a link is a financial event and not a draft you can walk away from for free. You can always recover the money by cancelling, but the hold is real from the first response, and it covers the amount plus the service fee.

The draft branch you have to handle

This is the one piece of create logic that catches people.

If the business has multi-approval switched on for payouts, the link comes back as draft and its URL is not live yet. It flips to pending once it has been approved in the dashboard. Without multi-approval you get pending straight away and the URL works immediately.

The approval count is not the same on both surfaces, which is the part to design around. A link created in the dashboard needs one approver. A link created over the API needs two distinct approvers, and neither can be the creator, because an API key authenticates an application rather than a person, so we cannot tell who triggered the call. Build for two.

So a 200 on create does not mean a usable link. Read the status, always. If it is draft, wait for the pending callback before you display or send that URL anywhere. Funds are held either way, draft included, so a link sitting unapproved is still costing you the reservation.

One more state worth handling: if multi-approval is turned off while links are still sitting in draft, those links are cancelled automatically and refunded. Your record of them should expect that ending too.

Listing, reading, cancelling

Past create, the surface is small and predictable.

GET  /api/v2/payout_links?status=pending
GET  /api/v2/payout_links/8421
POST /api/v2/payout_links/8421/cancel

The list filters on status and pages with page, default 1, and per_page, default 20 and capped at 100. The response wraps the array in the usual envelope with current_page, per_page, total_records and total_pages alongside it. Everything is scoped to your own account, so reading a link belonging to another merchant returns a 404 rather than a permission error.

Cancel works while a link is draft or pending, and it refunds the amount and the service fee to the balance the link was funded from. Two things about it:

  • It is not idempotent. Cancelling an already cancelled link returns an error rather than quietly succeeding. If your retry logic assumes otherwise, you will log failures that are not failures.
  • It is unavailable once the link is processing. The recipient has confirmed and the payout is in the send pipeline, so there is nothing left for you to pull back.

There is no update operation. A link cannot be edited after creation, so a wrong detail means cancel and recreate.

Callbacks, and what actually fires today

This is the section to read twice, because the honest answer is narrower than you would expect and it changes how you build.

A link carries one status through its whole life, and there are seven of them: draft, pending, processing, completed, expired, failed, canceled. Six of them emit a callback. Only draft does not, because a draft that gets approved announces itself as pending.

payout_link.status.pending
payout_link.status.processing
payout_link.status.completed
payout_link.status.expired
payout_link.status.failed
payout_link.status.canceled

pending is the useful one. It fires when a draft is approved and the URL goes live, which is exactly the signal you need before sharing a link, and it also fires on creation when your app does not require approval. canceled tells you a link was pulled and the money is back.

So the lifecycle is genuinely event-driven and you can build it that way. Three of the six are worth acting on rather than just logging:

  • completed is the one you reconcile against. It is the only event that means money actually left, so it is what should close the record on your side. Everything before it is in flight or recoverable.
  • failed and expired both mean your money is back. Neither is an error you need to chase. Handle them as an ordinary ending rather than an alert, or you will page somebody at two in the morning because a contractor went on holiday.
  • Switch on the event name, and ignore what you do not recognise. The envelope is event, object and data throughout. A handler that logs an unknown event rather than throwing on it survives anything added later.

One shape note, because it will bite anyone who has integrated our other callbacks: payout link events come wrapped in that event / object / data envelope, while most of api/v2 posts flat JSON. Do not reuse a parser without checking. Signing and retry behaviour are inherited from the same callback machinery as everything else.

Reconciliation, and the one thing to key off

Match on external_id. Not on amounts, not on timing, not on a recipient’s email. It comes back on every response and every callback, and it is the only field you control.

One caveat that differs from send requests: uniqueness on external_id is not enforced here. There is no index behind it and no idempotency key on this endpoint, so a retried create can genuinely produce two funded links carrying the same reference. Dedupe before you POST, not after.

Make the handler idempotent on your side too, and assume any event can arrive twice. Then treat completed as the only state that means money left the building. Every other ending refunds you in full, service fee included, and the fee is only ever kept on a completed payout. If you are reconciling this against ledger movements more broadly, how to automate crypto payouts via API covers the wider payout surface.

A sensible integration order

  1. Resolve your currency IDs and your funding ledger_account_id for the environment you are in. Do not hardcode either.
  2. Create one link with a real external_id and a real callback_url. Confirm the balance moved by the amount plus the fee.
  3. Read the status. Handle the draft branch before you handle anything else, because it is the branch that silently ships broken links.
  4. Cancel that link. Watch the refund land and the canceled callback arrive.
  5. Then wire the handler, and keep a slow reconciliation sweep behind it. Callbacks are the mechanism, but any payment integration wants a way to notice a link that has sat in one state longer than it should.

Doing it in that order means you have seen money move and come back before a recipient is ever involved.

FAQ

Why is every endpoint returning 404?

Most likely Payout Links is not enabled on your account yet. The feature check returns 404 rather than a permission error, so it is indistinguishable from a bad path. Ask us before you keep debugging.

When is my balance debited?

At create time, the moment the POST returns, for the amount plus the service fee. It is a hold rather than a charge, and it is refunded in full on every outcome except a completed payout.

How do I avoid sharing a link before it works?

Read the status on create. If it is draft, the URL is inactive until someone approves it in the dashboard, and the pending callback is your signal that it went live.

How do I know the recipient was actually paid?

The completed event. It is the only signal that means the money actually left, and it is what to reconcile against using your external_id. Reading the link works too, but you should not need to.

Can I create a link for someone without an email address?

No. recipient_email is required even when you are distributing the URL yourself, because the one-time code has to go somewhere.

Is there an idempotency key?

No, and external_id is not enforced as unique either. Guard against duplicate creates on your side.

Wrapping up

Create with one POST and know the balance is held the instant it returns. Read the status, because a draft is not a shareable link. Key everything off your own external_id, dedupe before you create rather than after, and treat completed as the only state that means money truly moved.

Six events cover the whole lifecycle, so build against them rather than polling, and treat completed as the only one that means money moved. If you want to see what the person on the other end experiences, how to collect a Payout Link walks through their side, and the checks they pass through are the ones we covered in how to run a compliant crypto payment operation.

Building crypto payouts into your stack? Start with us, or read up on crypto payouts first.

VB
Vilius Barbaravičius Posted: August 25, 2026
Share article

Accept crypto with CoinGate

Accept crypto with confidence using everything you need in one platform.