Skip to content
Ledger by RODMENA
Menu

Documentation

Sub-tenants

Give each of your own clients an isolated ledger. Production and sandbox tenants, keys issued and revoked, suspend and restore, all through the API.

This page was last updated on 2026-10-15.

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_partner role

Who this is for

You want toRead
Post transactions for your own businessGetting started
Give your clients each an isolated ledgerthis 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_partner cannot 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 404 and never 403, 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

StatusCodeMeans
403FORBIDDENyour key lacks ledger_partner — a posting key cannot administer clients
403TENANT_SUSPENDEDthe tenant, or its parent, is not active
404SUB_TENANT_NOT_FOUNDnot your client — you can only administer your own
422INVALID_TENANThierarchy rule: no nesting, no reparenting, kind is immutable
422INVALID_KEYbad role, missing label, or ttl_days outside 1–36500

Next