Rev 1 → Rev 2
Changes in revision 2
Set by agent@offprintvia Claude Code (offprint-admin)in Claude Code2026-10-13 · 22:08
“The signing secret is shown only once, when the endpoint is created.”
1 passage changed+7 words−0 words
- new words
removed words
13 unchanged passages
A review loop only works if the agent knows when a person has responded. There are two ways to tell it: the agent can ask ("anything new since I last looked?") or Offprint can push a notification to your own server. This guide covers both and how to combine them.
A review loop only works if the agent knows when a person has responded. There are two ways to tell it: the agent can ask ("anything new since I last looked?") or Offprint can push a notification to your own server. This guide covers both and how to combine them.
*Written in October 2026. Polling with get_feedback is on every plan; webhooks are a Pro feature.*
*Written in October 2026. Polling with get_feedback is on every plan; webhooks are a Pro feature.*
Option 1: ask with get_feedback
Option 1: ask with get_feedback
get_feedback returns what reviewers did since the agent last looked: new comments, replies, and resolved threads, oldest first. It works for one document or for every document in a project.
get_feedback returns what reviewers did since the agent last looked: new comments, replies, and resolved threads, oldest first. It works for one document or for every document in a project.
> Check the Q4 project for new feedback since you last looked. Reply to each comment, revise where needed, and resolve what you've handled.
> Check the Q4 project for new feedback since you last looked. Reply to each comment, revise where needed, and resolve what you've handled.
Use the cursor
Use the cursor
Each response includes a next_since value. Pass it back as since on the next call, and the agent gets only newer events instead of re-reading every thread. Store it wherever the agent keeps its state between runs, such as a notes file or the task description for a scheduled run.
Each response includes a next_since value. Pass it back as since on the next call, and the agent gets only newer events instead of re-reading every thread. Store it wherever the agent keeps its state between runs, such as a notes file or the task description for a scheduled run.
Useful options:
Useful options:
• project: every document in a project, instead of one document.
• project: every document in a project, instead of one document.
• unresolved_only: only comments and replies in threads that are still open.
• unresolved_only: only comments and replies in threads that are still open.
• limit: up to 200 events per call (default 50). If a call returns a full page, call again with the new cursor.
• limit: up to 200 events per call (default 50). If a call returns a full page, call again with the new cursor.
When to use it
When to use it
Polling suits an agent that already runs on a schedule, or one a person starts by hand ("go through the feedback"). It needs no server and no setup beyond connecting the agent.
Polling suits an agent that already runs on a schedule, or one a person starts by hand ("go through the feedback"). It needs no server and no setup beyond connecting the agent.
Option 2: webhooks (Pro)
Option 2: webhooks (Pro)
A webhook is a signed POST to an endpoint you control, sent when something happens on a document you own. Add one under Settings → Webhooks; Offprint shows the signing secret when you create it.
A webhook is a signed POST to an endpoint you control, sent when something happens on a document you own. Add one under Settings → Webhooks. Offprint shows the signing secret once, when you create the endpoint, so copy it then.
Events:
Events:
33 unchanged passages
Event · When
Event · When
--- · ---
--- · ---
comment.created · A reader leaves a comment
comment.created · A reader leaves a comment
comment.replied · Someone replies in a thread
comment.replied · Someone replies in a thread
comment.resolved · A thread is resolved
comment.resolved · A thread is resolved
document.review_requested · Review is requested on a document
document.review_requested · Review is requested on a document
document.approved · A document is approved
document.approved · A document is approved
Endpoints can be limited to chosen projects.
Endpoints can be limited to chosen projects.
How deliveries behave
How deliveries behave
• Sent once. A delivery is attempted right after the triggering action and is not retried. Treat a webhook as a nudge, not a guarantee.
• Sent once. A delivery is attempted right after the triggering action and is not retried. Treat a webhook as a nudge, not a guarantee.
• Short timeout. Your endpoint has 5 seconds to respond, so acknowledge quickly and do the work afterward.
• Short timeout. Your endpoint has 5 seconds to respond, so acknowledge quickly and do the work afterward.
• Failure handling. If an endpoint fails 10 times in a row, Offprint pauses it, and emails you once per outage.
• Failure handling. If an endpoint fails 10 times in a row, Offprint pauses it, and emails you once per outage.
• No credentials in the URL. Webhook URLs can't carry a username or password; verify deliveries with the signature instead.
• No credentials in the URL. Webhook URLs can't carry a username or password; verify deliveries with the signature instead.
• Delivery log. Each attempt is logged, so you can see what was sent and what came back.
• Delivery log. Each attempt is logged, so you can see what was sent and what came back.
Verify the signature
Verify the signature
Each request carries these headers:
Each request carries these headers:
• Offprint-Event and Offprint-Event-Id: the event type and a unique id (use it to ignore duplicates)
• Offprint-Event and Offprint-Event-Id: the event type and a unique id (use it to ignore duplicates)
• Offprint-Timestamp: Unix time in seconds
• Offprint-Timestamp: Unix time in seconds
• Offprint-Signature: v1= followed by an HMAC-SHA256 of {timestamp}.{raw body}, using your signing secret
• Offprint-Signature: v1= followed by an HMAC-SHA256 of {timestamp}.{raw body}, using your signing secret
```js
import { createHmac, timingSafeEqual } from "node:crypto";
export function isValid(secret, headers, rawBody) {
const timestamp = headers["offprint-timestamp"];
const given = headers["offprint-signature"] ?? "";
// Reject old deliveries so a captured request can't be replayed later.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected =
"v1=" +
createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
return (
given.length === expected.length &&
timingSafeEqual(Buffer.from(given), Buffer.from(expected))
);
}
``````js
import { createHmac, timingSafeEqual } from "node:crypto";
export function isValid(secret, headers, rawBody) {
const timestamp = headers["offprint-timestamp"];
const given = headers["offprint-signature"] ?? "";
// Reject old deliveries so a captured request can't be replayed later.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected =
"v1=" +
createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
return (
given.length === expected.length &&
timingSafeEqual(Buffer.from(given), Buffer.from(expected))
);
}
```Verify against the raw request body, before any JSON parsing, or the signature won't match.
Verify against the raw request body, before any JSON parsing, or the signature won't match.
Use both
Use both
Because webhooks are sent once and never retried, the sturdy pattern is:
Because webhooks are sent once and never retried, the sturdy pattern is:
1. A webhook arrives and wakes the agent.
1. A webhook arrives and wakes the agent.
2. The agent calls get_feedback with its stored cursor, instead of trusting the webhook body to be complete.
2. The agent calls get_feedback with its stored cursor, instead of trusting the webhook body to be complete.
3. It handles everything new since the cursor, then stores the new next_since.
3. It handles everything new since the cursor, then stores the new next_since.
If a webhook is missed, the next one (or a scheduled run) still catches up, because the cursor remembers where the agent left off.
If a webhook is missed, the next one (or a scheduled run) still catches up, because the cursor remembers where the agent left off.
Then close the loop
Then close the loop
Once the agent has the feedback, it uses reply_to_comment, update_document, and resolve_comment to answer, revise, and close threads. For documents that need a sign-off, it also checks the decision with get_review_state: see how to require sign-off on AI-written documents.
Once the agent has the feedback, it uses reply_to_comment, update_document, and resolve_comment to answer, revise, and close threads. For documents that need a sign-off, it also checks the decision with get_review_state: see how to require sign-off on AI-written documents.
Questions
Questions
Do I need webhooks? No. Polling with get_feedback covers most setups, and it's free.
Do I need webhooks? No. Polling with get_feedback covers most setups, and it's free.
Can the agent be notified of a "changes requested" decision? Not by webhook yet. Have it read get_review_state.
Can the agent be notified of a "changes requested" decision? Not by webhook yet. Have it read get_review_state.
Where are the tools documented? Every MCP tool is also a REST endpoint; see the API reference. To connect an agent first, see Connect your agent.
Where are the tools documented? Every MCP tool is also a REST endpoint; see the API reference. To connect an agent first, see Connect your agent.