# Take money for what your server does, with one key

URL: https://www.orla.finance/en/developers
Markdown twin of that page. Append `.md` to any Orla page URL to get one.

Your server asks Orla to price a call, charge a prepaid balance or mirror an invoice, and hears back, signed, when it is paid. The payer can be a person or a machine answering a 402.

An owner or an admin issues the key in the app, under Client credits, then Keys.

Base URL: https://app.orla.finance/api

#### A key that acts as its issuer

A key does what its issuer may do in one book, and stops on the next call once that person is removed or made a viewer. Orla keeps only the hash.

```
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.
- transactions: Files rows from a system Orla has no connector for into one push source, and touches nothing else. It ends by itself after 30, 90 or 180 days or a year.
- 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: On for listed books while it is new, and the payment routes have a switch of their own. Off, they answer 404; nothing minted before stops being payable.

#### You name a price, the payer gets a page

One call mints a payment request and returns a URL. Hand it to whoever should pay, a person or a machine, then read it back or wait 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: Gets a 402 with an x402 quote in USDC on Base, Ethereum or Polygon, signs an EIP-3009 authorization, and Orla settles it on chain and pays the gas. The floor is one dollar.
- One row a day: An endpoint's payments of a day are one income row that grows, with every payment and its payer under it. A chain reorganisation shrinks the row; the invoice it closed stays paid.

#### Under a dollar, charge a prepaid balance

Orla relays nothing under a dollar, because it pays the gas. So a client tops up 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 a payment request that fills it: hand its url to the client, and retry once paid. top_up_amount sets the sum.
- 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.

#### payment_request.paid, signed, to your address

Give the key a webhook address and your server hears when a request or a mirrored invoice is paid, or a client acts in their chat. Signed like every Orla webhook.

```
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 settled on the chain. data: payment_request_id, token, resource_id, amount, asset, network, tx_hash, payer, settled_at, memo.
- 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.

#### The whole surface, one line each

All under /integration, with the bearer, one scope each. JSON bodies, UUID identifiers, amounts as decimal strings. For a machine: orla.finance/openapi.json.

- 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/transactions (transactions): File up to 500 rows into the push source this key is bound to; one call is one batch
- POST /integration/accounts (accounts): Create an account
- GET /integration/accounts (accounts): List the accounts

#### 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 who paid each day.
- 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.

##### Rows in, from any other system

The direction is reversed: Orla holds no key of yours and calls nobody. Your automation, script or scheduled job sends the rows, and Orla files them.

- POST /integration/transactions: rows, each with external_id, account_key, currency, occurred_on and amount; payee and note if you have them. The key names the source, the body never does. The answer counts booked, duplicates, skipped_closed and rejected, and carries a batch_id.
- Sent twice, filed once: A row is known by your own external_id, so a retry or an overlapping run adds nothing.
- One call is one batch: A wrong run is undone whole from the source's list. Rows dated inside a closed period are skipped and counted, never filed quietly.
- Filed as imported: Pushed rows reach automations and event subscriptions with origin import, never as money arriving on a rail, so a leaked key cannot ship goods on a payment nobody made.
- A key with an end: A week before it runs out the person who issued it is reminded. New key issues a fresh one, and the old one works until it is revoked or runs out, so the automation does not stop during the swap.
- What it counts against: The rows it files come out of the space's synced rows for the month, the same allowance bank and exchange sync use.

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

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