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.
What you can do
Section titled “What you can do”- 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.
The sequence
Section titled “The sequence”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:
curl -s "$ELE_API_BASE/api/v1/payments/destination-requirements?country=SG¤cy=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.
2. Save the recipient
Section titled “2. Save the recipient”createPaymentContact saves a
recipient together with their destination. For a bank account, set rail to
fiat:
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.
3. Have the recipient approved
Section titled “3. Have the recipient approved”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:
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.
4. Get a quote
Section titled “4. Get a quote”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.
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.
5. Create the payment
Section titled “5. Create the payment”executePayment turns the quote into
a payment. A bank payment also needs:
payout_reason: a reason code, such asPYR001. The valid codes are inpayout_reasonsfromgetPaymentsConfig.statement_description: what the recipient’s bank statement shows, at most 35 characters.
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.
6. Have a person approve it
Section titled “6. Have a person approve it”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:
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.
7. Follow the payment
Section titled “7. Follow the payment”Poll getPayment until the payment
reaches a final state:
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.
Paying an Ele account
Section titled “Paying an Ele account”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:
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.
Crypto recipients
Section titled “Crypto recipients”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:
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:
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.
States
Section titled “States”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.
Step-up and approvals
Section titled “Step-up and approvals”| 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. |
Cancelling a payment
Section titled “Cancelling a payment”- Before approval: reject the request with
rejectRequest, or withdraw it withcancelRequest. The payment becomescancelled. - After approval: a bank payment cannot be cancelled through the API. A
crypto payment can be stopped with
cancelPaymentonly while it reportscancellable: true. It then becomescancelling, andcancelledonce the custody partner confirms. If the partner sends it first, it becomessucceeded.
curl -s -X POST "$ELE_API_BASE/api/v1/payments/$PAYMENT_ID/cancel" \ -H "Authorization: Bearer $ELE_TOKEN" \ -H "Idempotency-Key: $KEY"What to poll
Section titled “What to poll”getPaymentfor each payment in progress, as Staying in sync suggests.listApprovalsfor requests waiting for a person.getPaymentContactwhile a new contact waits for approval.
Common errors
Section titled “Common errors”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.