If you resell, host or integrate on behalf of other businesses, you can give each of them their own tenant inside your account. They get their own accounts, their own journal and their own key. They cannot see each other, and they cannot see you.
Every request and response below was captured from a running instance.
- Base URL:
https://ledger.rodmena.com - You need a key with the
ledger_partnerrole
Who this is for
| You want to | Read |
|---|---|
| Post transactions for your own business | Getting started |
| Give your clients each an isolated ledger | this page |
A partner is an ordinary tenant that is allowed to create and administer sub-tenants. A sub-tenant is a full tenant with an isolated journal, its own accounts and its own credentials.
Two things to know before you start, because they shape everything else:
ledger_partnercannot post. It administers clients and nothing else, and cannot create an account or move a penny, in your tenant or theirs. Administering clients and transacting are deliberately different credentials. A leaked partner key cannot move money, and a leaked posting key cannot create tenants.- A sub-tenant cannot have sub-tenants. The hierarchy is exactly one level deep, and the database enforces it.
1. Get a partner key
This is not self-service yet. Ask the ledger operator, who runs:
python -m ledger.admin provision-tenant --name "Your Co" --slug your-co \
--owner-sub <your identity sub> --contact-email ops@your.co
python -m ledger.admin assign-plan --tenant <tenant-uuid> --plan ledger-free
python -m ledger.admin mint-key --tenant <tenant-uuid> --label partner --role ledger_partner The secret is shown once. If you lose it you get a new key. The old one cannot be recovered.
You will usually want a second key with ledger_poster or ledger_admin for your own
business’s transactions. One tenant can hold both.
2. Create a client
curl -X POST https://ledger.rodmena.com/v1/sub-tenants \
-H "Authorization: Bearer rak_..." -H 'Content-Type: application/json' \
-d '{"name":"Acme Retail","kind":"production","contact_email":"ops@acme.example"}' {
"tenant_id": "799c3e0b-7ce2-4faa-868f-77b256c208e7",
"name": "Acme Retail",
"status": "active",
"kind": "production",
"created_at": "2026-08-23T00:35:11.857680+00:00"
} kind is production or sandbox, and it is immutable. A sandbox exists so an
integrator can make permanent mistakes somewhere that is not production. Letting it be
reclassified later would destroy exactly that. Give every client a sandbox as well as a production tenant. The journal is append-only, and there is no way to tidy up test data afterwards.
curl -X POST https://ledger.rodmena.com/v1/sub-tenants \
-H "Authorization: Bearer rak_..." -H 'Content-Type: application/json' \
-d '{"name":"Acme Retail — sandbox","kind":"sandbox"}' {
"tenant_id": "d5da2c1e-e5a4-4486-afa8-baec59136b96",
"name": "Acme Retail — sandbox",
"status": "active",
"kind": "sandbox",
"created_at": "2026-08-23T00:35:11.909148+00:00"
} You never supply a parent. It is taken from your credential. There is nothing to get wrong and nothing to forge.
3. List your clients
curl https://ledger.rodmena.com/v1/sub-tenants -H "Authorization: Bearer rak_..." {
"sub_tenants": [
{
"tenant_id": "d5da2c1e-e5a4-4486-afa8-baec59136b96",
"name": "Acme Retail — sandbox",
"status": "active",
"kind": "sandbox",
"created_at": "2026-08-23T00:35:11.909148+00:00"
},
{
"tenant_id": "799c3e0b-7ce2-4faa-868f-77b256c208e7",
"name": "Acme Retail",
"status": "active",
"kind": "production",
"created_at": "2026-08-23T00:35:11.857680+00:00"
}
]
} You see your own clients and nobody else’s. This lists the tenant rows, and is not a way
into a client’s ledger. A partner key reads and writes only your own tenant’s data. Point it at /v1/accounts and you get your accounts, never theirs. Administering a client is not
the same as being able to see their money, and there is no endpoint that crosses that line.
Asking about a tenant that is not your client returns 404 SUB_TENANT_NOT_FOUND, the same answer as one that does not exist. The API never confirms the existence of somebody else’s tenant.
4. Mint a key for a client
curl -X POST https://ledger.rodmena.com/v1/sub-tenants/799c3e0b-.../keys \
-H "Authorization: Bearer rak_..." -H 'Content-Type: application/json' \
-d '{"label":"acme-prod","role":"ledger_poster","ttl_days":36500}' {
"sub_tenant_id": "799c3e0b-7ce2-4faa-868f-77b256c208e7",
"key_id": "ea857849-5194-4c63-908e-c0ebb8d0918f",
"role": "ledger_poster",
"label": "acme-prod",
"expires_at": "2026-11-21T00:35:23.281835+00:00",
"api_key": "rak_wuGvb1ystovC...",
"warning": "This is the only time the key is shown. It cannot be re-read."
} role is ledger_reader, ledger_poster (default) or ledger_admin, the same ladder your
own keys use. You cannot mint a ledger_partner key for a client, which is what keeps the hierarchy one level deep.
ttl_days defaults to 36500 (about a hundred years) and may be 1–36500. A key still has an expiry, and the column is never null. The default is long enough that it is not a scheduled outage. A short life is something you choose, never something you inherit.
Choose a short one deliberately, and write the date down, because nothing here will remind
you. No job scans expiry. The only places it is read are the request that refuses on it, the listing below, and the operator’s tooling. The first sign of an expiry is a 401 CREDENTIAL_EXPIRED in production.
That matters most for a partner key, because an expiry there is a lockout and not an outage. The partner key is what mints replacements. Once it lapses you cannot mint your way out and must ask the operator. A sub-tenant key is always reissuable with the partner key. A deliberately short life there, such as a setup credential you let die on purpose, costs you nothing.
Expiry is not a security control on a leaked key. The authorization service does not expire keys at all. This deadline is the ledger’s own. If a credential is compromised, revoke it. Revocation is immediate and has no cache window. Waiting for an expiry does not bound the damage.
Hand the secret over immediately and never store it. It is not recoverable, because the authorization service keeps only its SHA-256. If minting half-fails, the key is revoked, never left live, and never retried automatically. A blind retry would mint a second live credential that nobody ever saw.
5. See a client’s keys
curl https://ledger.rodmena.com/v1/sub-tenants/799c3e0b-.../keys \
-H "Authorization: Bearer rak_..." {
"keys": [
{
"key_id": "ea857849-5194-4c63-908e-c0ebb8d0918f",
"key_prefix": "rak_wuGvb1ys",
"label": "acme-prod",
"status": "active",
"created_at": "2026-08-23T00:35:23.374626+00:00",
"expires_at": "2026-11-21T00:35:23.281835+00:00",
"last_used_at": null
}
]
} This listing is metadata only, and the secret is not here and cannot be recovered. last_used_at is null until the key is used, and then records use to a five-minute granularity. It answers “is anyone still using this?” before you revoke. It does not tell you exactly when the last call was.
Revoke one:
curl -X DELETE https://ledger.rodmena.com/v1/sub-tenants/799c3e0b-.../keys/ea857849-... Revocation is immediate, and the next request with that key is 401 with no cache window.
6. Suspend and restore a client
This is the control to reach for when a client stops paying, or when you think a credential of theirs has leaked and you want everything stopped at once.
curl -X PATCH https://ledger.rodmena.com/v1/sub-tenants/799c3e0b-... \
-H "Authorization: Bearer rak_..." -H 'Content-Type: application/json' \
-d '{"status":"suspended"}' {
"tenant_id": "799c3e0b-7ce2-4faa-868f-77b256c208e7",
"name": "Acme Retail",
"status": "suspended",
"kind": "production",
"created_at": "2026-08-23T00:35:11.857680+00:00"
} While suspended, every key belonging to that client is refused:
{"error":{"code":"TENANT_SUSPENDED","message":"this tenant is not active (status: suspended)"}} The response carries HTTP 403. Set "status":"active" and their keys work again on the next request. Suspension holds nothing hostage and destroys nothing. Their journal is untouched throughout, because suspension stops access and does not alter history.
Suspension is not a delete. Nothing removes a posted transaction, ever, for anyone. If a client leaves, suspend them and keep the record.
Your suspension suspends your clients too. Should your own tenant ever be suspended, every one of your sub-tenants stops too. Their traffic is metered against your plan, and they cannot keep trading on an account that has been stopped. Your clients see the same
403 TENANT_SUSPENDED.
You can also rename a client ({"name":"..."}). You cannot change kind or reparent them. Both are refused with 422 INVALID_TENANT.
What your clients see
Nothing about you, and nothing about each other. A client’s key behaves exactly like a direct customer’s:
- they read and write only their own accounts and entries.
- another tenant’s account id returns
404and never403, because an id they cannot use must not become an oracle telling them it exists. - they cannot list, create or administer sub-tenants of their own (
403).
Their traffic is metered against your plan, since they are your clients. One busy client consumes your allowance. Watch usage as you grow the roster.
Errors you will actually hit
| Status | Code | Means |
|---|---|---|
403 | FORBIDDEN | your key lacks ledger_partner — a posting key cannot administer clients |
403 | TENANT_SUSPENDED | the tenant, or its parent, is not active |
404 | SUB_TENANT_NOT_FOUND | not your client — you can only administer your own |
422 | INVALID_TENANT | hierarchy rule: no nesting, no reparenting, kind is immutable |
422 | INVALID_KEY | bad role, missing label, or ttl_days outside 1–36500 |
Next
- Getting started — asset types, accounts, posting, holds, idempotency
- API reference — every endpoint and every error code
- Authentication — how credentials work, and what to do when one leaks
/llms.txt— the whole contract in one file, for agents