REST API
Grant exposes a full REST API at /api/* with interactive Swagger documentation. All endpoints use JSON and follow consistent request/response patterns.
Swagger UI
The API ships with an interactive Swagger UI for exploring and testing every endpoint:
| Resource | URL |
|---|---|
| Swagger UI | http://localhost:4000/api-docs |
| OpenAPI JSON | http://localhost:4000/api-docs.json |
Swagger UI includes request/response schemas, example payloads, and a "Try it out" button for every endpoint. The OpenAPI spec can be imported into Postman, Insomnia, or used to generate client SDKs.
Authenticating in Swagger UI
Most endpoints require a JWT. Follow these steps to authenticate in the Swagger UI:
1. Get a token — Expand POST /api/auth/login under the Authentication tag, click "Try it out", and send:
{
"provider": "email",
"providerId": "admin@example.com",
"providerData": { "password": "YourPassword1!" }
}2. Copy the token — From the response, copy the accessToken value.
3. Authorize — Click the Authorize button (lock icon) at the top of the page, paste the token into the bearerAuth field (without the Bearer prefix), and click Authorize.
4. Test endpoints — All subsequent "Try it out" requests will include the token automatically.
Persist Authorization
Swagger UI is configured with persistAuthorization: true — your token survives page reloads until you click Logout in the Authorize dialog.
Base URL
http://localhost:4000/apiFor production deployments, replace with your deployed API URL.
Authentication
Include a JWT access token in the Authorization header:
Authorization: Bearer <accessToken>Obtain tokens via POST /api/auth/login (email/password) or POST /api/auth/register (new account). Refresh expired tokens with POST /api/auth/refresh.
Authentication Flow
# 1. Login
curl -s -X POST http://localhost:4000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"provider":"email","providerId":"admin@example.com","providerData":{"password":"YourPassword1!"}}'
# Response: { "success": true, "data": { "accessToken": "eyJ...", "refreshToken": "eyJ...", "accounts": [...] } }
# 2. Use the token
curl -s http://localhost:4000/api/organizations \
-H "Authorization: Bearer <accessToken>" \
-H "Content-Type: application/json"
# 3. Refresh when expired
curl -s -X POST http://localhost:4000/api/auth/refresh \
-H "Content-Type: application/json" \
-d '{"accessToken":"<expired>","refreshToken":"<refreshToken>"}'Endpoints by Tag
Every endpoint is fully documented in the Swagger UI. The table below links to each tag for quick navigation:
| Tag | Endpoints | Description |
|---|---|---|
| Authentication | 10 | Login, register, refresh, logout, OAuth, token exchange, authorization check |
| Me | 17 | Profile, sessions, password, auth methods, notifications, preferences, data export |
| Organizations | 5 | CRUD for organizations |
| Organization Invitations | 7 | Invite, accept, resend, renew, revoke |
| Organization Members | 3 | List, update role, remove |
| Projects | 4 | CRUD for projects |
| Users | 5 | CRUD for users within a scope |
| Roles | 4 | CRUD for roles |
| Groups | 4 | CRUD for groups (permission bundles) |
| Permissions | 4 | CRUD for permissions |
| Resources | 4 | CRUD for resources (RBAC targets) |
| API Keys | 5 | Create, list, revoke, delete, token exchange |
| Webhook Subscriptions | 8 | Project webhook CRUD, secret rotate, deliveries, replay |
| Tags | 4 | CRUD for tags |
REST-Only Endpoints
Some operations are only available via REST (not GraphQL):
GET /api/auth/github— Initiate GitHub OAuth (browser redirect)GET /api/auth/github/callback— GitHub OAuth callbackGET /api/auth/project/app-info— Public project-app branding and scopes for hosted sign-inGET /api/auth/project/consent-info— Public consent payload (resolved branding, scopes, signed-in user)POST /api/auth/cli-callback— Exchange CLI one-time code for tokensPOST /api/auth/token— Exchange API key for JWTPOST /api/auth/is-authorized— Check authorization (used by SDKs)GET /api/me/export— GDPR data export (file download)GET /.well-known/jwks.json— JWKS public key discovery (3 scoped variants)
See Transport Layers for a full comparison.
Scoping and Multi-Tenancy
Most endpoints require a scope to specify the tenant context. The scope is passed differently depending on the HTTP method:
GET / DELETE — Query parameters:
GET /api/roles?scopeId=<orgId>:<projectId>&tenant=organizationProjectPOST / PATCH — Request body:
{
"name": "Editor",
"scope": { "tenant": "organizationProject", "id": "<orgId>:<projectId>" }
}The tenant field indicates the level of the hierarchy. The id field is either a single UUID or two UUIDs joined by : (e.g., orgId:projectId).
| Tenant | Scope ID format | Example |
|---|---|---|
account | <accountId> | Personal account scope |
organization | <orgId> | Organization scope |
accountProject | <accountId>:<projectId> | Personal project |
organizationProject | <orgId>:<projectId> | Org project |
A request is authorized in exactly one scope. For a session token that is the scope the request carries: the X-Scope-Tenant and X-Scope-Id headers when present, otherwise the scope in the query or body. For an API key or a project-app token it is the scope of the token itself. A scoped operation acts only in the scope it was authorized in: when its query or body names a scope (scope, scope inside input, or scopeId with tenant), tenant and id must equal the authorized scope, and the request is refused with 403 FORBIDDEN otherwise, before any lookup or permission check, so the response does not reveal whether the target exists. An operation that names no scope is unaffected. system is never accepted as a scope named in a request. The same rule applies to the scope argument and the scope field of the input argument in GraphQL, where the refusal is a Forbidden error.
TIP
See Multi-Tenancy for details on tenant isolation and the hierarchy model.
Response Format
Success:
{
"success": true,
"data": { ... }
}Error:
{
"error": "Localized error message",
"code": "ERROR_CODE"
}See Error Handling for the full error reference.
Field Selection
Load related entities on-demand with the relations query parameter:
GET /api/roles?scopeId=...&tenant=organizationProject&relations=groups,tagsOnly base fields are returned by default. See Field Selection for available relations per resource.
Rate Limiting
Rate limits are configurable via environment variables (SECURITY_RATE_LIMIT_*). Auth endpoints have stricter limits than general endpoints. When a limit is exceeded, the API returns 429 Too Many Requests with a Retry-After header. See Configuration for details.
Related:
- Transport Layers — REST vs GraphQL comparison
- Error Handling — Error codes and status codes
- Integration Guide — End-to-end tutorial
- Server SDK — Protect your routes with
@grantjs/server - Client SDK — Permission-based UI with
@grantjs/client