Authentication
Tiptap services use JWTs (JSON Web Tokens) for authentication and authorization. You generate and sign tokens on your server, then pass them to Tiptap services. Each token identifies your environment, names the services it can access, and the actions it's authorized to perform on your resources.
Still using App IDs and per-service secrets?
If you authenticate with App IDs and per-service secrets, see Migrate to the new authentication. The previous setup is documented under Legacy authentication and keeps working.
How it works
- You create an asymmetric secret key pair in your Tiptap dashboard.
- Your server signs a JWT using the private key.
- Your client passes the JWT when connecting to a Tiptap service (AI, Convert, Documents).
- Tiptap verifies the token's signature with the public key and checks your subscription.
What a token controls
A single JWT carries everything a request needs: which services it can reach, what it's allowed to do, and on which resources.
- One token, many services. A single JWT authorizes any combination of AI, Convert, and Documents through the
audclaim. - Cross-service workflows. The AI service can act on your documents under the same token, with no service-to-service plumbing.
- Permissions inside the token. Declare exactly what's allowed via
action,resource, and optional constraints. - Zero-downtime key rotation. Run multiple active key pairs per environment and retire old keys without a switchover window.
Claims reference
Every JWT must include these registered claims:
| Claim | Type | Required | Description |
|---|---|---|---|
iss | string | Yes | Your environment hash ID (from the Tiptap dashboard) |
aud | string | string[] | Yes | Which services this token can access: "AI", "Convert", "Documents" |
iat | number | No | Issued-at timestamp (epoch seconds) |
exp | number | Yes | Expiration timestamp (epoch seconds) |
sub | string | No | User identifier (for audit trails and tracing) |
permissions | Permission[] | No | What this token is allowed to do. Omitting grants no specific actions. |
Signing the token
Asymmetric key pair (ES256) — default
When you create a secret in the dashboard, you receive an ECDSA private key (P-256). You sign tokens with your private key. Tiptap only stores the public key and uses it to verify tokens. Your private key never leaves your server.
import { SignJWT, importPKCS8 } from 'jose'
const privateKey = await importPKCS8(process.env.TIPTAP_PRIVATE_KEY, 'ES256')
const jwt = await new SignJWT({
permissions: [
{ action: 'Documents:Read', resource: '*' },
],
})
.setProtectedHeader({ alg: 'ES256' })
.setIssuer('your-environment-hash-id')
.setAudience(['Documents'])
.setIssuedAt()
.setExpirationTime('30m')
.sign(privateKey)Other languages
The JWT is a standard RFC 7519 token. Any JWT library that supports ES256 can sign tokens. The payload structure is the same regardless of language.
Python (PyJWT, requires pyjwt[crypto] for ES256):
import jwt, time
with open("private_key.pem", "rb") as f:
private_key = f.read()
payload = {
"iss": "your-environment-hash-id",
"aud": ["AI", "Documents"],
"iat": int(time.time()),
"exp": int(time.time()) + 1800,
"permissions": [
{"action": "Documents:Read", "resource": "*"}
]
}
token = jwt.encode(payload, private_key, algorithm="ES256")PHP (firebase/php-jwt):
use Firebase\JWT\JWT;
$privateKey = file_get_contents('private_key.pem');
$payload = [
'iss' => 'your-environment-hash-id',
'aud' => ['AI', 'Documents'],
'iat' => time(),
'exp' => time() + 1800,
'permissions' => [
['action' => 'Documents:Read', 'resource' => '*'],
],
];
$token = JWT::encode($payload, $privateKey, 'ES256');Permissions
Permissions define what a token is allowed to do. While they are optional, a token without permissions authenticates the request but grants no specific actions.
Each permission has an action and a resource (both required), plus optional constraints:
{
"permissions": [
{
"action": "Documents:Read",
"resource": "*",
"constraints": { "prefix": "team1_" }
}
]
}Actions
Actions follow the format Service:Operation. Each Tiptap service defines its own actions:
| Service | Actions |
|---|---|
| Documents | Documents:Read, Documents:Write, Documents:Comment |
| Documents REST API | Documents:Api:All grants full access to the Document Server's REST API. |
| Convert | Convert:Import:Docx, Convert:Export:Docx, Convert:Import:Markdown, Convert:Export:Markdown, Convert:Export:Doc, Convert:Export:Odt, Convert:Export:Epub, Convert:Export:Pdf, Convert:Fonts |
| AI | AI:Generation, AI:Toolkit |
Documents:Write implies Documents:Read and Documents:Comment — granting write access automatically grants the other two, so you don't need to list them separately.
Documents:Api:All grants access to every Document Server API endpoint, including managed settings. It's separate from the other Documents:* actions, which authorize document operations over the collaboration connection. Treat a token carrying it like an admin credential. Keep it server-side, scope its lifetime tightly, and never share it with clients.
Convert:* actions are scoped by file format and operation (Import vs. Export). A token only needs the actions for what it actually does — e.g. a token that only exports PDF needs Convert:Export:Pdf. Today only DOCX and Markdown support both directions; Doc, ODT, EPUB, and PDF are export-only. Convert:Fonts is unscoped and applies to the /fonts/* routes.
AI:Generation covers the text and image generation, suggestions, agent, and audio/speech endpoints. AI:Toolkit covers the toolkit endpoints. Neither action implies the other. AI actions are not scoped by resource today, so use "resource": "*" on AI permissions.
Actions are case-insensitive — "Documents:Read" and "documents:read" are equivalent. Resources and constraint values (prefix, suffix, in) remain case-sensitive because they can reference externally meaningful identifiers. Pick a consistent casing for your own code.
Resources
The resource field is required on every permission. It controls which resources the permission applies to:
| Value | Meaning | Example |
|---|---|---|
"*" | Wildcard — matches everything | { "action": "Documents:Read", "resource": "*" } |
"*" + constraints | Filtered wildcard — matches resources that pass the constraints | { "action": "Documents:Read", "resource": "*", "constraints": { "prefix": "team_" } } |
"doc_42" | Specific — matches only this exact resource | { "action": "Documents:Write", "resource": "doc_42" } |
Key rules:
- Use
"*"for wildcard access. A wildcard without constraints matches everything. - A specific resource matches only that exact resource.
Constraints
Constraints add fine-grained filtering on top of resource matching. They're useful when you want to allow access to a category of resources rather than listing each one.
{
"action": "Documents:Read",
"resource": "*",
"constraints": { "prefix": "team1_" }
}This grants read access to any document whose name starts with "team1_".
Available constraint fields:
| Field | Type | Description |
|---|---|---|
prefix | string | Resource name must start with this value |
suffix | string | Resource name must end with this value |
in | string[] | Resource name must be one of these exact values |
Combining fields within a single constraint uses AND logic (all must pass):
{ "prefix": "team1_", "suffix": "_published" }Matches "team1_report_published" but not "team1_report_draft".
An array of constraints uses OR logic (any one passing is enough):
[
{ "prefix": "team1_" },
{ "prefix": "team2_" }
]Matches "team1_doc" or "team2_doc".
Rules:
- The
constraintsfield can't be empty. Each constraint must declare at least one ofprefix,suffix, orin, and an array of constraints must contain at least one entry. Omit the field entirely for "no filtering". incannot be combined withprefixorsuffixin the same constraint object. Use separate constraints in an array instead.prefixandsuffixmust be non-empty strings.inmust be a non-empty array of strings.
Cross-service actions
Some actions cross service boundaries. The Document Server stores your documents, and other services act on it. For example, importing a DOCX through the Convert service writes the document and its comments to the Document Server. When an action reads or writes a document this way, the token needs both the originating action and the matching Documents action.
A token that imports a commented DOCX into a stored document needs both:
{
"permissions": [
{ "action": "Convert:Import:Docx", "resource": "*" },
{ "action": "Documents:Write", "resource": "*" }
]
}The resource is yours to define. Each service page calls out which cross-service actions it performs and the permissions they require.
Examples
Full access to all services
{
"iss": "env_abc123",
"aud": ["AI", "Convert", "Documents"],
"iat": 1722344565,
"exp": 1722344865,
"permissions": [
{ "action": "AI:Generation", "resource": "*" },
{ "action": "AI:Toolkit", "resource": "*" },
{ "action": "Documents:Write", "resource": "*" },
{ "action": "Convert:Import:Docx", "resource": "*" },
{ "action": "Convert:Export:Docx", "resource": "*" },
{ "action": "Convert:Import:Markdown", "resource": "*" },
{ "action": "Convert:Export:Markdown", "resource": "*" },
{ "action": "Convert:Export:Doc", "resource": "*" },
{ "action": "Convert:Export:Odt", "resource": "*" },
{ "action": "Convert:Export:Epub", "resource": "*" },
{ "action": "Convert:Export:Pdf", "resource": "*" },
{ "action": "Convert:Fonts", "resource": "*" }
]
}Documents:Write covers read and comment by implication, so only one Documents entry is needed.
Read-only access to specific documents
{
"permissions": [
{
"action": "Documents:Read",
"resource": "*",
"constraints": {
"in": ["document_a", "document_b"]
}
}
]
}AI access only
Grant the AI features the token needs. The example below allows both generation and toolkit endpoints:
{
"aud": ["AI"],
"permissions": [
{ "action": "AI:Generation", "resource": "*" },
{ "action": "AI:Toolkit", "resource": "*" }
]
}Document conversion
Grant only the format/direction pairs the token actually uses. The example below imports DOCX and exports PDF:
{
"aud": ["Convert"],
"permissions": [
{ "action": "Convert:Import:Docx", "resource": "*" },
{ "action": "Convert:Export:Pdf", "resource": "*" }
]
}Scoped by document prefix
Allow reading and commenting on documents that belong to a specific team:
{
"permissions": [
{
"action": "Documents:Read",
"resource": "*",
"constraints": { "prefix": "team-sales_" }
},
{
"action": "Documents:Comment",
"resource": "*",
"constraints": { "prefix": "team-sales_" }
}
]
}Write access to a single document
{
"permissions": [
{ "action": "Documents:Write", "resource": "meeting-notes-2024" }
]
}Security best practices
- Keep tokens short-lived. Set
expto 30 minutes or less. Generate a fresh token for each session or connection. - Use the least privilege. Only include the permissions and audiences the token actually needs. A token used only for Convert exports shouldn't include
"Documents"or"AI"in its audience. - Never expose your private key client-side. Token generation must happen on your server. Send the signed JWT to your client, never the signing key.
- Rotate keys periodically. Create a new key pair, update your server to sign with the new key, then retire the old one. Tiptap supports multiple active key pairs per environment to allow zero-downtime rotation.
- Set
subfor auditability. Including a user identifier insubhelps trace actions back to specific users in logs and audit trails.