Sagenta

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.

StateMeaning
receivedThe owner has tapped and typed. Screenshot and source hint are being attached.
publishedAvailable on the API, delivered by webhook, filed as an issue if GitHub is on. Your turn.
ready_for_reviewYou posted a preview. The reviewer (or a self-serve owner) is looking at it.
approvedApproved against that preview: API, webhook, and a GitHub review plus commit status if integrated. “Not quite” sends it back to published with a note.
liveYou 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.

Example · a site’s endpoints
A site’s endpoints in Sagenta: a webhook address and a Slack channel, both active, and the form that adds another. A site’s endpoints in Sagenta: a webhook address and a Slack channel, both active, and the form that adds another.
EventWhen
request.createdA request was published. Full annotation document in `data`.
request.updatedClassification, routing or text changed.
message.createdA thread message: a clarifying answer from the owner, or a reviewer sending it back.
preview.attachedA preview URL was posted, by you or by an integration.
request.ready_for_reviewThe preview is in front of a human.
approval.created`approve` or `reject`, bound to the preview’s `deployId` / `commitSha`.
request.liveClosed. 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: write
  • pull_requests: write — reviews and comments, not merges
  • statuses: write
  • checks: 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.

The open pieces on GitHub