> This page is also available as Markdown: append .md to its URL. The full index of Sikaru docs is at https://docs.sikaru.ai/llms.txt. Code examples use only operations from the public API reference at https://docs.sikaru.ai/api-reference.

# Personal channels

An HTTP channel starts with shared conversations and a separate binding credential. Enable **personal access** when your agent needs a person's own connected accounts or memory.

Private conversations require verified identity on every request. A project API key alone cannot read them or act for their owner.

Personal access supports signed-in project members and people authenticated by your application. Your customers do not need Sikaru accounts. Your application owns their sign-in, conversation interface, connection callback, and approval UI.

Personal channel operations on this page:

1. Register a trusted app: **Settings → Channels → Trusted Customer Apps**, or `POST /v1/projects/{project_id}/channels/identity-apps` (issuer, audience, public RSA JWK of at least 2048 bits, HTTPS connection callback URL, HTTPS private channel URL). The returned application ID is the assertion `kid`.
2. Enable personal access: `PUT /v1/projects/{project_id}/channels/bindings/{binding_id}/personal-access` with `{"enabled": true, "identity_app_ids": [...]}`. Errors: `409 agent_is_personal` (activating or disabling without it on a personal agent), `agent_is_shared` (shared agent), `401 Verified personal identity required` (personal HTTP message without proof).
3. User proof: RS256 JWT, header `typ: sikaru-user+jwt`, claims `iss`, `aud`, `sub`, `tenant_id`, `iat`, `exp` (integer Unix seconds, at most 600 seconds). Send as `X-Sikaru-User-Assertion`. Project members send `X-Sikaru-Member-Authorization: Bearer <access-token>`. HTTP bindings also send `Authorization: Bearer <binding-credential>`.
4. Send: `POST https://api.sikaru.ai/v1/channel-bindings/{binding_id}/messages` with `message_id`, `conversation_id`, `privacy: "private"`, `content`. SDK: `client.http_channels.invoke(...)`.
5. Poll: `GET /v1/personal-channel-bindings/{binding_id}/messages/{receipt_id}` returns status, output, `connection_prompts`, pending `actions`. SDK: `personal_channels.get_message` / `personalChannels.getMessage`; CLI `sikaru personal-channels get_message`.
6. On `connection-needed`: `POST .../connections/{connection_id}/authorize` (returns `url`, `state`, `expires_at`), then `POST .../connections/{connection_id}/complete` with `{"state","query"}`; or `PUT .../connections/{connection_id}/credentials` with `{"credentials":{...}}`.
7. Approvals: `POST .../tool-calls/{tool_call_id}/approval` with `decision` `approved` or `denied` and `idempotency_key`.
8. Files: `GET .../files`, `GET .../files/{file_id}/content`.
9. Slack DM link: `POST /v1/personal-channel-bindings/{binding_id}/slack-links` with `installation_id`; user DMs `sikaru verify <challenge>`; poll `GET .../slack-link-verifications/{verification_id}` until `status` is `verified`; operator calls `POST /v1/projects/{project_id}/channels/personal-slack-bindings` with `agent_id`, `installation_id`, `verification_id`, `identity_app_id`. Unlink: `DELETE /v1/personal-channel-bindings/{binding_id}/slack-links/{installation_id}`.

```mermaid
sequenceDiagram
    participant User
    participant App as Your backend
    participant Sikaru
    User->>App: Sign in
    App->>App: Sign user assertion (RS256 JWT)
    App->>Sikaru: Message + binding credential + assertion
    Sikaru-->>App: Receipt
    App->>Sikaru: Poll receipt with fresh proof
    Sikaru-->>App: Output, connection prompts, or pending actions
    App->>User: Show output, connect prompt, or approval
```

Your backend signs in its own user, signs a short-lived assertion for that user, and sends it with every message and poll. Sikaru returns a receipt. Polling the receipt returns the output, or a connection prompt or approval for the user to handle.

## Register your application

In **Settings → Channels → Trusted Customer Apps**, register:

* An issuer and audience that your backend will put in assertions.
* A public RSA JWK with at least a 2048-bit key. Keep the private signing key on your backend.
* A connection callback URL for account authorization, and a private channel URL where your users can sign in and continue a Slack conversation. Both use HTTPS.

The returned application ID is the assertion's `kid`. An operator can also register an app using `POST /v1/projects/{project_id}/channels/identity-apps` with a project credential.

Select the trusted app when you enable personal access on the binding. For API setup, use `PUT /v1/projects/{project_id}/channels/bindings/{binding_id}/personal-access`:

```json
{"enabled": true, "identity_app_ids": ["app_id"]}
```

Personal access belongs to **personal agents**. Every conversation of a personal agent is personal, so its channels need personal access before they go live.

| Situation                                                                        | Result                                    |
| -------------------------------------------------------------------------------- | ----------------------------------------- |
| Activating a personal agent's channel without personal access, or turning it off | `409` with code `agent_is_personal`       |
| Personal agent in a Slack room                                                   | Not supported                             |
| Enabling personal access for a shared agent                                      | Refused with `agent_is_shared`            |
| Message on a personal agent's HTTP channel without a user proof                  | `401 Verified personal identity required` |

Customer private identity comes from the signed `tenant_id` and `sub` claims. It never comes from message text, actor labels, email addresses, or a browser-supplied user ID.

## Prove the current user

Your backend verifies its own user's session, then signs an RS256 JWT with this header and claims (replace the timestamps with the current time):

```json
{"alg": "RS256", "typ": "sikaru-user+jwt", "kid": "app_id"}
```

```json
{
  "iss": "https://your-app.example",
  "aud": "your-sikaru-integration",
  "sub": "stable-user-id",
  "tenant_id": "tenant_id",
  "iat": 1790899200,
  "exp": 1790899500
}
```

* Issuer and audience must exactly match the registration.
* `iat` and `exp` are integer Unix seconds. Assertions last at most 600 seconds.
* Only issue assertions for the verified user and tenant. Refresh them through your backend as needed.

Expiry limits reuse of a copied assertion. It does not expire connected accounts, conversation history, memory, or already accepted work. Revoking an app, subject, or link still removes current access.

| Caller                                         | Header                                                 |
| ---------------------------------------------- | ------------------------------------------------------ |
| Your application's user                        | `X-Sikaru-User-Assertion: <assertion>`                 |
| Signed-in project member                       | `X-Sikaru-Member-Authorization: Bearer <access-token>` |
| HTTP binding (in addition to one of the above) | `Authorization: Bearer <binding-credential>`           |

A customer assertion grants no project administration privileges.

## Send a private HTTP message

Keep the binding credential on your backend. Set `privacy` explicitly:

```sh
curl https://api.sikaru.ai/v1/channel-bindings/BINDING/messages \
  -H "Authorization: Bearer $CHANNEL_CREDENTIAL" \
  -H "X-Sikaru-User-Assertion: $USER_ASSERTION" \
  -H 'Content-Type: application/json' \
  -d '{"message_id":"message-1","conversation_id":"private-conversation-1","privacy":"private","content":"Check my calendar"}'
```

* Use stable message IDs when you retry an uncertain submission.
* Omitting `privacy` keeps shared behavior.
* Use a new conversation ID when moving from shared to private. Existing conversations do not change audience or merge old memory.

The generated clients keep channel credentials separate from the project API key:

#### Python

```python
from sikaru_api import SikaruApi

client = SikaruApi(channel_credential=channel_credential)
receipt = client.http_channels.invoke(
    binding_id, message_id="message-1", conversation_id="private-conversation-1",
    privacy="private", content="Check my calendar",
    sikaru_user_assertion=user_assertion,
)
owned = client.personal_channels.get_message(
    binding_id, receipt.id,
    authorization=f"Bearer {channel_credential}",
    sikaru_user_assertion=user_assertion,
)
```

#### TypeScript

```typescript
const receipt = await client.personalChannels.getMessage(bindingId, receiptId, {
  authorization: `Bearer ${channelCredential}`,
  "X-Sikaru-User-Assertion": userAssertion,
});
```

#### CLI

```sh
sikaru personal-channels get_message \
  --binding-id "$BINDING_ID" --receipt-id "$RECEIPT_ID" \
  --authorization "Bearer $CHANNEL_CREDENTIAL" \
  --x-sikaru-user-assertion "$USER_ASSERTION"
```

The `personal_channels` methods take an optional per-call `authorization` with the literal `Bearer ` prefix. HTTP bindings require it; Slack bindings omit it. The client's project `api_key` and constructor `channel_credential` do not supply this per-call header.

In TypeScript the corresponding group is `personalChannels`; the CLI group is `personal-channels`. Header flags appear in each command's `--help`.

Omit `authorization` / `--authorization` when accessing a linked Slack binding.

## Continue the same turn

Poll `GET /v1/personal-channel-bindings/{binding_id}/messages/{receipt_id}` with the same owner's fresh proof. For HTTP, include the binding credential too. The response contains status, output, `connection_prompts`, and pending `actions`.

When status is `connection-needed`, use the prompt's connection ID:

#### Start authorization

`POST .../connections/{connection_id}/authorize` returns `url`, `state`, and `expires_at`. Send the user to `url`.

#### Complete it at your callback

At your registered callback, authenticate the same user and call `POST .../connections/{connection_id}/complete` with `{"state":"...","query":{...}}`. Pass the authorization callback query parameters in `query`.

#### Or submit credentials

For a connection that uses credentials instead of browser authorization, call `PUT .../connections/{connection_id}/credentials` with `{"credentials":{...}}` through your authenticated backend. Never put credentials in chat messages.

When approval is pending, show the action's product-visible arguments and let its owner choose. Call `POST .../tool-calls/{tool_call_id}/approval` with:

```json
{"decision": "approved", "idempotency_key": "stable-decision-id"}
```

`denied` is also supported.

> **Warning**
>
> Account completion and approvals wake the same turn. Do not submit the original message again to continue it. Generated mutation methods do not retry automatically. If delivery is uncertain, reuse the operation's existing identifiers and inspect the current receipt before deciding what to do next.

List the owner's files using `GET .../files`, then download bytes from `GET .../files/{file_id}/content`.

Every receipt, action, and file request rechecks owner access. Project-reader authority and another person's proof do not expose private content. A paused or revoked binding stops private access too.

## Link a private Slack DM

A shared Slack room never gains a person's accounts or private memory. Linking a DM requires proof from both the authenticated app user and the Slack recipient; email matching is not used.

#### Request a link

With the authenticated user's proof, call `POST /v1/personal-channel-bindings/{binding_id}/slack-links` with body `{"installation_id":"installation_id"}`. An HTTP bootstrap binding also needs its binding credential.

#### Verify in Slack

Have the user send `sikaru verify <challenge>` to the installed Slack bot in a DM, replacing `<challenge>` with the returned challenge.

#### Wait for verification

Poll `GET /v1/personal-channel-bindings/{binding_id}/slack-link-verifications/{verification_id}` with that same proof until `status` is `verified`.

#### Create the destination

An operator creates the destination with `POST /v1/projects/{project_id}/channels/personal-slack-bindings`, providing `agent_id`, `installation_id`, `verification_id`, and `identity_app_id`.

Slack status messages link to your registered private channel URL with `binding_id` and `receipt_id`. Authenticate the user there before loading content; the URL itself contains no authorization. Customer DM bindings require that URL to be configured. Project members can link and unlink their own Slack identity in the dashboard.

To unlink, call `DELETE /v1/personal-channel-bindings/{binding_id}/slack-links/{installation_id}` with the current owner's proof. Relinking does not grant old conversations to a new owner. Operators can revoke an entire trusted app or a specific subject/tenant through the app management endpoints.

## Control memory separately

Under the agent's Memory capability, **Shared Memory** and **Personal Memory** can be controlled independently. The main Memory switch disables both.

* Private turns read and write only user memory.
* Disabling Personal Memory turns channel memory off rather than falling back to shared memory.
* User memory follows the verified person within the same tenant and agent across explicitly linked private HTTP and Slack channels.

> **Note**
>
> Turning access or memory off takes effect on later access and capture checks. Revocation is an access control, not a way to undo a tool action already completed.

## Next steps

#### [Agents and releases](/build/agents-and-releases)

Author, review, and publish the agent behind a channel.

#### [Tools and connections](/build/tools-and-connections)

Set up the connections a person's turns can use.