API Reference
The Pactbound REST API lets you create and read handoffs, pull a tamper-evident audit log, and receive signed event webhooks. This is the complete v1 reference.
Overview
The API is organized around predictable, resource-oriented URLs and returns JSON for every response. All requests are made over HTTPS to:
https://pactbound.com/api/v1The API is a paid feature. It is available on the Pro plan and above. Create and manage API keys in Settings → API Keys. The same plans unlock outbound webhooks.
Authentication
Authenticate every request with a secret API key in the Authorization header as a bearer token. Keys are prefixed with pact_.
Authorization: Bearer pact_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxKeys are shown in full exactly once, at creation. Pactbound stores only a SHA-256 hash of the key, so a lost key cannot be recovered; revoke it and create a new one. Keep keys server-side and out of source control, mobile apps, and browsers.
- A missing or malformed key returns
401 Unauthorized. - If the organization is downgraded below Pro, existing keys stop working and return
401withAPI access requires a Pro plan or higher. - Keys can be given an optional expiry at creation.
Rate limits
Requests are rate limited per API key to 100 requests per minute. Over the limit, the API returns 429 Too Many Requests with a Retry-After header (seconds) and a retryAfter field in the body. Back off and retry after the indicated delay.
Conventions
- All request and response bodies are JSON; send
Content-Type: application/jsonon writes. - Successful single-resource responses wrap the object in a
datafield. Lists returndataplus apaginationobject. - Timestamps are ISO-8601 strings in UTC (for example
2026-06-28T14:03:00.000Z). - Identifiers are opaque strings. Do not parse or construct them.
- Errors return a non-2xx status and a JSON body with an
errormessage.
The pagination object looks like:
"pagination": {
"page": 1,
"limit": 20,
"total": 137,
"totalPages": 7,
"hasNextPage": true,
"hasPrevPage": false
}List handoffs
/v1/handoffsReturns the organization’s handoffs, newest first by default. Supports pagination, filtering, and sorting via query parameters.
Query parameters
| Parameter | Type | Description |
|---|---|---|
page | integer | Page number. Default 1. |
limit | integer | Items per page. Default 20, maximum 100. |
status | string | Filter by status, e.g. DRAFT, SENT, COMPLETED. |
templateId | string | Filter by the template used. |
search | string | Case-insensitive match on title and description. |
sortBy | string | One of createdAt, updatedAt, title, status. Default createdAt. |
sortOrder | string | asc or desc. Default desc. |
Example
curl "https://pactbound.com/api/v1/handoffs?status=SENT&limit=10" \
-H "Authorization: Bearer $PACTBOUND_API_KEY"Each item includes its recipients (with ackedAt), template, sender, and counts of items, acknowledgments, and credentials.
Create a handoff
/v1/handoffsCreates a handoff in DRAFT status. Sending the handoff and adding credential secrets are done from the dashboard, where recipient identity verification and the sealed delivery flow run.
Body
| Parameter | Type | Description |
|---|---|---|
titlerequired | string | Non-empty handoff title. |
description | string | Optional longer description. |
templateId | string | Optional template to base the handoff on. |
recipients | array | Optional list of { email, name?, role? }. role is one of PRIMARY, APPROVER, WITNESS, CC (default PRIMARY). |
senderId | string | Optional org member to attribute as sender. Defaults to the organization owner. |
Example
curl -X POST "https://pactbound.com/api/v1/handoffs" \
-H "Authorization: Bearer $PACTBOUND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Q3 client offboarding",
"description": "Credentials and disclosures for Acme Co.",
"recipients": [
{ "email": "owner@acme.com", "name": "Dana Lee", "role": "PRIMARY" },
{ "email": "legal@acme.com", "role": "WITNESS" }
]
}'Returns 201 Created with the new handoff in the data field.
Retrieve a handoff
/v1/handoffs/{id}Returns a single handoff with its full graph: items, recipients, disclosures, acknowledgments, audit log, Hedera events, sender, and template.
Credentials are returned as metadata only. Fields such as label, category, view count, and expiry are included, but the secret values are never exposed through the API; they are revealed only to a verified recipient in the sealed reveal flow.
curl "https://pactbound.com/api/v1/handoffs/HANDOFF_ID" \
-H "Authorization: Bearer $PACTBOUND_API_KEY"Returns 404 if no handoff with that id belongs to your organization.
Update a handoff
/v1/handoffs/{id}Updates a handoff. Only handoffs in DRAFT status can be updated; once sent, a handoff is immutable and this returns 409 Conflict.
Body
| Parameter | Type | Description |
|---|---|---|
title | string | New title. |
description | string | New description. |
templateId | string | Change the template. |
expiresAt | string | null | ISO-8601 expiry, or null to clear it. |
curl -X PUT "https://pactbound.com/api/v1/handoffs/HANDOFF_ID" \
-H "Authorization: Bearer $PACTBOUND_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "title": "Q3 client offboarding (revised)" }'Delete a handoff
/v1/handoffs/{id}Deletes a handoff. Only DRAFT handoffs can be deleted; a sent handoff is part of the permanent record and returns 409 Conflict. Returns { "deleted": true } on success.
curl -X DELETE "https://pactbound.com/api/v1/handoffs/HANDOFF_ID" \
-H "Authorization: Bearer $PACTBOUND_API_KEY"Audit log
/v1/audit-logReturns audit entries across your organization’s handoffs, newest first. Supports JSON (default) and CEF (Common Event Format) for direct ingestion into a SIEM.
Query parameters
| Parameter | Type | Description |
|---|---|---|
handoffId | string | Filter to one handoff. |
action | string | Filter by action, e.g. handoff.created, handoff.acknowledged. |
from | string | ISO-8601 start date (inclusive). |
to | string | ISO-8601 end date (inclusive). |
page | integer | Page number. Default 1. |
limit | integer | Items per page. Default 50, maximum 200. |
format | string | json (default) or cef. CEF returns text/plain, one event per line. |
Example (CEF for a SIEM)
curl "https://pactbound.com/api/v1/audit-log?format=cef&from=2026-06-01" \
-H "Authorization: Bearer $PACTBOUND_API_KEY"Webhooks
Webhooks push events to your server as they happen. Add an endpoint in Settings → Webhooks (Pro and above), choose the events to subscribe to, and store the signing secret shown once at creation.
Events
| Parameter | Type | Description |
|---|---|---|
handoff.sent | event | A handoff was sent to its recipients. |
handoff.acknowledged | event | A recipient acknowledged a handoff. |
handoff.completed | event | All required acknowledgments are in; the handoff is complete. |
credential.viewed | event | A recipient revealed a credential. |
Delivery and payload
Each event is delivered as an HTTP POST with a JSON body:
{
"event": "handoff.acknowledged",
"data": { /* event-specific fields */ },
"timestamp": "1782640980",
"webhookId": "wh_xxx"
}Every request carries these headers:
| Parameter | Type | Description |
|---|---|---|
X-Pactbound-Signature | header | sha256=<hmac>. See verification below. |
X-Pactbound-Timestamp | header | Unix seconds the event was signed. |
X-Pactbound-Event | header | The event type, mirroring the body. |
Verifying the signature
The signature is an HMAC-SHA256 of <timestamp>.<rawBody> keyed with your webhook signing secret. Compute it over the exact bytes you received, before any JSON re-serialization, and compare in constant time:
import crypto from "node:crypto"
function isValid(req, signingSecret) {
const signature = req.headers["x-pactbound-signature"] // "sha256=..."
const timestamp = req.headers["x-pactbound-timestamp"]
const rawBody = req.rawBody // exact received bytes
const expected =
"sha256=" +
crypto
.createHmac("sha256", signingSecret)
.update(`${timestamp}.${rawBody}`)
.digest("hex")
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected),
)
}Retries
Respond with a 2xx within 10 seconds. Failed deliveries (a 5xx, 408, 429, or a timeout) are retried up to three times with a backoff of 5s, 30s, then 300s. Other 4xx responses are treated as permanent and are not retried. Make your handler idempotent using webhookId.
Errors
The API uses conventional HTTP status codes. The body always includes an error message.
| Parameter | Type | Description |
|---|---|---|
400 | Bad Request | Malformed JSON, a missing required field, or an invalid parameter. |
401 | Unauthorized | Missing, malformed, expired, or revoked key, or a plan without API access. |
404 | Not Found | No such resource in your organization. |
409 | Conflict | The resource is not in a state that allows the operation (e.g. editing a sent handoff). |
429 | Too Many Requests | Rate limit exceeded. Honor the Retry-After header. |
500 | Server Error | Something went wrong on our end. Safe to retry idempotent reads. |