- Home
- Developers
API for developers
One REST API per team: read members, trainings and evaluations, manage assignments and cases, read the catalog and team analytics your team has made available — and get a webhook when a training finishes or an evaluation is scored.
This page gets you to a first successful call in about five minutes. The full reference lists every operation with its parameters, an example response and the errors it can return; the machine-readable form is at openapi.v1.json (OpenAPI 3.1, generated from the same code as these pages).
Requirements
The interface is part of the Team and Enterprise plans. A team admin or owner
creates the first key under Team → Integrations & API in the app; that needs
the team right schnittstellen (the owner has it by default and can grant
it to others).
Quickstart
Create a key
In the app under Team → Integrations & API → API keys. Pick the "Read only" template for your first try — the secret is shown exactly once, at creation.
Make your first call
GET /me needs no scope and confirms the key works: your tenant,
its scopes and the team's privacy switches.
curl https://app.sip.coach/api/public/v1/me \
-H "Authorization: Bearer sipk_live_ab12cd34_…"
import httpx
response = httpx.get(
"https://app.sip.coach/api/public/v1/me",
headers={"Authorization": "Bearer sipk_live_ab12cd34_…"},
)
print(response.json())
const res = await fetch("https://app.sip.coach/api/public/v1/me", {
headers: { Authorization: "Bearer sipk_live_ab12cd34_…" },
});
console.log(await res.json());
<?php
$context = stream_context_create(["http" => ["header" =>
"Authorization: Bearer sipk_live_ab12cd34_…"]]);
$response = file_get_contents("https://app.sip.coach/api/public/v1/me", false, $context);
echo $response;
Response:
{
"tenant_id": "6a9c3e3e-9b8e-4b7b-9e9d-2b7b1a2c3d4e",
"kind": "team",
"scopes": [
"users:read",
"trainings:read",
"evaluations:read"
],
"rate_limit_per_min": 600,
"privacy": {
"member_detail_enabled": true
}
}Keep going
With a key using the "Read and assignments" template, GET /users
lists members and GET /trainings their trainings — every other
operation is in the reference.
Authentication
Every request carries the key in Authorization: Bearer
sipk_live_<prefix>_<secret>. The key belongs to the team, not a
person; its scopes are fixed when it is created (widening them means creating a new
key). Revocation takes effect immediately; rotation lets the old and new key overlap
for a chosen grace period.
| Scope | Allows |
|---|---|
users:read / users:write | Read members / invite, change role, remove |
trainings:read | Training metadata |
evaluations:read | Evaluations (score, dimensions, tips — no transcript) |
assignments:read / assignments:write | Assignments |
cases:read / cases:write | Team cases |
catalog:read | Catalog within the team's effective policy |
analytics:read | Team analytics (aggregated) |
webhooks:manage | Webhook endpoints |
The app also offers the templates "Read only" (every
:read scope) and "Read and assignments" (adds
assignments:write) when creating a key, or a custom selection from the
table above.
Tenancy and rights
The tenant is fixed the moment the key is created — no call names a team, in the
path or the body. A foreign id returns 404, exactly like one that never
existed: from the outside, the two are indistinguishable.
Errors
Every error follows RFC 9457
(application/problem+json). code is stable and never
localized; title follows Accept-Language (de or
en).
{
"type": "https://sip.coach/entwickler/api/fehler#insufficient_scope",
"title": "Insufficient scope",
"status": 403,
"code": "insufficient_scope",
"detail": "scope 'assignments:write' required",
"request_id": "a7e8b2c1-4d3e-4f5a-9b6c-7d8e9f0a1b2c",
"required_scope": "assignments:write"
}| Status | Code | When |
|---|---|---|
| 400 | bad_request | A dependency rejected the request without its own code; the status follows that dependency. |
| 401 | unauthorized | Key missing, unknown, malformed, expired, revoked, or the caller's IP is not allowed — one code for every reason. |
| 403 | insufficient_scope | A required scope is missing; detail names it. |
| 403 | privacy_restricted | The team has turned off per-person detail (member_detail_enabled). |
| 403 | api_disabled | Tenant suspended or ended. |
| 403 | vorgabe_gesperrt | A policy assigned to the recipient blocks this situation, personality or industry (assignments only). |
| 404 | not_found | Either never existed, or belongs to a different tenant — indistinguishable from the outside. |
| 409 | conflict | The email already belongs to a team member or the team's owner, or a concurrent call with the same Idempotency-Key is still in progress. |
| 409 | seat_limit_reached | All of the team's seats are taken. |
| 409 | last_admin | The member is the team's only active admin; changing their role or removing them would leave the team without one. |
| 409 | owner | The team's owner cannot be removed or have their role changed through this API. |
| 422 | validation_error | A query, path or body value failed validation. A form error detected while parsing the request carries errors (a list); a business-rule error the application checks instead carries field (a single field name). |
| 422 | idempotency_mismatch | The Idempotency-Key header was reused with a different request body. |
| 422 | kombination_ungueltig | The situation, personality or industry id is unknown, not visible to this team, or violates a catalog rule (assignments only). |
| 429 | rate_limited | Limit reached; Retry-After names the number of seconds to wait. |
| 500 | internal | Unexpected error; the body deliberately reveals no internals — request_id helps when contacting support. |
| 503 | unavailable | The customer API is temporarily unavailable. |
Every code with an explanation: Error codes.
Rate limits and idempotency
120 requests per minute per key, plus a per-tenant limit your team can configure
(default 600/minute) — whichever is tighter applies. Every response carries
RateLimit-Limit, RateLimit-Remaining and
RateLimit-Reset.
POST calls accept an Idempotency-Key header (up to 100
characters, valid 24 hours): the same key with the same body returns the first
response again without repeating the action; the same key with a different body
returns 422 idempotency_mismatch.
Pagination
List routes (e.g. GET /users, GET /assignments) accept
?limit= (default 50, maximum 200) and ?cursor=, and respond
with a data field (the list) and next_cursor.
next_cursor is an opaque value — do not interpret it as a page number or
offset, just pass it back unchanged on the next call; null means there is
no further page.
Versioning
Path version v1. Within v1, only additive changes (new
fields, new routes, new webhook event types) — ignore unknown response fields. A
breaking change ships as v2 alongside v1 for at least twelve
months, announced via Deprecation/Sunset response headers and
an email to every admin with an active key.
Privacy
The team's privacy switches apply to the API exactly as they do in the app. With
member_detail_enabled off, routes with per-person data return only
aggregates or 403 privacy_restricted, and webhook bodies carry neither a
score nor a user_id.
CRM connections
A connected CRM system is set up in the app, under Team → CRM connections
— not through this API. Once a connection is active, GET /crm/deals
returns the synchronized deals and GET /crm/objections the team's
objection catalog; both routes are read-only, paginated like any other list route.