One identity. One file layer.
Here is how to plug into it.
Endorr Core owns accounts, sessions, OAuth/OIDC, organisations, teams, file identity and permissions. The Suite (and every product) is a client of Core: sign users in with Login with Endorr, then call the /v1 API with the access token. Nothing in this guide requires a shared database or a shared cookie.
Overview
| Concern | Who owns it | How the Suite uses it |
|---|---|---|
| Accounts, passwords, email verification, sessions | Core | Never touch. Redirect to Core for sign-in; Core hands back an ID token. |
| Organisations, roles, invitations, teams | Core (data + rules) | Suite renders the management UI on top of the /v1 organisation endpoints. |
| Files, folders, ACLs, share links, storage | Core (metadata + signed URLs) | Suite renders the browser; bytes go browser ↔ storage directly via signed URLs. |
| Sign-in, sign-up, email verification, password reset, consent, invitation acceptance | Core UI at https://auth.endorr.com | Link to them; do not rebuild them. Users come back via return_to / the OAuth callback. |
| Account profile, password, sessions, connected apps, members, teams, Cloud file browser, sharing UI | Suite | Build here, against /v1 with the account and organizations scopes. |
Base URL for everything below: https://auth.endorr.com. All product-facing endpoints are versioned under /v1. Responses are JSON with snake_case keys and ISO-8601 timestamps. Every response carries an x-request-id header; include it in bug reports.
Signing users in
The Suite is a confidential OAuth client using Authorization Code + PKCE, like PrintReadySheets and FileFixer. Your backend keeps the client secret and refresh tokens; the browser only ever holds your own Suite session. Do not try to read the Core session cookie: it is HttpOnly and scoped to auth.endorr.com, and cross-origin cookie calls to /v1 are rejected by the CSRF check by design.
1. Register the client
Core seeds an endorr-suite first-party client, which is the only kind allowed to request the account scope. Set its callback URLs and secret from the deployment environment and run the seed once:
Redirect URIs are matched exactly (scheme, host, port, path, no query, no fragment). http is only accepted for loopback hosts.
2. Discover the endpoints
3. Redirect to Core
Core signs the user in if needed, requires a verified email, shows the "Continue to Endorr Suite" consent screen the first time, and redirects back with code and state. Errors come back as error + error_description on the same callback (for example access_denied, login_required with prompt=none).
4. Exchange the code
Codes are single use and expire after 2 minutes. A failed exchange (wrong verifier, wrong redirect URI) consumes the code, so restart the flow rather than retrying.
Tokens and verification
| Token | Format | Lifetime | Use it for |
|---|---|---|---|
| id_token | RS256 JWT | 60 min | Establishing who signed in. Verify once, then create your own Suite session. |
| access_token | RS256 JWT (typ at+jwt) | 60 min | Authorization: Bearer on every /v1 call. |
| refresh_token | opaque, rotating | 60 days | Getting a new access token from your backend. Store the newest one only. |
Store sub as the user's endorr_user_id and org_id as their selected organisation. Never match accounts by email address: two people can share a mailbox, and Core has already proven control of it. Keys rotate; the JWKS keeps retired keys for 30 days, so cache the JWKS but refetch on an unknown kid.
Calling the API
Sending users to Core pages
Core hosts the screens that need Core's own session: sign-in, sign-up, verification, password reset, invitation acceptance. Link to them with return_to set to an absolute URL on one of your registered origins (any redirect URI or homepage origin of a registered client) and Core sends the user back there afterwards. Anything else falls back to Core's minimal signed-in page, which itself links to the Suite.
Organisation context
Every file and folder belongs to exactly one organisation. A bearer token carries the organisation chosen at sign-in (org_id) and all resource calls use it. To let a user work in another organisation they belong to, send them through /oauth/authorize again with org_id=… (consent is remembered, so it is a silent redirect) and swap the tokens. POST /v1/files/uploads and POST /v1/folders also accept an explicit organization_id.
Errors
| Status | Code | Meaning | What to show |
|---|---|---|---|
| 401 | unauthorized | No or invalid token / session | Re-authenticate |
| 403 | forbidden | Inside the organisation, but the role is insufficient (or the token lacks a scope) | "You need editor access" style messaging |
| 404 | not_found | Resource does not exist, is deleted, or belongs to another organisation. Core never distinguishes these. | "Not found" |
| 409 | conflict | State conflict, e.g. completing an upload before the bytes arrived | Retry or explain |
| 422 | validation_failed | Bad input; details[] lists { path, message } | Field errors |
| 429 | rate_limited | Too many attempts | Back off |
Scopes
| Scope | Grants |
|---|---|
openid | Confirm who you are with an Endorr ID token. |
profile | Your name. |
email | Your email address and whether it is verified. |
offline_access | Stay signed in to this product without asking again. |
organizations | The organisations you belong to and your role in them. |
files:read | Open your Endorr files in this product. |
files:write | Save new files and derivatives to Endorr. |
account | Manage your Endorr account: profile, password, sessions and connected apps. |
Request only what the Suite needs. Missing scopes come back as 403 forbidden with the scope named in the message. account lets a first-party client manage the signed-in account (profile, password, sessions, connected apps) on the user's behalf; Core refuses it for any client that is not marked first-party.
Rate limits
Per IP unless stated: token endpoint 60 / 1 min, authorisation 60 / 1 min, invitations 30 / 60 min per user, share-link resolution 60 / 1 min. Login and signup limits apply to Core's own pages only.
Organisations and teams
Roles are owner › admin › member. They control organisation administration only; they are not file permissions (see below). Every account gets a personal workspace organisation at signup; personal workspaces cannot invite members or be left. Business organisations are created with POST /v1/me/organizations.
| Action | Who may do it |
|---|---|
| Rename organisation, create/delete teams, add/remove team members | owner, admin |
| Invite members, revoke invitations, change member ↔ admin | owner, admin |
| Remove an admin | owner |
| Transfer ownership (PATCH role=owner; the previous owner becomes admin) | owner |
| Leave the organisation (DELETE own membership) | any non-owner member |
Removing a member also removes their team memberships and user-level resource grants in that organisation. Deleting a team removes the team's grants. Membership lists include you: true for the caller so the UI can disable self-actions.
Permissions
Resource roles are viewer › editor › manager and are evaluated by Core on every request. The effective role is the strongest of:
- Organisation owners and admins are
manageron everything in their organisation. - The creator of a file or folder is
manageron it. - Explicit grants on the resource itself or on any ancestor folder, to an
organization(everyone), ateam, or auserprincipal. Grants inherit downwards; there are no deny rules.
| Role | Can |
|---|---|
| viewer | List, read metadata, download, open in another product |
| editor | Viewer + upload into the folder, rename/move files, promote to permanent, delete files, record derivatives |
| manager | Editor + list/add/remove grants, create share links, rename/move/delete folders |
capabilities: { can_view, can_edit, can_manage } computed for the caller. Use it to enable or hide actions. Core still checks on the write, so a stale UI can never escalate.Principals must belong to the same organisation as the resource; Core rejects anything else with 422. Members without any grant get 403; users from other organisations get 404. Design list views around what the API returns: GET /v1/folders/:id/children already filters to what the caller may see.
Files and folders
The unit of the suite is the Endorr file ID (file_…). It is stable across products, storage providers and promotion from temporary to permanent. Pass IDs between products, never signed URLs.
Upload
Signed URLs live 10 minutes. Uploads into a folder need editor there; uploads to the organisation root need membership only. Files are temporary by default and expire after 30 days; PATCH { "storage_class": "permanent" } keeps them and clears expires_at without changing the ID. Show the expiry state in the UI ("Saved to Endorr for 30 days"); the Suite is where "Keep permanently" lives.
Open in another product
Folders
Derivatives and lineage
A fixed file, mockup or sheet is a new file. After creating it, record the edge so users can trace what happened:
GET /v1/files/:id returns lineage.derived_from[] and lineage.derivatives[]. created_via on every file is the client id that made it, so the Suite can show the product mark.
Deletion and retention
DELETE /v1/files/:id is a soft delete (editor). Objects are purged after 14 days by Core's retention job; the metadata row stays so lineage never dangles. GET /v1/storage/usage returns temporary and permanent bytes for the storage meter.
Invitations
The Suite renders the members page and calls the invite endpoints; Core sends the email and hosts the acceptance flow, because accepting may involve creating and verifying an account.
The recipient lands on https://auth.endorr.com/invite/<token>, signs in or creates an account with the invited address, and is added to the organisation. Invitations are valid for 7 days, single use, and only the invited address can accept. After acceptance Core shows its minimal signed-in page with a link to the Suite (DEFAULT_RETURN_URL); pass return_to on the invite link if you want them somewhere specific.
The SDK
@endorr/core-sdk wraps everything on this page for TypeScript, Next.js and React: PKCE and the token endpoints, ID-token verification against JWKS, a typed /v1 client, direct-to-storage uploads with progress, route handlers for the login/callback dance, and a small React provider. It has one runtime dependency (jose).
Keep tokens server-side. The browser talks to your own backend (or a thin proxy that appends the bearer header); only the signed storage URL is ever used directly from the browser.
Endpoint reference
Identity and account
| Endpoint | Auth | Scope | What it does |
|---|---|---|---|
GET/v1/me | session · bearer | — | Current user, selected organisation, token scope. |
PATCH/v1/me | session · bearer | account | Update profile (name). |
POST/v1/me/password | session · bearer | account | Change password with the current one; revokes other Core sessions. |
POST/v1/me/verification/resend | session · bearer | account | Send a new verification email. |
GET/v1/me/sessions | session · bearer | account | Active Core web sessions (device, IP, last seen). |
DELETE/v1/me/sessions/:id | session · bearer | account | Revoke one session. |
POST/v1/me/sessions/revoke-others | session · bearer | account | Sign out everywhere else. |
GET/v1/me/connected-apps | session · bearer | account | Products the user authorised, with granted permissions. |
DELETE/v1/me/connected-apps/:clientId | session · bearer | account | Disconnect a product (revokes its tokens). |
GET/v1/me/organizations | session · bearer | organizations | Organisations the user belongs to with their role. |
POST/v1/me/organizations | session · bearer | organizations | Create a business organisation (verified email required). |
GET/oauth/userinfo | bearer | — | OIDC userinfo: sub, email, name, org_id, organizations[] with the organizations scope. |
Organisations and teams
| Endpoint | Auth | Scope | What it does |
|---|---|---|---|
GET/v1/organizations/:id | session · bearer | organizations | Organisation details; 404 for non-members. |
PATCH/v1/organizations/:id | session · bearer | organizations | Rename (admin). |
GET/v1/organizations/:id/members | session · bearer | organizations | Member list with roles. |
PATCH/v1/organizations/:id/members/:userId | session · bearer | organizations | Change role; owner transfer with role=owner. |
DELETE/v1/organizations/:id/members/:userId | session · bearer | organizations | Remove member or leave. |
GET/v1/organizations/:id/invites | session · bearer | organizations | Pending invitations (admin). |
POST/v1/organizations/:id/invites | session · bearer | organizations | Invite by email (admin). |
DELETE/v1/organizations/:id/invites/:inviteId | session · bearer | organizations | Revoke invitation. |
GET/v1/organizations/:id/teams | session · bearer | organizations | Teams with members. |
POST/v1/organizations/:id/teams | session · bearer | organizations | Create team (admin). |
DELETE/v1/teams/:id | session · bearer | organizations | Delete team and its grants. |
PUT/v1/teams/:id/members/:userId | session · bearer | organizations | Add member to team. |
DELETE/v1/teams/:id/members/:userId | session · bearer | organizations | Remove from team. |
POST/v1/session/switch-organization | session | — | Change the selected organisation of a Core session. |
POST /v1/session/switch-organization and POST /v1/invites/:token work with the Core session only: they belong to Core's own pages. A product changes organisation context by re-authorising with org_id.Files, folders, permissions, shares
| Endpoint | Auth | Scope | What it does |
|---|---|---|---|
POST/v1/files/uploads | session · bearer | files:write | Create file record + signed upload URL. |
POST/v1/files/:id/complete | session · bearer | files:write | Confirm bytes, mark ready. |
GET/v1/files/:id | session · bearer | files:read | Metadata, capabilities, path, lineage. |
POST/v1/files/:id/download | session · bearer | files:read | Signed download URL. |
PATCH/v1/files/:id | session · bearer | files:write | Rename, move, promote to permanent. |
DELETE/v1/files/:id | session · bearer | files:write | Soft delete. |
POST/v1/files/:id/relationships | session · bearer | files:write | Record a derivative edge. |
GET/v1/files/recent | session · bearer | files:read | Recent visible files in the organisation (limit ≤ 50). |
GET/v1/storage/usage | session · bearer | files:read | Temporary/permanent bytes and file count. |
POST/v1/folders | session · bearer | files:write | Create folder. |
GET/v1/folders/:id | session · bearer | files:read | Folder metadata and path. :id may be root for children only. |
GET/v1/folders/:id/children | session · bearer | files:read | Folders and files the caller may see. |
PATCH/v1/folders/:id | session · bearer | files:write | Rename or move (manager). |
DELETE/v1/folders/:id | session · bearer | files:write | Soft delete subtree (manager). |
GET/v1/{files|folders}/:id/permissions | session · bearer | files:read | List grants (manager). |
PUT/v1/{files|folders}/:id/permissions | session · bearer | files:write | Create or update a grant (manager). |
DELETE/v1/{files|folders}/:id/permissions/:grantId | session · bearer | files:write | Remove a grant (manager). |
POST/v1/{files|folders}/:id/shares | session · bearer | files:write | Create share link (manager). |
DELETE/v1/shares/:id | session · bearer | files:write | Revoke share link. |
GET/v1/shares/resolve/:token | none | — | Resolve a share link (rate-limited). |
Local development
Locally, Core writes emails to .dev/outbox.jsonl (verification, reset and invite links included) and serves signed storage URLs itself, so the whole upload flow works without a bucket. Core's own smoke suite (pnpm smoke) shows every flow above as executable HTTP: see tests/smoke/ in the Core repository.
Go-live checklist
- Client registered with production callback URLs only; secret stored in the Suite's secret manager.
- ID tokens verified against JWKS with issuer and audience checks; nonce compared.
- Users keyed by
sub, never by email. - Refresh tokens stored server side, replaced atomically on rotation; 401 handling sends the user back through sign-in.
- All action buttons driven by
capabilities; 403 and 404 rendered distinctly. - Uploads go browser → storage via the signed URL, then
/complete; the Suite never proxies bytes. - Temporary-file expiry visible in the UI with a "Keep permanently" action.
- Cross-product links carry
file_…IDs, not signed URLs. - Storage bucket CORS allows the Suite origin (see
ops/b2-cors-rules.jsonin Core). - Account and organisation management screens live in the Suite; Core only hosts sign-in flows and links back with
return_to.
Endorr Core · integration guide generated from the running configuration. Questions go to the Core owners.
