API Documentation
The Sendabrief REST API lets you manage SOPs programmatically, create, read, update, and delete SOPs from any language or tool. It's available on the Sendabrief Pro plan.
You need an API key to use this API. Generate one in your settings →
Authentication
Every request must include an Authorization header with a Bearer token:
Authorization: Bearer sb_your_api_key_here
API keys are scoped to a workspace and carry Sendabrief Pro permissions. Keys are shown only once when created, store them securely (e.g. an environment variable). You can revoke a key at any time from Settings → API.
Base URL
https://sendabrief.com/api/v1
All endpoints are relative to this base URL.
Rate limiting
Each API key is limited to 100 requests per 60-second window (sliding). Every response includes the following headers:
X-RateLimit-LimitMaximum requests allowed per window (100)X-RateLimit-RemainingRequests remaining in the current windowX-RateLimit-ResetUnix timestamp (ms) when the window resetsWhen the limit is exceeded, the API returns 429 Too Many Requests with error code RATE_LIMITED.
CORS
All API endpoints support cross-origin requests from any origin. The API returns the following CORS headers:
Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET, POST, PATCH, DELETE, OPTIONS Access-Control-Allow-Headers: Authorization, Content-Type
Preflight OPTIONS requests are handled automatically on all routes.
Step object
Each entry in a SOP's steps array accepts the following fields. Only text is required, everything else is optional and passed through as-is when present, this is the same step shape the dashboard editor writes.
idstringStable identity for the step. Generated for you if omitted - required if a decision step's branch needs to target this step.textstringRequired. Plain-text instruction for this step.typestring"step" (default), "decision", or "end".richTextstringOptional rich-text HTML rendered instead of text when present.branchesarrayOnly meaningful when type is "decision": [{ id, label, targetStepId }]. targetStepId: null ends the procedure on that answer.slastringFree-text timing expectation, e.g. "Within 24 hours".screenshotUrlstringAnnotated screenshot URL for this step.annotationBoxobject{ x, y, width, height } in fractional (0-1) coordinates, paired with screenshotUrl.narrationTranscriptstringVoice narration captured for this step during a screen recording.sourceDocRefobject{ fileName, page?, section? } - which uploaded document this step came from.extractedMediaUrlstringImage/diagram carried over from a source document.order is always recomputed from the step's position in the array, it isn't settable directly.
Allowed input_type values
transcript, audio, chat, mixed, manual, document, screen_recording, text, meeting
Source document fields
When an uploaded document is already a written SOP, it is migrated rather than rewritten, and the sections a controlled procedure carries are preserved on these four fields. A SOP created any other way returns null for all of them (and an empty array for revision_history), never absent keys.
scopestringThe source document's Scope section, in its own words, including any stated exclusions. Null for a SOP that was generated rather than migrated.reference_docsstringThe source's References or Related Documents section. Null when it had none.source_sectionsarraySupporting sections from the source document that govern the whole procedure rather than one step, as {title, body}, in source order. Empty for generated SOPs.revision_historyarrayThe source's revision table, transcribed: [{ version?, date?, author?, summary? }]. Dates are kept exactly as the document wrote them and are never normalised. Empty array when the document had no such table.document_controlobject{ documentNumber?, version?, effectiveDate?, approver?, owner?, reviewCadence? } from the document's control header. Null when absent.These are read-only. They record what the uploaded file contained, so POST and PATCH ignore them. The document's own Purpose section is carried into description rather than a field of its own.
Webhooks
Subscribe to SOP lifecycle events using the endpoints below, or from Settings → Webhooks. Each event fires regardless of whether the change came from the dashboard or the API.
Event catalog
sop.createdSOP createdsop.updatedSOP updatedsop.deletedSOP deletedsop.viewedSOP viewedsop.acknowledgedSOP acknowledged (Mark as read)Payload
{
"id": "uuid",
"event": "sop.created",
"created_at": "2024-01-01T00:00:00Z",
"data": { /* the resource, same shape as the REST response */ }
}Verifying the signature
Every delivery includes an X-Sendabrief-Signature header:sha256=<hex digest>, an HMAC-SHA256 of the raw request body using the webhook's signing secret (returned once, at creation time). Recompute the HMAC over the exact bytes you received and compare, don't re-serialize the parsed JSON first.
Failed deliveries are retried once. Delivery history (status code, success/failure) is visible from Settings → Webhooks.
Errors
All errors follow a consistent JSON shape:
{
"error": "Human-readable message",
"code": "MACHINE_READABLE_CODE"
}Use the code field for programmatic error handling:
MISSING_AUTH401No Authorization header was sent.INVALID_KEY_FORMAT401Key does not start with sb_ or is the wrong length.INVALID_API_KEY401Key not found in database or has been revoked.KEY_REVOKED401Key exists but was explicitly revoked.PLAN_REQUIRED403The workspace is not on the Sendabrief Pro plan.RATE_LIMITED429More than 100 requests in the last 60 seconds.NOT_FOUND404The requested resource does not exist in this workspace.VALIDATION_ERROR400The request body failed validation.REVIEW_REQUIRED409The SOP has require_review set and can't be updated directly.INTERNAL_ERROR500Unexpected server error./sopsList SOPs
Returns a paginated list of all SOPs in your workspace, ordered by last-updated descending.
Query parameters
pageintegerNoPage number (default: 1)limitintegerNoResults per page, 1–100 (default: 20)Response
{
"data": [
{
"id": "uuid",
"title": "Onboard a new client",
"trigger": "When a contract is signed",
"steps": [
{ "id": "step-uuid", "order": 1, "type": "step", "text": "Send welcome email" },
{ "id": "step-uuid-2", "order": 2, "type": "end", "text": "Client onboarded" }
],
"roles": ["Account Manager"],
"description": null,
"definitions": [],
"input_type": "transcript",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z",
"verified_at": "2024-01-01T00:00:00Z",
"share_token": "shr_abc123",
"content_version": 1,
"unattached_media": [],
"folder_id": null,
"review_cadence": "none",
"assigned_verifier_id": null,
"review_due_at": null,
"require_review": false,
"scope": null,
"reference_docs": null,
"source_sections": [],
"revision_history": [],
"document_control": null,
"tags": [{ "id": "uuid", "name": "Onboarding" }]
}
],
"meta": {
"page": 1,
"limit": 20,
"total": 42,
"totalPages": 3
}
}Error codes
MISSING_AUTHNo Authorization header providedINVALID_API_KEYKey not found or revokedPLAN_REQUIREDWorkspace is not on the Sendabrief Pro planRATE_LIMITED100 req/min exceededExample
curl -X GET "https://sendabrief.com/api/v1/sops" \ -H "Authorization: Bearer sb_your_key_here"
/sopsCreate a SOP
Creates a new SOP from structured JSON. Does not use AI generation, steps are stored exactly as provided. Goes through the same create path as the dashboard, so it's versioned and counted the same way.
Request body
{
"title": "Onboard a new client", // required
"steps": [ // required, non-empty - see "Step object" below
{ "text": "Send welcome email" },
{ "text": "Schedule kickoff call" }
],
"trigger": "When a contract is signed", // optional
"roles": ["Account Manager"], // optional
"description": "Free-text description", // optional
"definitions": [], // optional, glossary entries: [{ "term", "definition" }]
"input_type": "transcript" // optional, default "transcript" - see allowed values below
}Response
{
"data": {
"id": "uuid",
"title": "Onboard a new client",
"trigger": "When a contract is signed",
"steps": [
{ "id": "step-uuid", "order": 1, "type": "step", "text": "Send welcome email" },
{ "id": "step-uuid-2", "order": 2, "type": "end", "text": "Client onboarded" }
],
"roles": ["Account Manager"],
"description": null,
"definitions": [],
"input_type": "transcript",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z",
"verified_at": "2024-01-01T00:00:00Z",
"share_token": "shr_abc123",
"content_version": 1,
"unattached_media": [],
"folder_id": null,
"review_cadence": "none",
"assigned_verifier_id": null,
"review_due_at": null,
"require_review": false,
"scope": null,
"reference_docs": null,
"source_sections": [],
"revision_history": [],
"document_control": null,
"tags": [{ "id": "uuid", "name": "Onboarding" }]
}
}Error codes
VALIDATION_ERRORtitle missing, steps empty/invalid, or input_type not recognizedINVALID_API_KEYKey not found or revokedPLAN_REQUIREDSendabrief Pro plan requiredRATE_LIMITED100 req/min exceededExample
curl -X POST "https://sendabrief.com/api/v1/sops" \
-H "Authorization: Bearer sb_your_key_here" \
-H "Content-Type: application/json" \
-d '{"title":"My SOP","steps":[{"text":"First step"}]}'/sops/:idGet a SOP
Fetches a single SOP by its UUID. The SOP must belong to the authenticated workspace.
Response
{
"data": {
"id": "uuid",
"title": "Onboard a new client",
"trigger": "When a contract is signed",
"steps": [
{ "id": "step-uuid", "order": 1, "type": "step", "text": "Send welcome email" },
{ "id": "step-uuid-2", "order": 2, "type": "end", "text": "Client onboarded" }
],
"roles": ["Account Manager"],
"description": null,
"definitions": [],
"input_type": "transcript",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z",
"verified_at": "2024-01-01T00:00:00Z",
"share_token": "shr_abc123",
"content_version": 1,
"unattached_media": [],
"folder_id": null,
"review_cadence": "none",
"assigned_verifier_id": null,
"review_due_at": null,
"require_review": false,
"scope": null,
"reference_docs": null,
"source_sections": [],
"revision_history": [],
"document_control": null,
"tags": [{ "id": "uuid", "name": "Onboarding" }]
}
}Error codes
INVALID_API_KEYKey not found or revokedNOT_FOUNDSOP does not exist or belongs to a different workspaceExample
curl -X GET "https://sendabrief.com/api/v1/sops/<id>" \ -H "Authorization: Bearer sb_your_key_here"
/sops/:idUpdate a SOP
Partially updates a SOP. Only the fields you include are changed, omit any field to leave it unchanged. Snapshots the current content into version history before applying, same as an edit made from the dashboard.
Request body
{
"title": "Updated title", // optional
"trigger": "New trigger text", // optional, pass null to clear
"steps": [ // optional, replaces all steps - see "Step object" below
{ "text": "Updated step one" }
],
"roles": ["Manager"], // optional, replaces roles array
"description": "Updated description", // optional, pass null to clear
"definitions": [], // optional, replaces definitions array
"input_type": "meeting" // optional - see allowed values below
}Response
{
"data": {
"id": "uuid",
"title": "Onboard a new client",
"trigger": "When a contract is signed",
"steps": [
{ "id": "step-uuid", "order": 1, "type": "step", "text": "Send welcome email" },
{ "id": "step-uuid-2", "order": 2, "type": "end", "text": "Client onboarded" }
],
"roles": ["Account Manager"],
"description": null,
"definitions": [],
"input_type": "transcript",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z",
"verified_at": "2024-01-01T00:00:00Z",
"share_token": "shr_abc123",
"content_version": 1,
"unattached_media": [],
"folder_id": null,
"review_cadence": "none",
"assigned_verifier_id": null,
"review_due_at": null,
"require_review": false,
"scope": null,
"reference_docs": null,
"source_sections": [],
"revision_history": [],
"document_control": null,
"tags": [{ "id": "uuid", "name": "Onboarding" }]
}
}Error codes
VALIDATION_ERRORRequest body empty, or a provided field is invalidINVALID_API_KEYKey not found or revokedNOT_FOUNDSOP does not exist in this workspaceREVIEW_REQUIREDSOP has require_review set - update it from the dashboard insteadExample
curl -X PATCH "https://sendabrief.com/api/v1/sops/<id>" \
-H "Authorization: Bearer sb_your_key_here" \
-H "Content-Type: application/json" \
-d '{"title":"Updated title"}'/sops/:idDelete a SOP
Permanently deletes a SOP. This action cannot be undone.
Response
{
"deleted": true,
"id": "uuid"
}Error codes
INVALID_API_KEYKey not found or revokedNOT_FOUNDSOP does not exist in this workspaceExample
curl -X DELETE "https://sendabrief.com/api/v1/sops/<id>" \ -H "Authorization: Bearer sb_your_key_here"
/webhooksList webhooks
Returns every webhook endpoint registered in your workspace. The signing secret is never included here, only at creation time.
Response
{
"data": [
{
"id": "uuid",
"name": "Zapier",
"url": "https://hooks.zapier.com/...",
"events": ["sop.created", "sop.updated"],
"enabled": true,
"created_at": "2024-01-01T00:00:00Z",
"last_triggered_at": "2024-01-02T00:00:00Z"
}
]
}Error codes
INVALID_API_KEYKey not found or revokedPLAN_REQUIREDWorkspace is not on the Sendabrief Pro planExample
curl -X GET "https://sendabrief.com/api/v1/webhooks" \ -H "Authorization: Bearer sb_your_key_here"
/webhooksRegister a webhook
Registers a new webhook endpoint. This is what lets an automation platform (Zapier, Make, your own service) subscribe itself instead of a human clicking through the dashboard - see the Webhooks section below for the event catalog and signature verification.
Request body
{
"url": "https://your-endpoint.example.com/hooks", // required (Zapier's "targetUrl" also accepted)
"events": ["sop.created", "sop.updated"], // required, at least one - see event catalog below
"name": "My integration" // optional, defaults to "API-registered webhook"
}Response
{
"data": {
"id": "uuid",
"name": "My integration",
"url": "https://your-endpoint.example.com/hooks",
"events": ["sop.created"],
"enabled": true,
"created_at": "2024-01-01T00:00:00Z",
"last_triggered_at": null,
"secret": "whsec_..."
}
}Error codes
VALIDATION_ERRORurl missing, events empty/unrecognized, or the URL fails validation (e.g. points at a private/internal address)INVALID_API_KEYKey not found or revokedPLAN_REQUIREDSendabrief Pro plan requiredExample
curl -X POST "https://sendabrief.com/api/v1/webhooks" \
-H "Authorization: Bearer sb_your_key_here" \
-H "Content-Type: application/json" \
-d '{"url":"https://your-endpoint.example.com/hooks","events":["sop.created"]}'/webhooks/:idGet a webhook
Fetches a single webhook by its UUID. Never includes the signing secret.
Response
{
"data": {
"id": "uuid",
"name": "Zapier",
"url": "https://hooks.zapier.com/...",
"events": ["sop.created"],
"enabled": true,
"created_at": "2024-01-01T00:00:00Z",
"last_triggered_at": null
}
}Error codes
INVALID_API_KEYKey not found or revokedNOT_FOUNDWebhook does not exist in this workspaceExample
curl -X GET "https://sendabrief.com/api/v1/webhooks/<id>" \ -H "Authorization: Bearer sb_your_key_here"
/webhooks/:idUpdate a webhook
Partially updates a webhook's name, URL, subscribed events, or enabled state.
Request body
{
"name": "New name", // optional
"url": "https://new-endpoint.example", // optional, re-validated
"events": ["sop.deleted"], // optional, replaces the events array
"enabled": false // optional, pause without deleting
}Response
{
"data": {
"id": "uuid",
"name": "New name",
"url": "https://new-endpoint.example",
"events": ["sop.deleted"],
"enabled": false,
"created_at": "2024-01-01T00:00:00Z",
"last_triggered_at": null
}
}Error codes
VALIDATION_ERRORRequest body empty, or a provided field is invalidINVALID_API_KEYKey not found or revokedNOT_FOUNDWebhook does not exist in this workspaceExample
curl -X PATCH "https://sendabrief.com/api/v1/webhooks/<id>" \
-H "Authorization: Bearer sb_your_key_here" \
-H "Content-Type: application/json" \
-d '{"enabled":false}'/webhooks/:idDelete a webhook
Unsubscribes and permanently deletes a webhook. This is the "unsubscribe" half of the Zapier REST Hook contract.
Response
{
"deleted": true,
"id": "uuid"
}Error codes
INVALID_API_KEYKey not found or revokedNOT_FOUNDWebhook does not exist in this workspaceExample
curl -X DELETE "https://sendabrief.com/api/v1/webhooks/<id>" \ -H "Authorization: Bearer sb_your_key_here"