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

# Conversation bindings

> Map one external thread to one Session, race-free, so concurrent first messages converge instead of forking.

An installed agent answers in threads: a reply chain or a ticket in a provider your integration bridges. A **ConversationBinding** is a governed projection from one external conversation to one [Session](/concepts/sessions-and-runs). An `interaction` binding turns "a message arrived in this thread" into "drive this Session"; a `delivery` binding is outbound-only.

A binding maps `(ExternalInstallation, canonical conversation key)` to exactly one Session and the [SurfaceScope](/concepts/installations) it lives in. Its projection policy explicitly selects event classes. `question`, `result`, and `failure` are durable projections; `progress` is an ephemeral live hint. The binding stores only projection message IDs, a reconciliation cursor, and the last projected canonical sequence, never a transcript. The Session event log remains the single source of truth; provider delivery is a downstream projection of that log.

## The canonical conversation key

Providers model threads differently; Checkfu normalizes them into one key with three kinds:

| `kind`    | Carries                                   | Is                         |
| --------- | ----------------------------------------- | -------------------------- |
| `ambient` | `root_external_id`                        | The top level of a channel |
| `thread`  | `root_external_id` + `thread_external_id` | A reply thread             |
| `direct`  | `root_external_id`                        | A DM                       |

Only a `thread` carries a `thread_external_id`; the shape is enforced. Your provider adapter is responsible for producing the canonical key from the provider's own thread identifiers.

## Resolve, don't create

On the live ingress path you call one endpoint per inbound message: **resolve**. It creates the Session on the first message of a conversation and returns the existing binding on every message after.

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl --request POST https://api.checkfu.com/v1/conversation-bindings/resolve \
    --header "Authorization: Bearer $CHECKFU_API_KEY" \
    --header "Checkfu-Version: 2026-08-27" \
    --header "Content-Type: application/json" \
    --data '{
      "external_installation_id": "exti_0123456789abcdef0123456789abcdef",
      "agent_installation_id": "aini_0123456789abcdef0123456789abcdef",
      "surface_scope_id": "surf_0123456789abcdef0123456789abcdef",
      "conversation": {
        "kind": "thread",
        "root_external_id": "C0123456789",
        "thread_external_id": "1700000000.000100"
      },
      "principal": "prin_0123456789abcdef0123456789abcdef"
    }'
  ```

  ```ts TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const binding = await checkfu.collaboration.conversationBindings.resolve({
    external_installation_id: "exti_0123456789abcdef0123456789abcdef",
    agent_installation_id: "aini_0123456789abcdef0123456789abcdef",
    surface_scope_id: "surf_0123456789abcdef0123456789abcdef",
    conversation: {
      kind: "thread",
      root_external_id: "C0123456789",
      thread_external_id: "1700000000.000100",
    },
    principal: "prin_0123456789abcdef0123456789abcdef",
  })
  ```
</CodeGroup>

You do **not** pass a session. Resolve derives and admits the installed Session for you, and forbids a caller-supplied `session_id`, `agent_deployment_id`, `agent_deployment_revision_number`, `version_policy`, `acted_as`, `harness`, `model_routing_profile_key`, `sandbox_profile_key`, `mounts`, or `caused_by`. It returns a `ConversationBinding` at `200`, carrying the `session_id` to drive.

Because resolve runs a full [installed admission](/concepts/installations#installed-session-admission-derives-everything), it can return admission errors like `422 model.no_eligible_model`, the same failures a Session create would.

## Racing first messages converge

Two people can hit **send** in the same fresh thread at the same instant. Without care, that is two Sessions for one conversation. Checkfu makes them converge, and the mechanism is worth understanding because it is not a caller idempotency key.

```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
%%{init: {"theme":"neutral","themeVariables":{"fontFamily":"ui-sans-serif, system-ui, sans-serif","primaryColor":"#F5F5F4","primaryBorderColor":"#A8A29E","primaryTextColor":"#282828","lineColor":"#78716C","secondaryColor":"#FAFAF9","tertiaryColor":"#FFFFFF"}}}%%
sequenceDiagram
    participant A as Message from Alice
    participant B as Message from Bob
    participant C as Checkfu
    A->>C: resolve (same conversation)
    B->>C: resolve (same conversation)
    Note over C: Session ID derived from<br/>(Workspace, installation, conversation)
    C-->>A: binding → session_1
    C-->>B: binding → session_1 (the same one)
```

The Session ID is **derived deterministically** from the Workspace, the ExternalInstallation, and the complete canonical conversation key. Both racers therefore select the same Session before either can mint a competing one, and a unique binding name claims the conversation. The first resolver freezes the installed admission and the initial requester; every later caller, including a different person, receives the existing binding and does not rewrite Session identity or mounts.

The ordering is crash-safe: a crash can leave an unbound Session that the next request completes, but never an active binding to a Session that was not durably initialized.

<Tip>
  An optional `Idempotency-Key` still gives you HTTP response replay, but it is **not** what makes two authors converge; the deterministic Session ID is. You get convergence even when the two messages carry different idempotency keys.
</Tip>

The message that lands a moment later is covered too. A `user.message` authored while the Session's first Run is still queued or provisioning is accepted, appended to the log, and ordered into the next Run. Checkfu does not refuse it with `409 runtime.invalid_transition` merely because the sandbox has not finished starting. A fast follow-up in a live thread therefore needs no client-side retry logic.

## Shared threads authorize each author

One conversation is one Session with one frozen service identity. A shared thread has many human authors, and each is checked on their own.

The author who wins the race becomes the initial requester. After that, every inbound message asserts its own `authored_by` Principal, and Checkfu rechecks that author's PermissionAssignments: a turn-starting `user.message` requires `invoke` on the Agent and the surface; driving an in-flight Run requires `steer`. The binding is not a blanket pass: it routes messages to a Session, and the Session's governance still applies to each author.

Your backend authenticates its own users and asserts each author as the `authored_by` Principal; Checkfu never authenticates end users itself.

## The administrative create

`POST /v1/conversation-bindings` (without `/resolve`) attaches an **existing** Session (one you already hold the `session_id` for) to another governed projection and returns `201`.

* An installed Session may gain another matching interaction or delivery projection.
* A direct API Session may gain only a `delivery` binding. The request must name the active `agent_installation_id` that governs the exact installation and surface.
* A delivery binding never admits provider input. `ConversationIngest` fails closed if its canonical conversation resolves to one.

This is how one Session remains one event log while appearing in several external destinations. Your provider credential remains in your own integration; neither the Session nor the harness receives it.

## Next steps

<CardGroup cols={2}>
  <Card title="Add an agent to your app" icon="rocket" href="/guides/add-an-agent-to-your-app">
    Placement, binding, and driving a thread, end to end.
  </Card>

  <Card title="Events" icon="list-timeline" href="/reference/events">
    Drive the Session the binding returned, and follow its log.
  </Card>
</CardGroup>
