Every request carries a bearer token in the Authorization header. A token belongs to exactly one company, and every request reads and writes that company’s books only; there is no company ID in the URL.
Authorization: Bearer numi_4f1c...| API key | OAuth access token | |
|---|---|---|
| For | Your own company, scripts and back-office jobs | Apps that other companies connect to |
| Acts as | The company, with no user | The user who approved the connection |
| Lifetime | Until revoked | 1 hour, refreshed with a rotating refresh token |
| Created in | Settings → API & apps | The consent screen in your app’s flow |
Scopes
Both kinds of token carry scopes. Each names a resource and what may be done with it: read, write to create and change drafts and records, or book to post to the ledger. A request without the scope it needs fails with 403 insufficient_scope, and the error’s scope field names what the route asks for.
| Resource | Read | Write | Book |
|---|---|---|---|
| Company, accounting years, chart of accounts and VAT codes | setup:read |
||
| Balances and trial balance | reports:read |
||
| Journal entries and ledger entries | journal:read |
journal:write |
journal:book |
| Invoices | invoices:read |
invoices:write |
invoices:book, which also sends |
| Contacts | contacts:read |
contacts:write |
|
| Products | products:read |
products:write |
|
| Documents | documents:read |
documents:write |
|
| Bank accounts and bank lines | bank:read |
Scopes are independent: journal:book does not include journal:write, and no write scope includes reading. Ask for every level you use, and no more. GET /v1/me shows what a token can do, and each endpoint in the API reference lists the scopes it accepts.
Booking scopes are for API keys, for your own app in your own company, and for reviewed partner apps. Booking through the API is your own approval of the entry: see Drafts and booking.
API keys
Create a key in Numina under Settings → API & apps and pick its permissions: read only, read and draft, or full access, which can also post to the ledger. Custom permissions set a level for each resource. Copy the key when it is shown: it is shown once. Keys start with numi_, and any member of the company can create them and revoke them from the same page.
OAuth
Numina is an OAuth 2.1 authorization server. Apps use the authorization code flow with PKCE (S256); the user signs in to Numina, picks the company to connect, and approves your scopes.
Send the user to the authorization endpoint
Generate a
code_verifier, derive itscode_challenge, and redirect:Text https://api.numina.app/oauth/authorize ?response_type=code &client_id=YOUR_CLIENT_ID &redirect_uri=https://yourapp.com/callback &scope=setup:read journal:read journal:write &state=RANDOM_STATE &code_challenge=CODE_CHALLENGE &code_challenge_method=S256Exchange the code for tokens
Numina redirects back with
codeand yourstate. Exchange the code within a few minutes:cURL curl -X POST https://api.numina.app/oauth/token \ -u YOUR_CLIENT_ID:YOUR_CLIENT_SECRET \ -d grant_type=authorization_code \ -d code=AUTHORIZATION_CODE \ -d redirect_uri=https://yourapp.com/callback \ -d code_verifier=CODE_VERIFIERSend the client ID and secret as HTTP Basic authentication, as
-udoes here. A secret in the request body is refused withinvalid_client.The response holds an
access_tokenvalid for an hour and arefresh_tokenvalid for 30 days.Refresh before the access token expires
cURL curl -X POST https://api.numina.app/oauth/token \ -u YOUR_CLIENT_ID:YOUR_CLIENT_SECRET \ -d grant_type=refresh_token \ -d refresh_token=REFRESH_TOKENRefresh tokens rotate: every refresh returns a new one and retires the old. Store the new token before you use the access token. A retired refresh token used more than 30 seconds after its refresh ends the connection, and the user has to connect again.
The server’s endpoints are also published as metadata at /.well-known/oauth-authorization-server, so standard OAuth libraries can discover them.
Revocation
A user can disconnect your app in Numina at any time, which revokes its tokens immediately. Expect a 401 on any request and send the user through the flow again.