Invoices, bills and expenses
This guide covers three kinds of paperwork:
- An invoice asks someone to pay the organization. Ele emails it to the payer, who pays in USDC or USDT.
- A bill is a supplier’s invoice that the organization pays. Ele reads the supplier’s document and pays it like any other bank payment.
- An expense records money already spent, with its receipt.
What you can do
Section titled “What you can do”- Save the people the organization invoices, called billing contacts.
- Create an invoice, have it approved, and let Ele email it.
- Match the stablecoins a payer sends against the invoice.
- Upload a supplier’s invoice as a bill, check what Ele read, and pay it.
- Record expenses, attach receipts and link them to transactions.
Sending an invoice
Section titled “Sending an invoice”1. Save the billing contact
Section titled “1. Save the billing contact”createBillingContact saves the
payer. Ele sends invoices to every address in emails:
curl -s "$ELE_API_BASE/api/v1/organizations/$ORGANIZATION_ID/billing-contacts" \ -H "Authorization: Bearer $ELE_TOKEN" \ -H "Idempotency-Key: $KEY" \ -H 'Content-Type: application/json' \ -d '{ "company_name": "Harbour Trading Ltd", "emails": ["accounts@harbour.example"], "address": "1 Harbour Road, Hong Kong", "tax_id": null }'2. Create the invoice
Section titled “2. Create the invoice”createInvoice creates a draft.
Each line item’s quantity, price and tax_percentage is a decimal string:
curl -s "$ELE_API_BASE/api/v1/organizations/$ORGANIZATION_ID/invoices" \ -H "Authorization: Bearer $ELE_TOKEN" \ -H "Idempotency-Key: $KEY" \ -H 'Content-Type: application/json' \ -d "{\"billing_contact_id\": \"$BILLING_CONTACT_ID\", \"issue_date\": \"2026-10-09\", \"due_date\": \"2026-11-08\", \"line_items\": [ {\"description\": \"Consulting, October\", \"quantity\": \"10\", \"price\": \"150.00\", \"tax_percentage\": \"0\"} ]}"Ele works out due_amount from the line items, tax included. An invoice has
no currency field: the payer pays in USDC or USDT, and one coin counts as one
unit of due_amount. due_date must not be before issue_date. If you leave out
internal_reference_number, Ele makes one, such as INV-3F9A1C.
Creating the first invoice for a contact gives that contact its own crypto deposit addresses. The payer sends money there.
To create an invoice, the organization must be verified and able to receive
crypto (crypto_in in
getOrganizationCapabilities),
and the person creating it must have passed their own identity check.
Otherwise Ele answers 422 accept_crypto_deposits_unavailable.
Change a draft with updateInvoice.
Only drafts can change. deleteInvoice
removes an invoice and withdraws its pending approval request, if it has one.
3. Submit it for approval
Section titled “3. Submit it for approval”submitInvoiceForApproval
moves the draft to awaiting_approval and opens an approval request. Its ID is
the invoice’s approval_request_id:
curl -s -X POST "$ELE_API_BASE/api/v1/organizations/$ORGANIZATION_ID/invoices/$INVOICE_ID/submit" \ -H "Authorization: Bearer $ELE_TOKEN" \ -H "Idempotency-Key: $KEY"4. Have a person approve it
Section titled “4. Have a person approve it”An owner or admin approves the request with
approveRequest, which needs step-up.
See Step-up and approvals:
curl -s -X POST "$ELE_API_BASE/api/v1/approvals/$APPROVAL_ID/approve" \ -H "Authorization: Bearer $ELE_TOKEN" \ -H "Idempotency-Key: $KEY"On approval the invoice becomes sent, and Ele emails it as a PDF to the
billing contact. The PDF asks the payer to send USDC or USDT to the contact’s
addresses on Ethereum and Solana. If the request is rejected, the invoice goes
back to draft.
To send the email again, call
resendInvoiceEmail. It works
only on a sent invoice.
5. Show where to pay
Section titled “5. Show where to pay”listBillingContactDepositAddresses
returns the same addresses the PDF shows, if you want to show them yourself:
curl -s "$ELE_API_BASE/api/v1/organizations/$ORGANIZATION_ID/billing-contacts/$BILLING_CONTACT_ID/deposit-addresses" \ -H "Authorization: Bearer $ELE_TOKEN"These addresses belong to the billing contact. They are not the organization’s own deposit addresses.
6. Match the payment to the invoice
Section titled “6. Match the payment to the invoice”When stablecoins arrive at a contact’s address, they are credited to the
organization like any crypto deposit, and the amount is also added to that
contact’s credit. A deposit is not matched to an invoice by itself.
listInvoiceAllocations shows
the contact’s credit ledger: deposits (with crypto_incoming) and the amounts
used for invoices (with billing_invoice_id):
curl -s "$ELE_API_BASE/api/v1/organizations/$ORGANIZATION_ID/billing-contacts/$BILLING_CONTACT_ID/allocations" \ -H "Authorization: Bearer $ELE_TOKEN"When the contact has enough credit, an owner or admin marks the invoice paid
with allocateInvoice. Leave out
credit to use the full due_amount, or send a smaller amount to accept a
part payment:
curl -s "$ELE_API_BASE/api/v1/organizations/$ORGANIZATION_ID/invoices/$INVOICE_ID/allocate" \ -H "Authorization: Bearer $ELE_TOKEN" \ -H "Idempotency-Key: $KEY" \ -H 'Content-Type: application/json' \ -d '{"credit": "1500.00", "notes": "Paid in USDC"}'The invoice becomes paid, and accepted_amount holds the amount used. If the
contact’s credit is too small, Ele answers 422 insufficient_balance.
deallocateInvoice undoes it,
returning the invoice to sent and the amount to the contact’s credit.
For totals across the organization or one contact, use
getInvoiceSummary and
getInvoiceAggregates.
Paying a bill
Section titled “Paying a bill”Bills must be turned on for the organization. Until they are, every bill call
answers 422 payables_not_enabled.
1. Upload the supplier’s invoice
Section titled “1. Upload the supplier’s invoice”uploadBill takes the document as a
multipart form. It accepts PDF, JPEG, PNG, GIF and WebP, up to 100 MB:
curl -s "$ELE_API_BASE/api/v1/organizations/$ORGANIZATION_ID/bills/upload" \ -H "Authorization: Bearer $ELE_TOKEN" \ -H "Idempotency-Key: $KEY" \ -F "file=@invoice.pdf"If you already stored the file with
createFile, call
createBill with its file_id instead.
The bill starts as extracting: Ele reads the supplier, amount, currency, due
date and bank details from the document in the background.
2. Wait for the reading, then check it
Section titled “2. Wait for the reading, then check it”Poll getBill until status is extracted,
or until extraction_failed_at is set:
curl -s "$ELE_API_BASE/api/v1/bills/$BILL_ID" \ -H "Authorization: Bearer $ELE_TOKEN"Show the person what Ele read and let them correct it with
updateBill. Send only the fields that
change; null clears a field. If the reading failed, the person fills the
fields in the same way, and the bill becomes extracted.
3. Attach the payment contact
Section titled “3. Attach the payment contact”The bill is paid to a payment contact. Find or create
one whose bank account matches the bill, then attach it with updateBill:
curl -s -X PATCH "$ELE_API_BASE/api/v1/bills/$BILL_ID" \ -H "Authorization: Bearer $ELE_TOKEN" \ -H "Idempotency-Key: $KEY" \ -H 'Content-Type: application/json' \ -d "{\"payment_contact_id\": \"$CONTACT_ID\"}"When the bill names a bank account, the contact must be paid into that same
account; otherwise Ele answers 422 contact_mismatch. Correcting the bill’s
bank details later removes the attached contact, so attach it again after.
4. Pay it
Section titled “4. Pay it”Pay the bill as in Paying out: take a quote with
preflightPayment for the attached contact, then call
executePayment with the bill’s ID in
bill_id:
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\", \"bill_id\": \"$BILL_ID\", \"payout_reason\": \"PYR001\", \"statement_description\": \"Invoice 7781\"}"A bill is paid by bank transfer only. The bill becomes payment_requested, and
its fiat_outgoing_id and approval_request_id name the payment and its
approval. From here the payment is approved and followed like any other. If the
payment is rejected, withdrawn or fails, the bill returns to extracted and
can be paid again.
Creating a bill never pays it. Only executePayment with bill_id does, and a
person still approves that payment.
To stop a bill, call cancelBill. Once a
bill is payment_requested, reject its approval request instead.
Recording an expense
Section titled “Recording an expense”1. Create the expense
Section titled “1. Create the expense”createExpense needs no fields. Send
what you know:
curl -s "$ELE_API_BASE/api/v1/organizations/$ORGANIZATION_ID/expenses" \ -H "Authorization: Bearer $ELE_TOKEN" \ -H "Idempotency-Key: $KEY" \ -H 'Content-Type: application/json' \ -d '{"description": "Client lunch", "category": "meals", "bill_amount": "84.50", "bill_currency": "SGD", "transaction_date": "2026-10-08"}'To attach a receipt in the same call, send a file_id from
createFile. To link a transaction, send
binding.
2. Attach the receipt
Section titled “2. Attach the receipt”attachExpenseReceipt takes the
receipt as a multipart form, or a stored file’s file_id as JSON. Set
analyze_receipt to have Ele read the receipt and fill in the expense:
curl -s "$ELE_API_BASE/api/v1/expenses/$EXPENSE_ID/receipts" \ -H "Authorization: Bearer $ELE_TOKEN" \ -H "Idempotency-Key: $KEY" \ -F "file=@receipt.jpg" \ -F "analyze_receipt=true"Reading happens during the call. If Ele cannot read the receipt, the receipt is still attached and the expense is left as it was.
Once the expense has an amount, Ele looks for one card transaction from the last 30 days with the same amount that is not linked to another expense. If it finds exactly one, it links it.
3. Link transactions
Section titled “3. Link transactions”bindExpenseTransaction links
a card transaction, a payment or an internal transfer. An expense can have
several; a transaction belongs to at most one expense:
curl -s "$ELE_API_BASE/api/v1/expenses/$EXPENSE_ID/bindings" \ -H "Authorization: Bearer $ELE_TOKEN" \ -H "Idempotency-Key: $KEY" \ -H 'Content-Type: application/json' \ -d "{\"target_type\": \"card_transaction\", \"target_id\": \"$CARD_TRANSACTION_ID\"}"Remove a link with
unbindExpenseTransaction,
using the binding’s id from the expense’s bindings.
4. Update the expense
Section titled “4. Update the expense”updateExpense changes only the fields
you send:
curl -s -X PATCH "$ELE_API_BASE/api/v1/expenses/$EXPENSE_ID" \ -H "Authorization: Bearer $ELE_TOKEN" \ -H "Idempotency-Key: $KEY" \ -H 'Content-Type: application/json' \ -d '{"status": "submitted"}'List expenses with listExpenses,
filtered by status, creator_id or dates. For a spreadsheet of expenses, see
exports in Accounts and balances.
States
Section titled “States”Invoices
Section titled “Invoices”state |
Meaning | What you do |
|---|---|---|
draft |
Being written. It can be changed. | Edit it, then submit it. |
awaiting_approval |
Waiting for an owner or admin. | Ask an approver. |
sent |
Approved and emailed to the payer. | Wait for the payer’s deposit, then allocate. |
paid |
Credit was allocated to it. | Nothing. Deallocate to undo. |
finalized |
Closed. Nothing in the API moves an invoice here today. | Nothing. |
next_valid_states lists the states the invoice can move to next. is_overdue
is true for a sent invoice whose due_date has passed, by UTC date.
status |
Meaning | What you do |
|---|---|---|
extracting |
Ele is reading the document. If extraction_failed_at is set, it gave up. |
Wait. If it gave up, have the person fill in the fields. |
extracted |
Ready to act on. | Check the fields, attach a contact, pay it. |
payment_requested |
A payment was created for it. The bill cannot change now. | Follow the payment in fiat_outgoing_id. |
cancelled |
Stopped for good. | Nothing. |
A bill stays payment_requested while its payment is approved and sent. Follow
the payment with getPayment. If the
payment is rejected, withdrawn or fails, the bill goes back to extracted.
Expenses
Section titled “Expenses”An expense’s status is a label for your own review process: draft,
submitted, approved, rejected or paid. A new expense is draft unless
you send another status. Ele does not move it or enforce an order, and setting
approved or paid moves no money.
Older expenses can also read back as pending or working. You cannot set
these, but give any switch on status a case for them.
Step-up and approvals
Section titled “Step-up and approvals”| Call | Needs |
|---|---|
Approving an invoice (approveRequest) |
Step-up, by an owner or admin. |
Approving a bill’s payment (approveRequest) |
Step-up, by an owner or admin. |
| Every other call in this guide | Neither. |
If the organization requires a second person, the person who submitted the invoice cannot approve it.
Who can do what
Section titled “Who can do what”| Operation | Roles |
|---|---|
Billing contacts, createInvoice, updateInvoice, submitInvoiceForApproval, deleteInvoice, resendInvoiceEmail |
Owner, admin, member |
allocateInvoice, deallocateInvoice |
Owner, admin |
listInvoices |
Owner and admin see all; a member sees the invoices they created |
| Bills | Owner, admin, member |
listExpenses |
Owner and admin see all; a member sees their own |
| Reading or changing an expense | Its creator, or an owner or admin |
What to poll
Section titled “What to poll”getBillevery few seconds while a bill isextracting.listInvoiceAllocationsorgetInvoiceSummarywhile you wait for a payer’s deposit.listApprovalsfor invoices and bill payments waiting for a person.
Common errors
Section titled “Common errors”| Status | type |
When | What to do |
|---|---|---|---|
400 |
invalid_content_type |
uploadBill with a file that is not PDF, JPEG, PNG, GIF or WebP. createBill answers 422 with the same type. |
Upload a supported type. |
400 |
file_too_large |
uploadBill with a file over 100 MB. |
Upload a smaller file. |
404 |
billing_contact_not_found |
The billing contact is unknown or deleted. | Choose another contact. |
409 |
invoice_not_editable |
updateInvoice on an invoice that is not a draft. |
Nothing to change; it has been submitted. |
409 |
invoice_not_submittable |
Submitting an invoice that is not a draft. | Read the invoice’s state. |
409 |
invoice_not_sent |
resendInvoiceEmail on an invoice that is not sent. |
Approve it first. |
409 |
invalid_state_for_allocation |
allocateInvoice on an invoice that is not sent. |
Read the invoice’s state. |
409 |
invalid_state_for_unallocation |
deallocateInvoice on an invoice that is not paid. |
Nothing to undo. |
422 |
accept_crypto_deposits_unavailable |
Creating an invoice before the organization or person is verified for crypto deposits. | Finish onboarding. |
422 |
insufficient_balance |
allocateInvoice for more than the contact’s credit. |
Wait for the deposit, or allocate less. |
422 |
payables_not_enabled |
Any bill call, or executePayment with bill_id, when bills are off. |
Contact Ele support. |
422 |
still_extracting |
Attaching a contact or paying while Ele is still reading the bill. | Wait for extracted. |
422 |
contact_mismatch |
The contact is not paid into the bank account the bill names. | Attach the right contact, or correct the bill. |
422 |
bill_has_no_contact |
executePayment with bill_id before a contact is attached. |
Attach one. |
422 |
quote_not_for_bill_contact, quote_not_for_bill_account |
The quote pays a different contact or account from the bill’s. | Take a quote for the bill’s contact. |
422 |
bill_requires_fiat_quote |
Paying a bill with a crypto quote. | Pay by bank transfer. |
422 |
already_requested |
Changing, cancelling or paying again a bill that is payment_requested. |
Follow the existing payment. |
422 |
cancelled |
Changing or paying a cancelled bill. | Nothing. |
422 |
file_not_uploaded |
The file_id names a file whose upload has not finished. |
Finish the upload, then retry. |
422 |
binding_target_taken |
The transaction is linked to another expense. | Unlink it there first. |
422 |
binding_already_present |
The transaction is already linked to this expense. | Nothing to do. |
For other types, see Errors.