Authentication & Tokens
The API gateway issues HS256 JWTs. Every call (except login) requires a bearer token in the Authorization header.
How it works
The api gateway validates credentials and mints a signed JWT (HS256). Each incoming request is verified against the JWT signature and a server-side session store — so logout is instant and revocation is real.
Tokens are accepted in two ways:
Authorization: Bearer <token>— standard header (preferred)?token=<token>— query parameter (for webhooks / download links)
POST /sessions — Login flows
All flows use POST /sessions. The type field selects the authentication mechanism:
| type | Required fields | Use case |
|---|---|---|
email | email, password | Standard login |
phone | phone, otp | Phone OTP (after POST /sessions/request) |
link | token | Magic link (after POST /sessions/request) |
token | token | One-time token (e.g. from POST /sessions/api/handoff), revoked on use. The session lasts 1 hour. |
external | externalEntity, externalApp | External partner SSO |
Example — email login
curl -X POST https://api.staging.contabeleza.com.br/sessions \
-H "Content-Type: application/json" \
-d '{"type":"email","email":"you@example.com","password":"secret"}'
Successful login response
On success the gateway returns a JSON object (not JSON:API) with the token and enriched session data:
{
"success": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiJ9…",
"token-type": "user",
"account": {
"id": "3",
"type": "personal",
"email": "you@example.com"
},
"businesses": [
{
"id": "900001",
"brand": "contabeleza",
"permissions": ["invoices:read", "invoices:write"]
}
]
}
}
Store data.token and pass data.businesses[0].id as the ilm-business header on subsequent calls.
JWT claims reference
| Claim | Type | Description |
|---|---|---|
accountId | string | The authenticated account id |
type | string | user | admin | token | external-app |
restrictedTo | string | Business id scope |
jti | string | Unique token id — stored in the server-side session store; revoked on logout |
iat | timestamp | Issued-at (Unix seconds) |
exp | timestamp | Expiry (Unix seconds) |
Token refresh
Call POST /sessions/token-refresh before exp to get a fresh token without re-authenticating:
curl -X POST https://api.staging.contabeleza.com.br/sessions/token-refresh \ -H "Authorization: Bearer $TOKEN"
Returns a new data.token. The old token is revoked immediately.
Forgot password flow
Three-step sequence — no auth required:
- Request reset —
POST /sessions/password/forgotwith{"email":"…"} - Validate token —
POST /sessions/password/validate-tokenwith{"reset-token":"…"} - Set new password —
POST /sessions/password/resetwith{"reset-token":"…","password":"…"}
API keys (external apps)
For machine-to-machine integrations use an API key, sent raw in the Authorization header — no Bearer or ApiKey prefix. The gateway converts it into a short-lived external-app JWT and checks the key's permissions on every call:
curl https://api.staging.contabeleza.com.br/fiscal-entities \ -H "Authorization: your-api-key-here"
An external app can also act for its own linked users:
POST /accounts/external— create or link the user's Contabeleza account and store anExternalCred(identity_key= the user's id on the app).POST /sessions/api{"accountId": "<identity_key>"}— a regular session for that user.POST /sessions/api/handoff— a one-time login token for that user (see below).
Manage API keys at GET/POST /external-app-tokens (admin).
Hand-off from ilove.me
ilove.me PRO users open Contabeleza from the account menu (Acessar Contabeleza) without logging in again. There is no OAuth server: ilove.me is an external app of Contabeleza, and each user is linked once through an ExternalCred.
- The ilove.me frontend calls
POST api.ilove.me/sessions/externalwith{"external-app":"contabeleza","handoff":true}and the user's ilove.me token. - First time only: the ilove.me API calls
POST /accounts/externalhere with its API key, creating or linking the Contabeleza account, and stores the link on its side too. - The ilove.me API calls
POST /sessions/api/handoffand gets atoken-type JWT valid for 30 seconds, revoked on first use. - It returns
{"data":{"url":"https://mei.contabeleza.com.br/session/login-by-token/<token>"}}and the frontend opens it. - mei-contabeleza exchanges the token with
POST /sessions(loginType: "token"). Users withpermissions.management.accountingon a business (or admins) stay onmei.contabeleza.com.br; everyone else is sent tomeipro.contabeleza.com.brwith the session cookie set on.contabeleza.com.br.
On staging the hosts are mei.staging.contabeleza.com.br and meipro.staging.contabeleza.com.br.
Managers
When the user is a manager (professional category manager) of the business selected in ilove.me, the ilove.me API first calls POST /sessions/api/business-access here with the user's contacts and the ilove.me business id. The business is found through accounting: the fiscal entity owned by the ilove.me business, and any Contabeleza business owning a fiscal entity with the same CNPJ. The user must be found (credential, or phone and email) and work in that business.
When not (reason: business_not_found, account_not_found, not_a_member), no account is created or linked: /sessions/external answers {"data":{"present-services":true,"reason":"…"}} and ilove.me shows its Contabeleza services page, with WhatsApp buttons for Quero saber mais and Já tenho contrato.
Account linking rules
POST /accounts/external never attaches an external user to someone else's account by guesswork:
- An existing credential for the same
identity_keywins. - An existing account is linked only when its phone and email both match, and only when the app sends
contacts_verified: true. ilove.me sends it when its account has logged in at least once. - A single matching contact never links accounts.
account_idis ignored for external-app callers. - Otherwise a new
personalaccount is created — without the phone if another account already has it.
Configuration (ilove.me API)
EXTERNAL_APPS_CONFIG = {
"contabeleza": {
"apiKey": "<contabeleza API key for iloveme>",
"apiUrl": "https://api.contabeleza.com.br",
"webUrl": "https://mei.contabeleza.com.br"
}
}
webUrl is required for hand-off; without it /sessions/external answers 500 ExternalAppWebUrlMissing. The ilove.me menu entry is behind the contabeleza-access feature flag (on in development, staging and qa).
Logout
DELETE /sessions removes the token's JTI from the server-side session store immediately. Any in-flight requests using the same token after this call will receive 401 Unauthorized.
curl -X DELETE https://api.staging.contabeleza.com.br/sessions \ -H "Authorization: Bearer $TOKEN"
Using the token
Swagger UI — open /docs, click Authorize, paste the token (without the Bearer prefix).
curl:
TOKEN=eyJhbGciOi... curl https://api.staging.contabeleza.com.br/accounts/me \ -H "Authorization: Bearer $TOKEN" \ -H "ilm-business: 900001"