Organisations & Staff API
The organisations endpoints live under /api/v1/organizations and cover the
organisation record itself plus the staff lifecycle: listing members, inviting
people, changing roles and employment settings, deactivating members, and bulk
user imports from a spreadsheet.
Authentication: every request needs a per-user API key. See API Keys & Authentication.
Role requirements below describe Runnit’s default role permissions (owner, admin, manager, member, freelancer). Organisations that use custom roles may grant access differently.
GET /api/v1/organizations/my
Section titled “GET /api/v1/organizations/my”Lists every organisation the key’s owner is an active member of, with the membership details for each. Any authenticated user can call it.
Example request
curl https://api.runnit.io/api/v1/organizations/my \ -H 'Authorization: Bearer rnk_your_key'Response is a JSON array of membership records:
[ { "id": "<membership-uuid>", "userId": "<user-uuid>", "organizationId": "<org-uuid>", "role": "admin", "isPrimary": true, "isFreelancer": false, "hourlyRate": null, "currency": "AUD", "joinedAt": "2025-03-02T04:11:09.000Z", "isActive": true, "organization": { "id": "<org-uuid>", "name": "Southbank Studio", "slug": "southbank-studio", "logoUrl": null } }]This endpoint is informational. Even though it lists all your organisations, the key still only works against the one it was created in.
GET /api/v1/organizations/users
Section titled “GET /api/v1/organizations/users”Returns a simplified list of users in the key owner’s primary organisation. Any authenticated user can call it.
Example request
curl https://api.runnit.io/api/v1/organizations/users \ -H 'Authorization: Bearer rnk_your_key'Response is a JSON array; each entry contains id, email, firstName,
lastName, displayName, avatarUrl, currentTitle, phone, skills,
preferredWorkTypes, and status.
Errors
| Status | When |
|---|---|
404 | The key’s owner is not an active member of any organisation. |
GET /api/v1/organizations/:id
Section titled “GET /api/v1/organizations/:id”Returns the organisation record. Any active member of the organisation can call it.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | UUID | Yes | The organisation id. Must be the organisation the key is bound to. |
Example request
curl https://api.runnit.io/api/v1/organizations/<org-id> \ -H 'Authorization: Bearer rnk_your_key'Response
{ "id": "<org-uuid>", "name": "Southbank Studio", "slug": "southbank-studio", "description": "Creative production studio.", "url": "https://southbank.example.com", "email": "hello@southbank.example.com", "phone": "+61 3 9000 0000", "addressLine1": "12 Example Street", "city": "Melbourne", "country": "Australia", "currency": "AUD", "tier": "standard", "settings": { "dailyCapacityHours": 8, "timezone": "Australia/Melbourne", "requireBriefBuilderAiReviewBeforePlanning": true }, "status": "active", "createdAt": "2024-11-20T22:14:00.000Z", "updatedAt": "2026-06-30T01:05:44.000Z"}Errors
| Status | When |
|---|---|
404 | No organisation exists with that id. |
PATCH /api/v1/organizations/:id
Section titled “PATCH /api/v1/organizations/:id”Updates the organisation’s details. Only organisation admins and owners can call it.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | UUID | Yes | The organisation id. |
Request body (all fields optional; send only what you want to change)
| Name | Type | Required | Description |
|---|---|---|---|
name | string | No | Organisation display name. |
description | string | No | Free-text description. |
settings | object | No | Organisation settings. settings.dailyCapacityHours must be a number between 1 and 24 when supplied. settings.timezone must be a valid IANA timezone such as Australia/Sydney; it controls the organisation calendar used by scheduling. settings.requireBriefBuilderAiReviewBeforePlanning is a boolean; absent or true makes Brief Builder wait for its current AI review before Planning, while false permits handoff during evaluation. settings.taskTable.defaultColumns sets the default visible columns of the project task table, as an ordered array of column ids (built-in ids such as status, assignee, hours, financial ids such as plannedCost, or custom:<key> for custom columns); null restores the standard layout. Kanban boards can override this per board. |
Example request
curl -X PATCH https://api.runnit.io/api/v1/organizations/<org-id> \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "description": "Creative production studio in Melbourne.", "settings": { "dailyCapacityHours": 7.5 } }'Response is the full updated organisation record, in the same shape as
GET /api/v1/organizations/:id.
Errors
| Status | When |
|---|---|
400 | settings.dailyCapacityHours is outside 1 to 24, or settings.timezone is not a valid IANA timezone. |
404 | No organisation exists with that id. |
GET /api/v1/organizations/:id/stats
Section titled “GET /api/v1/organizations/:id/stats”Returns simple headcount statistics. Any active member can call it.
Example request
curl https://api.runnit.io/api/v1/organizations/<org-id>/stats \ -H 'Authorization: Bearer rnk_your_key'Response
{ "memberCount": 24, "projectCount": 0, "activeProjects": 0 }memberCount is the number of active members. projectCount and
activeProjects are reserved fields and currently always return 0; use the
projects endpoints for project counts.
GET /api/v1/organizations/:id/members
Section titled “GET /api/v1/organizations/:id/members”Lists the organisation’s members as membership records with user details. Any active member can call it.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
isActive | string | No | Pass true to return only active members. Omit it to include deactivated members. |
role | string | No | Filter to one role: owner, admin, manager, member, or freelancer. |
isFreelancer | string | No | Pass true to return only freelancers. |
Example request
curl 'https://api.runnit.io/api/v1/organizations/<org-id>/members?isActive=true&role=freelancer' \ -H 'Authorization: Bearer rnk_your_key'Response is a JSON array of membership records:
[ { "id": "<membership-uuid>", "userId": "<user-uuid>", "organizationId": "<org-uuid>", "role": "freelancer", "isPrimary": false, "isFreelancer": true, "hourlyRate": 120, "currency": "AUD", "joinedAt": "2025-09-14T00:30:00.000Z", "isActive": true, "user": { "id": "<user-uuid>", "email": "sam@example.com", "firstName": "Sam", "lastName": "Nguyen", "displayName": "Sam Nguyen", "avatarUrl": null } }]POST /api/v1/organizations/:id/invite
Section titled “POST /api/v1/organizations/:id/invite”Invites a person to the organisation by email. Managers, admins, and owners can call it. Runnit emails the person an invitation link; the invitation expires after 2 days. If the email address has no Runnit account yet, a placeholder account is created and activated when they accept.
In a demo, sandbox, or training dataset, Runnit creates the invitation but
doesn’t send the email. Check emailSent in the response before telling the
person to look for it.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
email | string | Yes | A valid email address. |
role | string | Yes | Role to grant on acceptance: owner, admin, manager, member, or freelancer. |
isFreelancer | boolean | No | Mark the new member as a freelancer. Defaults to false. |
hourlyRate | number | No | Billable hourly rate to record for the new member. |
currency | string | No | Currency for hourlyRate. Defaults to USD. |
customMessage | string | No | Personal note from the inviter, up to 500 characters. Included in the invitation email above the acceptance link. |
Example request
curl -X POST https://api.runnit.io/api/v1/organizations/<org-id>/invite \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "email": "sam@example.com", "role": "member" }'Response
{ "message": "Invitation sent successfully", "invitationToken": "<token>", "emailSent": true}If email isn’t delivered, including in a non-production dataset, the request
still succeeds with emailSent: false and the message Invitation created without email delivery.
The person accepts the invitation through the Runnit app; there is no public API endpoint for accepting it on their behalf.
Errors
| Status | When |
|---|---|
400 | email is not a valid address, role is not one of the five roles, or customMessage is longer than 500 characters. |
409 | The person is already an active member of the organisation and has signed in before. |
PATCH /api/v1/organizations/:id/members/:userId
Section titled “PATCH /api/v1/organizations/:id/members/:userId”Changes a member’s role. Only organisation admins and owners can call it.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | UUID | Yes | The organisation id. |
userId | UUID | Yes | The member’s user id. |
Request body
| Name | Type | Required | Description |
|---|---|---|---|
role | string | Yes | New role: owner, admin, manager, member, or freelancer. |
Example request
curl -X PATCH https://api.runnit.io/api/v1/organizations/<org-id>/members/<user-id> \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "role": "manager" }'Response
{ "message": "Role updated successfully" }Errors
| Status | When |
|---|---|
400 | role is not one of the five roles. |
404 | The user has no membership record in the organisation. |
PATCH /api/v1/organizations/:id/members/:userId/settings
Section titled “PATCH /api/v1/organizations/:id/members/:userId/settings”Updates a member’s employment settings: the freelancer flag and their billable hourly rate. Managers, admins, and owners can call it.
Request body (at least one field is required)
| Name | Type | Required | Description |
|---|---|---|---|
isFreelancer | boolean | No | Whether the member is a freelancer. |
hourlyRate | number or null | No | Billable hourly rate, 0 or higher. Pass null to clear it. |
currency | string | No | Three-letter currency code, for example AUD. Stored in upper case. |
Example request
curl -X PATCH https://api.runnit.io/api/v1/organizations/<org-id>/members/<user-id>/settings \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "isFreelancer": true, "hourlyRate": 135, "currency": "AUD" }'Response
{ "id": "<membership-uuid>", "userId": "<user-uuid>", "organizationId": "<org-uuid>", "isFreelancer": true, "hourlyRate": 135, "currency": "AUD"}Errors
| Status | When |
|---|---|
400 | No settings supplied, hourlyRate is negative, or currency is not exactly three characters. |
404 | The user is not an active member of the organisation. |
DELETE /api/v1/organizations/:id/members/:userId
Section titled “DELETE /api/v1/organizations/:id/members/:userId”Deactivates a member, which is how you archive staff. Only organisation admins and owners can call it. The membership record is kept for history, but the person immediately loses access to the organisation and any API keys they created in it stop working.
Example request
curl -X DELETE https://api.runnit.io/api/v1/organizations/<org-id>/members/<user-id> \ -H 'Authorization: Bearer rnk_your_key'Response is 204 No Content.
Errors
| Status | When |
|---|---|
404 | The user has no membership record in the organisation. |
GET /api/v1/organizations/:id/users
Section titled “GET /api/v1/organizations/:id/users”Lists members with their full user profile details and their organisation
membership status. Any active member can call it. The rate fields
(hourlyRate on the membership, hourlyRate and actualCostRate on the
user) are financial data and only appear when the caller has financial-report
access (managers and above by default); other callers receive the same
records without those fields.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
role | string | No | Filter to one role: owner, admin, manager, member, or freelancer. |
isActive | string | No | Defaults to true. Pass false to return only inactive memberships. |
includePendingInvitations | boolean | No | Pass true to return active members and outstanding invitations together. Archived members are excluded and this option takes precedence over isActive. |
page | integer | No | Echoed back in the response. Defaults to 1. |
limit | integer | No | Echoed back in the response. Defaults to 100. |
All matching members are returned in a single response; page and limit are
included in the response body but do not currently slice the result.
Example request
curl 'https://api.runnit.io/api/v1/organizations/<org-id>/users?role=manager&includePendingInvitations=true' \ -H 'Authorization: Bearer rnk_your_key'Response
{ "members": [ { "id": "<membership-uuid>", "userId": "<user-uuid>", "organizationId": "<org-uuid>", "role": "manager", "isFreelancer": false, "hourlyRate": null, "currency": "AUD", "isActive": true, "membershipStatus": "active", "user": { "id": "<user-uuid>", "email": "priya@example.com", "firstName": "Priya", "lastName": "Shah", "displayName": "Priya Shah", "avatarUrl": null, "currentTitle": "Senior Producer", "shortBio": "Producer with a broadcast background.", "skills": ["production", "editing"], "yearsInIndustry": 9, "industryStartDate": "2017-07-14", "hourlyRate": 160, "actualCostRate": 95, "defaultRateCardRoleId": "<rate-card-role-uuid>", "status": "active", "lastLoginAt": "2026-07-06T23:41:12.000Z" } } ], "total": 1, "page": 1, "limit": 100}membershipStatus is active, pending, or inactive. A pending invitation
may have blank profile names when the email address doesn’t have a Runnit
account yet. Use the nested user’s email to identify that row.
PATCH /api/v1/organizations/:id/users/:userId/rate-settings
Section titled “PATCH /api/v1/organizations/:id/users/:userId/rate-settings”Updates a member’s internal cost rate and their default rate-card role. Managers, admins, and owners can call it. Both settings are scoped to this organisation: each organisation a person belongs to keeps its own cost rate and default role, and cost rate changes apply from today onwards (past time entries keep the rate they were priced with).
Request body
| Name | Type | Required | Description |
|---|---|---|---|
actualCostRate | number or null | No | Internal cost rate used for margin calculations, 0 or higher. Pass null to clear it from today onwards; historical entries keep their captured rate. |
actualCostRateCurrency | string | No | ISO 4217 code for the cost rate (a London employee can cost in GBP inside an AUD organisation). Defaults to the organisation’s reporting currency. |
defaultRateCardRoleId | UUID or null | No | An active rate-card role in the organisation (see the Rate Cards API). Pass null to clear it. |
Example request
curl -X PATCH https://api.runnit.io/api/v1/organizations/<org-id>/users/<user-id>/rate-settings \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "actualCostRate": 95, "defaultRateCardRoleId": "<rate-card-role-uuid>" }'Response
{ "id": "<user-uuid>", "actualCostRate": 95, "defaultRateCardRoleId": "<rate-card-role-uuid>", "hourlyRate": 160}Errors
| Status | When |
|---|---|
400 | defaultRateCardRoleId does not refer to an active rate-card role in the organisation. |
404 | The user is not an active member of the organisation. |
Bulk user imports
Section titled “Bulk user imports”Import staff from a spreadsheet in three steps: upload a file to build a preview, check the preview, then commit it. You can also roll back the most recent committed import. All four endpoints require an organisation admin or owner.
Supported file types are .xlsx, .xls, .xlsm, and .csv, up to 50 MB.
The import recognises these columns and maps common header names to them
automatically: email, firstName, lastName, displayName, phone,
currentTitle, shortBio, skills, yearsInIndustry, role,
isFreelancer, hourlyRate, and currency. A yearsInIndustry value is
stored as the industry start date it implies (today minus the value), so the
person’s years of experience keep increasing automatically after the import.
POST /api/v1/organizations/:id/user-imports/preview
Section titled “POST /api/v1/organizations/:id/user-imports/preview”Uploads a spreadsheet and returns a preview of what the import would do,
without changing anything. Send the file either as multipart/form-data with
a file field, or as JSON with a base64-encoded payload.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
file | file | Yes, for multipart | The spreadsheet, as a multipart form field. |
fileName | string | Yes, for JSON | The file name, including its extension. Not needed with multipart upload. |
fileDataBase64 | string | Yes, for JSON | The file content, base64 encoded. Not needed with multipart upload. |
mimeType | string | No | The file’s MIME type, when sending JSON. |
mappingOverrides | object | No | Map of spreadsheet column name to a recognised field name, or null to ignore that column. Overrides the automatic mapping. |
longBioSourceColumns | array of strings | No | Column names whose text is combined into each person’s long bio. |
With multipart upload, pass mappingOverrides and longBioSourceColumns as
JSON-encoded strings.
Example request
curl -X POST https://api.runnit.io/api/v1/organizations/<org-id>/user-imports/preview \ -H 'Authorization: Bearer rnk_your_key' \ -F 'file=@staff.xlsx' \ -F 'mappingOverrides={"Rate (AUD)":"hourlyRate"}'Response is 201 with the preview batch:
{ "batchId": "<batch-uuid>", "status": "preview_ready", "fileName": "staff.xlsx", "mapping": { "Email": { "field": "email", "source": "synonym", "confidence": null } }, "longBioSourceColumns": [], "detectedColumns": [{ "...": "..." }], "llmAnalysis": { "...": "..." }, "summary": { "totalRows": 18, "create": 15, "update": 2, "error": 1 }, "rows": [{ "...": "one entry per spreadsheet row with the planned operation" }]}Errors
| Status | When |
|---|---|
400 | No file supplied, unsupported file type, the file exceeds 50 MB, or mappingOverrides / longBioSourceColumns is not valid JSON. |
GET /api/v1/organizations/:id/user-imports/:batchId
Section titled “GET /api/v1/organizations/:id/user-imports/:batchId”Returns the batch’s current status, mapping, summaries, and per-row detail.
curl https://api.runnit.io/api/v1/organizations/<org-id>/user-imports/<batch-id> \ -H 'Authorization: Bearer rnk_your_key'Response contains batchId, status, fileName, mapping,
longBioSourceColumns, llmAnalysis, previewSummary, commitSummary,
committedAt, rolledBackAt, and rows. Returns 404 if the batch does not
belong to the organisation.
POST /api/v1/organizations/:id/user-imports/:batchId/commit
Section titled “POST /api/v1/organizations/:id/user-imports/:batchId/commit”Applies a previewed batch: creates and updates the users it described. No request body.
curl -X POST https://api.runnit.io/api/v1/organizations/<org-id>/user-imports/<batch-id>/commit \ -H 'Authorization: Bearer rnk_your_key'Response
{ "batchId": "<batch-uuid>", "status": "committed", "summary": { "inserted": 15, "updated": 2, "skipped": 0, "errored": 1 }}Errors
| Status | When |
|---|---|
400 | The batch is not in the preview-ready state (already committed, or rolled back). |
404 | The batch does not belong to the organisation. |
POST /api/v1/organizations/:id/user-imports/rollback-latest
Section titled “POST /api/v1/organizations/:id/user-imports/rollback-latest”Rolls back the most recently committed import batch. No request body.
curl -X POST https://api.runnit.io/api/v1/organizations/<org-id>/user-imports/rollback-latest \ -H 'Authorization: Bearer rnk_your_key'Response contains batchId, status (rolled_back), and a summary of
what was reversed.
Errors
| Status | When |
|---|---|
404 | No committed import batch is available to roll back. |
Workflow stages and boards
Section titled “Workflow stages and boards”Kanban boards are views over workflow stages. Every project sits in one stage; a board column shows one or more stages, and dropping a card moves the project to the column’s primary stage. Reading needs organisation membership; writing needs the kanban management permission.
GET /api/v1/organizations/:id/workflow-stages
Section titled “GET /api/v1/organizations/:id/workflow-stages”Lists the organisation’s workflow stages. Each stage has id, name,
category (draft, active, on_hold, done, or cancelled),
statusValue (the project status it syncs to), displayOrder, and
isSystem. System stages exist for every project status and cannot be
deleted.
PUT /api/v1/organizations/:id/workflow-stages
Section titled “PUT /api/v1/organizations/:id/workflow-stages”Bulk stage management. Send stages (rename or reorder existing stages by
id; create new ones with name, category, and displayOrder) and
deletions (each with stageId and reassignToStageId; projects on the
deleted stage move to the reassignment stage first). A stage’s category is
fixed after creation. Upserts apply atomically; creating or renaming to a
name another stage already has returns 409. Deletions run after the
upserts are saved, and any that fail are reported in a deletionErrors
array instead of failing the request.
GET /api/v1/organizations/:id/boards
Section titled “GET /api/v1/organizations/:id/boards”Lists the boards you can see. Users restricted to specific clients see the default board plus boards scoped to their accessible clients only.
POST /api/v1/organizations/:id/boards
Section titled “POST /api/v1/organizations/:id/boards”Creates a board. Body: name, optional description, optional
clientOrganizationId (must be an active client of the organisation), and
columns (each with name, displayOrder, stageIds, and
primaryStageId; a stage can appear in only one column per board).
You can create the stages a board needs in the same request. Send stages
with new entries carrying a tempKey, name, and category, then reference
those tempKeys inside columns[].stageIds and primaryStageId. The stages
and the board are created together: if anything fails, nothing is saved. The
response includes tempKeyMap (tempKey to created stage id) and the fresh
stages list. Creating a stage whose name already exists returns 409; use
the existing stage’s id instead.
Optional memberUserIds limits the board to those people (they must be
active organisation members, 400 otherwise). Only listed people and
holders of the kanban management permission see a limited board; everyone
else gets 404 for it. This organises navigation only: the projects on the
board remain visible wherever the viewer can already see them. The default
board cannot be limited.
Optional taskTableColumns sets the default visible columns of the project
task table for projects on this board, as an ordered array of column ids
(the same ids as the organisation setting above). Omit it to inherit the
organisation default.
GET /api/v1/boards/:boardId
Section titled “GET /api/v1/boards/:boardId”Returns the board with its columns (stage and brief-status mappings) plus
the organisation’s stages in one call. Boards you cannot see return 404.
PUT /api/v1/boards/:boardId
Section titled “PUT /api/v1/boards/:boardId”Updates board fields and, when columns is supplied, replaces the full
column set. The default board cannot be given a client scope.
Like board creation, this accepts stages (renames by id, new stages with
a tempKey and category) and stageDeletions (each with stageId and
reassignToStageId). Stage changes and the board update commit together, and
column references may use the tempKeys. Deletions run last because they move
projects; any that fail are reported in deletionErrors without undoing the
saved board.
memberUserIds replaces the board’s audience: a non-empty array limits the
board to those people, [] opens it to everyone again, and omitting the
field leaves the audience unchanged. Board payloads report
audienceRestricted and memberUserIds.
taskTableColumns sets the board’s default task-table columns: an array
sets them, null clears them back to inheriting the organisation setting,
and omitting the field leaves them unchanged. The default board follows the
organisation setting and rejects an array with 400. Board payloads report
the value under settings.taskTableColumns.
DELETE /api/v1/boards/:boardId
Section titled “DELETE /api/v1/boards/:boardId”Deletes a board. The default board cannot be deleted (400).
To move a project between stages, use
PATCH /api/v1/projects/:id/stage.
Other endpoints
Section titled “Other endpoints”The organisations router also exposes endpoints the Runnit app uses for browser sessions: logo upload and retrieval, legacy kanban column configuration, the shared custom-column registry used by the table-column settings editors, slug availability checks, choosing a primary organisation, leaving an organisation, and accepting invitations. They are not documented here because they are app concerns rather than integration surface.
Next step
Section titled “Next step”Manage billing rates with the Rate Cards API, or client relationships with the Clients API.