Invites
Invite users to join a board by email, and manage pending invites.
Board invites allow users to invite others to join a board by email. The inviter can include a personal message and pre-assign board positions. Invitees can accept (directly or via an invite code), and inviters can decline on behalf of an invitee or retract a pending invite.
Mostly a legacy surface
Eleven of the twelve endpoints below are legacy — their paths do not carry a
/api/v1 prefix and are not reachable at one. Only the invite-status probe
(GET /invite/status/:inviteCode) is registered on the versioned public
stack and reachable under /api/v1. See
Versioning for what "legacy" means in this
API.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/invite/:boardId/invites | List pending invites for a board (legacy) |
GET | /api/invite/sent | List invites the caller has sent, across boards (legacy) |
GET | /api/invite/all-boards | List pending invites sent, grouped by board (legacy) |
POST | /api/invite/:boardId/test | Send a test invite email to yourself (legacy) |
POST | /api/invite/:boardId | Create (or dedupe onto) an invite (legacy) |
POST | /api/invite/:boardInviteId/send-email | Send/resend the invite email (legacy) |
GET | /api/invite/:boardId/code | Get or create the board's invite code (legacy) |
PUT | /api/invite/accept/:boardInviteId | Accept an invite (legacy) |
PUT | /api/invite/join/:inviteCode | Accept an invite via invite code (legacy) |
PUT | /api/invite/decline/:boardInviteId | Decline an invite (legacy) |
PUT | /api/invite/retract/:boardInviteId | Retract a sent invite (legacy) |
GET | /api/v1/invite/status/:inviteCode | Check whether an invite code is still pending (public, no auth) |
Invite Objects
Two different shapes appear across these endpoints — the list endpoints return a projected view, not the raw stored invite.
Invite Detail (returned by list endpoints)
{
"inviteId": "60d5ec49f1a2c8b1f8e4e1a1",
"boardId": "60d5ec49f1a2c8b1f8e4e1b1",
"boardName": "Acme Corp Board",
"boardDescription": "Main governance board",
"inviteeEmail": "[email protected]",
"inviterId": "507f1f77bcf86cd799439011",
"inviterEmail": "[email protected]",
"inviterFirstName": "Alex",
"inviterLastName": "Admin",
"inviteMessage": "Looking forward to having you on the board!",
"status": "pending",
"createdAt": "2025-06-01T10:00:00.000Z",
"alsoInvitedBy": []
}| Field | Type | Description |
|---|---|---|
inviteId | string | Opaque invite ID |
boardId | string | The board the invite belongs to |
boardName | string | Board display name |
boardDescription | string | null | Board description |
inviteeEmail | string | Email address of the invited person (stored lower-cased) |
inviterId | string | User ID of the person who sent the invite |
inviterEmail | string | Inviter's email |
inviterFirstName | string | Inviter's first name |
inviterLastName | string | Inviter's last name |
inviteMessage | string | null | Optional personal message (max 500 characters) |
status | string | One of: pending, accepted, declined, retracted |
createdAt | string | ISO-8601 timestamp |
alsoInvitedBy | string[] | undefined | User IDs of other admins who re-invited the same email while this invite was pending |
Board Positions
Assignable via boardPositions on create; assigned to the invitee on acceptance.
| Value | Value | Value |
|---|---|---|
executive assistant | observer (default) | ceo |
board chair | board member | company secretary |
cfo | corporate lawyer | investor |
management roles | external consultant | it administrator |
chief of staff | other |
Duplicate Invites Are Deduped
At most one pending invite exists per (board, email) pair, and email matching is case-insensitive. Inviting an email that already has a pending invite on that board updates the existing invite instead of creating a second one.
On a dedupe:
inviterstays the user who created the original invite.- The re-inviting user is appended to
alsoInvitedBy, and the invite appears in their own sent list. messageandboardPositionssubmitted by a different inviter are ignored, so a re-invite cannot overwrite the original inviter's assignment. A re-invite by the original inviter does update both.
List Pending Invites for a Board (legacy)
GET /api/invite/:boardId/invitesResponse
Returns a bare array — not wrapped in { "data": ... }:
[
/* array of invite detail objects */
]Errors
| Code | Status | When |
|---|---|---|
UNAUTHENTICATED | 401 | Missing or invalid token |
FORBIDDEN | 403 | Token lacks invites:read scope, or user does not have access to this board |
RESOURCE_NOT_FOUND | 404 | Board does not exist |
List Invites Sent (legacy)
GET /api/invite/sentReturns invites the authenticated user has sent, across every board. JWT-only — API tokens cannot call this endpoint.
Response
Returns a bare array, same shape as above:
[
/* array of invite detail objects */
]Errors
| Code | Status | When |
|---|---|---|
UNAUTHENTICATED | 401 | Missing or invalid token, or called with an API token |
List Pending Invites Grouped by Board (legacy)
GET /api/invite/all-boardsReturns pending invites the authenticated user has sent, grouped by board. JWT-only.
Response
Returns a bare object keyed by board ID — not wrapped in { "data": ... }:
{
"60d5ec49f1a2c8b1f8e4e1b1": [
/* array of invite detail objects, pending only */
]
}Errors
| Code | Status | When |
|---|---|---|
UNAUTHENTICATED | 401 | Missing or invalid token, or called with an API token |
Send a Test Invite Email (legacy)
POST /api/invite/:boardId/testSends a preview of the invite email to the caller's own address, without creating an invite. Rate-limited per inviter.
Response
{
"message": "Test invite email sent successfully",
"sentTo": "[email protected]"
}Errors
| Code | Status | When |
|---|---|---|
UNAUTHENTICATED | 401 | Missing or invalid token |
FORBIDDEN | 403 | Caller is not a member of this board |
RESOURCE_NOT_FOUND | 404 | Board does not exist |
RATE_LIMITED | 429 | Too many test emails sent recently |
Create an Invite (legacy)
POST /api/invite/:boardIdInvites a user to a board by email. Optionally include a personal message and assign board positions. Requires at least collaborator access. Creating an invite does not send an email — use the send-email endpoint below.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
inviteeEmail | string | Yes | Email address of the person to invite (normalized to lowercase) |
message | string | No | Personal message (max 500 characters, HTML-escaped) |
boardPositions | string[] | No | Board positions to assign on acceptance (defaults to ["observer"]) |
Response
Returns 200:
{
"message": "Invite added to access list successfully",
"inviteId": "60d5ec49f1a2c8b1f8e4e1a1",
"deduped": false,
"triggerNudge": false
}| Field | Type | Description |
|---|---|---|
message | string | Human-readable result |
inviteId | string | ID of the pending invite (existing one when deduped is true) |
deduped | boolean | true when the request collapsed onto an invite that already existed |
triggerNudge | boolean | true when the response should prompt the inviter to add their name to their profile |
Errors
| Code | Status | When |
|---|---|---|
INVALID_REQUEST_BODY | 400 | Missing/invalid fields (e.g. invalid email) |
UNAUTHENTICATED | 401 | Missing or invalid token |
FORBIDDEN | 403 | Token lacks invites:write scope, or caller is not at least a collaborator |
BILLING_RESTRICTED | 403 | Billing not active |
RESOURCE_NOT_FOUND | 404 | Board does not exist |
RATE_LIMITED | 429 | Too many invites created recently |
Send/Resend the Invite Email (legacy)
POST /api/invite/:boardInviteId/send-emailSends (or resends) the invite email for a pending invite. JWT-only. Throttled to 1 email per hour per invite and 5 per 24 hours per recipient address.
Response
Success:
{
"message": "Invite email sent",
"sentTo": "[email protected]",
"lastSentAt": "2025-06-01T10:00:00.000Z",
"throttled": false
}Throttled (still 200):
{
"message": "...",
"sentTo": "[email protected]",
"lastSentAt": "2025-06-01T09:10:00.000Z",
"throttled": true,
"throttleReason": "per-invite"
}throttleReason is "per-invite" or "global-recipient", naming which limit applied.
Errors
| Code | Status | When |
|---|---|---|
UNAUTHENTICATED | 401 | Missing or invalid token, or called with an API token |
RESOURCE_NOT_FOUND | 404 | Invite does not exist |
Get or Create the Board's Invite Code (legacy)
GET /api/invite/:boardId/codeReturns the board's shareable invite code, creating one if it doesn't exist yet.
Response
Bare object, not wrapped in { "data": ... }:
{ "inviteCode": "a1b2c3d4-...-uuid" }Errors
| Code | Status | When |
|---|---|---|
UNAUTHENTICATED | 401 | Missing or invalid token |
FORBIDDEN | 403 | Caller is not a member of this board |
RESOURCE_NOT_FOUND | 404 | Board does not exist |
Accept an Invite (legacy)
PUT /api/invite/accept/:boardInviteIdAccepts a board invite by ID. Send no request body — this endpoint rejects any body. The authenticated user's email must match the invite's inviteeEmail (case-insensitive). On success, the user is added to the board with the positions specified in the invite.
Response
Bare object, not the invite/board-invite object:
{ "message": "Successfully accepted board invite", "triggerNudge": false }triggerNudge is true when the accepting user has no first name set, signaling the client to prompt for one.
Errors
| Code | Status | When |
|---|---|---|
UNAUTHENTICATED | 401 | Missing or invalid token |
FORBIDDEN | 403 | Token lacks invites:write scope, or the authenticated user's email does not match the invite |
RESOURCE_NOT_FOUND | 404 | Invite does not exist |
Accept an Invite by Code (legacy)
PUT /api/invite/join/:inviteCodeAccepts a board invite via the board's shareable invite code (distinct from a per-invite inviteToken) rather than a specific invite ID. Matches a pending invite by the caller's email on that board. Send no request body. JWT-only.
Response
{
"message": "Successfully joined the board",
"boardId": "60d5ec49f1a2c8b1f8e4e1b1",
"boardName": "Acme Corp Board"
}An invalid or expired code returns 404 with { "message": "Invalid or expired invite link", "code": "INVALID_INVITE_CODE" } — a bare body, not the standard error envelope.
Errors
| Code | Status | When |
|---|---|---|
UNAUTHENTICATED | 401 | Missing or invalid token, or called with an API token |
FORBIDDEN | 403 | No pending invite exists for the caller's email on this board |
RESOURCE_NOT_FOUND | 404 | Invite code does not exist |
Check an Invite Code's Status (public)
GET /api/v1/invite/status/:inviteCodeAn unauthenticated probe for whether an invite is still pending — used by the pre-signup join page to render "this invitation is no longer valid" instead of bouncing to sign-in. Requires both the invite code (path) and its per-invite token (query, i) to be valid UUIDv4 strings; the token is a capability credential, not merely an identifier, and is redacted from server logs.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
i | string | Yes | The invite's token (UUIDv4), from the emailed join link |
Response
{ "pending": true }No-oracle by design
Every failure mode — malformed invite code, malformed or missing i, unknown code, unknown/foreign token, retracted/declined/accepted invite — returns the exact same { "pending": false } with a 200 status. The response never distinguishes why an invite isn't pending, so this endpoint cannot be used to enumerate invites or confirm an email's registration status.
Errors
This endpoint does not return error responses for invalid input (see above). No auth-related errors apply since it is unauthenticated.
Decline an Invite (legacy)
PUT /api/invite/decline/:boardInviteIdDeclines a board invite. The authenticated user's email must match the invite's inviteeEmail. Send no request body — this endpoint rejects any body; the decline-vs-retract distinction is set by which route you call, not by a payload field.
Response
{ "message": "Successfully declined board invite" }Errors
| Code | Status | When |
|---|---|---|
UNAUTHENTICATED | 401 | Missing or invalid token |
FORBIDDEN | 403 | Token lacks invites:write scope, or the caller's email does not match the invite |
RESOURCE_NOT_FOUND | 404 | Invite does not exist |
Retract a Sent Invite (legacy)
PUT /api/invite/retract/:boardInviteIdRetracts a pending invite that you previously sent. Requires the caller to be the original inviter or a board admin. Send no request body — this endpoint rejects any body.
Response
Note
The response message says "declined" for both this endpoint and Decline above — a naming quirk in the shared handler, not a bug in your integration.
{ "message": "Successfully declined board invite" }Errors
| Code | Status | When |
|---|---|---|
UNAUTHENTICATED | 401 | Missing or invalid token |
FORBIDDEN | 403 | Token lacks invites:write scope, or caller is neither the original inviter nor a board admin |
RESOURCE_NOT_FOUND | 404 | Invite does not exist |