MobiariDocs

Authentication

Session tokens for apps, store API tokens for scripts, and one permission model behind both.

Every authenticated request carries a bearer token:

Authorization: Bearer <token>

The API accepts two kinds of token:

KindGet it fromReachesLifetimeUse it for
Session tokenPOST /v1/auth/loginEvery store and organization the person belongs toexpires_in (a day by default); renewed with a refresh tokenApps a person signs in to: the admin, mobile apps, your own UI
Store API token (mobi_st_…)Settings → API tokens in the store, or POST /v1/stores/{store_id}/api-tokensOne storeUntil revoked or its optional expiryScripts, integrations and automations for that store

Either way, the token can never do more than the user it belongs to. A store API token is also held to its store: a call that checks permissions anywhere else (another store, or an organization-level call such as the organization's subscription or creating a store) answers 403, with code missing_permission (creating an organization answers forbidden). There are no organization-wide or user-wide API tokens; for those calls, sign in and use a session token.

Signing in

curl -X POST https://api.mobiari.com/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "ana@example.com", "password": "…", "device_name": "Stock sync laptop"}'

device_name is optional; it labels the session in the user's session list. The response is told apart by status:

{
  "status": "authenticated",
  "token": "eyJhbGciOiJIUzI1NiJ9…",
  "token_type": "Bearer",
  "expires_in": 86400,
  "refresh_token": "mobi_rt_2f8K…",
  "refresh_token_expires_in": 2592000,
  "session_id": "0199a4f2-6c1e-7d3a-9b1f-3e2c8a5d7f10",
  "user": { "id": "…", "email": "ana@example.com", "...": "…" }
}
FieldMeaning
tokenThe access token. Send it as Authorization: Bearer <token>.
expires_inSeconds until token expires. Read it; don't hard-code a lifetime.
refresh_tokenTrades for a new pair at POST /v1/auth/refresh. Starts with mobi_rt_. Works once.
refresh_token_expires_inSeconds until the refresh token expires if it isn't used.
session_idThis sign-in, as listed by GET /v1/me/sessions.

Two-factor authentication

If the account has a second factor on, the password step answers with a challenge instead of tokens:

{
  "status": "two_factor_required",
  "challenge": "eyJ…",
  "methods": ["totp", "whatsapp"],
  "expires_in": 300
}

Finish within expires_in seconds with one of the methods:

MethodCalls
totpPOST /v1/auth/2fa/totp/verify with challenge and totp_code (from the authenticator app)
whatsappPOST /v1/auth/otp/send with challenge, then POST /v1/auth/otp/verify with challenge and code

Both verify calls return the signed-in session (token, refresh_token, …). New methods may be added: ignore values of methods you don't support.

Refreshing a session

Before token expires, or when a call returns 401 unauthorized, trade the refresh token for a new pair:

curl -X POST https://api.mobiari.com/v1/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{"refresh_token": "mobi_rt_2f8K…"}'

The response is a new session: the same fields as a successful sign-in, without status.

Refresh tokens rotate. Each one works once: the response carries a new refresh_token, and you must store it in place of the old one.

Reuse revokes the session. Presenting a refresh token that was already used is treated as theft: the whole session (every token descended from that sign-in) is revoked and the call fails with 401 refresh_token_reused. The user signs in again.

ErrorMeaningWhat to do
401 invalid_refresh_tokenUnknown or expired refresh tokenSign in again
401 refresh_token_reusedAlready used or revoked; the session is now signed out on every device holding itSign in again, and fix the client if it wasn't an attack

Refresh from one place at a time

Two tabs or threads refreshing with the same token at once is indistinguishable from a stolen token: the second call gets refresh_token_reused and signs the user out. Funnel refreshes through a single in-flight promise (or a lock), and have every waiting request use its result.

let refreshing: Promise<Session> | null = null;

function refresh(session: Session): Promise<Session> {
  refreshing ??= fetch('https://api.mobiari.com/v1/auth/refresh', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ refresh_token: session.refresh_token }),
  })
    .then(async (res) => {
      if (!res.ok) throw new SignedOut((await res.json()).code);
      return saveSession(await res.json()); // persist the NEW refresh_token
    })
    .finally(() => {
      refreshing = null;
    });
  return refreshing;
}

Signing out and sessions

curl -X POST https://api.mobiari.com/v1/auth/logout \
  -H "Content-Type: application/json" \
  -d '{"refresh_token": "mobi_rt_2f8K…"}'

Logout revokes the session: its refresh token and the access tokens issued for it stop working. It answers 204 even for an unknown or already revoked token, so it is safe to retry.

MethodPathPurpose
GET/v1/me/sessionsThe user's signed-in devices (a Page), most recently used first, with device_name, last_used_at and current.
DELETE/v1/me/sessions/{session_id}Sign one device out.

Store API tokens are not sessions and don't appear in this list.

Store API tokens

Each token belongs to one store and acts as the person who created it. Create one in the store under Settings → API tokens, or through the API:

curl -X POST "https://api.mobiari.com/v1/stores/$STORE_ID/api-tokens" \
  -H "Authorization: Bearer $BP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Stock sync script",
    "scopes": ["products.read", "orders.read"],
    "expires_at": null
  }'

The response includes the raw token once. The server keeps only a sha256 hash; if you lose the value, rotate the token.

mobi_st_<43 characters, URL-safe base64>

Keep the mobi_st_ prefix: it is what lets secret scanners (GitHub and others) recognize a leaked token. Tokens created before 2026-09-29 start with bod_ and keep working; rotating one gives it a mobi_st_ value.

How a token relates to its user

A token is always issued on behalf of a real user, and it can never do more than that user can. If the user is removed from the store, every token they issued stops working. There are no orphan tokens.

  • Audit attribution. Changes made with a token are recorded against the user who issued it, together with the token's name ("Aline, via the POS terminal").
  • Restricted tokens. For an integration that must stay narrow even if you're the owner, issue the token with only the scopes it needs, or invite a dedicated "integrations" member with limited permissions and issue the token as them.

Scopes

scopes are permission codes, the same ones on the team permissions page (products.read, orders.refund, …); GET /v1/permissions/catalog lists them all. ["*"] means everything the issuing user can do. When a request comes in:

effective permissions = the user's permissions ∩ the token's scopes

The narrowing also applies to the store owner: an owner's token with only products.read is genuinely read-only. You can't grant a token a permission you don't have yourself.

Rotate and revoke

ActionEndpointEffect
ListGET /v1/stores/{store_id}/api-tokensName, scopes, last 4 characters, last_used_at; never the raw value
RotatePOST /v1/stores/{store_id}/api-tokens/{token_id}/rotateSame token, new value; the old value stops working immediately. Use after a leak.
RevokeDELETE /v1/stores/{store_id}/api-tokens/{token_id}Gone for good. 204 No Content.
curl -X DELETE \
  "https://api.mobiari.com/v1/stores/$STORE_ID/api-tokens/$TOKEN_ID" \
  -H "Authorization: Bearer $BP_TOKEN"

Requests with a revoked token fail with 401 right away; there is no grace period.

401 or 403?

ResponseCauseFix
401 unauthorizedMissing header, expired session token, revoked or mistyped tokenRefresh the session, or check the token (did the mobi_st_ prefix get stripped?)
401 invalid_refresh_token / refresh_token_reusedRefresh failedSign in again
403 forbidden / missing_permissionThe token is valid, but the user or the token's scopes lack the permission, or a store API token was used outside its storeGrant the permission, issue a token with that scope, or use a token for the right store (a session token for organization-level calls)

Errors follow the shape described in API conventions.

On this page