Authentication

The Brane API supports two authentication strategies. Choose based on your use case:

Strategy Best for Header format
JWT Bearer User sessions, interactive apps Authorization: Bearer <accessToken>
API Key Scripts, integrations, AI agents X-Api-Key: <key>

incrm is the platform's technical name: it stays in the API's address (api.incrm.app), the API keys (incrm_, incrm_pub_), the webhook headers (X-InCRM-*) and the npm packages (@incrm-app/sdk, @incrm-app/mcp-server).

JWT Bearer Authentication

Sign In

Obtain an access token by posting credentials:

curl -X POST https://api.incrm.app/api/v1/auth/sign-in \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com", "password": "yourPassword123"}'

Response:

{
  "success": true,
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs..."
  }
}

The response also sets a refreshToken as an httpOnly cookie. This cookie is used exclusively for token refresh — it is never returned in the JSON body.

Sign in with Google

POST /api/v1/auth/google exchanges a Google ID token for the same token pair as password sign-in. Obtain the credential in the browser with Google Identity Services, then post it:

curl -X POST https://api.incrm.app/api/v1/auth/google \
  -H "Content-Type: application/json" \
  -d '{"credential": "<Google ID token>"}'

The response body and the refreshToken cookie are identical to POST /auth/sign-in, so the rest of this guide applies unchanged.

The endpoint both signs in and signs up — there is no separate registration call:

A credential Google did not issue for Brane — or one whose email Google has not verified — is rejected with 401024. If the deployment has no GOOGLE_CLIENT_ID configured, the endpoint returns 500001; Google sign-in is unavailable until an operator sets it.

Sign in with Google by redirect

Google Identity Services runs only on origins registered with Google, and Google allows no wildcards, so it can't run on a business's own subdomain (<business>.incrm.app). There the app signs in with Google by redirect instead, with top-level navigations only:

  1. The page sends the browser to GET /api/v1/auth/google/redirect?returnTo=<page>, where <page> is an address of the app (an origin the API takes credentialed requests from; any other answers 400281).
  2. The API remembers the sign-in in a short-lived cookie and sends the browser to Google.
  3. Google posts the ID token to POST /api/v1/auth/google/callback. The API checks that the answer belongs to the sign-in this browser started, signs the user in as POST /auth/google does, sets the refreshToken cookie and sends the browser back to <page> with ?google=signed-in, or ?google=failed. A user who cancels at Google returns to <page> unchanged.
  4. The page gets its access token from POST /api/v1/auth/token/refresh.

These are browser endpoints, not API calls: they aren't in the OpenAPI document.

Using the Access Token

Include the token in all subsequent requests:

curl https://api.incrm.app/api/v1/client \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

TypeScript SDK:

import { InCRMClient } from '@incrm-app/sdk'

const client = new InCRMClient({
  baseUrl: 'https://api.incrm.app',
  accessToken: 'eyJhbGciOiJIUzI1NiIs...',
})

Token Expiration

Token Default TTL
Access token 30 minutes
Refresh token 60 days, counted again from every refresh

Refreshing Tokens

When the access token expires, use the refresh cookie to get a new one:

curl -X POST https://api.incrm.app/api/v1/auth/token/refresh \
  -b "refreshToken=<cookie-value>"

Response:

{
  "success": true,
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs..."
  }
}

A new refresh token cookie is also set. Use the new refresh token from then on: the one you sent stops working 60 seconds later (the grace lets several tabs refresh with the same cookie at once). If the refresh token is missing, invalid or retired, you receive:

{
  "success": false,
  "error": {
    "code": 401012,
    "message": "Refresh token not found in whitelist"
  }
}

Logout

curl -X POST https://api.incrm.app/api/v1/auth/logout \
  -H "Authorization: Bearer <accessToken>"

Returns HTTP 204 No Content. Invalidates both the access token and refresh token.

Resetting the password (POST /api/v1/auth/password-reset/confirm) ends every session of the user: all their access and refresh tokens stop working.

API Key Authentication

API keys are designed for server-to-server integrations, CI/CD pipelines, and AI agent access.

Creating an API Key

Create a key via the API (requires JWT auth with apiKeys.create permission):

curl -X POST https://api.incrm.app/api/v1/api-key \
  -H "Authorization: Bearer <accessToken>" \
  -H "Content-Type: application/json" \
  -d '{"name": "My Integration", "scopes": ["clients.read", "invoices.read"]}'

Response:

{
  "success": true,
  "data": {
    "id": "abc123",
    "name": "My Integration",
    "key": "incrm_dGhpcyBpcyBhIHRlc3Qga2V5...",
    "keyPrefix": "incrm_dGhpcyB",
    "scopes": ["clients.read", "invoices.read"],
    "expiresAt": null,
    "isActive": true,
    "createdAt": "2025-01-01T00:00:00.000Z"
  }
}

Important: The key field is only returned once at creation time. Store it securely — it cannot be retrieved later.

Using an API Key

Send the key via the X-Api-Key header:

curl https://api.incrm.app/api/v1/client \
  -H "X-Api-Key: incrm_dGhpcyBpcyBhIHRlc3Qga2V5..."

Alternatively, you can pass it as a Bearer token (the incrm_ prefix signals API key auth):

curl https://api.incrm.app/api/v1/client \
  -H "Authorization: Bearer incrm_dGhpcyBpcyBhIHRlc3Qga2V5..."

TypeScript SDK:

import { InCRMClient } from '@incrm-app/sdk'

const client = new InCRMClient({
  baseUrl: 'https://api.incrm.app',
  apiKey: 'incrm_dGhpcyBpcyBhIHRlc3Qga2V5...',
})

Scopes

API keys can be scoped to specific permissions. If scopes is empty or omitted, the key inherits all permissions of the user who created it. When scopes are specified, the key is restricted to only those actions.

Scope format: resource.action (e.g., clients.read, invoices.create).

Key Lifecycle

Auth Error Responses

HTTP Status Error Code Message
400 400281 returnTo must be the address of a page of the app
401 401001 Invalid credentials
401 401004 Email is not verified
401 401010 Invalid access token
401 401011 Access token not found in whitelist
401 401012 Refresh token not found in whitelist
401 401013 Invalid refresh token
401 401014 Insufficient permissions for this action
401 401015 Business access required
401 401020 API key has expired
401 401021 API key has been revoked
401 401022 Invalid API key
401 401024 Invalid Google credential
403 403000 Forbidden resource
500 500001 Google sign-in is not configured

Rate Limits

Limits are counted per client IP address and per endpoint. Each endpoint has its own budget for each address, so requests to one endpoint never use up another's, and clients at different addresses never share a budget. Clients behind one shared address (an office network or a NAT gateway) do share it.

Unless listed below, an endpoint allows 50 requests per 60 seconds from each address. Some endpoints are stricter:

Endpoint Limit
POST /api/v1/auth/resend-activation 5 requests per 60 seconds
POST /api/v1/auth/google 10 requests per 60 seconds
POST /api/v1/public/inquiry 5 requests per 300 seconds
POST /api/v1/public/site-visit (a website's page views) 120 requests per 60 seconds
POST /api/v1/public/booking, POST /api/v1/public/booking/manage/{token}/cancel, POST /api/v1/public/booking/manage/{token}/reschedule 5 requests per 600 seconds each
GET /api/v1/public/booking/... (booking types, booking page, free slots, a booking by its token) 60 requests per 60 seconds each
GET /api/v1/public/shared-files/{token}, POST /api/v1/public/shared-files/{token}/download 30 requests per 60 seconds each
POST /api/v1/contact 5 requests per 60 seconds

Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds until the window resets) for the endpoint that was called. Exceeding a limit returns 429 with error code 429000 and a Retry-After header (seconds).

POST /api/v1/ai-chat/chat also limits the Brane answers a user has in progress at once: 3 on each API server. Another request is refused with 429 and error code 429002 until one of them finishes or is stopped. When the AI model is overloaded, the request returns 429 with error code 429001, or, if the answer has already started streaming, the stream ends with an error part carrying that code. Wait a minute before trying again.

POST /api/v1/ai-chat/transcribe (a voice message for Brane, turned into text) also allows each user 10 voice messages per 60 seconds on each API server. Another one is refused with 429 and error code 429003.

CORS

When using API keys, CORS is configured with credentials: false and origin: true (any origin). This allows API keys to be used from any domain without credential cookies.

JWT-based auth requires requests from an allowed origin and uses credentials: true for cookie-based refresh tokens.

Opaque (Origin: null) requests

The in-browser landing-page preview renders generated code inside a sandboxed srcdoc iframe (sandbox="allow-scripts"), which is an opaque origin. Requests it issues therefore carry Origin: null. To let a previewed page load live data at runtime, Origin: null is accepted only for explicitly public, read-only endpoints, with credentials disabled — currently just GET /api/v1/capabilities. Authenticated application endpoints continue to reject Origin: null, so the preview origin can never reach tenant data.

Public booking API

/api/v1/public/booking (see Bookings) answers requests from any origin, with or without X-Api-Key, so a business's website on any domain can book for its visitors. Credentials are disabled for every origin except Brane's own app. The landing-page preview (Origin: null) may only read there (GET); booking, cancelling and moving are rejected from it.

A website's page views

POST /api/v1/public/site-visit takes the page views a business's website counts without cookies. It answers any origin with credentials disabled, and accepts the publishable key in the JSON body (key) of a text/plain request as well as in X-Api-Key, so a page sends its views as simple requests, without a CORS preflight. The landing-page preview (Origin: null) is rejected.