# Authentication

> How your AI host signs in to Stensul with OAuth, what a token can do, which scopes to grant, and what a 401 or 403 means.

Source: https://stensul.com/developers/authentication/ · Last reviewed 2026-10-07

The Stensul MCP server uses the **OAuth 2.0 authorization-code flow**. A person signs in to Stensul in their browser, approves the connection, and from then on the AI host acts **as that person**, with that person's permissions. There is no API-key or client-credentials path for MCP: there is always a signed-in user behind every call.

## The flow

```text
You ──ask──▶ AI host ──(1) no token yet──▶ Stensul MCP server
                │                               │
                │◀──(2) sign-in URL─────────────┘
                │
   browser ─────▼───(3) log in + approve on your Stensul instance
                │
                └──(4) code ─▶ host exchanges it for tokens ─▶ (5) every tool call
                                                                 carries the token
```

1. The host connects to your per-instance endpoint, `https://mcp.stensul.com/<your-instance>/mcp`, and finds it needs a sign-in.
2. It opens Stensul's authorization URL, `https://mcp.stensul.com/<your-instance>/oauth/authorize`.
3. You log in to your own Stensul instance and approve.
4. Stensul redirects back with a code, and the host exchanges it at `https://mcp.stensul.com/<your-instance>/oauth/token`.
5. The host sends the token with every request. When it expires, the host refreshes it or asks you to sign in again.

Hosts that support remote OAuth connectors (Claude, ChatGPT, Writer, Copilot Studio, Claude Code) do all of this for you. The two sign-in tools in the [reference](https://stensul.com/developers/reference/) exist for hosts that can't.

## Three ways a client identifies itself

| Client type | Used by | What the admin creates | Secret? |
| --- | --- | --- | --- |
| Public client with PKCE | Claude, ChatGPT, Claude Code, Gemini Enterprise, Cursor, VS Code, Windsurf, Amazon Quick | An Authorization Code connection with **PKCE (Public)** on | No, the Client ID is enough |
| Confidential client | Writer, Copilot Studio | An Authorization Code connection with PKCE **off** | Yes, shown once at creation |
| Dynamic Client Registration | Atlassian Rovo, and Claude Code when it runs without a Client ID | Nothing; the client registers itself | No |

Admins create connections in **Admin > Settings > API connections**. The connection's redirect URI must include your instance name: `https://mcp.stensul.com/<your-instance>/oauth/callback`. A redirect URI without it fails with a mismatch.

## What a token can do

Every request passes **two independent checks**, and both must pass:

|  | Scope | Permission |
| --- | --- | --- |
| Belongs to | The connection | The signed-in person |
| Answers | Is this integration allowed to do this at all? | Is this person allowed to do this? |
| Set by | An admin, on the API connection | The person's role in Stensul |
| Changed in | Admin > Settings > API connections | Admin > Users & Roles |
| Error when missing | 403, *Invalid scope(s) provided.* | 403, *Action is unauthorized.* |

The scope is checked first. So the same connection behaves differently for different people: a connection with `emails:write` lets a campaign manager create emails and gives a read-only user nothing. **The MCP can never do more than the person could do in the Stensul app.**

## Scopes

| Scope | Grants | Tools |
| --- | --- | --- |
| `libraries:read` | List libraries and read their configuration | `stensul-get-libraries` |
| `emails:read` | Read emails, layouts and rendered previews; run quality checks | `stensul-list-emails`, `stensul-read-email`, `stensul-preview-email`, `stensul-run-quality-check` |
| `emails:write` | Create emails, edit content and settings, delete modules, copy emails | `stensul-build-email`, `stensul-create-email`, `stensul-copy-email`, `stensul-update-email-content`, `stensul-update-email-settings`, `stensul-add-email-module`, `stensul-delete-email-module`, `stensul-apply-email-variation` |
| `images:write` | Upload images and request upload links | `stensul-upload-images`, `stensul-image-upload-session` |
| `dam:read` | Browse the image gallery | `stensul-list-gallery-images` (and gallery search in `stensul-upload-images`) |
| `dynamic-content:read` | List a library's Segment Sets | `stensul-list-segment-sets` |
| `dynamic-content:write` | Apply Segment Sets and write per-segment content | `stensul-apply-segment-set`, `stensul-update-segment-content` |

`api-read` and `api-write` are for server-to-server integrations only. They do nothing on an MCP connection, and a connection created with only those two is rejected with *Invalid scope(s) provided.*

For drafting emails, `libraries:read`, `emails:read`, `emails:write` and `images:write` cover the usual flow. Add `dam:read` for the gallery and the two `dynamic-content` scopes for audience versions.

### Grant fewer scopes on purpose

- **Read-only integrations** (reporting, audits) need `emails:read` and `libraries:read` and nothing else.
- **A scope is a hard stop**, not an instruction the model can reinterpret. An assistant that should draft copy but never touch assets is a connection without `images:write`.
- **One connection per tool or team** lets you revoke one without breaking the others, and the audit trail shows which integration acted.

### Changing scopes later

- **Adding a scope** revokes every token for that connection. People sign in again on their next request and get the new scope.
- **Removing a scope** takes effect immediately for those capabilities.

## Dynamic Client Registration

Clients that self-register get **every scope Stensul defines**, and a person approves or declines the whole set at sign-in. DCR clients don't appear in the API connections list and can't be narrowed. What still limits them is the signed-in person's permissions. If you need a connection limited to specific scopes, create a static connection and configure the client with its Client ID.

## What an admin has to switch on

1. **The external API on your instance**, and the `access_admin_api_connections` permission for whoever creates connections. Your Stensul customer success manager enables these.
2. **An API connection** for each host (or rely on DCR where the host supports it).
3. **Host-side settings**: for Claude, allow `<your-instance>.stensul.com` under **Capabilities > Additional allowed domains** so image uploads work.
4. **User permissions**: the people using it need the same permissions they'd need in the app, for example `edit_campaign` to create or edit emails, `access_dashboard` to list emails, and `duplicate_email_between_libraries` to copy across libraries.

## When a call fails

| Status | What it means | What to do |
| --- | --- | --- |
| **401** | No valid token: it expired or was revoked (for example after a scope was added) | Sign in again. Hosts usually prompt you. The server tells the model to re-check sign-in on any 401 or 403 |
| **403**, *Insufficient OAuth scope* or *Invalid scope(s) provided.* | The connection lacks the scope | An admin adds it, then everyone signs in again |
| **403**, *Action is unauthorized.* | The person's role lacks the permission | Grant it in Admin > Users & Roles. Signing in again won't help |
| **403**, *Sent emails cannot be edited.* | The email was already sent | Copy it and edit the copy |
| **403**, *Your IP address is not in the allowed list* | The external API IP allowlist blocks the caller | An admin updates Settings > External API > Allowed IPs |
| **410** | A client is calling the removed shared `/mcp` endpoint with headers | Use your per-instance URL with OAuth |

**Quick test:** if it fails for everyone on a connection, it's a scope. If it fails for one person but works for another, it's a permission. If it works in the Stensul app but not in your AI host, it's a scope: the app doesn't use API scopes.

## Rate limits

The server limits requests per signed-in identity and, for the OAuth endpoints, per IP address. Stensul hasn't published the numbers yet. If you hit a limit, slow down and retry.
