> ## Documentation Index
> Fetch the complete documentation index at: https://docs.checkfu.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Connect your own messaging app

> Run a gateway in your service that maps each conversation in Slack, Telegram, WhatsApp, or another app to a Checkfu Session.

Checkfu does not operate messaging adapters. You connect a messaging app by running a small gateway in your own service. The gateway receives messages from the provider, sends them to a Checkfu Session through the ordinary API, and delivers the Agent's replies back through the provider.

<Warning>
  The Checkfu CLI and TypeScript SDK are private-alpha artifacts with no self-service public installation channel.
  Part of this flow is behind a rollout fence; a deployment that has not enabled it refuses the request rather than running it.
  Confirm [capability status](/getting-started/status) before making availability part of your application's contract.
</Warning>

## What the gateway does

| Step | Who does it |
| - | - |
| Verify the provider's signature or challenge | Your gateway |
| Decide which Checkfu credential and Agent the sender may use | Your gateway |
| Find or create the Session for the conversation | Your gateway, using `POST /v1/sessions` |
| Send the message with `POST /v1/sessions/{id}/events` | Your gateway |
| Read the Agent's reply from the Session event log | Your gateway |
| Send the reply through the provider | Your gateway |
| Admit the Session, run the Agent, record events, enforce permissions and approvals | Checkfu |

Checkfu does not store the mapping from a provider conversation to a Session. Keep that mapping in your own database, keyed by your tenant and the provider's conversation ID.

## 1. Create a Session for a new conversation

Create one Session the first time you see a conversation. Use a stable `Idempotency-Key` derived from your tenant and the provider's conversation ID, so a retried webhook returns the same Session. Store the returned `sess_…` ID next to the conversation.

You need an existing Agent and Environment. The [Quickstart](/getting-started/quickstart) creates both. Link [CLI access](/getting-started/cli-access) before using the CLI tab.

<CodeGroup>
  ```sh CLI theme={"theme":{"light":"github-light","dark":"github-dark"}}
  checkfu session create \
    --agent "$CHECKFU_AGENT_ID" \
    --environment "$CHECKFU_ENVIRONMENT_ID" \
    --idempotency-key "chat-session-$TENANT_ID-$CONVERSATION_ID"
  ```

  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl --fail-with-body --request POST \
    "https://api.checkfu.com/v1/sessions" \
    --header "Authorization: Bearer $CHECKFU_API_KEY" \
    --header "Checkfu-Version: 2026-08-31" \
    --header "Idempotency-Key: chat-session-$TENANT_ID-$CONVERSATION_ID" \
    --header "Content-Type: application/json" \
    --data '{
      "agent": { "type": "agent", "id": "'"$CHECKFU_AGENT_ID"'" },
      "environment_id": "'"$CHECKFU_ENVIRONMENT_ID"'"
    }'
  ```

  ```ts TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  // See the TypeScript SDK reference for access: /reference/typescript-sdk
  const session = await checkfu.sessions.create(
    {
      agent: { type: "agent", id: agentId },
      environment_id: environmentId,
    },
    { idempotencyKey: `chat-session-${tenantId}-${conversationId}` },
  )
  ```
</CodeGroup>

An API-key request for an Agent with governed Connection bindings also needs `acted_as`. See [Session identity](/reference/access#session-identity).

## 2. Send each message as Session input

For every verified incoming message, post a `user.message` event. Use the provider's message ID in the `Idempotency-Key` and in `caused_by.request_id` (1 to 512 characters). If the provider redelivers the message, the same key returns the same stored event.

<CodeGroup>
  ```sh CLI theme={"theme":{"light":"github-light","dark":"github-dark"}}
  checkfu session steer "$CHECKFU_SESSION_ID" \
    "$MESSAGE_TEXT" \
    --idempotency-key "chat-message-$TENANT_ID-$MESSAGE_ID" \
    --json
  ```

  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl --fail-with-body --request POST \
    "https://api.checkfu.com/v1/sessions/$CHECKFU_SESSION_ID/events" \
    --header "Authorization: Bearer $CHECKFU_API_KEY" \
    --header "Checkfu-Version: 2026-08-31" \
    --header "Idempotency-Key: chat-message-$TENANT_ID-$MESSAGE_ID" \
    --header "Content-Type: application/json" \
    --data '{
      "type": "user.message",
      "payload": {
        "content": "'"$MESSAGE_TEXT"'",
        "caused_by": { "kind": "api", "request_id": "'"$MESSAGE_ID"'" }
      }
    }'
  ```

  ```ts TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const inputEvent = await checkfu.sessions.events.send(
    session.id,
    {
      type: "user.message",
      payload: {
        content: messageText,
        caused_by: { kind: "api", request_id: messageId },
      },
    },
    { idempotencyKey: `chat-message-${tenantId}-${messageId}` },
  )
  ```
</CodeGroup>

The response means Checkfu stored the input. It does not mean the Agent has answered. Keep the returned `seq`.

## 3. Read the reply

Follow the event stream from just after your stored message, or page through the event log. `agent.message` events carry the reply text in `payload.content`. A `session.status_idle` whose `stop_reason` is not `requires_action` marks the end of the turn. See [Events](/reference/events) for the full catalog and [Resume a stream](/guides/resume-a-stream) for cursor handling.

<CodeGroup>
  ```sh CLI theme={"theme":{"light":"github-light","dark":"github-dark"}}
  checkfu session follow "$CHECKFU_SESSION_ID" --from "$CHECKFU_INPUT_SEQ" --until-boundary
  ```

  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl --fail-with-body --no-buffer \
    "https://api.checkfu.com/v1/sessions/$CHECKFU_SESSION_ID/events/stream" \
    --header "Authorization: Bearer $CHECKFU_API_KEY" \
    --header "Checkfu-Version: 2026-08-31" \
    --header "Last-Event-ID: $CHECKFU_INPUT_SEQ"
  ```

  ```ts TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  for await (const event of checkfu.sessions.events.stream(session.id, {
    lastEventId: inputEvent.seq,
  })) {
    if (event.type === "agent.message") {
      await sendThroughProvider(conversationId, event.payload.content)
    }
    if (
      event.type === "session.status_idle" &&
      event.payload.stop_reason !== "requires_action"
    ) {
      break
    }
  }
  ```
</CodeGroup>

Save the highest `seq` you have delivered for each conversation. After a restart, resume from that cursor so you do not resend earlier replies.

## 4. Deliver the reply through the provider

Your gateway calls the provider's send API with the reply text. Choose what to deliver. You can skip `agent.thinking`, tool events, and other internal events, and you can post a single message per turn.

If the Agent asks for an approval, the Session stops at a `requires_action` boundary. Show the request in your app, then decide it with the approval API. A button press in the provider is not a decision until your service calls `actionApprovals.decide`. See [Handle an approval](/guides/handle-an-approval).

## Your responsibilities

| Area | What you do |
| - | - |
| Provider credentials | Hold bot tokens and signing secrets. Verify signatures, challenges, and callback registration |
| Credentials for Checkfu | Keep the Workspace API key on your server. Never give it to an end user or to the messaging provider. It can carry more authority than any one chat user |
| User mapping | Decide which person in the provider may use which Agent, and with which `acted_as` Principal. Do not trust a provider display name, Session `metadata`, or a `collaboration_space_id` as an access grant |
| Conversation mapping | Store the conversation-to-Session map per tenant. Checkfu does not store it |
| Duplicates | Use stable idempotency keys for Session creation and for each message |
| When to respond | Decide which messages start work. Checkfu does not listen to a room on its own |
| Provider semantics | Handle edits, deletes, reactions, threads, and rate limits |
| Delivery | Choose the output, check the audience, send it, and keep your own delivery record |

## Delivery is not exactly-once

An idempotent API call and a stored event do not tell you whether the provider received your reply. If a send times out, you do not know whether the message arrived. Record the attempt and reconcile with the provider before you send again. Do not replay an uncertain send automatically.

A successful create or input request also does not mean a model answered. Support depends on the Harness, Environment, model, and resource combination you select. Check [capability status](/getting-started/status) before relying on one.

## Next steps

<CardGroup cols={2}>
  <Card title="Resume a stream" href="/guides/resume-a-stream">
    Keep a cursor so a restart does not resend or skip events.
  </Card>

  <Card title="Handle an approval" href="/guides/handle-an-approval">
    Decide an ActionApproval from your own interface.
  </Card>

  <Card title="Events" href="/reference/events">
    Look up the event types a gateway reads.
  </Card>

  <Card title="Add an agent to your app" href="/guides/add-an-agent-to-your-app">
    Use the same Session API from any backend.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.