Skip to content

Webhooks

Your own systems hear about your money the moment it moves.

Point an address of yours at a space. Orla calls it with a signed JSON body when money lands or leaves, an invoice is paid, a payout goes out, a payment waits for a signature or is stopped.

In the app: Connections, the card Event subscriptions. An owner or an admin adds the address and picks the events.

Six events

One name on the wire, the same name on the screen.

Pick the ones you want, or pick none and hear about all of them, including events added later. A move between your own accounts arrives with origin internal, so an automation that ships goods on payment is not fired by your own sweep.

EventWhen it firesFields beyond the common ones
money.inA row landed in the book that increases an account.transaction_id, account_id, direction (in), amount, currency, kind (income, expense, transfer), origin (rail, manual, import, internal), occurred_on
money.outA row landed in the book that decreases an account.The same as money.in, with direction out
invoice.paidAn invoice the space issued is settled in full. Fires once per invoice.invoice_id, number (your document number), amount, currency
payout.executedA payment left on a rail and the rail confirmed it.payment_id, amount, currency, rail (a chain name, bank, or book for a payment marked paid by hand), transaction_id
payment.awaiting_approvalA payment met an approval rule and waits for signatures.payment_id, amount, currency, required_approvals
payment.blockedA payment was refused before it left: a policy rule, or a screening hold on the recipient.payment_id, amount, currency, reason (a stable error code, not a sentence)

The body

Amounts and identifiers. Never names.

Every body carries event, version and at, then the fields of its event. Amounts are unsigned decimal strings; the direction is a word of its own, so a refund cannot disagree with itself. There are no counterparty names, notes or addresses in any event: if your address ever leaks, it costs a number and a timestamp, not your address book.

POST /your/address HTTP/1.1
Content-Type: application/json
X-Orla-Event: money.in
X-Orla-Event-Id: 11b8c41e-47b5-407b-a19d-bb12d90a82b5
X-Orla-Delivery-Attempt: 1
X-Orla-Signature: sha256=5c8456449f0e...

{"event":"money.in","version":1,
 "transaction_id":"5c5cbbda-f6a7-4907-9c10-e167a238b6e2",
 "account_id":"3a7b76ee-7acd-414e-9066-af408c2b74b7",
 "direction":"in","amount":"1.00000000","currency":"USD",
 "kind":"income","origin":"manual","occurred_on":"2026-09-18",
 "at":"2026-09-18T13:14:20.312110+00:00"}

Common fields

event
The event name, the same string as the X-Orla-Event header.
version
The body version, 1 today. Within a version a field is only ever added, never renamed or removed.
at
When the event was written, ISO 8601 with an offset.
amount
A decimal string with eight places, never a float, never signed.

The signature

Compute it on the bytes you received, before you parse them.

X-Orla-Signature carries sha256= and the HMAC-SHA256 of the raw request body under the secret shown once when the subscription was made. Compare the two strings in constant time. Drop a repeat by X-Orla-Event-Id: it stays the same on every attempt of one delivery.

import hashlib, hmac

def verify(secret: str, body: bytes, header: str) -> bool:
    digest = hmac.new(secret.encode(), body, hashlib.sha256)
    want = "sha256=" + digest.hexdigest()
    return hmac.compare_digest(header, want)
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(secret, body, header) {
  const digest = createHmac("sha256", secret).update(body);
  const want = "sha256=" + digest.digest("hex");
  if (header.length !== want.length) return false;
  return timingSafeEqual(Buffer.from(header), Buffer.from(want));
}

body is the raw bytes of the request, not an object you serialised again: a JSON library that reorders keys or changes spacing changes the digest.

Delivery

At least once, within a minute, for about a day.

What to answer

Any 2xx within ten seconds
That counts as delivered. Do the work after you answer, not before.
Anything else
A retry: after 1 minute, 5, 15, then 1 hour, 3, 6 and 12. Eight attempts over about a day, then the delivery is marked as given up in the journal.
A day of nothing but failures
The subscription is paused and the owner of the space is told. Fix the address and press Resume in the same place.

What you can see

The journal
Every delivery for thirty days: event, status, attempt, response code, and a Retry button on one row.
Twins
A receiver that answers slowly may be sent the same event twice. Drop the second by X-Orla-Event-Id.

Setting it up

An address, a choice of events, a secret you copy once.

The address

Public https on port 443
Resolved again at every send. A private range, a loopback or a metadata address is refused, whatever the hostname says that day.
Redirects are not followed
Answer at the address you gave.

Who and how many

Owner or admin
A subscription carries everything that happens in the space and does not filter by role, so the people who may add one are the people who already see it all.
Plans
Pro carries one subscription per space, Scale five, Enterprise twenty five, and a pack adds five more. Free and Starter do not include it.
The secret
Shown once, at creation and at rotation. Orla keeps only an encrypted copy, so New secret is the only answer to a lost one.

This page as Markdown, for an agent: https://orla.finance/en/webhooks.md

See it on your own books.

Thirty minutes: we connect an account, drop a real bill in, and close a month together.