> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getcitable.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> How Citable's MCP server resolves your brand and user context — and how to set it up over stdio or HTTP.

Every MCP session is scoped to one brand and one user. The brand controls what data the session sees and what writes it can make; the user is recorded on every action for audit. Neither is ever passed as a tool input — both are resolved from the transport layer.

## Auth context shape

When the server accepts your connection, it resolves a context that looks like:

```typescript theme={null}
interface AuthContext {
  brandId: number;     // brand this session reads + writes for
  userId: number;      // user behind the call (audit trail)
  apiKeyId: number | null;  // the API key id, if HTTP-authenticated
  source: string;      // 'stdio:env' | 'http:api_key'
}
```

The `brandId` is the most important field — every read filters by it, every write rejects cross-brand IDs against it.

## Transport options

### stdio (local clients)

Used by Claude Desktop, Cursor, and other local MCP clients. Brand and user come from environment variables you set in your client config:

```bash theme={null}
CITABLE_BRAND_ID=123
CITABLE_USER_ID=456
```

The user must be an active member of the brand. The check runs at connection time, so a misconfigured env throws immediately rather than failing later on a tool call.

### Streamable HTTP (remote agents)

Used by hosted agents and remote integrations. Auth is an API key in the `Authorization` header:

```
Authorization: Bearer <api_key>
```

The key maps to a stored `(brand, user, api_key)` tuple. The resolved API key id is recorded on the auth context so every write is traceable back to a specific key.

## Brand scoping

Every read query filters by your auth context's `brandId` at the database layer. Every write tool that accepts an ID looks up that ID and rejects with a 403 if it belongs to a different brand. There's no MCP-side workaround — the same guard applies whether the client is Claude Desktop, a hosted agent, or any other caller.

Example: `approve_ticket({ ticket_code: "OTHER-001" })` resolves the ticket, sees it belongs to a different brand, and rejects with a 403. The audit log records the failed attempt.

## API key lifecycle

Create API keys in the Citable web app under **Settings → API Keys**. Each key:

* Belongs to one user (the creator) and one brand
* Cannot be used to act on any other brand
* Can be revoked anytime — revoked keys fail auth immediately
* Records `last_used_at` on every successful call

Rotate by creating a new key, updating your integration, and revoking the old one. Keys are shown once at creation time and never displayed again. Treat them like any other secret.

## Rate limits

Each brand gets a per-hour rate limit. Response headers tell you where you stand:

```
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1715731200
```

The default tier gets 100 calls/hour. Higher tiers raise the read limit; write limits stay in place across all tiers as cost protection on the platform side. If you need a higher limit for a specific integration, get in touch from your account.
