Who owns what
You own every Linear-shaped thing. Checkfu never verifies Linear’s signature, never holds your signing secret, and never operates the Linear integration — the same fence the custom channel adapter guide draws, applied to Linear.
Create the webhook in Linear’s API settings for exactly the resource types
you map —
Issue and Comment — scoped to the team or teams you cover.
A production host must acknowledge within Linear’s five-second window. The
staged host still awaits body ingestion, Checkfu calls, and provider work;
it has not established that deadline. Verified deliveries the adapter drops
return 200. Linear retries a non-200 up to three times over six hours and may
eventually disable the webhook.
Verify the delivery over the exact raw bytes
Linear signs each delivery with the webhook subscription’s signing secret: a hex-encoded HMAC-SHA256 of the raw request body, in theLinear-Signature header. After verifying those bytes, read the signed
body’s webhookTimestamp and require it to be within one minute of your
clock, as Linear recommends.
Linear-Timestamp and Linear-Delivery headers are outside the signature;
changing them must never make an old body fresh. Keep the delivery UUID only
as a transport coordinate.
One issue, one canonical conversation
The whole shape of this adapter is the canonical key: every comment on an issue resolves the same conversation binding, and therefore drives the same Session, for the issue’s whole life.conversationBindings.resolve creates the binding
or returns the existing one, so your adapter never remembers which threads
it has seen. Ingest with the comment’s id as external_event_id, and a
Linear redelivery converges server-side on replayed — the same disposition,
the same receipt — instead of a second turn.
Comments drive; edits and deletions observe
Three rules keep the thread honest:- A comment by a person drives. Resolve the author to a Principal with
a namespaced stable id — never a display name or email — then ingest
invokewith the comment body. A bot or integration author never drives, including your own posted replies arriving back as webhooks. - An edit or deletion observes. Ingest
message_edited/message_deletedwith the comment’s namespaced id and the actor’s Principal when the actor is a person; an authorless edit or deletion settles as an authorless observation — no fabricated person, no borrowed acting Principal. - Issue state is routing, not conversation. An issue’s status, assignee, and team decide coverage and which reviewed resources the work targets, through your frozen mapping. The public observation kinds carry no “issue status changed” meaning, so inventing a drive for one fights the one-Session contract.
The receipt is an exclusive cursor
A driven ingest returns the authored event’s exactevent_id and
event_sequence. That pair is an exclusive observation cursor: read
the Session log strictly after event_sequence, and you can never replay
an earlier turn’s reply or stop at a historical settlement. Never
substitute the Session’s latest sequence, and never treat a missing
sequence as zero — a driven receipt without one is a failure, not a
default.
seq. A withheld message has no
postable content. Listing covers events already persisted; a production
adapter also needs durable observation of replies arriving after this read.
That asynchronous projection remains unqualified in the staged example.
A replayed, observed, or filtered receipt does not start a new drive.
Do not start another posting pass for that delivery. A replay can still
refer to an earlier drive whose reply needs the durable projection described above.
Record provider reply outcomes
Before you call Linear, claim the presentation; after you settle, record the fact. The claim is content-free and has exactly one winner per key, so two replicas of your adapter racing the same reply produce one comment.errors, commentCreate.success, and the
returned comment ID before reporting a posted reply. HTTP 200 alone does not
prove success. Linear documents rate limiting as HTTP 400 with the
RATELIMITED error code; only an explicit rate-limit refusal is retried here.
Server failures, transport loss, malformed replies, and partial GraphQL results
settle indeterminate because the comment may already exist. The staged
src/linear-client.mjs implements this classification. See
Linear error handling and
rate limits.
Repeating settle with the same dispatch_token replays the committed fact
without re-entering the provider. A Linear redelivery replays the stored ingest
receipt, so it cannot recover an indeterminate post by driving a second turn.
Posting back needs a governed Linear credential. Your own client under a
reviewed connection is the supported path; a hosted supplier route for
Linear authorization is rollout-gated — check capability
status before making it part of your contract.
Prove it before you point Linear at it
The reference adapter ships with deterministic suites that need no credentials, running against doubles of the public SDK surface and the Linear client: signature verification and replay windows, occurrence dedupe, closed decoding, mapping refusal, bounded retry with the indeterminate rule, and conversation continuity — one issue = one Session, exclusive-cursor projection, one-winner posting, redelivery convergence.Next
Build a custom channel adapter
The five public calls this recipe rides, for any provider surface.
Trigger a fix PR from Sentry
The fresh-Session half of the loop, when an event really is one-shot.