Developers
Take money for what your server does, with one key.
An integration key lets your own server ask Orla to price a call, charge a client's prepaid balance, mirror an invoice into a book and hear back, signed, when any of it is paid. The payer can be a person on a page or a machine answering a 402; your server does not care which.
Base URL: https://app.orla.finance/api
In the app: Client credits, then Keys. An owner or an admin issues the key, picks what it may do, and copies it once.
Paying rather than selling? The agent API is the other door. The events of the whole book are on webhooks.
The key
A bearer token that acts as the person who issued it, in one book.
There is no separate machine identity to manage. A key is minted by a person, does what that person may do in that one book, and dies with them: remove the person from the book, or demote them to a viewer, and every key they issued stops on the next call. Orla keeps the hash; the plaintext is shown once.
curl https://app.orla.finance/api/integration/ping \
-H "Authorization: Bearer oit_..."Shape
- oit_ and 43 characters
- Sent as Authorization: Bearer on every call to /integration. Nothing else in the API accepts it, so a leaked key reaches exactly these endpoints, inside one book, within its scopes.
- Shown once
- At issue, with the webhook secret beside it whether or not an address was given. Orla keeps the key as a hash and the secret encrypted, for signing. A lost key is revoked and reissued; there is no second look.
- Up to ten a book
- Enough for a billing server, a bridge and a staging copy. Revoke from the same screen; the key stops on the next call.
Scopes
A scope names a family of routes, not a permission: inside the book the issuing person's own rights still apply on top.
- payments
- Mint a payment request, read whether it was paid, charge a client's prepaid balance. The narrowest scope and the one a seller comes here for.
- invoices
- Create an invoice, set its seller and client, send it, record a payment, void it, remind. The bridge for a system of record that keeps its own invoices.
- contacts
- Create a contact.
- accounts
- Create and list the accounts money lands on.
- chats
- Mint the code that connects a client's Telegram group to the bot, and read whether it is connected. What the client then says arrives as webhook events, not by a read.
Limits
- 240 calls a minute per key
- Generous for a sync job, far below anything that hurts. Past it, 429 and a retry after a moment.
- Idempotency-Key
- A header on every write that costs somebody money: a request minted twice is two requests, a charge retried without the key is two charges. Reuse the key on a retry and the answer is the first one.
- A rollout
- The key door is on for listed books while it is new, and the payment routes have a switch of their own. Off, they answer 404 and nothing minted before stops being payable.
Price a call
Your server names a price. The payer gets a page that answers a person and a machine alike.
One call mints a payment request: an amount on one of your accounts, a memo, and the endpoint it is for. What comes back is a URL. Your server hands that URL to whoever should pay and reads the request back, or waits for the webhook.
curl -X POST https://app.orla.finance/api/integration/payment-requests \
-H "Authorization: Bearer oit_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"account_id": "3a7b76ee-7acd-414e-9066-af408c2b74b7",
"amount": "2.50",
"resource": "https://api.example.com/v1/summarize",
"memo": "summarize, 1 call"
}'
{ "id": "…", "token": "…", "url": "https://app.orla.finance/pay/…",
"amount": "2.50000000", "currency": "USD", "status": "active",
"memo": "summarize, 1 call", "created_at": "2026-09-18T13:14:20+00:00",
"expires_at": "2026-09-19T13:14:20+00:00", "amount_received": null,
"resource_id": "…" }What comes back
- url
- The whole point of the answer. Put it in your 402 for a machine, or in a link for a person: the same page serves both.
- status and amount_received
- active, paid or cancelled (a request that lapsed is cancelled, there is no fourth word), and how much has landed on a request paid in parts. Read it back with GET /integration/payment-requests/{token}, scoped to your own book.
- expires_at
- When the request lapses: the deadline of the account's watch, or a day from minting on an account nothing tracks. Never null, so your server always knows how long to wait.
- resource
- The endpoint being charged for. It becomes a card on your Endpoints screen with a call count, and a whole day of its payments lands in the book as one income row.
Who pays and how
- A person
- Opens the URL and pays the way the page offers: a card, a bank transfer, a stablecoin to an address the book controls.
- A machine
- Fetches the URL with Accept: application/json and gets a 402 with an x402 quote: USDC on Base, Ethereum or Polygon, exact amount, a pay-to address the book has proven it controls. It signs an EIP-3009 authorization, sends it back, and Orla puts the transfer on the chain and pays the gas. The rail has a floor of one dollar; under it, charge a prepaid balance instead.
- One row a day
- A metered endpoint sells thousands of calls; the book does not want thousands of legs. All of an endpoint's payments of a day are one income row that grows, and the detail under it lists every payment with who paid it.
Charge a call
Under a dollar, charge a prepaid balance instead of the chain.
A transfer Orla relays cannot be smaller than a dollar, because Orla pays its gas. A metered endpoint sells calls for a tenth of a cent. The bridge is a prepaid balance per client: the client pays once, above the floor, and every call after that is a line in a ledger.
curl -X POST https://app.orla.finance/api/integration/charges \
-H "Authorization: Bearer oit_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "client_ref": "acme-42", "account_id": "3a7b…", "amount": "0.002",
"resource": "https://api.example.com/v1/summarize" }'
{ "paid": true, "client_ref": "acme-42", "balance_usd": "4.99800000",
"charged_usd": "0.002", "entry_id": "…", "top_up": null, "balance_url": null }
{ "paid": false, "client_ref": "acme-42", "balance_usd": "0.00000000",
"charged_usd": null, "entry_id": null,
"top_up": { "url": "https://app.orla.finance/pay/…", "amount": "1.00000000", … },
"balance_url": "https://app.orla.finance/credit/…" }Two answers and no third
- paid
- The balance carried the call. charged_usd is what this call cost, spelled as you sent it; balance_usd is what is left, at the ledger's eight decimals. Serve the call.
- refused, with a way on
- The balance could not carry it. top_up is an ordinary payment request that fills the balance: hand its url to the client as your 402 or your link, and retry the call once it is paid. top_up_amount in your request sets the sum; left out, it is the price of this call or the rail's floor of one dollar, whichever is larger.
- Never an overdraft
- A negative balance is a loan, and a prepaid balance does not lend. The money is yours from the day a top-up lands; what is left is service you owe.
The client's side
- balance_url
- A page for the client: what is left, what was paid in, every call and top-up. Put it beside the payment link in your answer; it needs no account and names you by your public payment handle only.
- client_ref
- Your own identifier for the client. Orla does not parse it; it keys the balance, and the client appears under Client credits with it until you name the contact.
Hear about it
payment_request.paid, signed, to the address on the key.
Give the key a webhook address when you issue it and your server hears the moment a request it minted, or an invoice it mirrored, is paid, and whenever a client acts in their chat with the bot. Same signature as every Orla webhook: sha256= and the HMAC-SHA256 of the raw body under the secret shown once beside the key.
POST /your/address HTTP/1.1
Content-Type: application/json
X-Orla-Event: payment_request.paid
X-Orla-Signature: sha256=5c8456449f0e...
{"event": "payment_request.paid", "space_id": "…",
"at": "2026-09-18T13:14:21.417208+00:00",
"data": {"payment_request_id": "…", "token": "…", "resource_id": "…",
"amount": "2.5", "asset": "USDC", "network": "base",
"tx_hash": "0x…", "payer": "0x…",
"settled_at": "2026-09-18T13:14:20+00:00",
"memo": "summarize, 1 call"}}The body
- event, space_id, at
- The event name, the same string as the X-Orla-Event header; the book it happened in; when it was sent, ISO 8601 with an offset. There is no version field on these events.
- data
- The fields of the event, listed below. Amounts are decimal strings without trailing zeros; identifiers are UUIDs.
Events
- payment_request.paid
- A request your server minted, including the top-up a refused charge asked for, settled on the chain. data: payment_request_id, token, resource_id (null when nothing was filed), amount, asset, network, tx_hash, payer, settled_at, memo. A request a person paid on its page is not pushed; read it back.
- invoice.paid
- An invoice your bridge mirrored is settled in full, by any road. data: invoice_id, number (your document number), currency, total, total_paid, via (manual, settlement, card or write_off).
- invoice.claimed_paid
- The client pressed "I paid" in their chat. data: invoice_id, contact_id, client (their name), number, total, currency. Nothing is booked until a person on your side confirms.
- invoice.deferral_requested
- The client asked for more time. data: invoice_id, contact_id, days, due (the date proposed), client, number, total, currency.
- client.question
- The client wrote to the bot. data: contact_id, client, text (up to the chat's limit, never run through a model).
- client.file
- The client dropped a file, filed under their contact. data: contact_id, client, name (the filename).
Delivery
- Three attempts within a minute
- At once, after five seconds, after thirty. A machine payment is a live exchange, so the schedule is short; a server that was down for longer reads the request back with GET.
- Any 2xx within ten seconds
- Counts as delivered. Do the work after you answer, not before.
- The address
- Public https, resolved again at every send. A private range, a loopback or a metadata address is refused whatever the hostname says that day.
The events of the whole book (money in and out, payouts, approvals) are a separate subscription, described on the webhooks page. Webhooks.
Every endpoint
The whole surface, one line each.
All under /integration, all with the bearer, each behind one scope. Bodies are JSON; identifiers are UUIDs; amounts are decimal strings.
| Endpoint | Scope | What it does |
|---|---|---|
GET /integration/ping | any | Which book and which scopes this key has |
POST /integration/payment-requests | payments | Price one call; comes back with the page's URL |
GET /integration/payment-requests/{token} | payments | Was it paid, and how much of it |
POST /integration/charges | payments | Charge one call to a client's prepaid balance, or get the request that tops it up |
POST /integration/invoices | invoices | Mirror an invoice into the book |
POST /integration/invoices/{id}/seller | invoices | Issuer requisites for one invoice, when the bridge issues for more than one entity |
POST /integration/invoices/{id}/client | invoices | The client of one invoice |
GET /integration/invoices/{id} | invoices | The invoice as the book has it |
POST /integration/invoices/{id}/send | invoices | Send it to the client |
POST /integration/invoices/{id}/payments | invoices | Record a payment against it |
POST /integration/invoices/{id}/void | invoices | Void it |
POST /integration/invoices/{id}/remind | invoices | Remind the client |
POST /integration/contacts | contacts | Create a contact |
POST /integration/contacts/{id}/telegram-link | chats | The /linkclient code that connects the client's Telegram group to the bot; Pro |
GET /integration/contacts/{id}/chats | chats | Whether that Telegram group is connected, and its title |
POST /integration/accounts | accounts | Create an account |
GET /integration/accounts | accounts | List the accounts |
Around the key
What the rest of Orla adds to a paid endpoint.
On your side
- Endpoints
- Every resource you charged for is a card under Client credits: address, call count, last payment, and the payments of any day with who paid. A payer of an endpoint becomes a contact on your press; a pay link or an invoice paid by a machine names its payer by itself.
- The catalogue
- Tick Findable on a card and it appears in Orla's public list of paid services, the x402 discovery list agents read. Address and name only, never volumes.
- Your own checkout
- Prove you control a site and a payment request may be shown inside your page, in a frame. Everything else Orla serves refuses to be framed.
- The facilitator
- Already running an x402 middleware? Point its facilitator URL at Orla: it checks the payer's signature, puts the transfer on the chain, pays the gas, and files the money under the endpoint.
On the payer's side
- Agents
- An agent with an Orla wallet pays your 402 by itself, inside the ceilings its owner set: which hosts, how much a request, how much a day. The agent API is its own page.
- From a shell
- orla fetch <url> in the Orla CLI does the same with an agent key from the environment: the resource on stdout, the receipt on stderr.
Errors
A code you can branch on, not prose.
Every refusal is one JSON body: error, a stable code; detail, a sentence for a person; details, structured fields when there are any.
What you will meet
- 401
- The key is unknown, revoked, or its person was demoted to a viewer. One message for all three on purpose: a leaked key must not be able to map the system.
- 403
- The key lacks the scope for this route, or its person is no longer a member of the book (Not a member of this space).
- 404
- The rollout is off for this book, or the request or resource is not yours.
- 422
- The body: a missing account, an amount the account cannot take, a webhook address that is not public https.
- 429
- Over the minute's calls. Wait and retry with the same Idempotency-Key.
See it on your own books.
Thirty minutes: we connect an account, drop a real bill in, and close a month together.