API reference
Every route the API serves, who may call it, and how sign-in, workspaces, permissions, billing, API keys, errors, lists and limits work.
For the Full Stack package
Base URL and response format
Every route is served under the /api prefix. Locally the base URL is http://localhost:8000/api. On a server it is your API's address followed by /api, the same value the website and the dashboard read from NEXT_PUBLIC_API_BASE_URL (Environment variables). Files are served outside the prefix: the kit's brand files at /media/… and uploads at /media/uploads/….
Request bodies are JSON, except file uploads. Every answer comes in the same envelope:
{
"success": true,
"data": { },
"message": ""
}data holds the result. message is a short code some writes fill in, such as project_created or invitation_sent, and is empty otherwise. Workspaces, projects and tasks are named by their public ids (org_forma, prj_01); members, invitations, keys and files by their number. Project and task texts carry both languages as { "en": { … }, "ar": { … } }.
The health check needs no token:
curl http://localhost:8000/api/health{"success":true,"data":{"status":"ok","database":"up"},"message":""}Until the database answers and holds its tables, it answers 503 with "status":"unavailable" and "database":"down". The Docker image's health check reads that status, and Docker Compose starts the website and the dashboard only once it passes.
Browsers may call the API only from the addresses in CORS_ORIGIN. Left unset it allows http://localhost:3030 and http://localhost:3031, and in production the API refuses to start without it (CORS problems).
Errors
A failure keeps the envelope, with success: false, data: null and the HTTP status:
{
"success": false,
"data": null,
"message": "seat_limit_reached",
"errors": { "email": ["…"] }
}errorsappears on a validation failure (400), with the problems per field.- An unknown field in a body is refused with
400: every body takes only the fields listed below. - When the API has a sentence for the error,
messageis that sentence in the request's language (Accept-Language: arfor Arabic): a refused sign-in, every429, closed sign-ups, an account guarded from a password or email change, a missing webhook secret, a subscription Stripe could not cancel, a platform role or permission the caller does not hold, and maintenance mode among them. - Otherwise, on
401,403and404messageis a general sentence for the status, and on400,402,409,410and503it is the reason as a short code (seat_limit_reached,slug_taken,over_plan_limit,payment_declined,preview_expired,invitation_expired,payment_unavailable), which a client turns into its own sentence. A downgrade refused for usage also carriesblockers. 402means a plan upgrade the payment provider would not charge:payment_declined,payment_needs_action(the bank wants the payment confirmed) orpayment_method_missing. The plan stays as it was.- While platform staff have maintenance mode on, every route outside the short list in Maintenance mode answers
503to anyone who is not platform staff, with the sentence inmessageandcode: "maintenance_mode". 401means no token, an expired one, or wrong credentials.403means signed in without the permission, or a write to a suspended workspace.404also answers for a workspace the caller is not a member of, so a stranger cannot tell which workspaces exist.- A request over a limit answers
429(Limits per address).
Signing in
Sign in
Terminalcurl -X POST http://localhost:8000/api/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"owner@northstack.example","password":"YOUR_PASSWORD"}'Put the seeded password from Installation in place of
YOUR_PASSWORD, or use an account of your own.data.access_tokenis the token, withtoken_typeBearer,expires_in, andadmin, the account with its platform roles, permissions and workspaces.Call a protected route
Terminalcurl http://localhost:8000/api/workspaces \ -H "Authorization: Bearer YOUR_TOKEN"That lists the account's workspaces with its role in each. Every workspace route then takes the workspace's public id in its path:
Terminalcurl "http://localhost:8000/api/workspaces/org_forma/projects?limit=20" \ -H "Authorization: Bearer YOUR_TOKEN"
remember: truesigns a token for 30 days andremember: falseone for 12 hours; left out, the token lastsJWT_EXPIRATION,7dby default.- An unknown email and a wrong password both answer
401with the same message, after the same password-hashing work, so neither the answer nor its timing tells which emails have accounts. - Ten failed sign-ins from one address within 15 minutes answer
429until the oldest failure is 15 minutes old (RATE_LIMIT_LOGIN). Successful sign-ins are not counted. - Tokens are stateless:
POST /api/auth/logoutanswers at once, and signing out is the client forgetting its token.
| Method | Path | Who can call it | What it does |
|---|---|---|---|
POST | /api/auth/login | Anyone | Sign in: email, password, optionally remember |
POST | /api/auth/signup | Anyone | A new account, signed in at once and ready for onboarding: name (2 to 80 characters), email, password (8 to 128) and acceptTerms (true). A taken email answers 409 with email_taken; while the platform's signups_enabled setting is off it answers 403 |
POST | /api/auth/forgot-password | Anyone | email, optionally locale (en, ar). Mails a reset link that works once for one hour when the account exists, and answers the same either way |
POST | /api/auth/reset-password | Anyone | token (from the link) and the new password (8 to 128). A used or expired link answers 401 |
POST | /api/auth/logout | Anyone | Answers { signedOut: true } |
GET | /api/auth/me | Any signed-in account | The account, its platform roles and permissions, and its workspaces |
PATCH | /api/auth/me | Any signed-in account | Change firstName, lastName, email or phone |
PATCH | /api/auth/me/preferences | Any signed-in account | theme (system, light, dark), reducedMotion, locale (en, ar) and lastWorkspaceId, the workspace the next sign-in opens |
PATCH | /api/admins/profile/password | Any signed-in account | Change the account's own password: current_password, password, password_confirmation |
Workspaces and permissions
- Workspace routes start with
/api/workspaces/:workspaceId, the workspace's public id. On every request the API checks that the caller has an active membership there (otherwise404) and holds the permission the route names (otherwise403). The role is the membership's: one person can be the owner of one workspace and an admin of another. - A workspace holds four roles (
owner,admin,member,billing) and its own matrix of 14 permissions:workspace.read,workspace.update,workspace.delete,projects.read,projects.write,members.invite,members.remove,roles.manage,billing.read,billing.manage,keys.manage,audit.read,ai.readandai.apply. The owner holds them all, andworkspace.deleteandroles.managestay with the owner whatever the matrix says. A change to the matrix applies to the next request. - A suspended workspace keeps every
GETand refuses every other method with403. - A past-due workspace keeps everything until
BILLING_GRACE_DAYS(7 by default) have passed; then it cannot create projects, invitations or API keys (403), while every read and every other write keeps working. - Platform routes (
/api/platform,/api/users,/api/admins,/api/roles,/api/settings) check the account's platform permissions instead, granted through platform roles. The seed creates Platform admin, with all 21, and Support, which reads every platform page and exports reports. A route that lists several permissions accepts any one of them.
Lists and paging
{ "data": [ ], "total": 12, "page": 1, "limit": 7, "totalPages": 2 }- Projects, archived projects, activity, chat conversations and their messages, and the platform's organizations and users page with
pageandlimit, at most 100 a page. The defaults are 7 projects, 12 events, 20 conversations and 10 organizations or users. - Staff, platform settings and platform roles page with
pageandpage_countinstead (10 by default, at most 100). Platform roles page only when both are given; otherwise every role is returned. - Lists come newest first (projects, users and organizations by their last change), with the id as a tiebreaker, unless a
sortis given; platform roles come by name. The projects list also returnscountsper tab. - What belongs to one workspace and is shown whole answers without paging: a board's tasks, members, invitations, API keys, a project's files, the latest 36 invoices and the latest 8 notifications.
Workspaces, members and the roles matrix
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/workspaces | Any signed-in account | The account's workspaces with its role in each, for the switcher |
GET | /api/workspaces/slug-availability | Any signed-in account | ?slug=: whether a workspace address is free, with a suggestion when it is not |
POST | /api/workspaces | Any signed-in account | Onboarding: name (2 to 80), slug (3 to 48 lowercase letters, digits and hyphens), region (europe, north_america, asia_pacific) and optionally invitations, up to 20 { email, role }. The caller becomes the owner, on a Starter trial as long as the platform's trial_days. A taken slug answers 409 with slug_taken, and while signups_enabled is off it answers 403 |
GET | /api/workspaces/:workspaceId | A member with workspace.read | The workspace: name, address, plan, usage against the plan's limits and its settings. billingEmail is null unless the caller holds billing.read or billing.manage |
PATCH | /api/workspaces/:workspaceId | A member with workspace.update | Change name, slug, locale (en, ar), timeZone (an IANA zone such as Europe/Berlin), domainConfigured, emailNotifications (the billing emails) or weeklyDigest |
DELETE | /api/workspaces/:workspaceId | The workspace's owner | confirmName (the workspace's name, typed out) and the owner's password. The workspace is soft-deleted, and its Stripe subscription is canceled first; if Stripe refuses, it answers 409 and the workspace is kept |
GET | /api/workspaces/:workspaceId/members | A member with workspace.read | The members with their roles. search, role |
PATCH | /api/workspaces/:workspaceId/members/:memberId | A member with roles.manage | Change a member's role (admin, member, billing). The owner's row answers 403 |
DELETE | /api/workspaces/:workspaceId/members/:memberId | A member with members.remove | Remove a member. The owner cannot be removed, and an admin may not remove another admin |
POST | /api/workspaces/:workspaceId/members/transfer-ownership | The workspace's owner | memberId and the owner's password. The member becomes the owner and the previous owner an admin |
GET | /api/workspaces/:workspaceId/roles | A member with workspace.read | The roles matrix: each permission, in six groups, and the roles that hold it |
PUT | /api/workspaces/:workspaceId/roles | The workspace's owner | Save the matrix: matrix, each permission mapped to the roles that hold it. An unknown permission or role answers 400 |
Invitations
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/workspaces/:workspaceId/invitations | A member with workspace.read | status (pending, the default, or all). Answers invitations and seats, the seats used and left on the plan |
POST | /api/workspaces/:workspaceId/invitations | A member with members.invite | Invite email as role (admin, member, billing). The link is mailed and works for 7 days. A full plan answers 409 with seat_limit_reached; already_member and already_invited are refused the same way |
POST | /api/workspaces/:workspaceId/invitations/:id/resend | A member with members.invite | A new link, valid for 7 more days. Once a minute at most (resend_too_soon), and five times per invitation (resend_limit_reached) |
DELETE | /api/workspaces/:workspaceId/invitations/:id | A member with members.invite | Revoke an invitation |
GET | /api/invitations/:token | Anyone | What an invitation link opens: the workspace's name and address, the role, the invited email partly hidden, and whether it is still open |
POST | /api/invitations/:token/accept | The invited account, signed in | Join the workspace. An expired or revoked link answers 410; an account whose email is not the invited one answers 403; a full plan answers 409 with seat_limit_reached |
Invitation emails go through the API's SMTP settings, or into its log while SMTP_HOST is empty, link included.
Projects and files
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/workspaces/:workspaceId/projects | A member with projects.read | Projects with counts per tab. status (all, active, completed, or one status), state (in_progress, review, blocked, completed), search, ownerId, dueBefore (YYYY-MM-DD), sort (updated_at, due_date, name, progress), order (asc, desc), page, limit |
GET | /api/workspaces/:workspaceId/projects/archived | A member with projects.read | The archived projects, with the same filters |
GET | /api/workspaces/:workspaceId/projects/owners | A member with projects.read | The members who own projects, for the owner filter |
GET | /api/workspaces/:workspaceId/projects/:projectId | A member with projects.read | One project with its owner, due date, progress and task counts |
POST | /api/workspaces/:workspaceId/projects | A member with projects.write | Create a project: translations (en required, ar optional, each name up to 120 characters and description up to 2000), and optionally status, ownerId (a member), dueDate (YYYY-MM-DD) and progress (0 to 100) |
PATCH | /api/workspaces/:workspaceId/projects/:projectId | A member with projects.write | Change any of those fields |
DELETE | /api/workspaces/:workspaceId/projects/:projectId | A member with projects.write | Archive a project. Nothing is deleted |
POST | /api/workspaces/:workspaceId/projects/:projectId/restore | A member with projects.write | Bring an archived project back |
GET | /api/workspaces/:workspaceId/projects/:projectId/activity | A member with projects.read | The project's latest 100 events |
GET | /api/workspaces/:workspaceId/projects/:projectId/files | A member with projects.read | The project's files, newest first |
POST | /api/workspaces/:workspaceId/projects/:projectId/files | A member with projects.write | Attach a file: multipart file, 10 MB at most. Its size counts toward the plan's storage |
GET | /api/workspaces/:workspaceId/projects/:projectId/files/:fileId/download | A member with projects.read | The file's url, name and mime |
Creating a project in a past-due workspace whose grace period has ended answers 403.
Tasks and the board
A task is listed and created under its project, and changed by its own id under the workspace.
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/workspaces/:workspaceId/projects/:projectId/tasks | A member with projects.read | The board's tasks in column order. assigneeId, priority, search |
POST | /api/workspaces/:workspaceId/projects/:projectId/tasks | A member with projects.write | Create a task: translations (en required, each title up to 160 characters and description up to 4000), and optionally status (backlog, the default, in_progress, review, done), priority (low, normal, high), assigneeId (a member) and dueDate |
PATCH | /api/workspaces/:workspaceId/tasks/:taskId | A member with projects.write | Change any of those fields |
POST | /api/workspaces/:workspaceId/tasks/:taskId/move | A member with projects.write | Move a card: status (the column) and beforeId, the card it lands above, or nothing for the column's end |
DELETE | /api/workspaces/:workspaceId/tasks/:taskId | A member with projects.write | Archive a task |
An assignee who is not a member answers 400 with assignee_not_member. Every change writes an event to the activity trail, such as task.moved with the columns it went from and to.
Overview, activity, notifications and search
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/workspaces/:workspaceId/overview | A member with workspace.read | The overview: projects by status, members and pending invitations, task events per day for the last seven days, the launch checklist, up to three projects due within the period and the latest six events. period (7d, 30d, the default, 90d). Without projects.read the project counts, the chart and the projects due come back zero or empty |
GET | /api/workspaces/:workspaceId/activity | A member with audit.read | The activity trail, with the people in it as actors. action (one action such as task.moved, or a family ending in a dot such as task.), actorId, from and to (YYYY-MM-DD), search, page, limit |
GET | /api/workspaces/:workspaceId/activity/export | A member with audit.read | The same filters as a CSV file (date, actor, action, subject, details), up to 5,000 rows. Cells that would run as a spreadsheet formula are escaped |
GET | /api/workspaces/:workspaceId/notifications | A member with workspace.read | The bell: the latest 8 events and how many are newer than the reader's last look |
POST | /api/workspaces/:workspaceId/notifications/read | A member with workspace.read | Mark everything up to now as read |
GET | /api/workspaces/:workspaceId/search | A member with workspace.read | q (2 to 100 characters): up to five projects, tasks and members each. Projects and tasks appear only for a member with projects.read |
The overview's latest events and the bell show the whole trail to a caller with projects.read or audit.read, only the billing. events to one with billing.read and neither of those, and nothing to anyone else.
Billing, its webhook and the test checkout
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/workspaces/:workspaceId/billing | A member with billing.read | The subscription: plan, status, price, renewal, a cancellation or downgrade waiting for the renewal, the past-due grace period, usage and any checkout in progress |
GET | /api/workspaces/:workspaceId/billing/invoices | A member with billing.read | The latest 36 invoices |
GET | /api/workspaces/:workspaceId/billing/invoices/:invoiceId/download-link | A member with billing.read | A signed link to the invoice's PDF, valid for five minutes, built from PUBLIC_API_URL |
POST | /api/workspaces/:workspaceId/billing/preview | A member with billing.manage | What changing to planId (starter, pro, scale) costs: upgrade or downgrade, when it takes effect, today's charge (chargeTodayCents), the next invoice and any blockers (seats, API requests or storage over the smaller plan's limits). An upgrade of a running Stripe subscription is priced by Stripe and answers chargesCardOnFile: true with a quote that holds that price for 30 minutes; the test checkout prorates the difference over the rest of the period. The current plan answers 400 with same_plan, except on a first payment (a trial, an ended trial or a canceled workspace), where any plan is charged its full price |
POST | /api/workspaces/:workspaceId/billing/checkout | A member with billing.manage | planId, optionally locale and the preview's quote. An upgrade of a running Stripe subscription is made at once and charged to the payment method on file (upgraded: true), the renewal date unchanged; a refused charge answers 402 with payment_declined, payment_needs_action or payment_method_missing, and an expired or foreign quote 409 with preview_expired. Any other upgrade answers the hosted checkout's url and its session. A downgrade is scheduled for the renewal (scheduled: true), on Stripe through a subscription schedule, or refused with 409 over_plan_limit and the blockers |
POST | /api/workspaces/:workspaceId/billing/checkout/:sessionId/sync | A member with billing.read | After the return from checkout: read the session back from the provider and answer the session and the summary. The plan changes only once the provider says it is paid |
POST | /api/workspaces/:workspaceId/billing/keep-plan | A member with billing.manage | Take back a downgrade scheduled for the renewal; answers the summary. Nothing scheduled answers 409 with no_scheduled_change |
POST | /api/workspaces/:workspaceId/billing/portal | A member with billing.manage | The Stripe customer portal's url, for the payment method; url: null for a workspace not billed through Stripe |
POST | /api/workspaces/:workspaceId/billing/cancel | A member with billing.manage | Cancel at the end of the period. A downgrade scheduled for the renewal is dropped |
POST | /api/workspaces/:workspaceId/billing/resume | A member with billing.manage | Keep a subscription that was set to cancel |
PATCH | /api/workspaces/:workspaceId/billing/email | A member with billing.manage | Change the billing email |
GET | /api/billing/invoices/download | Anyone holding the signed link | ?token=: the invoice as a PDF. A wrong token answers 401 and an expired one 403 |
POST | /api/billing/webhooks/stripe | Stripe | Stripe's events, verified with STRIPE_WEBHOOK_SECRET against the Stripe-Signature header; a missing or wrong signature answers 400, and so does every delivery while STRIPE_WEBHOOK_SECRET is empty, with a message naming it. Subscriptions use checkout.session.completed, invoice.paid, invoice.payment_failed and customer.subscription.deleted, each event id applied once. Licence orders arrive on the same route (The licence checkout) |
GET | /api/billing/simulator/:sessionId | A member with billing.manage in the session's workspace | The test checkout's session, for the dashboard's own payment page |
POST | /api/billing/simulator/:sessionId/complete | A member with billing.manage in the session's workspace | Test payment: the plan changes as if the provider had confirmed it |
POST | /api/billing/simulator/:sessionId/cancel | A member with billing.manage in the session's workspace | Leave the test checkout without paying |
- Checkout uses Stripe Checkout when
STRIPE_SECRET_KEYis set, and the built-in test checkout otherwise, or wheneverBILLING_PROVIDER=simulator. The test checkout charges nobody. - The return address alone never changes a plan: only the signed webhook, or the session read back on
sync, does. - Each plan's Stripe price is created on first use, with the lookup key
northstack_<plan>_usd_<cents>_month, so nothing is set up in Stripe first. - A completed Stripe checkout for a workspace that already has a Stripe subscription cancels the old one, with its unused time credited.
invoice.paidstores Stripe's invoice once (invoices are unique per provider invoice id), with its amount and period, moves the workspace's period to the one an invoice starts, and on a renewal applies a downgrade scheduled for it. - On the test checkout the API renews each period at its end (a paid invoice, the next 30 days, a scheduled downgrade applied) and ends one set to cancel. A trial that reaches its end becomes past due, with the grace period. Usage counters start again at zero each period. This runs on every billing read and once an hour.
- Point your Stripe webhook at
https://<your API>/api/billing/webhooks/stripewith the events above pluscheckout.session.expired.
API keys and the public API
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/workspaces/:workspaceId/api-keys | A member with keys.manage | The workspace's keys: name, prefix, scopes, environment, last use and who made it. Never the secret |
POST | /api/workspaces/:workspaceId/api-keys | A member with keys.manage | name (2 to 60), scopes (one or more of projects:read, projects:write, tasks:read, tasks:write) and environment (production, the default, or demo). Answers the key with its secret, shown this once; only its SHA-256 hash is kept |
DELETE | /api/workspaces/:workspaceId/api-keys/:id | A member with keys.manage | Revoke a key. It stops working at once |
GET | /api/v1/projects | An API key with projects:read | The workspace's 100 most recently updated projects |
POST | /api/v1/projects | An API key with projects:write | Create a project, with the same body as the dashboard's |
PATCH | /api/v1/projects/:projectId | An API key with projects:write | Change a project, with the same body as the dashboard's |
GET | /api/v1/projects/:projectId/tasks | An API key with tasks:read | One project's tasks |
POST | /api/v1/projects/:projectId/tasks | An API key with tasks:write | Create a task, with the same body as the board's |
curl http://localhost:8000/api/v1/projects \
-H "Authorization: Bearer nst_live_…"- A production key's secret starts with
nst_live_, a demo key's withnst_demo_. A demo key works only while the API runs in demo mode. - No key, an unknown or revoked one answers
401; a key without the route's scope answers403; a suspended workspace refuses the write. - Every route but the project list runs as the key's creator: what it creates is recorded as made by them, with the dashboard's validation, activity events and plan limits. It answers
404once the creator has left the workspace, and the three writes answer403once the creator's role no longer holdsprojects.write. - Every call counts toward the plan's API requests, which the billing page and the plan limits read.
Platform overview and organizations
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/platform/overview | Staff with platform.view | Monthly recurring revenue and its history, the plan mix and the organizations by status. range (30d, 90d, 6m, the default, 12m) |
GET | /api/platform/overview/export | Staff with reports.export | Every organization as a CSV file: organization, id, plan, status, seats, mrr_usd, created |
GET | /api/platform/organizations | Staff with organizations.view | Organizations. search (name or address), plan (starter, pro, scale), status (trialing, active, past_due, canceling, canceled, suspended), page, limit |
GET | /api/platform/organizations/:id | Staff with organizations.view | One organization, by its public id |
POST | /api/platform/organizations/:id/suspend | Staff with organizations.manage | reason (3 to 300 characters). The workspace keeps its reads and refuses every write. A suspended one answers 409 |
POST | /api/platform/organizations/:id/reactivate | Staff with organizations.manage | Lift a suspension |
Users, staff, platform roles and settings
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/users | Staff with users.view | Every account. search, role (owner, admin, member or billing for a workspace role held anywhere, staff for a platform role, none for accounts in no workspace), page, limit |
GET | /api/users/deleted | Staff with users.view | The deleted accounts, with the same filters |
GET | /api/users/statistic | Staff with users.view | Totals: accounts, deleted, staff, without a workspace, new in the last 30 days |
GET | /api/users/deleted/:username | Staff with users.view | One deleted account |
GET | /api/users/:username | Staff with users.view | One account |
POST | /api/users | Staff with users.create | Create an account: first_name, last_name, email, password (8 to 128), optionally phone |
PATCH | /api/users/:username | Staff with users.update | Change first_name, last_name, email or phone. A new email is guarded as below |
PATCH | /api/users/:username/change-password | Staff with users.update | Set a new password (8 to 128), guarded as below |
DELETE | /api/users/:username | Staff with users.delete | Move an account to the deleted list. An account that owns a workspace answers 409 with user_owns_workspace until ownership moves |
POST | /api/users/deleted/:username/restore | Staff with users.restore | Bring a deleted account back |
GET | /api/admins | Staff with admins.view | Every account with its platform roles. email, name (first name, last name or username), phone, a row matching any one given, page, page_count |
GET | /api/admins/statistics | Staff with admins.view | Counts for the staff page |
GET | /api/admins/:id | Staff with admins.view | One account, by id or username |
POST | /api/admins | Staff with admins.create | Add a staff account: first_name, last_name, email, password and password_confirmation (6 characters or more), optionally phone, phone_code, profile_picture |
PATCH | /api/admins/:id | Staff with admins.edit | Change those fields; a new password or email is guarded as below |
PATCH | /api/admins/:id/roles | Staff with admins.assign_roles | Set an account's platform roles: role_ids. Each role granted or taken away must hold only permissions the caller holds itself, or it answers 403 (role_outranks_caller). Its open dashboard is told at once |
DELETE | /api/admins/:id | Staff with admins.delete | Remove an account. One that owns a workspace answers 409 |
PATCH | /api/admins/profile | Staff with admins.edit | Change the signed-in staff member's own profile |
GET | /api/roles | Staff with roles.view | Platform roles. name, guard_name, created_from, created_to, page, page_count |
GET | /api/roles/statistics | Staff with roles.view | Counts for the roles page |
GET | /api/roles/select | Staff with roles.view | Every role's id and name, for a picker |
GET | /api/roles/permissions | Staff with roles.view | Every platform permission, grouped by area |
GET | /api/roles/:id | Staff with roles.view | One role with its permissions |
POST | /api/roles | Staff with roles.create | Add a role: name. A name in use answers 409 |
PUT | /api/roles/:id | Staff with roles.edit | Rename a role: name |
POST | /api/roles/:id/permissions | Staff with roles.assign_permissions | Set a role's permissions, by name. The caller may give only permissions it holds, and change only a role that holds none it lacks, or it answers 403 (permission_outranks_caller). Everyone holding it is told at once |
DELETE | /api/roles/:id | Staff with roles.delete | Delete a role. One that someone holds answers 400 |
GET | /api/settings | Staff with settings.view | The platform settings. search, category, page, page_count |
GET | /api/settings/:key | Staff with settings.view | One setting |
PATCH | /api/settings/:key | Staff with settings.edit | Change one setting: value |
DELETE | /api/settings/:key | Staff with settings.edit | Delete one setting |
The guard on passwords and emails: a caller other than the account itself may set its password or change its email only when they are a Platform admin or hold every platform permission the account holds; otherwise the API answers 403. The email is guarded too, because a password reset is sent to it.
The guard on roles: nobody can grant, remove or raise a platform role beyond their own permissions, so admins.assign_roles or roles.assign_permissions alone never leads to the Platform admin role.
The 21 platform permissions are platform.view, reports.export, organizations.view, organizations.manage, users.view, users.create, users.update, users.delete, users.restore, admins.view, admins.create, admins.edit, admins.delete, admins.assign_roles, roles.view, roles.create, roles.edit, roles.delete, roles.assign_permissions, settings.view and settings.edit.
Public settings and maintenance mode
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/public-settings | Anyone | maintenanceMode and trialDays (the platform's trial_days, 14 when unset): what onboarding states as the trial length, and whether the dashboard shows its maintenance screen. Open during maintenance |
While the platform setting maintenance_mode is true, every request from anyone who is not platform staff (an account holding platform.view) answers 503, with code: "maintenance_mode" and the message "The platform is closed for maintenance. Please try again shortly." A missing or wrong token gets the same 503. These stay open to everyone:
GET /api/healthandGET /api/public-settings;POST /api/auth/loginandPOST /api/auth/logout, where sign-in still refuses anyone who is not staff with the503once the credentials are right;POST /api/billing/webhooks/stripeand the signed invoice download;- the MCP endpoint, which keeps its own key, and
GET /api/demo/status; - the website's
POST /api/contactand every/api/licensesroute, so the contact form and the licence checkout keep working.
Everything else is closed to non-staff, sign-up, password recovery, invitations, onboarding, the public API under /api/v1, uploads and the assistant included. The list is back-end/src/common/constants/maintenance.constants.ts.
The contact form
| Method | Path | Who can call it | What it does |
|---|---|---|---|
POST | /api/contact | Anyone | name (2 to 120), email, topic (licensing, setup, product), message (20 to 4000 characters), clientToken (8 to 64 letters, digits, _ or -) and the hidden field website. Answers { id } |
- A repeated
clientTokenreturns the first message's id instead of storing a second one, so a retry is safe. - A filled
websitefield is treated as a bot: the answer looks the same, and nothing is stored. - Each address may send five an hour; the next answers
429. - Messages are stored in the
contact_messagestable and forwarded toCONTACT_INBOXwhen it is set.
The licence checkout
| Method | Path | Who can call it | What it does |
|---|---|---|---|
GET | /api/licenses/checkout/options | Anyone | The payment methods (card, paypal), the provider taking each (stripe, paypal or simulator), and testMode, true when neither moves real money |
POST | /api/licenses/checkout | Anyone | license (single, studio), email, name (2 to 120), method (card, paypal), optionally locale (en, ar), and clientToken. Answers order, the order's id, and url, the page to pay on. A payment page that cannot be opened answers 503 with payment_unavailable |
GET | /api/licenses/orders/:id | Anyone holding the order's id | The order: reference, licence, amount, email, name, method, provider, status (pending, paid, cancelled, failed), test, and licenseKey once it is paid |
POST | /api/licenses/orders/:id/confirm | Anyone holding the order's id | The buyer is back from Stripe or PayPal: ask the provider whether the payment went through, then answer the order |
POST | /api/licenses/orders/:id/cancel | Anyone holding the order's id | The buyer left the payment page without paying |
POST | /api/licenses/orders/:id/simulate | Anyone holding the order's id | The test checkout's Pay or Cancel: outcome (pay, cancel). An order taken by Stripe or PayPal answers 404 |
- The price is the server's own, from
back-end/src/modules/licenses/constants/license-offers.ts; the browser never sends one. - A card goes to Stripe Checkout when
STRIPE_SECRET_KEYis set, PayPal to PayPal whenPAYPAL_CLIENT_IDandPAYPAL_CLIENT_SECRETare set, and either to the test checkout otherwise. In demo mode a provider with live keys is never offered. - An order is paid once, by whichever comes first of Stripe's
checkout.session.completedonPOST /api/billing/webhooks/stripe, the return'sconfirm, or the test checkout's Pay. Only that one issues the licence key and mails the receipt.checkout.session.expiredmarks the order failed. - The order's id is a long random string carried only by the buyer's return links. A repeated
clientTokenanswers the first order. Each address may start ten orders an hour.
Uploads and media
| Method | Path | Who can call it | What it does |
|---|---|---|---|
POST | /api/helpers/upload | Any signed-in account | Store a picture: multipart file (10 MB at most), path for the folder and for for the size preset (profile, cover, logo, or default). Answers the addresses by size: original, at most 1920 pixels wide, and one resized copy, all JPEG. A file that is not a readable image answers 400 |
Pictures and project files go to the Cloudflare R2 bucket when its five R2_* variables are set, and to MEDIA_UPLOAD_DIR otherwise, served at PUBLIC_MEDIA_URL followed by /uploads/….
Everything the API serves under /media carries X-Content-Type-Options: nosniff, and a file that is not a raster image, a PDF, plain text, audio or video (an HTML or SVG file, for one) is sent with Content-Disposition: attachment, so it downloads rather than opening as a page on the API's address. Files in an R2 bucket are served by the bucket, with the type they were uploaded with.
Limits per address
| Route | Limit | Variable |
|---|---|---|
POST /api/auth/login | 10 failed sign-ins per 15 minutes | RATE_LIMIT_LOGIN |
POST /api/auth/signup | 10 per hour | None |
POST /api/auth/forgot-password | 5 per 15 minutes | None |
POST /api/auth/reset-password | 10 per 15 minutes | None |
POST /api/contact | 5 per hour | None |
POST /api/licenses/checkout | 10 per hour | None |
POST …/invitations/:id/resend | Once a minute, five times per invitation | None |
A request over a limit answers 429, with a sentence naming what was limited and how long to wait, such as "Too many sign-ups from this address. Wait an hour, then try again." The counts live in the API's memory, so a restart clears them. Behind a reverse proxy, set TRUST_PROXY to the number of proxies in front of the API, or every visitor is counted as the proxy's one address.
The AI assistant, its conversations and proposals
Included with your purchase. Sign in to read, or open it in your download.
The assistant's streaming route, its model list, the saved conversations and the task proposals.
MCP server
Included with your purchase. Sign in to read, or open it in your download.
The MCP endpoint for coding agents, how it is authorised and which workspace it reads.
Live permission updates over socket.io
Included with your purchase. Sign in to read, or open it in your download.
The socket.io namespace the dashboard listens on, its event and how a connection is authorised.
Demo mode routes
Included with your purchase. Sign in to read, or open it in your download.
The routes that exist only for a public demo build.