Skip to content
AXOWLDevelopers
Quickstart

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.

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:

KeyWhat it reaches
Org-wide keyEvery endpoint for your own org.
Key scoped to one App Group, or to one applicationOnly 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 keyThe 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.

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: trueThe org can sign. Safe to bind your resources to it.
ready: falseThe 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.

ready = the org has an Org Master Seal
AND its owner has registered a Personal Seal

Both 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.

Owning an organization and being able to use one are different things, and so are the messages:

SituationTell the user
No Axowl account at allThey need to become an account owner first — see The account-owner gate
Account, no orgCreate an organization on Axowl
Org exists, ready: falseSetup isn’t finished — register the seal. Do not say “your organization was created” as if it succeeded
ready: true, already boundChoose a different organization
ready: true, freeProceed

Paths are relative to /api/public/v1. Every endpoint takes X-Api-Key unless marked anonymous.

MethodPathWhat it does
GET/meThe org that issued the key (ready included).
GET/app-groupsYour App Groups: [{ id, name, description, iconUrl, brandColor, isPublic }].
GET/orgs/operatorsActive Owner and Admin members with their login email.
POST/end-users/importBulk-import end users from another auth provider.
GET/monetizationYour billing policy: take rate, volume cap, which App Groups collect. See Billing & monetization.
MethodPathWhat it does
GET/invoicesMonthly statements, newest first. Query limit (default 24, max 120).
GET/invoices/{id}One statement with its lines and seal evidence.
GET/invoices/{id}/verifyAnonymous. Recomputes a sealed statement’s hash. 403 UPGRADE_REQUIRED unless the issuing org is on the Business plan or higher.

See Invoices.

MethodPathWhat it does
GET/filesList files. Query trashed, q, page, pageSize (max 200).
POST/filesUpload a file (multipart/form-data, part file).
GET/files/{id}/downloadGet a download link.
DELETE/files/{id}Move a file to trash.
POST/files/{id}/restoreRestore a file from trash.
PATCH/files/{id}/shareChange a file’s share mode.
POST/files/{id}/share/rotateRotate a file’s share link.
GET/storage/usageStorage used, limit, and what is in trash.
GET/storage/lifecycle/rulesList lifecycle rules.
POST/storage/lifecycle/rulesCreate a lifecycle rule.
PUT/storage/lifecycle/rules/{id}Update or pause a rule.
DELETE/storage/lifecycle/rules/{id}Remove a rule.

See Storage.

All of these except /orgs/owner-status need a platform partner key (403 otherwise).

MethodPathExtra authWhat 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-statusBearerWhether 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-profileBearer (owner)Write an org’s public description and website.
POST/orgs/upload-logoBearer (owner)Upload an org logo.
POST/orgs/app-groups—App Groups of the org matched by owner email and name.
POST/orgs/app-groups/createBearer (owner)Create an App Group in an org the person owns.
POST/app-groups/by-key—One App Group by its appgroup_… key.

For a payment engine that charges on behalf of Axowl orgs. All need a platform partner key.

MethodPathExtra authWhat it does
POST/orgs/monetization—Another org’s billing policy. Body { orgId }. App Groups come back as keys without names.
POST/orgs/seller-policy/acceptBearer (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.

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.

POST /api/public/v1/orgs
X-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.

POST /api/public/v1/orgs/by-owner
X-Api-Key: ah_live_…
{ "ownerEmail": "[email protected]" }

Returns every org that email owns, each with ready.

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.

POST /api/public/v1/orgs/by-id
X-Api-Key: ah_live_…
{ "orgId": "8f3a…" }

Returns the org — id, name, slug, ready and its public profile fields. 404 means no such org.

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.

POST /api/public/v1/app-groups/by-key
X-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.

POST /api/public/v1/orgs/app-groups/create
X-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.

POST /api/public/v1/orgs/update-profile
X-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-logo
Content-Type: multipart/form-data

Form fields: orgId and a logo file (PNG, JPEG, WebP or SVG, up to 5 MB), with the same two headers. Returns { logoUrl }.

GET /api/public/v1/me

Returns 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.

GET /api/public/v1/orgs/operators

Returns 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",
"operators": [ { "email": "[email protected]", "role": "Owner" },
{ "email": "[email protected]", "role": "Admin" } ] }

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.

POST /api/public/v1/end-users/import
X-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.

GET /api/public/v1/invoices
GET /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.

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.