Reference
API
How the web tier reaches the API, authentication, response format and the main endpoints.
On this page
The Setrya API is the backend on your settlement host. It serves the web app and nothing else: every request has to come through the web tier, which proves itself with a shared key. This page covers how the two connect, the request rules, the response format, the error codes and every endpoint.
How requests reach the API
The browser never calls the API directly. It calls /api/v1/... on the web app's own origin, and the web app relays the request.
| Step | What happens |
|---|---|
| Browser to web app | A normal same-origin request to /api/v1/... with the session cookie |
| Web app to API | The relay forwards only accept, content-type, cookie, origin, user-agent and x-setrya-request, then adds the edge key and the client's address |
| API to web app | The relay passes back only cache-control, content-disposition, content-type and any set-cookie |
The relay refuses a request body over 4 MB with HTTP 413. If the API can't be reached, it answers HTTP 502 with code INTERNAL and the message "The settlement server is not reachable. Try again in a moment."
The web tier connects with three settings:
| Setting | Purpose |
|---|---|
SETRYA_API_URL |
The API's base URL, for example https://api.example.com:8444 |
SETRYA_EDGE_KEY |
The shared secret, at least 32 characters, the same value as on the API |
SETRYA_API_CA |
The API's self-signed certificate in PEM. The web tier trusts only this certificate for the connection |
Edge headers
Every request under /api/ must carry the edge key. Without it, the API answers HTTP 403 with code FORBIDDEN and "This endpoint is not public."
| Header | Set by | Meaning |
|---|---|---|
x-setrya-edge-key |
The web tier | The shared secret, compared in constant time |
x-setrya-client-ip |
The web tier | The browser's address, taken from x-forwarded-for. It is the only client address the API trusts, for rate limits and the audit log |
GET /healthz answers {"ok":true} without the edge key, for container health checks.
Sessions and access levels
Signing in sets an HttpOnly, SameSite=Strict cookie named setrya_session. A session lasts 12 hours, or 7 days if you chose to stay signed in. The API stores only a SHA-256 hash of the token.
Each endpoint needs one of three access levels:
| Level | Needs |
|---|---|
none |
Nothing, for setup, sign-in and recipient links |
session |
A valid session |
unlocked |
A valid session and an unlocked vault |
A missing session gets UNAUTHENTICATED. A locked vault gets VAULT_LOCKED. Actions that move money, approve a period, export data or change security also need a fresh step-up token, which you get from POST /api/v1/auth/step-up with your password. A step-up is tied to one action and to the exact state it covers, such as the snapshot hash and the recipients, and is used up once spent.
Request rules
Every request other than GET and HEAD must pass all three checks, or it gets HTTP 403 with code CSRF_REJECTED:
- The
Originheader must be one of the origins inSETRYA_PUBLIC_ORIGIN, which is a comma-separated list. - The body must be
application/jsonormultipart/form-data. - The header
x-setrya-request: 1must be present.
JSON bodies may be up to 256 KB. A body that doesn't match the endpoint's schema gets INVALID_INPUT with a message for each field.
Response format
A success returns HTTP 200:
{ "ok": true, "data": { "id": "6f0c…" }, "requestId": "3d2b9a1e-…" }
Amounts are strings of whole zatoshis or exact decimals, never floating-point numbers. bigint values are sent as strings.
An error returns this shape:
{
"ok": false,
"error": {
"code": "INVALID_INPUT",
"message": "Check the highlighted fields.",
"fieldErrors": { "displayName": "Enter a name" },
"canRetry": false,
"recoveryAction": "/sign-in"
},
"requestId": "3d2b9a1e-…"
}
fieldErrors and recoveryAction appear only when they apply. An unexpected failure returns INTERNAL with "Something failed on the server. Reference" and the first 8 characters of the request ID, which you can find in the API's logs. File downloads, such as CSV exports and backups, return the file itself instead of this envelope.
Error codes
| Code | HTTP status | Meaning |
|---|---|---|
UNAUTHENTICATED |
401 | No valid session |
STEP_UP_REQUIRED |
401 | This action needs a fresh password confirmation |
FORBIDDEN |
403 | Not allowed, for example a wrong password or an archived project |
CSRF_REJECTED |
403 | The request failed the origin, content type or header check |
NOT_FOUND |
404 | No such record, or not yours |
STATEMENT_REVOKED |
410 | The statement link was revoked |
STATEMENT_EXPIRED |
410 | The statement link expired |
INVALID_INPUT |
422 | The body didn't pass validation |
VAULT_LOCKED |
423 | Unlock the workspace first |
RATE_LIMITED |
429 | Too many attempts. Wait a minute |
INTERNAL |
500 | Unexpected server failure |
DUPLICATE_IMPORT, SOURCE_AMBIGUOUS, AGREEMENT_OVERLAP |
409 | The import or agreement conflicts with existing records |
STALE_PERIOD_REVISION, RATE_REQUIRED, RATE_EXPIRED |
409 | The period changed, or a rate is missing or expired |
ADDRESS_UNSUPPORTED, ADDRESS_CHANGED, CHAIN_MISMATCH |
409 | The payout address or network doesn't fit the payment |
WALLET_UNAVAILABLE, WALLET_NOT_SYNCED, WALLET_LOCKED, INSUFFICIENT_SHIELDED_FUNDS |
409 | The wallet can't pay right now |
PRIVACY_POLICY_REJECTED, TRANSACTION_MISMATCH, SIGNATURE_FAILED |
409 | The wallet refused, or the prepared transaction didn't match the approval |
BROADCAST_OUTCOME_UNKNOWN, REORG_DETECTED |
409 | See Payment states |
RESTORE_FAILED |
409 | A backup file couldn't be opened with that password |
CONFLICT |
409 | The record changed or is in a state that doesn't allow this |
A few endpoints override the status, for example a refused setup token returns 403.
Views
Server-rendered pages read their data from one endpoint:
GET /api/v1/views/:name?param=value
A view returns the same envelope, encoded so that bigint, Date and Map values survive the trip. It is sent as application/vnd.setrya.wire+json, and those values are tagged:
{ "$setrya": "bigint", "v": "460000000" }
{ "$setrya": "date", "v": "2026-10-28T12:00:00.000Z" }
{ "$setrya": "map", "v": [["partner-id", { "…": "…" }]] }
An unknown view name returns 404. Most views need unlocked access and take a projectId parameter.
| Group | Views |
|---|---|
| Gates | auth.status (access none, returns only ownerExists, signedIn and unlocked), auth.unlock (access session) |
| Workspace | shell.home, shell.project, shell.overview, shell.activity |
| Onboarding | onboarding.layout, onboarding.hub, onboarding.workspace, onboarding.source, onboarding.import, onboarding.partner, onboarding.agreement, onboarding.wallet, onboarding.ready |
| Revenue | revenue.list, revenue.import, revenue.sources, revenue.importDetail |
| Partners | partners.list, partners.new, partners.detail, partners.edit |
| Agreements | agreements.list, agreements.new, agreements.detail, agreements.edit |
| Periods | periods.list, periods.new, periods.detail, periods.exceptions, periods.close, periods.review, periods.send |
| Payments | payments.list, payments.detail |
| Statements | statements.list, statements.detail, print.statement |
| Wallet | wallet.overview, wallet.activity, wallet.transactions |
| Settings | settings.index, settings.privacy, settings.settlement, settings.security, settings.integrations, settings.project |
Endpoints
All paths start with /api/v1. :projectId and the other path parameters are UUIDs.
Setup and sign-in
| Method and path | Access | Purpose |
|---|---|---|
GET /bootstrap/status |
none | Whether an owner exists |
POST /bootstrap/owner |
none | Create the owner and vault once, with the setup token |
POST /auth/session |
none | Sign in |
DELETE /auth/session |
none | Sign out |
POST /auth/password |
unlocked | Change the password, which signs out every other session |
DELETE /auth/sessions |
unlocked | Sign out every other session |
POST /auth/step-up |
unlocked | Confirm your password for one action and get a step-up token |
POST /vault/unlock |
session | Unlock the workspace with the vault passphrase |
POST /vault/lock |
session | Lock the workspace now |
POST /vault/passphrase |
unlocked | Change the vault passphrase. Records are not re-encrypted |
POST /settings/auto-lock |
unlocked | Set the idle lock, 1 to 240 minutes |
Projects and sources
| Method and path | Access | Purpose |
|---|---|---|
GET /projects |
unlocked | List projects |
POST /projects |
unlocked | Create a project |
GET /projects/:projectId |
unlocked | Read a project |
PATCH /projects/:projectId |
unlocked | Update a project's settings |
POST /projects/:projectId/onboarding |
unlocked | Record the onboarding step |
GET /projects/:projectId/sources |
unlocked | List revenue sources |
POST /projects/:projectId/sources |
unlocked | Add a source |
PATCH /projects/:projectId/sources/:sourceId |
unlocked | Update a source |
GET /projects/:projectId/search?q= |
unlocked | Search partners, periods and payments in the unlocked workspace |
Revenue
| Method and path | Access | Purpose |
|---|---|---|
POST /projects/:projectId/imports/preview |
unlocked | Upload one CSV. Returns its header, ten sample rows and the file hash |
POST /projects/:projectId/imports/normalize |
unlocked | Apply a mapping and see each row's type and issues |
GET /projects/:projectId/imports |
unlocked | List imports |
POST /projects/:projectId/imports |
unlocked | Commit an import |
POST /projects/:projectId/imports/:importId/void |
unlocked | Void an import |
GET /projects/:projectId/imports/:importId/csv |
unlocked | Download an import's rows as CSV |
POST /projects/:projectId/revenue/classify |
unlocked | Change how rows are classified |
POST /projects/:projectId/revenue/manual |
unlocked | Add a manual revenue entry |
Partners and agreements
| Method and path | Access | Purpose |
|---|---|---|
GET /projects/:projectId/partners |
unlocked | List partners |
POST /projects/:projectId/partners |
unlocked | Add a partner |
PATCH /projects/:projectId/partners/:partnerId |
unlocked | Update a partner |
POST /projects/:projectId/partners/:partnerId/address |
unlocked | Set a new payout address version |
POST /wallet/validate-recipient |
session | Check an address against the ZIP 316 rules, this installation's network and the payer's pool. No outside lookup |
GET /projects/:projectId/agreements |
unlocked | List agreements |
POST /projects/:projectId/agreements |
unlocked | Create an agreement |
POST /projects/:projectId/agreements/check |
unlocked | Check a draft agreement for overlaps and pool commitments before saving |
POST /projects/:projectId/agreements/:agreementId/revisions |
unlocked | Add a new version of an agreement |
POST /projects/:projectId/agreements/:agreementId/end |
unlocked | End an agreement |
Periods and rates
| Method and path | Access | Purpose |
|---|---|---|
POST /projects/:projectId/periods/preview |
unlocked | Dry run of a period's calculation. Nothing is saved |
GET /projects/:projectId/periods |
unlocked | List periods |
POST /projects/:projectId/periods |
unlocked | Create a period |
GET /projects/:projectId/periods/:periodId |
unlocked | Read a period |
POST /projects/:projectId/periods/:periodId/recalculate |
unlocked | Recalculate a draft period |
POST /projects/:projectId/periods/:periodId/exceptions/:exceptionId/preview |
unlocked | See what a decision on an exception would change |
POST /projects/:projectId/periods/:periodId/exceptions/:exceptionId/resolve |
unlocked | Decide an exception |
POST /projects/:projectId/periods/:periodId/rates |
unlocked | Attach or clear an approved rate on a draft period, then recalculate |
POST /projects/:projectId/periods/:periodId/freeze |
unlocked | Freeze a hashed snapshot for review |
POST /projects/:projectId/periods/:periodId/approve |
unlocked | Approve the snapshot. Needs a step-up and the expected total in zatoshis |
POST /projects/:projectId/periods/:periodId/revise |
unlocked | Start a new revision |
POST /projects/:projectId/periods/:periodId/discard |
unlocked | Delete a draft that was never frozen or approved |
POST /projects/:projectId/periods/:periodId/close |
unlocked | Close a period, with a decision for each unpaid line |
GET /projects/:projectId/periods/:periodId/report |
unlocked | Download the period report as CSV |
GET /projects/:projectId/rates |
unlocked | List rates |
POST /projects/:projectId/rates |
unlocked | Enter a rate with its source and time |
Payments
| Method and path | Access | Purpose |
|---|---|---|
POST /payments/prepare |
unlocked | Build and inspect a transaction for an approved batch. Returns the existing one if a transaction is already in progress |
GET /payments/transactions/:txId |
unlocked | A transaction's state, txid, fee, confirmations and attempts. Never the PCZT or signed bytes |
POST /payments/transactions/:txId/sign |
unlocked | Authorise with a step-up token. Queues the prove, sign and broadcast job |
POST /payments/transactions/:txId/cancel |
unlocked | Cancel before signing, with a reason |
POST /payments/transactions/:txId/reconcile |
unlocked | Look the transaction up on the chain now |
POST /payments/transactions/:txId/resubmit |
unlocked | Resubmit the identical signed bytes after an unknown outcome |
POST /payments/transactions/:txId/declare-not-broadcast |
unlocked | Mark an investigated transaction as never broadcast, after its expiry height |
GET /projects/:projectId/payments/export |
unlocked | Download payouts as CSV. addresses=1 adds addresses, txids=0 leaves txids out |
GET /wallet/health |
session | The wallet and chain status. fresh=1 skips the 15-second cache |
Statements and sharing
| Method and path | Access | Purpose |
|---|---|---|
GET /projects/:projectId/statements/:statementId/csv |
unlocked | Download a statement as CSV |
POST /projects/:projectId/statements/export |
unlocked | Return up to 500 statements to your browser, which encrypts them with a password before saving |
POST /projects/:projectId/statements/:statementId/shares |
unlocked | Store an encrypted share. Only ciphertext reaches the server |
POST /projects/:projectId/statements/:statementId/shares/:shareId/revoke |
unlocked | Revoke a share link |
GET /public-shares/:shareId |
none | Fetch a share's ciphertext for the recipient's browser to decrypt |
POST /public-shares/:shareId/acknowledgments |
none | Record the recipient's own acknowledgment, once per link |
Backups
| Method and path | Access | Purpose |
|---|---|---|
POST /backup |
unlocked | Download an encrypted backup of every table |
POST /backup/verify |
unlocked | Check that a backup opens with its password. Writes nothing |
Rate limits
Unauthenticated and sensitive endpoints are limited for each client address or session, for example 8 sign-ins every 5 minutes for each address and each email, 6 unlock attempts every 5 minutes, 5 setup attempts a minute, 10 step-ups every 5 minutes, 10 prepares a minute and 30 share fetches a minute. A limited request gets RATE_LIMITED with canRetry: true.