Authentication
The Magic Dashboard API uses a Kobana Account access token to authenticate every request.
The token
The token is an RS256 JWT issued by the Kobana Authorization Server (account.kobana.com.br). This API only verifies it, against the public key that server publishes — it holds no signing key and cannot issue any token. The org claim identifies your organization: all returned data is automatically scoped to it.
Requirements checked on every request:
| Claim | Requirement |
|---|---|
iss | The Authorization Server identifier, as a URL: https://account[.{env}].kobana.com.br |
aud | This API's URL: https://api.pages[.{env}].kobana.com.br. May be a list — the rule is contains, not equality |
exp | Required. A token without expiration is refused |
org | The Kobana account the token acts on behalf of — it resolves the organization here, and it is required |
sub | The person (users.public_id). Used for auditing and attribution; it scopes nothing |
jti | Unique token id — the basis for revocation and auditing |
:::caution Change from the API key
Until September 2026 the credential was a key created in the dashboard (kb_…), with a public prefix and a secret shown only once. The key was both the credential and what verified it — whoever read the key store could act as any organization, and every rotation was a conversation with a customer. Now the Authorization Server issues tokens and the platform only verifies them. A key created in Settings → API no longer authenticates this API.
:::
The token is different between Sandbox and Production — one environment's aud is not valid in the other. Do not reuse tokens across environments.
Sending the token
Include the Authorization: Bearer <token> header in every request.
export KOBANA_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxx
curl -i \
-H "Authorization: Bearer $KOBANA_TOKEN" \
-H 'Content-Type: application/json' \
-H 'User-Agent: My Name (myname@example.com)' \
-X GET 'https://api.dashboards.sandbox.kobana.com.br/v1/organization'
Valid request
With a valid token, the response carries the authenticated Organization's data.
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Request-Id: req_01HABCDEF
{
"id": "org_01HORG",
"name": "My Company",
"email": "contact@mycompany.com",
"document_type": "cnpj",
"document_number": "12345678000190",
"environment": "sandbox",
"created_at": "2026-01-01T00:00:00.000Z"
}
Invalid request
A missing, malformed or revoked token returns 401 Unauthorized.
curl -i \
-H "Authorization: Bearer invalidtoken" \
-H 'Content-Type: application/json' \
-H 'User-Agent: My Name (myname@example.com)' \
-X GET 'https://api.dashboards.sandbox.kobana.com.br/v1/organization'
HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
{
"error": {
"code": "invalid_token",
"message": "Invalid or expired access token."
}
}
Fine-grained codes help you find the error right away: invalid_token (missing, malformed or invalid signature), invalid_audience (token for another Kobana app), token_revoked and unknown_organization — the latter is a 403: the token is valid, the organization just isn't here.
If our Authorization Server is down, the response is 5xx, not 401. This is deliberate: a 401 would send you debugging a perfectly good credential during our outage. Try again in a moment.
:::note sub is not the tenant
sub is the person and org is the organization — they are uuids from different tables, so swapping them raises no format error. A token without org is refused instead of falling back to sub: that fallback would match no organization and answer 403 to everything, with the symptom pointing to permissions rather than to the claim that was read.
:::
Scopes
Token scopes have two orthogonal axes: the resource, always prefixed by the app (dashboards/users), and the verb, a global scope without prefix (write). A resource without a verb means read. Writing requires both: the resource scope and the verb.
| Scope | What it reaches |
|---|---|
dashboards/builder | Portal builder: pages, draft, publishing and versions |
dashboards/users | Portal users and the mapping that creates them |
dashboards/portal | The portal: access, branding, look, SEO, tracking, domains and sign-in providers |
dashboards/connectors | Connectors (data sources) |
dashboards/entities | Entities and their records |
dashboards/assets | File library |
dashboards/ai | AI usage and billing |
dashboards/workspaces | Workspaces |
dashboards/members | Dashboard members, roles and permissions |
dashboards/admin | Organization and settings, notifications and e-mail templates, events, webhooks, audit, API keys, backups, exports, imports, usage and rate limits |
| Resource wildcard | dashboards/all — reads all the resources above, and grants no verb |
| Global verb | write — every write (POST, PUT, PATCH, DELETE) on the resources the token carries |
Two examples: dashboards/users reads portal users and does not write; dashboards/users write also invites, updates and deletes them, and still does not reach the dashboard members.
The granular form dashboards/users:write is an exception in the grammar, not the default format: it satisfies both axes for that one resource, and exists for "read one resource, write another". dashboards/* is a registration wildcard and is never issued in a token — matching here is always literal. There is no read scope: reading is the resource without a verb.
:::info Scopes are not enforced yet
The scopes above are already this API's vocabulary, but they are not enforced yet: they first need to be registered in the Authorization Server, and until then no token in circulation carries a scope — enforcing now would either allow everything (theater) or block every live integration. Today what binds the token to this API is the aud, which the verifier requires. Once enforcement starts, a missing scope answers 403 insufficient_scope; ask for the minimum scope of each integration now so you don't need to change it later.
:::
Best practices
- Never commit tokens to git. Use environment variables or vaults (AWS Secrets Manager, Vault, etc.).
- Use separate tokens per integration (CRM, ERP, frontend, etc.) — it makes revoking one easy without taking down the others, and auditing by
jtistays readable. - Ask for the minimum scopes needed (principle of least privilege) — a resource without a verb when the integration only reads.
- Revoke any leaked token immediately: revocation is by
jtiand applies on the next call, without waiting forexp. - HTTPS is mandatory — HTTP calls are rejected.