Skip to navigation

Personal channels

Let a personal agent act with each person's own connected accounts and memory by signing a user proof on every HTTP or Slack DM request.
View as Markdown

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.

Sign in Sign user assertion (RS256 JWT) Message + binding credential + assertion Receipt Poll receipt with fresh proof Output, connection prompts, or pending actions Show output, connect prompt, or approval User Your backend Sikaru

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:

{"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.

SituationResult
Activating a personal agent’s channel without personal access, or turning it off409 with code agent_is_personal
Personal agent in a Slack roomNot supported
Enabling personal access for a shared agentRefused with agent_is_shared
Message on a personal agent’s HTTP channel without a user proof401 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):

{"alg": "RS256", "typ": "sikaru-user+jwt", "kid": "app_id"}
{
"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.

CallerHeader
Your application’s userX-Sikaru-User-Assertion: <assertion>
Signed-in project memberX-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:

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:

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,
)

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:

1

Start authorization

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

2

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.

3

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:

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

denied is also supported.

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.

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.

2

Verify in Slack

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

3

Wait for verification

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

4

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.

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