Pactbound

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/v1

The 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_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Keys 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 401 with API 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/json on writes.
  • Successful single-resource responses wrap the object in a data field. Lists return data plus a pagination object.
  • 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 error message.

The pagination object looks like:

"pagination": {
  "page": 1,
  "limit": 20,
  "total": 137,
  "totalPages": 7,
  "hasNextPage": true,
  "hasPrevPage": false
}

List handoffs

GET/v1/handoffs

Returns the organization’s handoffs, newest first by default. Supports pagination, filtering, and sorting via query parameters.

Query parameters

ParameterTypeDescription
pageintegerPage number. Default 1.
limitintegerItems per page. Default 20, maximum 100.
statusstringFilter by status, e.g. DRAFT, SENT, COMPLETED.
templateIdstringFilter by the template used.
searchstringCase-insensitive match on title and description.
sortBystringOne of createdAt, updatedAt, title, status. Default createdAt.
sortOrderstringasc 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

POST/v1/handoffs

Creates 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

ParameterTypeDescription
titlerequiredstringNon-empty handoff title.
descriptionstringOptional longer description.
templateIdstringOptional template to base the handoff on.
recipientsarrayOptional list of { email, name?, role? }. role is one of PRIMARY, APPROVER, WITNESS, CC (default PRIMARY).
senderIdstringOptional 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

GET/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

PUT/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

ParameterTypeDescription
titlestringNew title.
descriptionstringNew description.
templateIdstringChange the template.
expiresAtstring | nullISO-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

DELETE/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

GET/v1/audit-log

Returns 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

ParameterTypeDescription
handoffIdstringFilter to one handoff.
actionstringFilter by action, e.g. handoff.created, handoff.acknowledged.
fromstringISO-8601 start date (inclusive).
tostringISO-8601 end date (inclusive).
pageintegerPage number. Default 1.
limitintegerItems per page. Default 50, maximum 200.
formatstringjson (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

ParameterTypeDescription
handoff.senteventA handoff was sent to its recipients.
handoff.acknowledgedeventA recipient acknowledged a handoff.
handoff.completedeventAll required acknowledgments are in; the handoff is complete.
credential.viewedeventA 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:

ParameterTypeDescription
X-Pactbound-Signatureheadersha256=<hmac>. See verification below.
X-Pactbound-TimestampheaderUnix seconds the event was signed.
X-Pactbound-EventheaderThe 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.

ParameterTypeDescription
400Bad RequestMalformed JSON, a missing required field, or an invalid parameter.
401UnauthorizedMissing, malformed, expired, or revoked key, or a plan without API access.
404Not FoundNo such resource in your organization.
409ConflictThe resource is not in a state that allows the operation (e.g. editing a sent handoff).
429Too Many RequestsRate limit exceeded. Honor the Retry-After header.
500Server ErrorSomething went wrong on our end. Safe to retry idempotent reads.