Swagger UI openapi.json openapi.yaml

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:

typeRequired fieldsUse case
emailemail, passwordStandard login
phonephone, otpPhone OTP (after POST /sessions/request)
linktokenMagic link (after POST /sessions/request)
tokentokenOne-time token (e.g. from POST /sessions/api/handoff), revoked on use. The session lasts 1 hour.
externalexternalEntity, externalAppExternal 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

ClaimTypeDescription
accountIdstringThe authenticated account id
typestringuser | admin | token | external-app
restrictedTostringBusiness id scope
jtistringUnique token id — stored in the server-side session store; revoked on logout
iattimestampIssued-at (Unix seconds)
exptimestampExpiry (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:

  1. Request reset — POST /sessions/password/forgot with {"email":"…"}
  2. Validate token — POST /sessions/password/validate-token with {"reset-token":"…"}
  3. Set new password — POST /sessions/password/reset with {"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 an ExternalCred (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.

  1. The ilove.me frontend calls POST api.ilove.me/sessions/external with {"external-app":"contabeleza","handoff":true} and the user's ilove.me token.
  2. First time only: the ilove.me API calls POST /accounts/external here with its API key, creating or linking the Contabeleza account, and stores the link on its side too.
  3. The ilove.me API calls POST /sessions/api/handoff and gets a token-type JWT valid for 30 seconds, revoked on first use.
  4. It returns {"data":{"url":"https://mei.contabeleza.com.br/session/login-by-token/<token>"}} and the frontend opens it.
  5. mei-contabeleza exchanges the token with POST /sessions (loginType: "token"). Users with permissions.management.accounting on a business (or admins) stay on mei.contabeleza.com.br; everyone else is sent to meipro.contabeleza.com.br with 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_key wins.
  • 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_id is ignored for external-app callers.
  • Otherwise a new personal account 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"