Coingate

Accept crypto with CoinGate

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

Tutorials

Managing Payout Beneficiaries: Pay the Same People Repeatedly

Register a recipient once, attach one or more payout settings, and every payment after that is a lightweight send request. The full beneficiary model on the CoinGate payout API, including the three gates that hold a payout back and the environment-specific IDs that break sandbox integrations in production.
Managing Payout Beneficiaries: Pay the Same People Repeatedly
Last updated: August 13, 2026 12 min read
VB
Vilius Barbaravičius

If you pay someone once, you just send a payout. If you pay the same people over and over, affiliates every month, contractors every fortnight, sellers on a marketplace every week, you do not want to re-enter their details each time. You want to register them once and then reference them.

That is what beneficiaries are for. And there is a second reason they exist, which matters more than the convenience: since the EU Transfer of Funds Regulation took effect on 30 December 2024, every crypto transfer out of a regulated provider has to carry identified beneficiary data. Registering a recipient is how that obligation gets satisfied once instead of on every single payment.

This guide covers the whole beneficiary model on the CoinGate crypto payout API: registering a recipient, giving them one or more payout targets, and then paying them, again and again, without re-registering anyone. If you have read our guide on how to automate crypto payouts via API, this is the piece that adds the persistent-recipient layer on top. If you have not touched the API at all yet, start with the developer integration guide and come back.


Re-typing the same wallet addresses every payout run? Store them once instead.


Standard auth and base URL, with sandbox at https://api-sandbox.coingate.com/v2. Payout endpoints take form-encoded data, and nested fields use bracket notation like person[first_name]:

Authorization: Token YOUR_API_TOKEN

The three objects, and how they relate

Paying someone repeatedly involves three things that stack:

  • A beneficiary is who you are paying. It holds identity, not destination: person or business, name, email, country.
  • A payout setting is one way to pay them. A currency, a network, and a crypto address. A beneficiary can have several.
  • A send request is one actual payment, aimed at a payout setting.

The split matters because the send request attaches to the payout setting and reads the identity through it. So the wallet you are paying and the person you are paying are separate records, screened separately, and only the first two persist. Register once, and every future payment is a new send request pointing at a stored payout setting. No re-entry, no duplication.

Step one: register the beneficiary

curl --request POST "https://api.coingate.com/v2/beneficiaries" 
  -H "Authorization: Token YOUR_API_TOKEN" 
  -H "Content-Type: application/x-www-form-urlencoded" 
  --data-urlencode "beneficiary_type=person" 
  --data-urlencode "person[first_name]=Joe" 
  --data-urlencode "person[surname]=Doe" 
  --data-urlencode "email=joe@example.com" 
  --data-urlencode "country=LTU" 
  --data-urlencode "currency_id=8" 
  --data-urlencode "platform_id=5" 
  --data-urlencode "crypto_address=bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"

The required fields are beneficiary_type (person or business), email, country as an alpha-3 code, and then the destination trio: currency_id, platform_id and crypto_address. A person needs person[first_name] and person[surname], with person[date_of_birth] available and worth sending. A business needs business[company_name] and business[company_code], where the code can be a company registration number or an LEI.

One field that catches people out: country_state is required when the country has states, and it wants the state’s alpha-2 code. Paying anyone in the US without it will fail.

A useful convenience: creating a beneficiary also creates their first payout setting from that currency, platform and address. So the response already contains a beneficiary_payout_settings array, and each entry has the id you need to actually pay them:

{
  "id": 1,
  "beneficiary_type": "person",
  "email": "joe@example.com",
  "beneficiary_payout_settings": [
    {
      "id": 2,
      "crypto_address": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
      "platform": { "id": 5, "id_name": "bitcoin", "title": "Bitcoin" },
      "currency": { "id": 8, "symbol": "BTC" }
    }
  ]
}

If you are paying out on XRP, there is one more field. crypto_address_metadata carries the destination tag, and the docs are explicit that it is for XRP and is not used on the other networks currently supported. Send an XRP payout to an exchange deposit address without the tag and the exchange has no way to credit it, so capture it at registration.

Registration is also the first compliance gate. CoinGate validates the address format and checksum for the chain you named and screens it against sanctions and blacklists. Fail either and the payout setting is rejected there and then, which is much easier to handle at signup time than mid-payout-run.

Do not hardcode the currency and platform IDs

This is the mistake worth spending a paragraph on, because it is silent. currency_id and platform_id are not global constants. The docs put it plainly: the id values for currencies and platforms are different between environments. Look at how far apart they actually are:

ValueLiveSandbox
BTC81
EUR112
Bitcoin network56

Every one of those numbers is valid somewhere, which is exactly the problem. A sandbox integration that works perfectly and then goes live with the same constants does not throw an error. In live, currency_id=1 is US dollars and platform_id=6 is Litecoin, so you get a plausible-looking request that means something else entirely.

So resolve them at runtime, per environment, and cache them by symbol. GET /v2/currencies?symbol=BTC returns the currency with its enabled platforms nested inside, which gets you both IDs in one call and survives CoinGate adding a network.

Step two, when needed: add more payout settings

Say the same contractor wants USDC sometimes and BTC other times, which comes up constantly once you are paying freelancers and remote teams in crypto. Add a second payout setting to the beneficiary you already have:

curl --request POST "https://api.coingate.com/v2/beneficiary_payout_settings" 
  -H "Authorization: Token YOUR_API_TOKEN" 
  -H "Content-Type: application/x-www-form-urlencoded" 
  --data-urlencode "beneficiary_id=1" 
  --data-urlencode "currency_id=USDC_CURRENCY_ID" 
  --data-urlencode "platform_id=BASE_PLATFORM_ID" 
  --data-urlencode "crypto_address=0x..."

The returned id is the beneficiary_payout_setting_id you send to. One beneficiary, many ways to pay them, each a stable reference. Note the placeholders in that sample rather than numbers, for the reason in the section above.

Step three: send the payment

Now the actual payout. A send request references the balance you are paying from, the payout setting, and the amount:

curl --request POST "https://api.coingate.com/v2/send_requests" 
  -H "Authorization: Token YOUR_API_TOKEN" 
  -H "Content-Type: application/x-www-form-urlencoded" 
  --data-urlencode "ledger_account_id=01JNQWKKJ6WXN8BZT1Y66B6G9H" 
  --data-urlencode "beneficiary_payout_setting_id=2" 
  --data-urlencode "amount=100.0" 
  --data-urlencode "amount_currency_id=11" 
  --data-urlencode "purpose=July affiliate payout" 
  --data-urlencode "external_id=payout-2026-07-joe" 
  --data-urlencode "callback_url=https://yourapp.com/coingate/payout"

The ledger_account_id is the balance the money leaves from, which you read from the ledger accounts endpoint. purpose is required, not decorative, because it feeds the transfer and AML record that has to accompany the payment.

Here is the powerful part. amount_currency_id is the currency you express the amount in, and it does not have to match either what the beneficiary receives or what leaves your balance. You can say “pay 100 EUR worth” out of a Bitcoin balance to a recipient who takes USDC, and CoinGate handles both conversions.

Set external_id to your own reference. It has to be unique and caps at 50 characters, which is what makes it usable as an idempotency key: check for it before resending, and a retried payout does not pay someone twice. It also comes back on the callback, so reconciling against your own records is a direct lookup.

Three things that can hold a payout back

A send request does not always fire immediately, and there are three separate reasons for that. Handle all of them.

Conversion confirmation. If the currency being sent differs from the currency debited from your balance, the response comes back with an actions_required object holding confirm and cancel URLs. You confirm with a PATCH, and the locked rate expires after one minute, so this is not a step to leave for a human:

curl --request PATCH "https://api.coingate.com/v2/send_requests/{id}/confirm" 
  -H "Authorization: Token YOUR_API_TOKEN"

The matching /cancel is your clean exit. Before you confirm, you can walk away and nothing has moved. After you confirm, you cannot, because the rate is locked and the exchange may already be running.

Two-factor authentication. If 2FA is enabled on the API app, the send request is created as a draft and has to be confirmed by hand in the dashboard under Payouts, Outgoing Payments. A draft waiting on 2FA sits for up to 30 days before expiring. For fully automated payout runs you will typically run with 2FA off on the payout app, but make that call deliberately rather than discovering it in production.

Compliance screening. This is the one people forget, because the address already passed a check at registration. That check was the light one. Every send triggers full analytics on the destination wallet plus AML screening on the transaction itself, which is why canceled exists as a terminal status for compliance reasons. A stored payout setting is a saved address, not a standing approval.

Tracking the payout

Set callback_url and CoinGate posts to it on every status change. A send request moves through draft (awaiting 2FA), in_progress (compliance checks running, balance deducted), processing (on the network) and completed. The unhappy endings are expired (conversion not confirmed inside the minute, or 2FA not confirmed inside 30 days), failed (rejected by the network) and canceled (stopped for compliance). Mark a payout done on completed, and nowhere earlier: in_progress means your balance moved, not that the recipient has anything.

The callback is also where the three-currency model becomes legible, and it is worth reading once before you write your reconciliation code. It carries input_amount and input_currency for what you asked for, sending_amount and sending_currency for what the beneficiary actually receives, and balance_debit_amount and balance_debit_currency for what left your ledger. The rates that connect them come through as input_to_sending_rate and sending_to_balance_debit_rate, with a fees object holding the service fee and, where a conversion ran, the conversion fee.

Two more fields earn their keep. blockchain_transactions gives you the transaction IDs and confirmation counts, which is what you show a recipient asking where their money is. And requestable_type tells you what created the send request, because batch payouts and payout links produce send requests too, so a callback handler that assumes your own API call made every one of them will eventually be wrong.

Paying many at once

This model scales to a batch without anything new. Register everyone once, then loop:

for payout in this_months_affiliates:
    send_request = create_send_request(
        ledger_account_id=FUNDING_ACCOUNT,
        beneficiary_payout_setting_id=payout["setting_id"],
        amount=payout["amount"],
        amount_currency_id=EUR_ID,          # resolved at startup, not hardcoded
        purpose=f"Affiliate payout {payout['period']}",
        external_id=f"aff-{payout['id']}-{payout['period']}",
    )
    if send_request.get("actions_required"):
        confirm(send_request["actions_required"]["confirm"])

Because the beneficiaries already exist, a monthly payout run is just a list of send requests against stored settings. Confirm conversions inside the loop rather than in a second pass, or the one-minute window will expire on the requests at the front of your queue. For the wider strategy of running payouts at this scale, see our guide to mass crypto payouts for affiliate networks.

To see which currencies and networks you can pay out to at all, GET /v2/send_requests/supported-currencies lists them and needs no auth, so you can call it before you have even generated a token. It is a shorter list than the currencies you can accept payments in, so check it before promising a recipient a particular coin.

Frequently asked questions

What is the difference between a beneficiary and a payout setting?

A beneficiary is the recipient, a person or a business, and holds their identity. A payout setting is one way to pay them: a specific currency, network and crypto address. A beneficiary can have several payout settings, and you send payments to a payout setting rather than to the beneficiary directly.

Do I have to register a recipient before every payout?

No, and that is the whole point of beneficiaries. Register once, and every future payment is a new send request referencing the stored beneficiary_payout_setting_id. Creating a beneficiary even auto-creates their first payout setting for you.

Can I pay in one currency and have the recipient receive another?

Yes. On a send request, amount_currency_id sets the currency you express the amount in, and it can differ from both what the beneficiary receives and what is debited from your balance. CoinGate converts, applying a service fee and a conversion fee.

Why is my payout stuck in draft?

Two-factor authentication is enabled on the API app, so the send request needs manual confirmation in the dashboard under Payouts, Outgoing Payments. It stays a draft for up to 30 days and then expires.

Why did my sandbox integration break in production?

Almost certainly hardcoded IDs. currency_id and platform_id differ between sandbox and live, and both sets are valid numbers, so nothing errors. Resolve them per environment from /v2/currencies at startup.

How do I stop a retry from paying someone twice?

Set a unique external_id on every send request, up to 50 characters, and check for it before resending. It works as an idempotency key and also comes back on the status callback so you can match a payout to your own records.

Wrapping up

Repeat payouts come down to separating who you pay from how you pay them. Register a beneficiary once, attach one or more payout settings, and every payment after that is a lightweight send request against a stored setting. Handle the three gates, confirm conversions inside a minute, decide about 2FA on purpose, and expect compliance to look at every send rather than just the first. Track to completed through the callback, lean on external_id for idempotency, and resolve your IDs per environment. Do that, and paying a hundred affiliates next month is a loop, not a data-entry job.

Building recurring crypto payouts into your platform? Start with us.

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

Accept crypto with CoinGate

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