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 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:
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.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, 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. 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. The message that lands a moment later is covered too. Auser.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 ownauthored_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
deliverybinding. The request must name the activeagent_installation_idthat governs the exact installation and surface. - A delivery binding never admits provider input.
ConversationIngestfails closed if its canonical conversation resolves to one.
Next steps
Add an agent to your app
Placement, binding, and driving a thread, end to end.
Events
Drive the Session the binding returned, and follow its log.