Authentication
Two ways to authenticate against the API — and one permission model that ties them together.
Every request needs Authorization: Bearer <token>. The backend
accepts two token shapes:
| Shape | Issued by | Lifetime | Typical use |
|---|---|---|---|
| JWT | POST /login | 24 hours | The admin dashboard, short-lived browser sessions |
Personal Access Token (bod_…) | POST /stores/{id}/api-tokens | Until you revoke | Long-running scripts, integrations, automations |
You can use either interchangeably on every endpoint. The auth
middleware checks the bod_ prefix first and dispatches accordingly.
How a PAT relates to its user
A token is always issued on behalf of a real user. It can never do more than that user can. If the issuing user is later removed from the store, every token they issued stops working — there are no orphan tokens.
This matters for two reasons:
- Audit attribution. When a token writes an order, the change is recorded against the user who issued the token. Combined with the token name, you can trace any change back to a real human plus a piece of software ("Aline issued via the POS terminal").
- Restricted tokens via restricted users. If you want a truly read-only token even though you are the store owner, the cleanest pattern is to invite a teammate ("integrations bot") with limited permissions and issue the token in their name.
The scope bitmask
When you create a token you pick a set of permissions from the same
checklist that appears on the team-permissions page. Internally each
permission is one bit in an i64 mask.
At authentication time:
effective_permission = user.permissions & token.scope_bitsThis is enforced at the lowest level in authz::has_permission, which
means the store-owner shortcut also honors the mask. An owner-issued
PAT with products.read only is genuinely read-only — the system does
not silently expand it.
Bit math
Permission bits go up to position 61 today. JavaScript Number loses
precision above 2^53, so if you're calculating scope masks in JS,
use BigInt. The admin UI handles this for you when you check boxes.
Token format
bod_<43-char-base64>- The
bod_prefix is so GitHub-style secret scanning recognizes a leaked token and notifies you. Don't strip it. - The server stores only a sha256 hash. The raw value is shown to you exactly once on create or rotate.
- Default expiry is "none" — tokens live until you revoke or rotate.
Rotation
Two ways to swap a token's value:
- Rotate — same row, same name, same scope, brand-new raw value. Old value stops working immediately. Use this when a value has leaked.
- Revoke + create new — different row, fresh
last_used_at. Use this when the use case has changed.
Both flows return the new raw token once in the response body. After that, gone.
Revoking
curl -X DELETE \
"https://bp.mobicms.com.br/stores/$STORE_ID/api-tokens/$TOKEN_ID" \
-H "Authorization: Bearer $BP_TOKEN"204 No Content means done. Any request carrying the revoked token returns 401 immediately — no grace period.
What happens on 401?
| Reason | Fix |
|---|---|
| Token revoked or expired | Issue a new one and update the client |
| Hash doesn't match anything | Probably a typo or stripped bod_ prefix |
| JWT past 24h | Re-authenticate via /login |
A 403 means the token is valid but its scope doesn't cover that endpoint. Check the permission column on the tokens table — the answer is usually obvious.