Overview
The external API is an organization-scoped private-beta surface. It currently exposes requests and workflows only. There is no public audit/activity endpoint or SDK package in this release.
The response path is asynchronous: create a request, then poll its request resource or provide a webhook URL and handle the signed request.resolved event.
Authentication
Send an Obsidian Guard API key as a bearer token. Keys begin with obs_, are created by an authorized workspace manager, are shown once, and are stored by the service as one-way hashes.
curl "https://app.obsidian-guard.com/api/v1/workflows" \
-H "Authorization: Bearer obs_example_replace_me"Environments
The published host above is the configured application host, but API access is not generally available. Use it only after the Obsidian Guard team provisions your private-beta organization and key.
Local development uses the same /api/v1 path on the developer's local application origin. There is no separate public sandbox host or test-data environment documented for customers today.
Versioning
The major version is part of the URL: /api/v1. Backward-incompatible changes require a new major path. Private-beta fields may be clarified or extended; integrations should ignore unknown response fields.
Idempotency
Request creation does not currently accept an idempotency header or enforce uniqueness on external_ref. A retry of POST /requests can create another request. Generate and persist your own operation identifier, inspect results after timeouts, and avoid automatic unsafe retries.
Webhook deliveries for the same request-resolution event are queued with a unique request/event key, but consumers must still process retries idempotently using event plus request_id.
Responses and errors
All responses use a JSON envelope with success, either data or error, and meta.timestamp plus a trace meta.request_id. The trace ID is not the request resource UUID.
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Request body failed validation. See the error details.",
"issues": [{ "field": "workflow_id", "message": "workflow_id must be a valid UUID" }]
},
"meta": { "timestamp": "2026-07-22T18:00:00.000Z", "request_id": "req_exampletrace" }
}| Code | HTTP | Meaning |
|---|---|---|
| UNAUTHORIZED | 401 | API key is missing or invalid. |
| FORBIDDEN | 403 | The API key does not have the required capability. |
| API_NOT_ENABLED | 403 | API access is not enabled for the organization plan. |
| NOT_FOUND | 404 | The v1 route is unknown. |
| WORKFLOW_NOT_FOUND | 404 | The workflow is missing or unavailable. |
| REQUEST_NOT_FOUND | 404 | The request is missing from the API key's organization. |
| VALIDATION_ERROR | 400 | The JSON body or field values are invalid. |
| WORKFLOW_INACTIVE | 422 | The workflow is not active. |
| WORKFLOW_NO_GUARDIANS | 422 | The workflow has no assigned reviewers. |
| REQUEST_NOT_PENDING | 422 | Only a pending request can be cancelled. |
| RATE_LIMITED | 429 | The API-key rate limit has been reached. |
| INTERNAL_ERROR | 500 | The server could not complete the operation. |
Error payloads currently include a documentation URL for the code. Treat the machine-readable code and HTTP status as authoritative during private beta.
Rate limits
The default is 100 API requests per minute per API key, configurable by the service environment. When blocked, the API returns HTTP 429 with Retry-After, X-RateLimit-Remaining, and X-RateLimit-Reset. Back off until the advertised reset instead of retrying in a tight loop.
Pagination
GET /requests uses offset pagination: limit defaults to 20 and is clamped to 1–100; offset defaults to 0. The response includes items, total, limit, and offset. Workflow listing is not paginated.
Endpoints
/api/v1/requestsCreate a request
workflow_id (UUID), title (1–500 characters), optional description (up to 5,000), context object, metadata, external_ref (up to 255), and webhook_url (valid URL). Returns 201 with the serialized request.
Requires submit-request capability.
/api/v1/requestsList requests
Optional status, workflow_id, external_ref, limit, and offset query parameters. Results are newest first and include total, limit, and offset.
Requires view-request capability.
/api/v1/requests/:idGet a request
Returns one organization-scoped request by UUID, including approvals and decision counts.
Requires view-request capability.
/api/v1/requests/:id/cancelCancel a pending request
Body: { reason: string } with 1–500 characters. Only pending requests can be cancelled.
Requires cancel-pending-request capability.
/api/v1/workflowsList active workflows
Returns all active, non-archived workflows for the organization associated with the API key, ordered by name.
Requires authenticated API access.
/api/v1/workflows/:idGet a workflow
Returns one non-archived organization-scoped workflow by UUID.
Requires authenticated API access.
Create-request example
curl -X POST "https://app.obsidian-guard.com/api/v1/requests" \
-H "Authorization: Bearer obs_example_replace_me" \
-H "Content-Type: application/json" \
-d '{
"workflow_id": "00000000-0000-4000-8000-000000000001",
"title": "Review protected operation",
"description": "Private-beta test request",
"context": { "order_reference": "example-order" },
"external_ref": "example-operation-001",
"webhook_url": "https://example.invalid/webhooks/obsidian-guard"
}'The example uses placeholder values and the reserved .invalid domain. Replace every value in an approved environment. Do not put secrets or unnecessary personal data in context.
Webhooks
When webhook_url is set on request creation, an approved or rejected resolution queues a request.resolved event. Cancelled and expired requests are not part of the current outbound event catalog.
{
"event": "request.resolved",
"request_id": "00000000-0000-4000-8000-000000000002",
"external_ref": "example-operation-001",
"status": "approved",
"resolved_at": "2026-07-22T18:05:00.000Z",
"decisions": [{
"guardian": "Example reviewer",
"decision": "approved",
"comment": null,
"decided_at": "2026-07-22T18:05:00.000Z"
}]
}Verify the signature
Read the raw request body before JSON parsing. Compute HMAC-SHA256 with the signing secret provisioned for the beta integration, encode it as lowercase hex, prefix it with sha256=, and compare it in constant time with X-Obsidian-Guard-Signature.
import { createHmac, timingSafeEqual } from "node:crypto";
const expected = "sha256=" + createHmac("sha256", webhookSigningSecret)
.update(rawBody)
.digest("hex");
const valid = timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));Delivery waits up to 10 seconds for an HTTP 2xx response. The durable queue currently allows up to three attempts with short backoff. Return 2xx only after your idempotent handler has accepted the event.
Security guidance
- Keep API and webhook secrets server-side and rotate or revoke them after suspected exposure.
- Validate the webhook signature over the unmodified raw body before acting.
- Allowlist expected workflow IDs and validate every returned status before executing downstream work.
- Use least-privilege application credentials for the action your system performs after approval.
- Do not treat an approval response as proof that a separate Shopify or financial action succeeded.
- Log the Obsidian Guard resource ID and trace request ID without logging bearer tokens or sensitive context.
Changelog
2026-07-22 · v1 private-beta documentation audit
Reference aligned with implemented request/workflow routes, bearer authentication, plan gating, offset pagination, environment-configurable rate limiting, create-request non-idempotency, and the request.resolved webhook.
For access or implementation support, contact the private-beta team.
