Skip to content

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.
  • 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.

createBillingContact saves the payer. Ele sends invoices to every address in emails:

Terminal window
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
}'

createInvoice creates a draft. Each line item’s quantity, price and tax_percentage is a decimal string:

Terminal window
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.

submitInvoiceForApproval moves the draft to awaiting_approval and opens an approval request. Its ID is the invoice’s approval_request_id:

Terminal window
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"

An owner or admin approves the request with approveRequest, which needs step-up. See Step-up and approvals:

Terminal window
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.

listBillingContactDepositAddresses returns the same addresses the PDF shows, if you want to show them yourself:

Terminal window
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.

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):

Terminal window
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:

Terminal window
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.

Bills must be turned on for the organization. Until they are, every bill call answers 422 payables_not_enabled.

uploadBill takes the document as a multipart form. It accepts PDF, JPEG, PNG, GIF and WebP, up to 100 MB:

Terminal window
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.

Poll getBill until status is extracted, or until extraction_failed_at is set:

Terminal window
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.

The bill is paid to a payment contact. Find or create one whose bank account matches the bill, then attach it with updateBill:

Terminal window
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.

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:

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\",
\"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.

createExpense needs no fields. Send what you know:

Terminal window
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.

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:

Terminal window
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.

bindExpenseTransaction links a card transaction, a payment or an internal transfer. An expense can have several; a transaction belongs to at most one expense:

Terminal window
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.

updateExpense changes only the fields you send:

Terminal window
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.

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.

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.

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.

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
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.