Skip to content

Paying out

A payment sends money from an organization’s balance to someone else. Every payment waits for a person to approve it before any money moves.

Payments travel on one of three rails. A rail is the kind of network the money moves over:

  • fiat: a bank transfer.
  • crypto: a transfer to a wallet on a blockchain.
  • ele_id: a transfer to another Ele account, named by its Ele ID.
  • Save recipients, called payment contacts, with the bank account, wallet or Ele ID they are paid at, called the destination.
  • Get a quote: what the payment costs and what the recipient receives.
  • Create a payment from the quote.
  • Have a person approve it.
  • Follow it until it succeeds or fails.

1. Find out which bank details a country needs

Section titled “1. Find out which bank details a country needs”

Each country and currency needs different bank details: an IBAN in one place, a sort code or an IFSC code in another. Ask getPaymentDestinationRequirements:

Terminal window
curl -s "$ELE_API_BASE/api/v1/payments/destination-requirements?country=SG&currency=SGD" \
-H "Authorization: Bearer $ELE_TOKEN"
[
{ "name": "accountNumber", "requirement": "required", "label": "Account Number", "predefined_options": null, "choices": null },
{ "name": "bankName", "requirement": "required", "label": "Bank Name", "predefined_options": null, "choices": null },
{ "name": "swiftCode", "requirement": "required", "label": "SWIFT Code", "predefined_options": null, "choices": null }
]

Build your form from this list. Show each field’s label, and require the ones whose requirement is required.

The names come back in camelCase, such as accountNumber. The destination you send in step 2 uses snake_case for the same fields, such as account_number. Convert each name before you send it.

When a field has choices, the person must pick one. For the bank name, each choice’s id is the bank’s ID: send it as bank_id on the destination. bank_details.bank_name is still required alongside it.

The list covers bank details only. Some routes also need the recipient’s street address, which the list never includes. Collect it and send it as street_address on the destination.

To see which countries and currencies Ele can pay out to, read fiat_supported_countries and fiat_supported_currencies from getPaymentsConfig.

createPaymentContact saves a recipient together with their destination. For a bank account, set rail to fiat:

Terminal window
curl -s "$ELE_API_BASE/api/v1/organizations/$ORGANIZATION_ID/payment-contacts" \
-H "Authorization: Bearer $ELE_TOKEN" \
-H "Idempotency-Key: $KEY" \
-H 'Content-Type: application/json' \
-d '{
"name": "Lim Supplies",
"tag": "vendor",
"destination": {
"rail": "fiat",
"account_type": "business",
"account_holder_name": "Lim Supplies Pte Ltd",
"currency": "SGD",
"country_alpha2": "SG",
"bank_details": {
"bank_name": "DBS Bank Ltd",
"account_number": "0123456789",
"swift_code": "DBSSSGSG"
}
}
}'

For a crypto wallet, send rail set to crypto with the wallet address and a coin_id from payment_coins in getPaymentsConfig. For another Ele account, send rail set to ele_id and their ele_id.

Ele registers a bank account with its payout partner while it saves it. If the partner refuses the details, the call fails with a message saying why. The destination’s status is active once it is registered; only an active destination can be paid.

If the account already belongs to another contact in the organization, Ele answers 409 duplicate_payment_contact, and details.existing_payment_contact_id names that contact. Pay that one instead.

To change where a contact is paid, an owner or admin calls setPaymentContactDestination. A new destination needs approval again.

Ele refuses to replace bank details with 422 payments_awaiting_approval while a payment to the old details is still awaiting approval. details.fiat_outgoing_ids lists those payments by ID; approve, reject or cancel them first.

A fiat contact’s destination includes the payee’s street_address, so an edit can resend it rather than ask for it again. The read uses country_code where the input takes country_alpha2: rename it before you send it.

By default, a new contact must be approved before anyone can pay it. The contact’s approval_request holds the request’s id and status. An owner or admin approves it like any other request, which needs step-up:

Terminal window
curl -s -X POST "$ELE_API_BASE/api/v1/approvals/$APPROVAL_ID/approve" \
-H "Authorization: Bearer $ELE_TOKEN" \
-H "Idempotency-Key: $KEY"

See Step-up and approvals. If the organization has turned this check off, the contact is approved as soon as it is saved.

Bank and crypto payments start with a quote from preflightPayment. Give the contact and exactly one amount: landing for what the recipient receives, or deducting for what leaves the organization’s balance.

Terminal window
curl -s "$ELE_API_BASE/api/v1/payments/preflight" \
-H "Authorization: Bearer $ELE_TOKEN" \
-H "Idempotency-Key: $KEY" \
-H 'Content-Type: application/json' \
-d "{\"payment_contact_id\": \"$CONTACT_ID\",
\"landing\": {\"amount\": \"1000.00\", \"currency\": \"SGD\"}}"

A bank payment is paid from the USD balance unless you send source_currency, such as "EUR", to pay from another balance. That works only where Ele has a route from that currency to the recipient’s currency and country.

The quote gives deducting, landing, the fees in fee_components and total_fee, the exchange rates in fx, and expires_at. For a bank payment, payment_rail is the bank network the quote was priced for: local or swift. A crypto quote has a travel_rule object; see Crypto recipients below.

Show the person the amounts from the quote. Do not work them out yourself; see Money.

A quote expires. Use it before expires_at. Today a bank quote lasts 15 minutes and a crypto quote 5 minutes, but read expires_at rather than relying on these numbers. You can read a quote again with getQuote.

A contact paid by Ele ID has nothing to quote: Ele refuses the preflight with not_quotable. Go to Paying an Ele account.

executePayment turns the quote into a payment. A bank payment also needs:

  • payout_reason: a reason code, such as PYR001. The valid codes are in payout_reasons from getPaymentsConfig.
  • statement_description: what the recipient’s bank statement shows, at most 35 characters.
Terminal window
curl -s "$ELE_API_BASE/api/v1/payments/execute" \
-H "Authorization: Bearer $ELE_TOKEN" \
-H "Idempotency-Key: $KEY" \
-H 'Content-Type: application/json' \
-d "{\"quote_id\": \"$QUOTE_ID\",
\"payout_reason\": \"PYR001\",
\"statement_description\": \"Invoice 1042\"}"

Send an Idempotency-Key, created once for this payment and kept for every retry; Idempotency explains how to retry safely.

A quote pays once. Using it a second time, under another key, is refused.

The response is the payment, with status set to awaiting_approval and an approval_request_id. No money has moved yet.

An owner or admin approves the request in approval_request_id with approveRequest, which needs step-up. A person must complete the step-up: your server passes on their code, and never answers it by itself. See Step-up and approvals.

When the request is approved, Ele checks the balance again and sends the money.

Approval can take hours, longer than a quote lasts. If a bank payment’s quote has expired by the time the person approves, Ele refuses the approval with 422 unprocessable_entity and the request stays pending. That error type does not say why, so check first: before you ask the person to approve, read the payment’s quote with getQuote (its ID is the payment’s quote_id). If expires_at has passed, refresh the price with requotePayment, show the person the new quote, and then ask them to approve:

Terminal window
curl -s -X POST "$ELE_API_BASE/api/v1/payments/$PAYMENT_ID/requote" \
-H "Authorization: Bearer $ELE_TOKEN" \
-H "Idempotency-Key: $KEY"

requotePayment works only while the payment is awaiting_approval, and only for bank payments.

Poll getPayment until the payment reaches a final state:

Terminal window
curl -s "$ELE_API_BASE/api/v1/payments/$PAYMENT_ID" \
-H "Authorization: Bearer $ELE_TOKEN"

A payment can be read by the person who created it and by the organization’s approvers. Its transaction_id finds it in the transactions list.

A payment to another Ele account has no quote and no fee. It is in US dollars, and the recipient receives exactly the amount. Create it with createPayment:

Terminal window
curl -s "$ELE_API_BASE/api/v1/payments" \
-H "Authorization: Bearer $ELE_TOKEN" \
-H "Idempotency-Key: $KEY" \
-H 'Content-Type: application/json' \
-d "{\"payment_contact_id\": \"$CONTACT_ID\",
\"amount\": {\"amount\": \"250.00\", \"currency\": \"USD\"},
\"payout_reason\": \"PYR001\",
\"statement_description\": \"September fees\"}"

It waits for approval like any other payment. createPayment accepts only contacts paid by Ele ID; for a bank or crypto contact it answers unsupported_rail.

A crypto quote is asked for the same way. A coupled amount’s currency is the coin’s symbol, as in the contact’s coin, such as USDC:

Terminal window
curl -s "$ELE_API_BASE/api/v1/payments/preflight" \
-H "Authorization: Bearer $ELE_TOKEN" \
-H "Idempotency-Key: $KEY" \
-H 'Content-Type: application/json' \
-d "{\"payment_contact_id\": \"$CONTACT_ID\",
\"landing\": {\"amount\": \"500.00\", \"currency\": \"USDC\"}}"

Send crypto amounts with at most 2 decimal places. source_currency applies to bank payments only.

For some transfers, rules on crypto require Ele to know who owns the receiving wallet. When a crypto quote’s travel_rule.required is true and travel_rule.complete is false, record the owner first with updateCryptoDestinationTravelRule. The wallet’s ID is the quote’s destination_id. Set mark_as_own_account if the wallet is the organization’s own, or give the recipient’s counterparty details:

Terminal window
curl -s -X PATCH "$ELE_API_BASE/api/v1/crypto-destinations/$DESTINATION_ID/travel-rule" \
-H "Authorization: Bearer $ELE_TOKEN" \
-H "Idempotency-Key: $KEY" \
-H 'Content-Type: application/json' \
-d '{
"counterparty": {
"person_type": "legal",
"name": "Lim Supplies Pte Ltd",
"street_address": {"address_line1": "8 Marina Blvd", "city": "Singapore", "country_alpha2": "SG"}
}
}'

Until the details are complete, executePayment refuses the payment.

A payment’s status:

status Meaning What you do
awaiting_approval Waiting for a person to approve or reject it. Ask an approver. Poll slowly.
submitted Approved and handed to the bank or network. Poll.
processing Being processed. Poll.
on_hold Held for a check. It continues once the check clears. Poll. Contact Ele support if it stays held.
cancelling A crypto cancellation was requested and is being confirmed. Poll.
succeeded The money was delivered. It can still be reversed later. Check now and then for a few days.
failed It did not go through. Show the person. Fix the cause and make a new payment.
cancelled It was rejected or withdrawn before it was sent. Nothing.
reversed It was returned after being sent, for example by the receiving bank. Show the person.

failed, cancelled and reversed are final. status_detail holds Ele’s internal state, for diagnosis only: do not branch on it.

Call Needs
createPaymentContact, preflightPayment, executePayment, createPayment Neither.
Approving a contact or a payment (approveRequest) Step-up, by an owner or admin.
Rejecting (rejectRequest) or withdrawing (cancelRequest) a request Neither.
  • Before approval: reject the request with rejectRequest, or withdraw it with cancelRequest. The payment becomes cancelled.
  • After approval: a bank payment cannot be cancelled through the API. A crypto payment can be stopped with cancelPayment only while it reports cancellable: true. It then becomes cancelling, and cancelled once the custody partner confirms. If the partner sends it first, it becomes succeeded.
Terminal window
curl -s -X POST "$ELE_API_BASE/api/v1/payments/$PAYMENT_ID/cancel" \
-H "Authorization: Bearer $ELE_TOKEN" \
-H "Idempotency-Key: $KEY"

Several refusals from executePayment come back as 422 unprocessable_entity, with the reason only in message: an expired quote, a missing or unknown payout reason, a statement description over 35 characters, an unapproved contact, a balance too small, or missing travel-rule details. Show the message to the person.

Status type When What to do
404 quote_not_found The quote ID is unknown. Take a new quote.
409 quote_already_used The quote already paid for another payment. Take a new quote.
409 duplicate_payment_contact The account belongs to another contact. Use the contact in details.existing_payment_contact_id.
409 idempotency_key_reused createPayment: the key already made a payment with other details. Use a new key for a new payment.
409 not_cancellable cancelPayment on a payment that cannot be cancelled now. Nothing to do; follow the payment.
422 not_quotable A preflight for a contact paid by Ele ID. Use createPayment.
422 unsupported_rail createPayment for a bank or crypto contact. Use the preflight and executePayment.
422 payment_contact_not_approved createPayment for a contact not yet approved. Have the contact approved.
422 insufficient_funds createPayment: the balance does not cover the payment. Add money, or pay less.
422 unknown_payout_reason createPayment: the payout_reason code is not one Ele knows. Use a code from getPaymentsConfig.
422 unprocessable_entity On approval: a bank payment’s quote has expired. Call requotePayment, then approve again.
422 self_approval_not_allowed The organization needs a second person, and the creator tried to approve. Ask another approver.
422 payout_beneficiary_incomplete On approval: the recipient’s bank details are missing something. Fix the recipient’s details, then make a new payment.
422 payout_requote_required On approval: the payment cannot go out at this price. Call requotePayment, then approve again.

For other types, see Errors.