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" } }.
1. Create the service user
Section titled “1. Create the service user”An organization owner or admin does this once, signed in as a person (a service user cannot create or manage other service users).
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.
2. Create a stream
Section titled “2. Create a stream”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.
3. Add people
Section titled “3. Add people”People already in the organization join by user id:
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:
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.
4. Connect an agent
Section titled “4. Connect an agent”An agent created with the service token is owned by the service user. Register it, then put it on the stream roster:
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.
5. Define a cycle
Section titled “5. Define a cycle”A cycle is a trigger name plus ordered phases. Each phase names an agent that is on the stream roster.
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.
6. Start a run
Section titled “6. Start a run”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.
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.
What a service user cannot do
Section titled “What a service user cannot do”- 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=trueorGET /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).