> ## 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.

# Custom channel adapter

> The smallest honest adapter for a provider Checkfu does not rent.

A minimal reference adapter for a provider Checkfu does not rent: one HTTP
handler, one platform client, no framework. It is the shape
[Build a custom channel adapter](/guides/build-a-custom-channel-adapter)
describes, reduced to the parts that are load-bearing — signature
verification, identity, binding, ingest, observation from the exclusive
cursor, and the one-winner presentation fence.

## Golden journeys

* Verify a provider webhook and reject a forged one
* Drive one turn into a bound Session, idempotent under provider redelivery
* Read the settled reply from the event log, never from elapsed time
* Post exactly once across two replicas racing the same reply

## Platform surfaces exercised

ExternalInstallations, SurfaceScopes, AgentInstallations, ExternalIdentities,
ConversationBindings, `ingestConversation`, and the four
ConversationPresentation operations. No other operation, and no private hook.

## The whole adapter

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { createHmac, randomUUID, timingSafeEqual } from "node:crypto"
import { createServer } from "node:http"

import { Checkfu } from "@checkfu/sdk"

const checkfu = new Checkfu({ apiKey: required("CHECKFU_API_KEY") })
const channelKey = "acme"

// Paired once, out of band. These three ids are the adapter's configuration.
const installationId = required("ACME_EXTERNAL_INSTALLATION_ID")
const placementId = required("ACME_AGENT_INSTALLATION_ID")
const scopeId = required("ACME_SURFACE_SCOPE_ID")
const signingSecret = required("ACME_SIGNING_SECRET")

/**
 * Read one required variable at boot. A missing secret must stop the process,
 * not surface later as a confusing crash inside signature verification on the
 * first real delivery.
 */
function required(name) {
  const value = process.env[name]
  if (value === undefined || value === "") throw new Error(`acme-adapter: ${name} is required`)
  return value
}

/**
 * Yours, always. Checkfu owns your provider's app registration no more than
 * it owns your provider's uptime, so it cannot verify this for you. A forged
 * payload that gets past here is a real message to the platform.
 */
const verified = (raw: Buffer, signature: string): boolean => {
  const expected = createHmac("sha256", signingSecret).update(raw).digest()
  // `Buffer.from` on malformed hex silently truncates rather than throwing, so
  // the length check is load-bearing: without it a short forged signature
  // could reach a comparison it was never long enough to lose.
  const provided = Buffer.from(signature, "hex")
  return expected.length === provided.length && timingSafeEqual(expected, provided)
}

const handleTurn = async (event) => {
  // 1. The speaker becomes a Principal through stable provider ids only.
  const identity = await checkfu.collaboration.externalIdentities.resolve({
    external_installation_id: installationId,
    external_user_id: `${channelKey}:${event.user_id}`,
    display_name: event.user_display_name,
  })

  // 2. One canonical conversation, create-or-return. `kind` is required:
  //    `thread` carries a thread id; `ambient` and `direct` carry null.
  const conversation = {
    kind: "thread",
    root_external_id: `${channelKey}:${event.channel_id}`,
    thread_external_id: event.thread_id ?? event.message_id,
  }
  await checkfu.collaboration.conversationBindings.resolve({
    external_installation_id: installationId,
    agent_installation_id: placementId,
    surface_scope_id: scopeId,
    conversation,
    principal: identity.principal.id,
  })

  // 3. Exactly one authored event, idempotent on the provider's own id. No
  //    client-side retry: a provider redelivery is the retry path, and it
  //    converges server-side on external_event_id.
  const receipt = await checkfu.collaboration.conversationBindings.ingest({
    external_installation_id: installationId,
    agent_installation_id: placementId,
    surface_scope_id: scopeId,
    conversation,
    external_event_id: `${channelKey}:${event.message_id}`,
    principal: identity.principal.id,
    input: { kind: "invoke", content: event.text, is_direct_mention: event.mentions_bot },
  })
  if (receipt.disposition !== "driven") return // filtered or already seen

  // 4. The receipt's coordinates are an EXCLUSIVE cursor. Read strictly after
  //    event_sequence, so a reused conversation never replays an earlier
  //    turn's answer. The response type makes both coordinates nullable
  //    because no-drive dispositions carry null, so a driven receipt missing
  //    either one is a corrupt response: fail loudly rather than letting
  //    `null` become a zero that replays the whole Session.
  if (receipt.event_sequence === null || receipt.event_id === null) {
    throw new Error("acme-adapter: driven ingest returned no authored coordinate")
  }

  const reply = await settledReplyAfter(receipt.session_id, receipt.event_sequence)
  if (reply === null) return // best effort: no reply is a real outcome

  // 5. Claim before posting. The loser of this claim posts nothing.
  const key = {
    conversation_binding_id: receipt.binding.id,
    session_id: receipt.session_id,
    interaction_id: receipt.event_id,
    kind: "final",
    revision: String(reply.throughSequence),
    delivery_role: "reply",
  }
  const claimed = await checkfu.collaboration.conversationPresentations.claim({
    external_installation_id: installationId,
    key,
    through_sequence: reply.throughSequence,
  })
  if (!claimed.won) return

  const dispatchToken = randomUUID()
  const dispatch = {
    external_installation_id: installationId,
    key,
    generation: claimed.attempt.generation,
    owner: claimed.attempt.owner,
    dispatch_token: dispatchToken,
  }
  await checkfu.collaboration.conversationPresentations.beginDispatch(dispatch)

  // 6. The one provider call this adapter is allowed to make for this key.
  //    An outcome you cannot read settles indeterminate — never a blind retry,
  //    because retrying an unknown outcome is how one answer becomes two.
  let outcome
  try {
    await postToAcme(event.channel_id, event.thread_id, reply.text)
    outcome = { state: "posted", provider_message_id: null }
  } catch {
    outcome = { state: "indeterminate" }
  }
  await checkfu.collaboration.conversationPresentations.settle({ ...dispatch, outcome })
}

createServer((request, response) => {
  const chunks = []
  request.on("data", (chunk) => chunks.push(chunk))
  request.on("end", () => {
    const raw = Buffer.concat(chunks)
    if (!verified(raw, request.headers["x-acme-signature"] ?? "")) {
      response.writeHead(401).end()
      return
    }
    // Parse BEFORE acknowledging. A signed body that is not JSON is a real
    // rejection, and parsing it as an argument to handleTurn would throw
    // outside the promise the .catch attaches to — after the 200 was already
    // sent, so the provider would never retry and the process would die.
    let event
    try {
      event = JSON.parse(raw.toString("utf8"))
    } catch {
      response.writeHead(400).end()
      return
    }
    // Acknowledge inside the provider's window; keep the turn supervised.
    response.writeHead(200).end()
    void handleTurn(event).catch((error) => {
      process.stderr.write(`acme-adapter: turn failed: ${String(error)}\n`)
    })
  })
}).listen(8790)
```

`settledReplyAfter` and `postToAcme` are the two functions this listing leaves
to you. The first streams the Session event log from the exclusive cursor and
returns the interaction result — settlement is read from the log, never from
elapsed time and never from your provider's delivery receipts. The second is
your provider's own API call, and it is the only place provider knowledge
lives.

## Honest gaps

* **This is a listing, not a runnable package.** Checkfu's runnable examples
  live in the separate `checkfu-examples` repository, which this repository
  pins by revision and does not write. Turning this into a
  `@checkfu-examples/custom-channel-adapter` package — with test doubles at
  both boundaries and a replayable golden journey, like `slack-teammate` and
  `support-desk` — is open work, tracked as V-CHN-024's gap.
* **No public-contract insufficiency was found writing it.** Every call above
  is a served public operation. If you hit one that is not, record it as a gap
  and open an issue; do not route around it with a private hook (D115).
* **Approvals are deliberately absent.** An approval in any channel is a plain
  link into the Checkfu-native approval surface. An adapter that renders
  interactive approval controls in a provider's UI is doing something this
  reference will never show.
