For developers
Two ends of a loop, and an API in between.
Sagenta publishes requests and collects approvals. Everything in the middle is yours. This page is the contract your pipeline builds against: a versioned REST API, signed webhooks, and an optional GitHub integration that never touches your code.
Status. API v1 is built to this contract and running ahead of launch; keys are issued when the application opens. The reference webhook consumer, verification snippets in three languages and a pipeline template are in the open packages repository, linked at the foot of this page.
Lifecycle
A request is a row that moves through five states. Sagenta moves it into published; you move it out.
| State | Meaning |
|---|---|
| received | The owner has tapped and typed. Screenshot and source hint are being attached. |
| published | Available on the API, delivered by webhook, filed as an issue if GitHub is on. Your turn. |
| ready_for_review | You posted a preview. The reviewer (or a self-serve owner) is looking at it. |
| approved | Approved against that preview: API, webhook, and a GitHub review plus commit status if integrated. “Not quite” sends it back to published with a note. |
| live | You told us it shipped, or the merge event told us. The owner is notified. |
REST API
Base URL https://app.sagenta.com/api/v1. Per-site or per-org keys, sent as a bearer token; sagenta_test_ keys only work against staging. Responses carry the annotation document verbatim under annotation, with Sagenta’s lifecycle fields beside it.
Reporting back is two calls. Everything else is reads.
GET /requests?status=published— what’s waiting for you.POST /requests/{id}/previews— attach a preview;status: "ready"moves it to review.POST /requests/{id}/status—{"status": "live"}closes the loop.GET /events?since=<cursor>— every webhook envelope, pollable, 30 days back.
/v1 only ever gains fields, endpoints and event types. Anything else is /v2.
curl -X POST https://app.sagenta.com/api/v1/requests/4f1c9a2e-…/previews \
-H "Authorization: Bearer sagenta_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://pr-142.harborview.pages.dev/",
"provider": "cloudflare-pages",
"commitSha": "b7e1c4d",
"status": "ready"
}'
# later, after you ship it
curl -X POST https://app.sagenta.com/api/v1/requests/4f1c9a2e-…/status \
-H "Authorization: Bearer sagenta_live_..." \
-H "Content-Type: application/json" \
-d '{ "status": "live" }'Webhooks and signatures
Register an endpoint per site and get a secret, shown once. Every delivery is a JSON envelope {id, type, createdAt, siteId, data} where data is the full resource, so you never need a follow-up call to act.
Deliveries are at-least-once with about 24 hours of retries. Deduplicate on Sagenta-Event-Id. Verify Sagenta-Signature over the raw body and reject timestamps older than five minutes. It is the Stripe scheme, so your language already has a verified example.

| Event | When |
|---|---|
| request.created | A request was published. Full annotation document in `data`. |
| request.updated | Classification, routing or text changed. |
| message.created | A thread message: a clarifying answer from the owner, or a reviewer sending it back. |
| preview.attached | A preview URL was posted, by you or by an integration. |
| request.ready_for_review | The preview is in front of a human. |
| approval.created | `approve` or `reject`, bound to the preview’s `deployId` / `commitSha`. |
| request.live | Closed. Owner notified. |
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map(kv => kv.split('=')));
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (age > 300) return false; // replay window
const expected = createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest('hex');
return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}Annotation schema
Every request is a valid Annotation Format Schema v1.1 document. Sagenta’s additions live under a sagenta key so anything that already speaks AFS can consume it unchanged. The extension is published as @sagenta/schema with types and JSON Schema.
sourceHint— file and line, when the Astro integration emitted it.anchors— a CSS selector and the quoted text with context, so the pin can be found again after a redeploy.classification— content, design, feature, bug or question.screenshotUrl,viewport,consoleErrors— what the owner saw.status,previews,approvals— the lifecycle, beside the document on the request resource.
{
"id": "6b0f2d2e-1a7a-4c0e-9d7d-4d6c5b3f2a11",
"comment": "We're open Saturdays now, 10 to 2.",
"elementPath": "tr:nth-of-type(2) > td:nth-of-type(2)",
"timestamp": "2026-09-02T14:03:11Z",
"x": 523, "y": 307,
"element": "td",
"url": "https://pr-142.harborview.pages.dev/contact",
"boundingBox": { "x": 523, "y": 307, "width": 88, "height": 29 },
"sagenta": {
"siteId": "02d913d9-…", "requestId": "e389…",
"authorRole": "owner",
"scope": "element",
"anchors": {
"css": "tr:nth-of-type(2) > td:nth-of-type(2)",
"textQuote": { "exact": "Closed", "prefix": "Saturday", "suffix": "Sunday" }
},
"sourceHint": { "file": "src/pages/contact.astro", "line": 12 },
"viewport": { "width": 390, "height": 780 },
"widgetVersion": "0.1.0"
}
}GitHub integration
Optional, per site. When it is on, Sagenta files each published request as an issue with the annotation document in a fenced block, the screenshot and the source hint; picks up previews from check runs, commit statuses and deployment events; and posts the approval back as a pull-request review plus a sagenta/approval commit status you can require in branch protection.
What it cannot do. The app does not request the contents permission, or workflows, actions, administration, secrets or members. It cannot commit, push or merge, by the shape of its token rather than by policy. Installation tokens are minted per repository with an explicit permission subset.
Permissions requested
issues: writepull_requests: write— reviews and comments, not mergesstatuses: writechecks: read,deployments: read,metadata: read
Astro integration
@sagenta/astro injects the capture widget on preview and staging builds and leaves a data-sagenta-src attribute on rendered elements, so a request arrives with the source file and line it came from. That hint is the most valuable thing in the payload: whoever makes the change can go straight to the file instead of guessing from “the blue button in the sidebar”.
The widget is plain TypeScript in a shadow root, under 30 KB, loaded from cdn.sagenta.com/widget/v1/widget.js with a public site key. It can create annotations for one site and nothing else.