Authentication
WarmHub authenticates API requests using Bearer tokens passed in the Authorization header. For programmatic access, create a personal access token (PAT) using the CLI or the endpoints below.
Token Types
Section titled “Token Types”| Type | How to obtain | Manages credentials | Expires |
|---|---|---|---|
| JWT | Interactive login (CLI or web app) | Yes | Session-based |
| PAT | POST /api/pats (requires JWT) | No | 30 days default, 1 year max |
PAT management endpoints require JWT authentication — you cannot create, list, or revoke PATs using another PAT.
Scopes
Section titled “Scopes”PATs carry scopes that limit their access:
| Scope | Grants |
|---|---|
repo:read | Read repositories, queries, shapes |
repo:write | Commits, shape mutations |
repo:configure | Subscriptions, credentials, repo settings |
repo:admin | Delete, archive, visibility |
org:read | Read org profiles |
org:configure | Create repos, manage members |
org:admin | Rename, archive org |
Scopes are independent — each must be requested explicitly.
If no scopes are specified when creating a PAT, the token has no scope restrictions on standard endpoints. JWT tokens are not subject to PAT scope restrictions (they are still limited by the user’s org membership and role).
Using a Token
Section titled “Using a Token”Pass the token in the Authorization header:
curl -H "Authorization: Bearer eyJhbGciOi..." \ https://api.warmhub.ai/api/repos/myorg/myrepo/headPOST /api/pats
Section titled “POST /api/pats”Create a new personal access token. The token value is returned once — store it securely.
Auth: JWT only (no PAT).
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Token name (alphanumeric, hyphens, underscores) |
scopes | Array<{ resource?: string; permissions: string[]; allowedMatches?: string[] }> | No | Resource-scoped permission entries (see below) |
structured | boolean | No | Set true when you need multiple entries for the same resource. Omit it for single-entry-per-resource payloads. |
description | string | No | Human-readable description |
expiresAt | number | No | Expiration as epoch milliseconds. Default: 30 days. Max: 1 year. |
Each scope entry binds a resource to a set of permissions:
| Field | Type | Required | Description |
|---|---|---|---|
resource | string | No | Resource name: "org/repo" (repo-scoped), "org" (org-level), or omitted (global wildcard) |
permissions | string[] | Yes | Permissions for this resource: "repo:read", "repo:write", etc. |
allowedMatches | string[] | No | Glob patterns restricting which thing names this entry can access. Repo-scoped entries only — ignored (with warning) on org-level and global entries. |
allowedMatches behavior:
- Repo-scoped entries only.
allowedMatcheson org-level or global wildcard entries is stripped and a warning is returned in the response. - Patterns use glob syntax, for example
Signal/*orConfig/**. - When present, the token can only read or write thing names that match at least one pattern.
- Omitting
allowedMatchesmeans no name restriction for that entry. - An empty array (
[]) is valid and means deny-all for that entry.
The structured flag controls how duplicate entries are handled:
structured: true— multiple entries for the same resource are allowed, so you can assign differentallowedMatchesto different permissions. Each(resource, permission)pair must still be unique — duplicate pairs are rejected with aVALIDATION_ERROR.structuredomitted orfalse— duplicate resource entries are rejected (legacy behavior). Use this when each resource appears at most once.allowedMatcheswithoutstructured—allowedMatchesdoes not requirestructured: trueas long as the payload uses only one entry per resource.
Scope hierarchy (most specific wins): repo-scoped > org-level > global wildcard. Scopes can only narrow access, never escalate beyond the user’s role.
Response 201
Section titled “Response 201”{ "token": "eyJhbGciOi...", "name": "ci-deploy", "scopes": [ { "resource": "myorg/myrepo", "permissions": ["repo:read", "repo:write"] } ], "expiresAt": 1743724800000, "createdAt": 1741132800000}The response may include an optional warnings array (e.g., when allowedMatches was stripped from a non-repo entry).
Errors
Section titled “Errors”| Code | Status | Description |
|---|---|---|
ALREADY_EXISTS | 409 | A token with this name already exists for your account |
VALIDATION_ERROR | 400 | Invalid name format or scope values |
NOT_FOUND | 404 | A resource named in a scope entry does not exist |
FORBIDDEN | 403 | A scope entry requests permissions that exceed your role on the target resource |
UNAUTHENTICATED | 401 | Missing or invalid authentication |
RATE_LIMITED | 429 | Too many PAT creation requests. Honor Retry-After before retrying. |
Example
Section titled “Example”# Single repo scopecurl -X POST https://api.warmhub.ai/api/pats \ -H "Authorization: Bearer <jwt>" \ -H "Content-Type: application/json" \ -d '{ "name": "ci-deploy", "scopes": [ { "resource": "myorg/myrepo", "permissions": ["repo:read", "repo:write"] } ], "description": "CI/CD pipeline token" }'
# Multiple scopes — repo-specific override + org-level fallbackcurl -X POST https://api.warmhub.ai/api/pats \ -H "Authorization: Bearer <jwt>" \ -H "Content-Type: application/json" \ -d '{ "name": "mixed-bot", "scopes": [ { "resource": "myorg/private-repo", "permissions": ["repo:read", "repo:write"] }, { "resource": "myorg", "permissions": ["repo:read"] } ] }'
# Name-restricted token with allowedMatches (set structured: true)curl -X POST https://api.warmhub.ai/api/pats \ -H "Authorization: Bearer <jwt>" \ -H "Content-Type: application/json" \ -d '{ "name": "signal-writer", "structured": true, "scopes": [ { "resource": "myorg/myrepo", "permissions": ["repo:read"], "allowedMatches": ["Signal/*", "Config/*"] }, { "resource": "myorg/myrepo", "permissions": ["repo:write"], "allowedMatches": ["Signal/*"] } ] }'GET /api/pats
Section titled “GET /api/pats”List all personal access tokens for the authenticated user.
Auth: JWT only (no PAT).
Response 200
Section titled “Response 200”[ { "name": "ci-deploy", "description": "CI/CD pipeline token", "scopes": [ { "resource": "myorg/myrepo", "permissions": ["repo:read", "repo:write"] } ], "expiresAt": 1743724800000, "createdAt": 1741132800000 }]| Field | Type | Description |
|---|---|---|
name | string | Token name |
description | string | Human-readable description (if set) |
scopes | Array<{ resource?: string; permissions: string[]; allowedMatches?: string[] }> | Resource-scoped permission entries (if set) |
expiresAt | number | Expiration timestamp (epoch ms) |
revokedAt | number | Revocation timestamp (present only if revoked) |
createdAt | number | Creation timestamp (epoch ms) |
Example
Section titled “Example”curl https://api.warmhub.ai/api/pats \ -H "Authorization: Bearer <jwt>"DELETE /api/pats/:name
Section titled “DELETE /api/pats/:name”Revoke a personal access token. Revoking an already-revoked token is a no-op (idempotent).
Auth: JWT only (no PAT).
Path Parameters
Section titled “Path Parameters”| Parameter | Type | Description |
|---|---|---|
name | string | Token name (URL-encoded) |
Response 200
Section titled “Response 200”{ "ok": true}Errors
Section titled “Errors”| Code | Status | Description |
|---|---|---|
NOT_FOUND | 404 | No token with this name exists for your account |
UNAUTHENTICATED | 401 | Missing or invalid authentication |
Example
Section titled “Example”curl -X DELETE https://api.warmhub.ai/api/pats/ci-deploy \ -H "Authorization: Bearer <jwt>"