Public API (ah_live_)
/api/public/v1 is the documented surface for external developers. It is versioned: the
contract is stable and breaking changes go to v2.
Authenticate
Section titled “Authenticate”X-Api-Key: ah_live_…An Org API key identifies one organization and returns that org’s data only — never another org’s. Issue and rotate keys in Settings → Keys. Keys are SHA-256 hashed at rest and shown once.
What a key may call depends on its kind:
| Key | What it reaches |
|---|---|
| Org-wide key | Every endpoint for your own org. |
| Key scoped to one App Group, or to one application | Only GET /me, GET /app-groups (its own group only — for an application key, the App Group that application belongs to) and POST /end-users/import (into that group only). The invoice endpoints answer 403 insufficient_scope; every other endpoint answers 401. |
| Platform partner key | The cross-org endpoints marked Partner below — the ones that look up, create or change an org other than the key’s own. A key that is not a platform partner’s gets 403 on them. Partner access is granted by Axowl to a specific organization; no key setting turns it on. |
Endpoints marked Bearer act on behalf of a signed-in person. They need a second header,
Authorization: Bearer <end-user token>, issued by Axowl for an application of the org that owns
the key. The person is always taken from that token, never from the request body. The token is
checked against the org’s one active application; an org with several active applications has to
have the integration’s application designated on the Axowl side first — see
The account-owner gate.
The whole surface is rate-limited to 60 requests per minute per IP address; beyond that the
answer is 429.
ready — existence is not usability
Section titled “ready — existence is not usability”Every org DTO carries a ready boolean. It is the single most important field on this surface.
{ "id": "…", "name": "Acme", "slug": "acme", "ready": false, "description": null, "website": null, "logoUrl": null, "brandColor": null, "countryIso": "US" }| Meaning | |
|---|---|
ready: true | The org can sign. Safe to bind your resources to it. |
ready: false | The org exists but cannot sign yet. Binding to it produces a shell that fails later. |
Gate on ready, never on mere existence. An org that isn’t ready will accept your binding and
then fail at the moment something has to be sealed — far from where the mistake was made.
How it is decided
Section titled “How it is decided”ready = the org has an Org Master Seal AND its owner has registered a Personal SealBoth halves are required. A corporation’s Master Seal is auto-issued at creation, so on its own it is always true and would never catch an owner who skipped sealing. The owner’s Personal Seal is the real signal.
The state your gate should show
Section titled “The state your gate should show”Owning an organization and being able to use one are different things, and so are the messages:
| Situation | Tell the user |
|---|---|
| No Axowl account at all | They need to become an account owner first — see The account-owner gate |
| Account, no org | Create an organization on Axowl |
Org exists, ready: false | Setup isn’t finished — register the seal. Do not say “your organization was created” as if it succeeded |
ready: true, already bound | Choose a different organization |
ready: true, free | Proceed |
All endpoints
Section titled “All endpoints”Paths are relative to /api/public/v1. Every endpoint takes X-Api-Key unless marked anonymous.
Your own org
Section titled “Your own org”| Method | Path | What it does |
|---|---|---|
GET | /me | The org that issued the key (ready included). |
GET | /app-groups | Your App Groups: [{ id, name, description, iconUrl, brandColor, isPublic }]. |
GET | /orgs/operators | Active Owner and Admin members with their login email. |
POST | /end-users/import | Bulk-import end users from another auth provider. |
GET | /monetization | Your billing policy: take rate, volume cap, which App Groups collect. See Billing & monetization. |
Invoices
Section titled “Invoices”| Method | Path | What it does |
|---|---|---|
GET | /invoices | Monthly statements, newest first. Query limit (default 24, max 120). |
GET | /invoices/{id} | One statement with its lines and seal evidence. |
GET | /invoices/{id}/verify | Anonymous. Recomputes a sealed statement’s hash. 403 UPGRADE_REQUIRED unless the issuing org is on the Business plan or higher. |
See Invoices.
Files and storage
Section titled “Files and storage”| Method | Path | What it does |
|---|---|---|
GET | /files | List files. Query trashed, q, page, pageSize (max 200). |
POST | /files | Upload a file (multipart/form-data, part file). |
GET | /files/{id}/download | Get a download link. |
DELETE | /files/{id} | Move a file to trash. |
POST | /files/{id}/restore | Restore a file from trash. |
PATCH | /files/{id}/share | Change a file’s share mode. |
POST | /files/{id}/share/rotate | Rotate a file’s share link. |
GET | /storage/usage | Storage used, limit, and what is in trash. |
GET | /storage/lifecycle/rules | List lifecycle rules. |
POST | /storage/lifecycle/rules | Create a lifecycle rule. |
PUT | /storage/lifecycle/rules/{id} | Update or pause a rule. |
DELETE | /storage/lifecycle/rules/{id} | Remove a rule. |
See Storage.
Partner: organizations and App Groups
Section titled “Partner: organizations and App Groups”All of these except /orgs/owner-status need a platform partner key (403 otherwise).
| Method | Path | Extra auth | What it does |
|---|---|---|---|
POST | /orgs | — | Create an org owned by ownerEmail and return a fresh key for it. |
POST | /orgs/by-owner | — | Every org an email owns. |
POST | /orgs/match | — | The org an email owns with a given name. |
POST | /orgs/by-id | — | One org by id. |
POST | /orgs/owner-status | Bearer | Whether the signed-in person has an owner account, and the orgs they own. Any org-wide key. See The account-owner gate. |
POST | /orgs/update-profile | Bearer (owner) | Write an org’s public description and website. |
POST | /orgs/upload-logo | Bearer (owner) | Upload an org logo. |
POST | /orgs/app-groups | — | App Groups of the org matched by owner email and name. |
POST | /orgs/app-groups/create | Bearer (owner) | Create an App Group in an org the person owns. |
POST | /app-groups/by-key | — | One App Group by its appgroup_… key. |
Partner: payment engine signals
Section titled “Partner: payment engine signals”For a payment engine that charges on behalf of Axowl orgs. All need a platform partner key.
| Method | Path | Extra auth | What it does |
|---|---|---|---|
POST | /orgs/monetization | — | Another org’s billing policy. Body { orgId }. App Groups come back as keys without names. |
POST | /orgs/seller-policy/accept | Bearer (owner) | Record that the org’s owner accepted the current seller policy. Body { orgId }. 403 OWNER_ONLY if the person is not the owner. |
POST | /marketplace/purchases | — | Report a marketplace purchase result: event: "paid" installs the app for the buying org, "canceled" removes it. Safe to resend. |
POST | /app-purchases | — | Report an app member’s order state (paid, renewed, past due, cancel requested, ended, refunded). Signals older than the last one applied are ignored. Safe to resend. |
POST | /marketplace/issuers/status | — | For up to 200 issuers (groupKey, orgId or both) answer active, suspended, deleted or unknown, in request order. |
POST | /billing/topups | — | Report a completed wallet top-up so it is recorded in the org’s audit ledger. No money moves here. |
Endpoints that need explaining
Section titled “Endpoints that need explaining”These are the ones where calling them correctly is not obvious from the schema. The first eight —
from Create an org to Write back an org’s profile and logo — are Partner endpoints: an
ordinary org key gets 403 on them.
Create an org
Section titled “Create an org”POST /api/public/v1/orgsX-Api-Key: ah_live_…
{ "ownerEmail": "[email protected]", "name": "Acme", "slug": "acme", "type": "company", "countryIso": "US" }Creates an org owned by ownerEmail; if no account has that email, one is created. Returns
{ id, slug, name, apiKey } — apiKey is a new ah_live_ key for the created org.
ownerEmail, name, slug and countryIso are required (400); taxId is optional. type is
corporation (also corp, company) or spc; any other value, or none, creates a sole
proprietorship. A slug that is already taken answers 409.
List the orgs an email owns
Section titled “List the orgs an email owns”POST /api/public/v1/orgs/by-ownerX-Api-Key: ah_live_…
Returns every org that email owns, each with ready.
Match one org by name
Section titled “Match one org by name”POST /api/public/v1/orgs/match{ "ownerEmail": "…", "orgName": "Acme" }Returns the org that email owns whose name equals orgName (case-insensitive). 404 when no
account has that email, or when that email owns no org with that name.
Read one org by id
Section titled “Read one org by id”POST /api/public/v1/orgs/by-idX-Api-Key: ah_live_…
{ "orgId": "8f3a…" }Returns the org — id, name, slug, ready and its public profile fields. 404 means no such org.
List an org’s app groups (brands)
Section titled “List an org’s app groups (brands)”POST /api/public/v1/orgs/app-groups{ "ownerEmail": "…", "orgName": "Acme" }Returns [{ key, name, description }]. key is the App Group’s public key (appgroup_…) — the
only identifier this surface joins on. Store that, and send it back as groupKey below and
wherever a brand binds to a group.
This is every App Group of that org, for partners that treat an App Group as a brand (the org
is the company; an App Group is one product or service line). An empty list is a normal answer: an
org may not use App Groups yet. 404 when the email or the org name does not match, as for
/orgs/match.
Read one app group (brand) by key
Section titled “Read one app group (brand) by key”POST /api/public/v1/app-groups/by-keyX-Api-Key: ah_live_…
{ "groupKey": "appgroup_1c9d…" }Returns { groupKey, id, orgId, name, description, iconUrl, brandColor }. 404 means no such
group — it may have been deleted.
Create an app group (brand)
Section titled “Create an app group (brand)”POST /api/public/v1/orgs/app-groups/createX-Api-Key: ah_live_…Authorization: Bearer <end-user token your org signed>
{ "orgId": "…", "name": "Acme Pay", "description": "…", "iconUrl": "https://…", "brandColor": "#ff6d48" }Creates an App Group in an org the signed-in user owns, and returns it in the same shape as
by-key above — keep the groupKey. Use it when a brand is being listed on your side, so the
customer never has to leave for the dashboard to make the group first.
It runs the dashboard’s own creation, not a copy of it: the same permission check, the same first
app (Web, Development), the same audit event and owner notification. The person comes from the
Bearer token, never from the body — 403 means that person does not own the org (or your key is not
a platform partner’s); 403 with errorCode: "PERMISSION_DENIED" means the owner is not permitted
to create App Groups. orgId and name are required (name ≤ 200), description ≤ 1000,
iconUrl ≤ 500, and brandColor must be #RRGGBB. name must be unique within the org
(case-insensitive, deleted groups not counted) — a name already in use is a 400 whose error
says so.
An App Group is the brand: the org is the company, and each App Group is one product or service line. If you are listing brands, read the name and branding from here instead of asking the customer to type them into a second form. Two hand-entered copies of one brand name drift apart the day either side is edited, and the customer has no way to tell which one is showing.
orgId comes back so you can confirm the group still belongs to the company you think it does,
rather than trusting a binding you stored earlier.
Write back an org’s profile and logo
Section titled “Write back an org’s profile and logo”POST /api/public/v1/orgs/update-profileX-Api-Key: ah_live_…Authorization: Bearer <end-user token your org signed>
{ "orgId": "…", "description": "…", "website": "https://…" }Updates the public description and website of an org the signed-in person owns and returns the
org. null leaves a field unchanged; "" clears it. 403 means the person does not own the org.
POST /api/public/v1/orgs/upload-logoContent-Type: multipart/form-dataForm fields: orgId and a logo file (PNG, JPEG, WebP or SVG, up to 5 MB), with the same two
headers. Returns { logoUrl }.
The org behind the key
Section titled “The org behind the key”GET /api/public/v1/meReturns the org that issued the key, in the same shape as the other org lookups. A key scoped to one App Group may call it.
The org’s operators (Owner and Admin)
Section titled “The org’s operators (Owner and Admin)”GET /api/public/v1/orgs/operatorsReturns the active Owner and Admin members of the org that issued the key, each with their login email and role:
{ "orgId": "b0111111-0000-4b00-8b00-0000000bbb01",Use it when your product needs to know who runs it — who may open an operator screen, and who
receives operator mail — instead of keeping a hand-written list that drifts from the org’s real
roles. Only the key’s own org is ever returned; app-group-scoped keys get 401. Cache it for a few
minutes on your side; roles change rarely.
Import end users (migration)
Section titled “Import end users (migration)”POST /api/public/v1/end-users/importX-Api-Key: ah_live_…
{ "appGroupId": "…", "source": "clerk", "sendInvites": false, "users": [ … ] }Bulk-imports users exported from another auth provider (Clerk, Kinde, Supabase, Firebase, Auth0,
NextAuth …) into one App Group — max 500 rows per request, each row answering independently
(created / duplicate / invalid / limit) with the new endUserId and the row’s own
sourceUserId for your foreign-key re-mapping. The totals are total, created, duplicates,
invalid and limited (rows refused by the tester limit of an app group in development).
source is a 1–50 character slug of [a-z0-9-_]. A rejected request answers 400 with error set
to no_rows, too_many_rows, invalid_source or invalid_app_group (the group is not one of the
key’s org).
Password hashes, when supplied, are stored dormant (provenance only — no login path reads
them): imported users sign in passwordlessly with the same email. The full walkthrough per source
platform lives in the Migration guides.
Billing statements
Section titled “Billing statements”GET /api/public/v1/invoicesGET /api/public/v1/invoices/{id}Monthly invoices for the calling org. Axowl is prepaid — an invoice records what was charged and whether your credit covered it, never a balance due. Every line carries the ledger reference it was charged under, so amounts are verifiable independently of this API.
Invoices need an org-wide key issued by someone who can view billing (the organization owner). If
the key lists allowed scopes, they must include view.billing. Otherwise the answer is
403 insufficient_scope.
See Invoices.
Re-check on the server
Section titled “Re-check on the server”Treat anything a browser hands back as a claim. A user can navigate straight to your form without
ever visiting Axowl, so re-run the same ready check server-side before you commit the work. This
is the rule the payment flow already follows: the redirect is a hint, the server decides.