Skip to content

Provision a team from your own system

A system you run, such as an internal agent platform, can set up streams for a team without anyone clicking through the app. It does this with a service user: a non-human member of your organization with its own bearer token. It calls the same HTTP API the app uses, so there is nothing separate to learn.

All calls go to https://api.streams.chat with Authorization: Bearer <token>. Responses are { "data": ... } or { "error": { "code", "message" } }.

An organization owner or admin does this once, signed in as a person (a service user cannot create or manage other service users).

Terminal window
curl -X POST https://api.streams.chat/api/identity/service-users \
-H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
-d '{ "displayName": "Acme agent platform", "role": "admin" }'

The response holds the service user and a token that starts with sk_service_. The token is shown once. Store it in your secret manager. role is admin (default) or member; a service user is never an owner or a guest.

To replace a token, call POST /api/identity/service-users/{id}/rotate; the old token stops working immediately. DELETE /api/identity/service-users/{id} revokes it for good. Both are for people with the owner or admin role.

Terminal window
curl -X POST https://api.streams.chat/api/streams \
-H "Authorization: Bearer $SERVICE_TOKEN" -H "Content-Type: application/json" \
-d '{ "title": "Team Atlas", "visibility": "private" }'

The service user becomes the stream’s owner. Creating a stream is not idempotent: if your call may be retried, look the stream up first with GET /api/streams?q=Team%20Atlas.

People already in the organization join by user id:

Terminal window
curl -X POST https://api.streams.chat/api/streams/$STREAM_ID/members \
-H "Authorization: Bearer $SERVICE_TOKEN" -H "Content-Type: application/json" \
-d '{ "userIds": ["<user id>"] }'

Someone who is not in the organization yet needs an organization invite first, then they can be queued on the stream by email:

Terminal window
curl -X POST https://api.streams.chat/api/identity/invitations \
-H "Authorization: Bearer $SERVICE_TOKEN" -H "Content-Type: application/json" \
-d '{ "email": "newcomer@example.com", "role": "member" }'
curl -X POST https://api.streams.chat/api/streams/$STREAM_ID/members \
-H "Authorization: Bearer $SERVICE_TOKEN" -H "Content-Type: application/json" \
-d '{ "emails": ["newcomer@example.com"] }'

The invite email is attributed to the person who created the service user. The stream response lists them under pending until they accept.

An agent created with the service token is owned by the service user. Register it, then put it on the stream roster:

Terminal window
curl -X POST https://api.streams.chat/api/agents \
-H "Authorization: Bearer $SERVICE_TOKEN" -H "Content-Type: application/json" \
-d '{ "name": "Acme bot", "endpointUrl": "https://agents.example.com/hook" }'

Use registration.agentUserId from the response as the id to add to the stream ({ "userIds": ["<agentUserId>"] }). The webhook your endpoint must serve is described in the webhook contract.

A cycle is a trigger name plus ordered phases. Each phase names an agent that is on the stream roster.

Terminal window
curl -X POST https://api.streams.chat/api/cycles/streams/$STREAM_ID/cycles \
-H "Authorization: Bearer $SERVICE_TOKEN" -H "Content-Type: application/json" \
-d '{
"triggerName": "triage",
"description": null,
"phasesTemplate": [
{
"id": "<new uuid>",
"agentId": "<agentUserId>",
"goal": "Summarize the report and propose a severity",
"instructions": null,
"autoAdvance": true,
"phaseName": "Triage",
"operatorPool": []
}
]
}'

Phase ids are uuids you generate. operatorPool takes people only: a service user cannot be listed in it. Who operates the first phase of a run depends on the pool:

  • Empty pool: whoever posts the trigger message operates it. When your service posts it, that is the service user. Any stream member, including the service, can still advance the run; assign a teammate (see the one-person pool below) if a person should own the phase.
  • One person: that person operates it, whoever starts the run. Use this when a specific teammate should own the phase: "operatorPool": ["<user id>"].
  • Several people: the phase starts with no operator until one of them is assigned.

Post a message that carries the cycle trigger. The server starts the run; no second call is needed. The message body is rich text with a cycleTrigger element (the same thing the app produces when someone types /triage). You choose $NEW_MESSAGE_ID: generate a uuid and reuse the same one when you retry, so a retry cannot post the message twice.

Terminal window
curl -X PUT https://api.streams.chat/api/chat/streams/$STREAM_ID/messages/$NEW_MESSAGE_ID \
-H "Authorization: Bearer $SERVICE_TOKEN" -H "Content-Type: application/json" \
-d '{
"content": {
"blocks": [
{
"type": "rich_text",
"elements": [
{
"type": "paragraph",
"children": [
{ "type": "cycleTrigger", "triggerName": "triage" },
{ "type": "text", "text": " Customer cannot log in" }
]
}
]
}
]
},
"assetIds": []
}'

Find the run with GET /api/cycles/runs?streamIds=$STREAM_ID&rootMessageId=$NEW_MESSAGE_ID. POST /api/cycles/streams/$STREAM_ID/runs with { "rootMessageId": "..." } is the idempotent way to confirm or repair a run for a message that was already posted.

  • It is not a person: it cannot be @mentioned, so mentioning it creates no notification and invokes nothing.
  • It does not appear in the member and people lists the app shows. Ask for it explicitly with GET /api/identity/members?includeServices=true or GET /api/identity/users?kind=service.
  • It cannot create, rotate, list or delete service users.
  • It acts only inside your organization, with the access its role gives it (and stream access follows stream membership, as for everyone).