---
title: "The Talkspirit Partner API"
description: "The Partner API is a versioned REST contract for named integration partners. It is authenticated with an API key that an administrator of your organisation creates, and it is not open to self-serve signup."
category: integrations
section: api-and-webhooks
type: Reference
lastUpdated: 2026-09-30
locale: en
canonical: https://support.talkspirit.com/en/integrations/partner-api
---

# The Talkspirit Partner API


## The Talkspirit Partner API

Talkspirit exposes a REST API under a `/v1` prefix so that an integration can read an organisation's members, structure, meetings, projects, goals and working agreements, and write in a few precise places, without anyone working in the Talkspirit interface. Every response is JSON, and most failures are an RFC 7807 problem document with a stable machine-readable `code`. Rate limiting and timeouts are the two exceptions: their bodies have their own shape.

It is a **partner** API, not a public one. The resource set is deliberately narrow, access is provisioned per named integration, and there is no developer portal where you can sign up or mint a credential yourself. If you want to connect a cloud storage service or another built-in integration, no API work is involved: see [How do I add or remove an integration?](../integrations/enable-a-cloud-file-picker).

## What has to be in place before a call succeeds

Two conditions, both of them owned by the Talkspirit organisation you integrate with:

1. **The API module is enabled on that organisation.** It is an optional module and it is off by default. Without it, every request is rejected with `401` even when the key itself is valid. If the module is switched off later, keys that were already issued stop working, after a short propagation delay.
2. **You have an API key for that organisation.** An administrator of the organisation creates it in Administration, under Security, on the **API** page. Talkspirit support does not hand keys out, and you cannot create one from the partner side.

Ask the administrator who owns the organisation to arrange both. The procedure is in [Managing API tokens as an admin](../integrations/manage-api-tokens-as-an-admin).

## Where is the reference documentation?

The API documents itself. The service publishes, under the same host that serves the API:

- `/v1/docs`, an interactive reference rendered with Scalar. It reads `/v1/openapi.json`, lists every operation with its schemas, and can send test requests from the page.
- `/v1/openapi.json`, the machine-readable OpenAPI document, for generating a client.

Both are readable without a credential. `/v1/redoc` used to be a separate reading page and now answers `301` to `/v1/docs`, so there is one reference page rather than two.

The contract is generated from the running service and checked in continuous integration, so it cannot drift away from what the API actually serves. Treat it as the authority: this article explains the shape, the reference explains every field. It carries its own version number, distinct from the `/v1` path: this article was checked against **0.65.0**.

## How authentication works

Pass your key as a bearer token on every request:

```http
Authorization: Bearer <your API key>
```

The key is an opaque string beginning with `ts_partner_`. Talkspirit stores only a fingerprint of it, so the full value is shown once, at creation, and cannot be recovered afterwards. A lost key is replaced, never retrieved. Do not confuse it with an MCP key, which begins with `tsk_`, is created by each member in their own account area, and authenticates the separate MCP endpoint rather than this API.

Behind the scenes the service verifies your key, then exchanges it for a short-lived token belonging to a technical user dedicated to your integration. Your integration therefore sees exactly what that technical user is allowed to see: the circles, roles and boards it has been given in the organisation, and, for a task that belongs to no project, only the tasks that user may see in the product. A scope on the key never widens that.

Treat the key like a password. Keep it out of source control, and ask the administrator to revoke and reissue it if it is ever exposed. Revocation takes effect immediately, with a grace window of up to about five minutes for a token already exchanged.

## What the API exposes

Forty-eight operations under `/v1`, plus `GET /healthz`. Nine of them write; everything else reads.

| Resource | Read endpoints | Scope |
| --- | --- | --- |
| Users | `GET /v1/users`, `GET /v1/users/{user_id}`, `GET /v1/users/{user_id}/memberships` | `users:read` |
| Every membership of the organisation | `GET /v1/memberships` | `users:read` |
| Circles | `GET /v1/circles`, `GET /v1/circles/{circle_id}`, `GET /v1/circles/{circle_id}/members` | `circles:read` |
| Roles | `GET /v1/roles`, `GET /v1/roles/{role_id}`, `GET /v1/roles/{role_id}/members` | `roles:read` |
| Role templates | `GET /v1/role-templates`, `GET /v1/role-templates/{role_template_id}` | `roles:read` |
| Custom-field definitions | `GET /v1/custom-fields` | `roles:read`, `circles:read` or `users:read`, depending on the `entity_type` you ask for |
| Meetings | `GET /v1/meetings`, `GET /v1/meetings/{meeting_id}` | `meetings:read` |
| Projects | `GET /v1/projects`, `GET /v1/projects/{project_id}`, `GET /v1/projects/{project_id}/comments` | `projects:read` |
| Sections | `GET /v1/sections`, `GET /v1/sections/{section_id}` | `projects:read` |
| Labels | `GET /v1/labels`, `GET /v1/labels/{label_id}` | `projects:read` |
| Tasks | `GET /v1/tasks`, `GET /v1/tasks/{task_id}`, `GET /v1/tasks/{task_id}/comments` | `tasks:read` |
| Goals | `GET /v1/goals`, `GET /v1/goals/{goal_id}`, `GET /v1/goals/{goal_id}/key-results`, `GET /v1/goals/{goal_id}/comments` | `goals:read` |
| Time periods | `GET /v1/time-periods`, `GET /v1/time-periods/{time_period_id}` | `goals:read` |
| Working agreements | `GET /v1/documents`, `GET /v1/documents/{document_id}`, `GET /v1/documents/{document_id}/content`, `GET /v1/documents/{document_id}/comments` | `documents:read` |
| Tensions | `GET /v1/tensions`, `GET /v1/tensions/{tension_id}` | `tensions:read` |
| Activity log | `GET /v1/audit-logs` | `audit_log:read` |
| Your own identity | `GET /v1/whoami` | none |

Two groupings are worth noting because they do not follow the path name: sections and labels are read with `projects:read`, and time periods with `goals:read`.

The text of a working agreement is a sub-resource of its own, `GET /v1/documents/{document_id}/content`, so listing documents never carries a page of full texts. It serves the version the key's account sees in the product: the published one, or the latest draft when nothing has been published yet. A working agreement that exists but holds no text answers `200` with an empty text rather than `404`.

`GET /v1/memberships` returns every membership held in the organisation, one page per call, so a full sync of the structure costs as many calls as there are pages rather than one call per member. Each row is exactly a row of `GET /v1/users/{user_id}/memberships`, with the member's `user_id`. Pass `?updated_since=` to receive only the memberships added or edited since a given instant; such a walk cannot see a membership that was removed, so reconcile with a full walk from time to time.

`GET /v1/audit-logs` is the organisation's activity log, newest first: connections (successful and refused), publications, comments, and accesses to and exits from groups. Because it covers every member, the account behind the key must be an administrator of the organisation as well as the key carrying `audit_log:read`. Pass `?occurred_after=` to read only what happened since a given instant.

Several lists also accept filters, for example the roles one member holds, the circles one member belongs to, the roles created from one role template, or the tensions put on one meeting's agenda. The reference documentation lists every filter an endpoint accepts.

### Fields that need a word of explanation

Several fields carry more meaning than their name suggests.

- **`last_activity_at`, on a member.** `GET /v1/users`, `GET /v1/users/{user_id}` and the response of `PATCH /v1/users/{user_id}` publish the member's last recorded activity. It is an activity signal, not a sign-in: it is recorded when an authenticated member uses the product, and refreshed about once an hour rather than on every request, so read it as "active recently" and never as an authentication event. It is `null` when no activity was ever recorded, and for a member who has been anonymised. Calls you make with your own key record activity for the technical user behind the key, never for the members it reads or updates.
- **`assigned_at` and `updated_at`, on an assignment.** Every row of `GET /v1/roles/{role_id}/members`, `GET /v1/circles/{circle_id}/members` and `GET /v1/users/{user_id}/memberships`, and every member row returned by `GET /v1/roles` and `GET /v1/roles/{role_id}` with `?include=members`, carries both. `assigned_at` is when the assignment began: for an organisation migrated from Holaspirit, the date the assignment had there, not the date of the migration. Rows whose source carried no such date fall back to the instant of their import run, which is how you recognise them — they all share one value. `updated_at` is when the row was last written: its label, its decision-maker and administrator flags, a leave or a return. It does not move when an allocation changes, so it is not a feed of assignment changes, and neither field is an assignment history.
- **`options`, on a member custom-field definition.** `GET /v1/custom-fields?entity_type=user` publishes the complete option catalogue of every single-select and multiple-select field, where it used to publish nothing: every configured option, including the ones no member holds yet, as a `{value, label}` pair. `value` is the option id that `PATCH /v1/users/{user_id}/custom-fields` accepts; `label` is what a member's value usually reads as, so match a member's value on `label` first and on `value` if that fails, and expect two options of the same field to be able to share a label. Options come ordered by label, up to 5,000 per field, and `options_truncated` on the same definition says whether that ceiling bit. A select field with no option configured yet reports an empty list; `null` still means the field's type has no option list at all. The values on `GET /v1/users?include=custom_fields` are unchanged.

- **`source_template_id`, on a role.** The role template the role was created from, the id `GET /v1/role-templates` returns, or `null` when the role was created without one. A role template is a reusable role definition, such as "Secretary". An invited copy of a role reads `null`: its `source_role_id` leads to the role that carries the template. If your organisation was migrated from Holaspirit, template ids changed with the migration, so read them again from `GET /v1/role-templates`.
- **The personal fields of a member.** `email`, `phone`, `last_activity_at` and the custom-field values reach your key as they reach the technical user in the product. An administrator reads them all. A member reads them all except an email its owner chose to hide. A guest who shares no active group with that member reads none of them. A withheld field is `null`. The technical user behind a key is usually not an administrator, so expect `email` to be `null` for the members who hid it.
- **`priority`, on a tension.** It can be `NONE`, which means nobody has set one, and that is what a tension created without a priority now records. Treat a value you do not recognise as a future value rather than an error.

**Retyping a select field moves its values between two slots.** A field an administrator changes from single-select to multiple-select stops reading as `value`, a single value, and starts reading as `values`, a list — and a write has to use the same slot. Submitting the wrong one is refused with a `422 INVALID_CUSTOM_FIELD_VALUE` whose message names the slot to use; it is never quietly coerced.

### The nine writes

| Operation | Scope | What to know |
| --- | --- | --- |
| `POST /v1/tasks` | `tasks:write` | Creates a task on every call, so it is not safe to retry blindly. |
| `POST /v1/tensions` | `tensions:write` | Creates a tension. An omitted `priority` records `NONE`. |
| `PATCH /v1/tensions/{tension_id}` | `tensions:write` | Edits a tension. |
| `PATCH /v1/tensions/{tension_id}/status` | `tensions:write` | Moves a tension through its statuses. |
| `DELETE /v1/tensions/{tension_id}` | `tensions:delete` | **A hard delete with no undo.** See below. |
| `PATCH /v1/users/{user_id}` | `users:write` | Edits a member. |
| `PATCH /v1/users/{user_id}/custom-fields` | `users:write` | Sets custom-field values on a member. |
| `PATCH /v1/circles/{circle_id}/custom-fields` | `circles:write` | Sets custom-field values on a circle. |
| `PATCH /v1/roles/{role_id}/custom-fields` | `roles:write` | Sets custom-field values on a role. |

**Deleting a tension is irreversible.** The tension and its links are gone, and no read returns them afterwards. It sits behind its own `tensions:delete` scope, which `tensions:write` deliberately does not grant, so the capability has to be given explicitly rather than inherited. A tension is also deletable by its creator only: a key can delete the tensions its own technical user created, and nothing else.

## Scopes

An administrator picks scopes when the key is created. Sixteen are defined:

- Ungated: `users:read`, `users:write`, `audit_log:read`
- Structure module: `roles:read`, `roles:write`, `circles:read`, `circles:write`, `tensions:read`, `tensions:write`, `tensions:delete`, `documents:read`
- Goals module: `goals:read`
- Projects module: `projects:read`, `tasks:read`, `tasks:write`
- Meetings module: `meetings:read`

They do not all appear at once. A scope is only offered when the module it reads from is active on the organisation, so `tasks:read` is absent where Projects is off, and the whole Structure block is absent where Structure is off. Ask the administrator which modules the organisation runs before you design against a scope.

`audit_log:read` reads the activity log, `GET /v1/audit-logs`. It is not enough on its own: the account behind the key must also be an administrator of the organisation, otherwise the route answers `403 FORBIDDEN_UPSTREAM`.

## Making your first request

`GET /v1/whoami` needs no scope and doubles as a credential check. It returns the partner name your key was issued under, the organisation it belongs to, the scopes it carries, `user_id`, the member your key acts as, and `created_by_user_id`, the administrator who created the key (`null` for an older key or one a Talkspirit operator created). When a call is refused or returns less than you expected, `user_id` is the account to look at.

```
curl -H "Authorization: Bearer <your API key>" \
     https://partner-api.talkspirit.com/v1/whoami
```

A `200` means the key itself is valid and tells you what it carries. It does not prove the organisation still accepts the account behind it: `whoami` checks the key alone and never calls Talkspirit, so it keeps answering `200` while every endpoint that needs a scope answers `403`. Anything else is in the error table below.

## Conventions to build against

- **Versioning.** Everything sits under `/v1`. A breaking change ships under a new prefix instead of changing this one.
- **Pagination.** List endpoints take `?cursor=` and `?limit=` and answer with a `data` array plus a `pagination` object carrying `next_cursor` and `has_more`. The default `limit` is 50 everywhere. The maximum is 199, with two exceptions: 100 on `GET /v1/users` and 50 on `GET /v1/meetings`. The cursor is opaque: pass it back untouched. There is no `page`, `skip` or `offset` parameter in any spelling.
- **Field expansion.** Responses carry a minimal set of fields. Ask for more with `?include=`, a comma-separated list documented on each endpoint. A field you did not request is absent from the response rather than `null`, so an absent list means "not requested" and `[]` means "requested, and there are none".
- **Server to server only.** Cross-origin requests from a browser are not supported.
- **Retries.** Every `GET` is safe to retry. The writes are not idempotent: `POST /v1/tasks` and `POST /v1/tensions` create a record on every call, so deduplicate on your side rather than retrying blindly.

## What the errors mean

| Status and code | What happened |
| --- | --- |
| `401 AUTHENTICATION_REQUIRED` | No `Authorization` header on the request. |
| `401 AUTHENTICATION_INVALID` | The key is unknown or revoked, or the organisation does not have API access. The message tells the two apart. |
| `403 FORBIDDEN_SCOPE` | The key is valid but was not granted the scope this endpoint needs. |
| `403 FORBIDDEN_UPSTREAM` | The key has the scope, but the account behind it may not make this change to a member, or is not an administrator for the activity log. An administrator has to grant it. |
| `403 FORBIDDEN_SIGN_IN_DISABLED`, `FORBIDDEN_MODULE_DISABLED`, `FORBIDDEN_ACCOUNT_SUSPENDED`, `FORBIDDEN_ACCOUNT_UNKNOWN` or `FORBIDDEN_ACCESS_DENIED` | The scope is there, but the organisation refuses the account behind the key: its sign-in method has been switched off, the module this endpoint reads is off, the account is suspended, it no longer exists in the organisation, or it may not see what you asked for. Branch on the `code`. |
| `400 INVALID_CURSOR` or `INVALID_INCLUDE` | The pagination token is malformed or past its ceiling, or an `include` token is not one this endpoint accepts. |
| `409 ACTIVITY_LOG_DISABLED` | The organisation has never switched its activity log on. An administrator changes that setting; retrying does not help. |
| `429 RATE_LIMIT_EXCEEDED` or `UPSTREAM_RATE_LIMITED` | Too many requests for this key, or Talkspirit itself is limiting requests behind the API. Wait the number of seconds in the `Retry-After` header. |
| `502 UPSTREAM_ERROR` or `UPSTREAM_AUTH_ERROR` | A service this endpoint depends on failed or answered unexpectedly. Nothing changed, so retry with backoff. |
| `504` | The request passed the server's time limit and was abandoned. Whether a write took effect is undefined, so repeat it only if repeating it is safe. |

A refusal by the organisation, the five `403` codes in the table, is final: neither retrying nor minting a new key clears it. An administrator of the organisation has to act. Until contract 0.49.0 those refusals, and Talkspirit's own rate limiting, both arrived as a `502`, which reads as temporary. An integration written against that behaviour retries a refusal for ever and ignores `Retry-After` on a throttle, so check yours against the table above.

Error bodies never say whether a key exists or whether it was the scope that failed, so do not try to infer state from them.

## What's next?

- [Managing API tokens as an admin](../integrations/manage-api-tokens-as-an-admin)
- [How do I add or remove an integration?](../integrations/enable-a-cloud-file-picker)
