API Reference

This page documents all Web Resource Ledger API endpoints (v0.6.0). See the Authentication guide for how to obtain and use API keys.

Base URLs

  • Production: https://api.webresourceledger.com
  • Staging: https://staging.webresourceledger.com

Authentication: Most endpoints require a Bearer token in the Authorization header. Tenant endpoints use per-tenant API keys; admin endpoints use the infrastructure ADMIN_KEY. See the Authentication guide for details.

Health

Service health

GET /health No auth

Health check

Responses

200 — Service is running.
Property Type Required Description
status "ok" Yes
legal object Yes
  terms string (uri) Yes
  policy string (uri) Yes
{
  "status": "ok",
  "legal": {
    "terms": "https://github.com/benpeter/web-resource-ledger/blob/main/TERMS.md",
    "policy": "https://github.com/benpeter/web-resource-ledger/blob/main/CONTENT-POLICY.md"
  }
}

Captures

Web page capture lifecycle

GET /v1/captures Tenant key

List captures

Returns a paginated list of captures for the authenticated tenant in descending chronological order (newest first by default). Supports filtering by status, URL prefix, and date range, with configurable sort order. Pagination is offset-based. Use `offset` and `limit` to navigate pages. The response includes a `total` count for the matched query.

Parameters

Name Location Required Type Description
limit query No integer Number of results per page. Default 20. Values above 100 are silently clamped to 100. Values below 1 or non-integers return 400.
offset query No integer Zero-based offset for pagination. Default 0 (first page).
status query No "pending" | "complete" | "failed" Filter results to captures with this status. Omit to return all statuses.
url query No string Filter by URL prefix match. Minimum 4 characters. Must not contain `%` or `_` wildcard characters. Returns captures whose URL starts with the given string.
created_after query No string (date-time) Filter to captures created at or after this ISO 8601 timestamp (inclusive). Use with `created_before` for date range queries.
created_before query No string (date-time) Filter to captures created before this ISO 8601 timestamp (exclusive). Must be after `created_after` if both are provided.
sort query No "created_at" | "-created_at" Sort order. `-created_at` (default) for newest first, `created_at` for oldest first.

Responses

200 — Paginated list of capture summaries. Returns empty data array when no captures exist.
Property Type Required Description
data array of object Yes Capture summaries for the current page. Empty array when no results; never null.
  id string Yes Unique capture identifier. Also serves as the access secret for per-capture access.
  status "pending" | "complete" | "failed" Yes Lifecycle state of the capture.
  url string (uri) Yes The URL that was captured.
  createdAt string (date-time) Yes ISO 8601 timestamp when the capture was submitted.
  completedAt string (date-time) No Present when status is "complete". ISO 8601 timestamp when capture completed.
  renderQuality "full" | "partial" No Present when status is "complete". Indicates whether the page fully rendered or was captured after a navigation timeout.
  failedAt string (date-time) No Present when status is "failed". ISO 8601 timestamp when capture failed.
  error string No Present when status is "failed". Human-readable failure reason.
  retryable boolean No Present when status is "failed". True if submitting a new capture may succeed.
pagination object Yes Offset-based pagination metadata.
  total integer Yes Total number of results matching the query (before pagination).
  offset integer Yes Zero-based offset of the first result in this page.
  hasMore boolean Yes True when additional results exist beyond the current page.
  limit integer Yes Effective page size used for this response.
{
  "data": [],
  "pagination": {
    "total": 0,
    "offset": 0,
    "hasMore": false,
    "limit": 20
  }
}
400 — Bad request — missing or malformed body, missing `url` field, invalid URL scheme.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 400,
  "title": "Bad Request",
  "detail": "Request body is missing or not valid JSON."
}
401 — Unauthorized — missing, malformed, or invalid Authorization header.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 401,
  "title": "Unauthorized",
  "detail": "Authorization header is required."
}
429 — Too Many Requests — rate limit or monthly quota exceeded. The response body includes a `limitType` extension field that distinguishes the specific 429 variant: - `limitType` absent: IP-based rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'tenant'`: Per-tenant rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'quota'`: Monthly capture quota exhausted. A `quota` extension object is present with `limit`, `used`, `resource`, `resetsAt`, and (for batch) `requested`. Retrying before `resetsAt` will continue to fail.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 429,
  "title": "Too Many Requests",
  "detail": "Rate limit exceeded. Try again later."
}
500 — Internal Server Error — database read failed.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 500,
  "title": "Internal Server Error",
  "detail": "Could not list captures"
}
503 — Service Unavailable -- required configuration is missing or service is at capacity.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 503,
  "title": "Service Unavailable",
  "detail": "Service is at capacity. Retry in 10 seconds."
}
POST /v1/captures Tenant key

Submit a URL for capture

Enqueues a headless-browser capture of the given URL. Returns immediately with a capture ID; poll the status URL to determine when the capture is complete. Monthly quota: each accepted capture counts against the tenant's monthly capture quota. When the quota is exhausted the endpoint returns 429 with `limitType: 'quota'` in the response body. Quota headers (`X-Quota-Limit`, `X-Quota-Used`, `X-Quota-Remaining`) are present on successful 202 responses. CORS: This endpoint supports cross-origin requests from allowed origins. Preflight (OPTIONS) is handled automatically. Configure allowed origins via the CORS_ORIGINS environment variable.

Request body required

Property Type Required Description
url string (uri) Yes Public http or https URL to capture. Must not resolve to a private IP.
{
  "url": "https://example.com"
}

Responses

202 — Capture accepted and queued.
Property Type Required Description
id string Yes Unique capture identifier. Also serves as the access secret for per-capture access.
statusUrl string (uri) Yes Absolute URL to poll for capture status.
note string Yes Advisory message about related API capabilities.
{
  "id": "cap_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
  "statusUrl": "https://api.webresourceledger.com/v1/captures/cap_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6/status",
  "note": "Use GET /v1/captures to list and search your captures."
}
400 — Bad request — missing or malformed body, missing `url` field, invalid URL scheme.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 400,
  "title": "Bad Request",
  "detail": "Request body is missing or not valid JSON."
}
401 — Unauthorized — missing, malformed, or invalid Authorization header.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 401,
  "title": "Unauthorized",
  "detail": "Authorization header is required."
}
415 — Unsupported Media Type — Content-Type is not application/json.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 415,
  "title": "Unsupported Media Type",
  "detail": "Content-Type must be application/json"
}
422 — Unprocessable Content — private IP, embedded credentials, or double-encoded URL.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 422,
  "title": "Unprocessable Content",
  "detail": "Host resolves to a private IP address."
}
429 — Too Many Requests — rate limit or monthly quota exceeded. See `limitType` in the response body to distinguish rate limiting (`limitType` absent or `'tenant'`) from quota exhaustion (`limitType: 'quota'`).
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 429,
  "title": "Too Many Requests",
  "detail": "Rate limit exceeded. Try again later."
}
503 — Service Unavailable -- service is at capacity.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 503,
  "title": "Service Unavailable",
  "detail": "Service is at capacity. Retry in 10 seconds."
}
OPTIONS /v1/captures No auth

CORS preflight for POST /v1/captures

Handles CORS preflight requests for POST /v1/captures. Returns 204 with appropriate CORS headers when the requesting origin matches the configured allowlist. CORS headers are absent when the origin is not allowed.

Responses

204 — Preflight accepted.
403 — Origin not allowed.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
POST /v1/captures/batch Tenant key

Submit multiple URLs for capture

Enqueues headless-browser captures for a list of URLs in a single request. Designed for bulk archival use cases: legal holds, compliance monitoring, CI pipelines that need to snapshot multiple pages simultaneously. Each URL is validated and rate-limited independently. A URL that fails validation or hits a per-item rate limit does not affect other items in the batch. Rate-limit tokens are consumed per URL, not per request. Monthly quota: each accepted URL counts against the tenant's monthly capture quota. When the quota is exhausted mid-batch, remaining items receive per-item 429 errors with `limitType: 'quota'`. If the quota is exhausted before processing begins (e.g., zero remaining), a top-level 429 is returned with `limitType: 'quota'` and a `quota.requested` field indicating how many captures were requested. Quota headers (`X-Quota-Limit`, `X-Quota-Used`, `X-Quota-Remaining`) are present on 207 responses. The response is always 207 Multi-Status when the batch structure itself is valid (non-empty urls array, within size limits). Top-level 400, 401, 429, and 503 are returned when the entire request cannot be processed (auth failure, pre-flight rate limit, service at capacity, or the batch structure is invalid). This endpoint is server-to-server only — no CORS headers are emitted.

Request body required

Property Type Required Description
urls array of object Yes List of URLs to capture. Each item is an object with a url field. Maximum batch size is configurable (default 20, hard cap 100). Duplicate URLs are allowed — each gets its own capture ID.
  url string (uri) Yes Public http or https URL to capture. Must not resolve to a private IP.
{
  "urls": [
    {
      "url": "https://example.com"
    },
    {
      "url": "https://example.org/page"
    }
  ]
}

Responses

207 — Multi-Status. Returned when the batch structure is valid. Each item in `items` corresponds to the URL at the same index in the request. Check each item's `status` field — 202 means accepted, 4xx/5xx means that item failed. The summary counts accepted vs failed items.
Property Type Required Description
items array of object | object Yes Per-URL outcome in the same order as the input urls array. Length always equals the input array length.
summary object Yes
  total integer Yes Total number of items in the request.
  accepted integer Yes Number of items accepted for capture (status 202).
  failed integer Yes Number of items that failed validation or rate limiting.
{
  "items": [
    {
      "status": 202,
      "url": "https://example.com",
      "id": "cap_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
      "statusUrl": "https://api.webresourceledger.com/v1/captures/cap_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6/status"
    },
    {
      "status": 202,
      "url": "https://example.org/page",
      "id": "cap_b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7",
      "statusUrl": "https://api.webresourceledger.com/v1/captures/cap_b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7/status"
    }
  ],
  "summary": {
    "total": 2,
    "accepted": 2,
    "failed": 0
  }
}
400 — Bad request — invalid batch structure (missing urls field, empty array, batch size exceeds configured maximum, or malformed JSON body).
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 400,
  "title": "Bad Request",
  "detail": "Field 'urls' must contain at least one item"
}
401 — Unauthorized — missing, malformed, or invalid Authorization header.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 401,
  "title": "Unauthorized",
  "detail": "Authorization header is required."
}
415 — Unsupported Media Type — Content-Type is not application/json.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 415,
  "title": "Unsupported Media Type",
  "detail": "Content-Type must be application/json"
}
429 — Too Many Requests — pre-flight rate limit or monthly quota exceeded before any item was processed. See `limitType` in the response body: absent means rate limit, `'quota'` means monthly quota exhausted. When `limitType` is `'quota'`, the `quota` object includes a `requested` field with the batch size that was attempted.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 429,
  "title": "Too Many Requests",
  "detail": "Rate limit exceeded. Try again later."
}
503 — Service Unavailable — service is at capacity.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 503,
  "title": "Service Unavailable",
  "detail": "Service is at capacity. Retry in 10 seconds."
}
GET /v1/captures/{captureId}/status No auth

Poll capture status

Returns the current state of a capture. No authentication is required — the capture ID acts as the access secret. Poll until status is "complete" or "failed".

Parameters

Name Location Required Type Description
captureId path Yes string Capture ID returned by POST /v1/captures.

Responses

200 — Capture found. Check `status` field for current state.
Property Type Required Description
id string Yes Unique capture identifier. Also serves as the access secret for per-capture access.
status "pending" | "complete" | "failed" Yes Lifecycle state of the capture.
captureUrl string (uri) No Present when status is "complete". URL to retrieve capture metadata and artifact links.
error string No Present when status is "failed". Human-readable failure reason.
retryable boolean No Present when status is "failed". True if submitting a new capture may succeed.
{
  "id": "cap_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
  "status": "pending"
}
404 — Not found.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 404,
  "title": "Not Found",
  "detail": "Capture not found."
}
GET /v1/captures/{captureId} No auth

Retrieve a completed capture

Returns metadata and artifact links for a completed capture. The capture ID acts as the access secret -- no authentication header is required. Returns 404 if the capture does not exist or has not yet completed.

Parameters

Name Location Required Type Description
captureId path Yes string Capture ID returned by POST /v1/captures.

Responses

200 — Capture found and complete.
Property Type Required Description
id string Yes Unique capture identifier. Also serves as the access secret for per-capture access.
status "complete" Yes Always "complete" -- this endpoint only returns completed captures.
url string (uri) Yes The URL that was captured.
createdAt string (date-time) Yes ISO 8601 timestamp when the capture was submitted.
completedAt string (date-time) Yes ISO 8601 timestamp when the capture completed.
renderQuality "full" | "partial" Yes Render quality of the capture. 'full' when the page reached the load milestone before capture (followed by an adaptive settle phase, up to 3s). 'partial' when the capture was taken after a navigation timeout but DOMContentLoaded had fired. Defaults to 'full' for captures created before this feature.
render object No Rendering process metadata. Present on captures created after this feature. When absent, the page reached the load milestone (consistent with renderQuality: full).
  waitUntilReached "domcontentloaded" | "load" | "networkidle" Yes Highest browser readiness milestone confirmed before the page was captured. "load" means the load event fired (default for new captures). "networkidle" means fewer than two open network connections for 500ms (used by older captures -- retained in enum for backward compatibility). "domcontentloaded" means the DOM was parsed but some resources may still be loading.
  timedOut boolean Yes True when the navigation did not reach the target milestone (load) within the timeout window. When true, renderQuality is "partial".
  durationMs integer Yes Wall-clock milliseconds from navigation start to the point the page was captured (either at load + adaptive settle, or at timeout).
  settleMs integer No Wall-clock milliseconds spent in the post-load adaptive settle phase. Settle ends when in-flight requests drain (settleReason: idle) or the 3s hard cap is reached (settleReason: cap). Absent on partial captures and captures created before this feature.
  settleReason "idle" | "cap" No Why the settle phase ended. "idle" means all non-persistent in-flight requests completed within the quiescence window. "cap" means the 3s hard cap was reached before quiescence. Absent on partial captures and captures created before this feature.
  stages object No Per-stage timing breakdown. Present on captures created after stage-level instrumentation was deployed. Absent on earlier captures.
artifacts object Yes Named artifact URLs for a complete capture. All fields present for a complete capture except headers, which is absent if the HTTP header fetch failed or timed out. The html artifact is served as text/plain with Content-Disposition: attachment to prevent XSS -- do not embed in iframes or inject as HTML. The `screenshot` field always points to the best-available screenshot (backward compatible). `screenshotBefore` is the pre-consent-dismissal screenshot; present only when consent was detected and dismissed.
  screenshot string (uri) Yes Full-page PNG screenshot (best available). When consent was dismissed, this is the post-dismissal screenshot. Served via the API Worker with Content-Type: image/png and Content-Disposition: attachment.
  screenshotBefore string (uri) No Pre-consent-dismissal screenshot. Present only when consent was detected and successfully dismissed. Corresponds to the page state before the CMP was closed. Served as image/png with Content-Disposition: attachment.
  html string (uri) Yes Rendered HTML after JavaScript execution. Served as text/plain with Content-Disposition: attachment -- NOT as text/html -- to prevent stored XSS.
  headers string (uri) No JSON object of HTTP response headers captured at fetch time. Set-Cookie values are redacted to [redacted]. Absent if the header fetch failed or timed out.
wacz object No WACZ bundle metadata. Present when status is "complete" AND a signing key was configured. Absent otherwise -- not an error condition.
  url string (uri) Yes URL to the signed WACZ bundle. Served via the API Worker with Content-Type: application/wacz+zip and Content-Disposition: attachment.
  bundleHash string Yes SHA-256 hash of the canonical JSON of the WACZ datapackage.json, in the form "sha256:{hex}". Use to verify bundle integrity.
  size integer Yes Size of the WACZ bundle in bytes.
verifyUrl string (uri) No URL to the human-readable verification page for this capture's WACZ bundle. Present only when `wacz` is present. Omitted when the capture has no WACZ bundle (e.g., partial captures from navigation timeouts).
captureSettings object No Settings and metadata about how the capture was performed. Present when capture settings were recorded (e.g., consent handling was attempted).
  version integer Yes Schema version for captureSettings.
  consent object No Consent handling metadata. Present when consent opt-out was attempted.
{
  "id": "cap_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
  "status": "complete",
  "url": "https://example.com",
  "createdAt": "2024-01-15T10:30:00.000Z",
  "completedAt": "2024-01-15T10:30:45.123Z",
  "renderQuality": "full",
  "artifacts": {
    "screenshot": "https://api.webresourceledger.com/v1/captures/cap_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6/artifacts/screenshot",
    "html": "https://api.webresourceledger.com/v1/captures/cap_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6/artifacts/html",
    "headers": "https://api.webresourceledger.com/v1/captures/cap_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6/artifacts/headers"
  },
  "wacz": {
    "url": "https://api.webresourceledger.com/v1/captures/cap_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6/artifacts/wacz",
    "bundleHash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
    "size": 204800
  },
  "verifyUrl": "https://api.webresourceledger.com/v1/verify/cap_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
}
404 — Not found.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 404,
  "title": "Not Found",
  "detail": "Capture not found."
}
GET /v1/captures/{captureId}/artifacts/{name} No auth

Download a capture artifact

Returns the raw artifact binary or text for a completed capture. The capture ID acts as the access secret -- no authentication header is required. Responses include Content-Disposition: attachment to prevent browser rendering.

Parameters

Name Location Required Type Description
captureId path Yes string Capture ID returned by POST /v1/captures.
name path Yes "screenshot-before" | "screenshot" | "html" | "headers" | "wacz" Artifact name to retrieve.

Responses

200 — Artifact found. Content-Type depends on artifact name.
404 — Not found.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 404,
  "title": "Not Found",
  "detail": "Capture not found."
}

Verification

WACZ bundle cryptographic verification

GET /v1/verify/{captureId} No auth

Verify a capture's WACZ bundle

Performs cryptographic verification of the WACZ bundle for a completed capture and returns the result. No authentication required. Verification runs up to four checks: artifactHashes, bundleHash, signature, and (for v0.2.0 bundles) timestamp. The timestamp check verifies an RFC 3161 independent timestamp against the bundle hash. Legacy v0.1.0 bundles produce three checks; the timestamp check is absent. Content negotiation: browsers (Accept: text/html) receive a self-contained HTML verification page that fetches the JSON result client-side. API clients receive JSON directly. **A 200 status does NOT mean the capture is verified -- check the `verified` field.** The endpoint returns 200 for all reachable captures, even when verification fails. Captures without a WACZ bundle (including partial captures from navigation timeouts) return 404 from this endpoint.

Parameters

Name Location Required Type Description
captureId path Yes string Capture ID to verify.

Responses

200 — Verification result. Check the `verified` boolean field -- a 200 status does not mean the capture passed verification.
Property Type Required Description
verified boolean Yes True only when all checks pass or skip. False for any failure.
capture object Yes Capture identity and timing metadata, extracted from the database record.
  id string Yes Unique capture identifier. Also serves as the access secret for per-capture access.
  createdAt string (date-time) Yes ISO 8601 timestamp when the capture was submitted.
  completedAt string (date-time) Yes ISO 8601 timestamp when the capture completed.
  renderQuality "full" | "partial" No Render quality of the verified capture.
signing object | null Yes Cryptographic metadata from the WACZ digest document. Null when the WACZ bundle was not found in storage or the digest document is absent.
checks array of object Yes Verification check results, always in order: artifactHashes, bundleHash, signature, and (for v0.2.0 bundles) timestamp. Legacy v0.1.0 bundles produce three checks; v0.2.0 bundles produce up to four.
  name "artifactHashes" | "bundleHash" | "signature" | "timestamp" Yes Which check this result represents. artifactHashes verifies individual file integrity; bundleHash verifies the overall archive; signature verifies the Ed25519 signature over the bundle hash; timestamp verifies the RFC 3161 independent timestamp when present (v0.2.0 bundles only).
  status "pass" | "fail" | "skip" Yes Outcome of the check. "skip" means the check was not performed because a prerequisite check failed.
  detail string No Human-readable failure reason. Present only when status is "fail" or "skip" with context.
captureSettings object No Settings and metadata about how the capture was performed. Present when capture settings were recorded.
  version integer Yes Schema version for captureSettings.
  consent object No Consent handling metadata. Present when consent opt-out was attempted.
{
  "verified": true,
  "capture": {
    "id": "cap_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
    "createdAt": "2024-01-15T10:30:00.000Z",
    "completedAt": "2024-01-15T10:30:45.123Z",
    "renderQuality": "full"
  },
  "signing": {
    "bundleHash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
    "signature": "base64encodedSignatureHere==",
    "publicKey": "base64encodedPublicKeyHere=",
    "signedAt": "2024-01-15T10:30:44.000Z",
    "timestamp": {
      "genTime": "2024-01-15T10:30:46.000Z",
      "tsa": "https://freetsa.org/tsr"
    }
  },
  "checks": [
    {
      "name": "artifactHashes",
      "status": "pass"
    },
    {
      "name": "bundleHash",
      "status": "pass"
    },
    {
      "name": "signature",
      "status": "pass"
    },
    {
      "name": "timestamp",
      "status": "pass"
    }
  ]
}
404 — Not found.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 404,
  "title": "Not Found",
  "detail": "Capture not found."
}
422 — Unprocessable Content — WACZ bundle exceeds maximum verifiable size.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 422,
  "title": "Unprocessable Content",
  "detail": "WACZ bundle exceeds maximum verifiable size"
}
429 — Too Many Requests — rate limit or monthly quota exceeded. The response body includes a `limitType` extension field that distinguishes the specific 429 variant: - `limitType` absent: IP-based rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'tenant'`: Per-tenant rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'quota'`: Monthly capture quota exhausted. A `quota` extension object is present with `limit`, `used`, `resource`, `resetsAt`, and (for batch) `requested`. Retrying before `resetsAt` will continue to fail.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 429,
  "title": "Too Many Requests",
  "detail": "Rate limit exceeded. Try again later."
}
503 — Service Unavailable -- required configuration is missing or service is at capacity.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 503,
  "title": "Service Unavailable",
  "detail": "Service is at capacity. Retry in 10 seconds."
}

Signing

Signing key management

GET /.well-known/signing-key No auth

Retrieve the service's Ed25519 public signing key

Returns the Ed25519 public key used to sign WACZ bundles. Clients can use this to independently verify bundle signatures without trusting the verification endpoint.

Responses

200 — Signing key available.
Property Type Required Description
algorithm "Ed25519" Yes Always "Ed25519".
publicKey string Yes Base64-encoded 32-byte Ed25519 public key.
keyId string Yes Key fingerprint -- first 8 hex chars of SHA-256(raw public key bytes). Used to identify which historical key signed a given WACZ bundle.
{
  "algorithm": "Ed25519",
  "publicKey": "MCowBQYDK2VwAyEA47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=",
  "keyId": "a1b2c3d4"
}
429 — Too Many Requests — rate limit or monthly quota exceeded. The response body includes a `limitType` extension field that distinguishes the specific 429 variant: - `limitType` absent: IP-based rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'tenant'`: Per-tenant rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'quota'`: Monthly capture quota exhausted. A `quota` extension object is present with `limit`, `used`, `resource`, `resetsAt`, and (for batch) `requested`. Retrying before `resetsAt` will continue to fail.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 429,
  "title": "Too Many Requests",
  "detail": "Rate limit exceeded. Try again later."
}
503 — Service Unavailable -- required configuration is missing or service is at capacity.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 503,
  "title": "Service Unavailable",
  "detail": "Service is at capacity. Retry in 10 seconds."
}
GET /.well-known/signing-keys No auth

List all historical signing keys

Returns all Ed25519 public keys that have been used to sign WACZ bundles, including the current key. Third-party verifiers can use this to look up the correct key for a given WACZ bundle using the keyId field from signedData in the bundle's datapackage-digest.json.

Responses

200 — List of archived signing keys.
Property Type Required Description
keys array of object Yes All historical signing keys, including the current one.
  keyId string Yes Key fingerprint.
  algorithm "Ed25519" Yes
  publicKey string Yes Base64-encoded 32-byte Ed25519 public key.
  archivedAt string (date-time) Yes ISO 8601 timestamp when the key was first archived.
429 — Too Many Requests — rate limit or monthly quota exceeded. The response body includes a `limitType` extension field that distinguishes the specific 429 variant: - `limitType` absent: IP-based rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'tenant'`: Per-tenant rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'quota'`: Monthly capture quota exhausted. A `quota` extension object is present with `limit`, `used`, `resource`, `resetsAt`, and (for batch) `requested`. Retrying before `resetsAt` will continue to fail.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 429,
  "title": "Too Many Requests",
  "detail": "Rate limit exceeded. Try again later."
}
503 — Service Unavailable -- required configuration is missing or service is at capacity.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 503,
  "title": "Service Unavailable",
  "detail": "Service is at capacity. Retry in 10 seconds."
}

Admin

API key management (admin-only)

GET /v1/admin/keys Admin key

List API keys

Returns all API keys, optionally filtered by tenant. Revoked keys are excluded by default. Raw key values are never included. Rate limited to 5 requests per 60 seconds per IP. ```bash curl https://api.webresourceledger.com/v1/admin/keys \ -H "Authorization: Bearer $ADMIN_KEY" | jq . # Filter by tenant curl "https://api.webresourceledger.com/v1/admin/keys?tenant=default" \ -H "Authorization: Bearer $ADMIN_KEY" | jq . # Include revoked keys curl "https://api.webresourceledger.com/v1/admin/keys?include=revoked" \ -H "Authorization: Bearer $ADMIN_KEY" | jq . ```

Parameters

Name Location Required Type Description
tenant query No string Filter results to keys belonging to this tenant ID.
include query No "revoked" Pass "revoked" to include revoked keys in the response. Omit to return only active keys.

Responses

200 — List of API key summaries. Raw key values are never included.
Property Type Required Description
data array of object Yes API key summaries. Empty array when no keys exist.
  keyHash string Yes SHA-256 hex digest of the raw key. Used as the identifier for revocation.
  tenantId string Yes Tenant this key belongs to.
  scopes array of string Yes Scopes granted to this key.
  name string Yes Human-readable label for the key.
  createdAt string (date-time) Yes ISO 8601 timestamp when the key was created.
  createdBy string Yes Who created the key. Currently always "admin".
  revoked boolean No Present and true when the key has been revoked.
  revokedAt string (date-time) No ISO 8601 timestamp when the key was revoked. Present only when revoked is true.
{
  "data": [
    {
      "keyHash": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
      "tenantId": "default",
      "scopes": [
        "capture"
      ],
      "name": "ci-pipeline",
      "createdAt": "2026-03-17T12:00:00.000Z",
      "createdBy": "admin"
    }
  ]
}
401 — Unauthorized -- missing or invalid admin key.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 401,
  "title": "Unauthorized",
  "detail": "Invalid admin key"
}
429 — Too Many Requests — rate limit or monthly quota exceeded. The response body includes a `limitType` extension field that distinguishes the specific 429 variant: - `limitType` absent: IP-based rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'tenant'`: Per-tenant rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'quota'`: Monthly capture quota exhausted. A `quota` extension object is present with `limit`, `used`, `resource`, `resetsAt`, and (for batch) `requested`. Retrying before `resetsAt` will continue to fail.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 429,
  "title": "Too Many Requests",
  "detail": "Rate limit exceeded. Try again later."
}
503 — Service Unavailable -- ADMIN_KEY is not configured.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 503,
  "title": "Service Unavailable",
  "detail": "Admin API is not configured"
}
POST /v1/admin/keys Admin key

Create a new API key

Creates a new per-tenant API key and returns the raw key exactly once. Store the returned key immediately -- it cannot be retrieved after this response. Rate limited to 5 requests per 60 seconds per IP. Auth check happens after rate limit. ```bash curl -X POST https://api.webresourceledger.com/v1/admin/keys \ -H "Authorization: Bearer $ADMIN_KEY" \ -H "Content-Type: application/json" \ -d '{"tenantId": "default", "scopes": ["capture"], "name": "ci-pipeline"}' | jq . ``` Save the key: ```bash curl ... | jq -r .key > /tmp/wrl-key-default.txt ```

Request body required

Property Type Required Description
tenantId string Yes Tenant this key belongs to. Must match /^[a-z0-9_-]{1,64}$/. Callers may use tenantId as a database key without additional sanitization.
scopes array of "capture" | "read" | "admin" Yes Non-empty list of scopes to grant. Valid values: capture, read, admin. 'capture' implies 'read'. 'admin' does NOT imply 'capture' or 'read'. Note: the 'admin' scope is reserved for a future migration from the ADMIN_KEY env var to per-tenant admin keys. It currently has no runtime effect on access control -- admin endpoints use ADMIN_KEY only.
name string Yes Human-readable label for the key. 1-128 characters using letters, digits, spaces, and _ . : - characters.
{
  "tenantId": "default",
  "scopes": [
    "capture"
  ],
  "name": "ci-pipeline"
}

Responses

201 — Key created. The raw key is returned exactly once. Store it immediately.
Property Type Required Description
key string Yes Raw API key prefixed with "wrl_live_". Store this value now -- it cannot be retrieved after this response.
keyHash string Yes SHA-256 hex digest of the raw key. Used as the database primary key and for revocation.
tenantId string Yes Tenant this key belongs to.
scopes array of string Yes Scopes granted to this key.
name string Yes Human-readable label for the key.
createdAt string (date-time) Yes ISO 8601 timestamp when the key was created.
warning "Store this key now. It cannot be retrieved after this response." Yes Always "Store this key now. It cannot be retrieved after this response."
{
  "key": "wrl_live_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789abcdefghij",
  "keyHash": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
  "tenantId": "default",
  "scopes": [
    "capture"
  ],
  "name": "ci-pipeline",
  "createdAt": "2026-03-17T12:00:00.000Z",
  "warning": "Store this key now. It cannot be retrieved after this response."
}
400 — Bad request -- missing or invalid fields. Field validation messages identify the specific problem.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 400,
  "title": "Bad Request",
  "detail": "Field 'tenantId' is required"
}
401 — Unauthorized -- missing or invalid admin key.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 401,
  "title": "Unauthorized",
  "detail": "Invalid admin key"
}
415 — Unsupported Media Type -- Content-Type is not application/json.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 415,
  "title": "Unsupported Media Type",
  "detail": "Content-Type must be application/json"
}
429 — Too Many Requests — rate limit or monthly quota exceeded. The response body includes a `limitType` extension field that distinguishes the specific 429 variant: - `limitType` absent: IP-based rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'tenant'`: Per-tenant rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'quota'`: Monthly capture quota exhausted. A `quota` extension object is present with `limit`, `used`, `resource`, `resetsAt`, and (for batch) `requested`. Retrying before `resetsAt` will continue to fail.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 429,
  "title": "Too Many Requests",
  "detail": "Rate limit exceeded. Try again later."
}
503 — Service Unavailable -- ADMIN_KEY is not configured.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 503,
  "title": "Service Unavailable",
  "detail": "Admin API is not configured"
}
DELETE /v1/admin/keys/{keyHash} Admin key

Revoke an API key

Marks an API key as revoked. The key is immediately recorded as revoked in D1. Revocation takes effect within 60 seconds due to distributed edge caching. This operation is idempotent -- revoking an already-revoked key returns 200 with the existing revocation record. Rate limited to 5 requests per 60 seconds per IP. ```bash curl -X DELETE "https://api.webresourceledger.com/v1/admin/keys/$KEY_HASH" \ -H "Authorization: Bearer $ADMIN_KEY" | jq . ```

Parameters

Name Location Required Type Description
keyHash path Yes string SHA-256 hex digest of the key to revoke (64 hex chars).

Responses

200 — Key revoked (or was already revoked -- operation is idempotent).
Property Type Required Description
keyHash string Yes SHA-256 hex digest of the raw key.
tenantId string Yes Tenant this key belonged to.
scopes array of string Yes Scopes the key had.
name string Yes Human-readable label for the key.
createdAt string (date-time) Yes ISO 8601 timestamp when the key was created.
revoked "true" Yes Always true in this response.
revokedAt string (date-time) Yes ISO 8601 timestamp when the key was revoked.
{
  "keyHash": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
  "tenantId": "default",
  "scopes": [
    "capture"
  ],
  "name": "ci-pipeline",
  "createdAt": "2026-03-17T12:00:00.000Z",
  "revoked": true,
  "revokedAt": "2026-03-17T15:00:00.000Z"
}
401 — Unauthorized -- missing or invalid admin key.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 401,
  "title": "Unauthorized",
  "detail": "Invalid admin key"
}
404 — Key not found.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 404,
  "title": "Not Found",
  "detail": "API key not found."
}
429 — Too Many Requests — rate limit or monthly quota exceeded. The response body includes a `limitType` extension field that distinguishes the specific 429 variant: - `limitType` absent: IP-based rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'tenant'`: Per-tenant rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'quota'`: Monthly capture quota exhausted. A `quota` extension object is present with `limit`, `used`, `resource`, `resetsAt`, and (for batch) `requested`. Retrying before `resetsAt` will continue to fail.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 429,
  "title": "Too Many Requests",
  "detail": "Rate limit exceeded. Try again later."
}
503 — Service Unavailable -- ADMIN_KEY is not configured.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 503,
  "title": "Service Unavailable",
  "detail": "Admin API is not configured"
}
GET /v1/admin/usage Admin key

Get tenant usage counters

Returns per-tenant usage counters for the specified billing period. Defaults to the current calendar month (UTC) if no period is specified. Rate limited to 5 requests per 60 seconds per IP. Auth check happens after rate limit. ```bash # Current period usage curl "https://api.webresourceledger.com/v1/admin/usage?tenant=default" \ -H "Authorization: Bearer $ADMIN_KEY" | jq . # Specific period curl "https://api.webresourceledger.com/v1/admin/usage?tenant=default&period=2026-03" \ -H "Authorization: Bearer $ADMIN_KEY" | jq . ```

Parameters

Name Location Required Type Description
tenant query Yes string Tenant ID to query usage for.
period query No string Billing period in YYYY-MM format. Defaults to the current calendar month (UTC) if omitted.

Responses

200 — Usage counters for the specified tenant and period.
Property Type Required Description
tenantId string Yes Tenant identifier.
period string Yes Billing period in YYYY-MM format.
captureCount integer Yes Number of captures completed in this period.
storageBytes integer Yes Total R2 storage bytes consumed by captures in this period.
apiCallCount integer Yes Number of authenticated API calls in this period.
updatedAt string | null Yes Last time counters were updated. Null if no activity in the period.
{
  "tenantId": "default",
  "period": "2026-03",
  "captureCount": 42,
  "storageBytes": 157286400,
  "apiCallCount": 128,
  "updatedAt": "2026-03-22T14:30:00.000Z"
}
400 — Bad request — missing or malformed body, missing `url` field, invalid URL scheme.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 400,
  "title": "Bad Request",
  "detail": "Request body is missing or not valid JSON."
}
401 — Unauthorized — missing, malformed, or invalid Authorization header.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 401,
  "title": "Unauthorized",
  "detail": "Authorization header is required."
}
404 — Tenant not found.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 404,
  "title": "Not Found",
  "detail": "Tenant 'nonexistent' not found"
}
429 — Too Many Requests — rate limit or monthly quota exceeded. The response body includes a `limitType` extension field that distinguishes the specific 429 variant: - `limitType` absent: IP-based rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'tenant'`: Per-tenant rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'quota'`: Monthly capture quota exhausted. A `quota` extension object is present with `limit`, `used`, `resource`, `resetsAt`, and (for batch) `requested`. Retrying before `resetsAt` will continue to fail.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 429,
  "title": "Too Many Requests",
  "detail": "Rate limit exceeded. Try again later."
}

Webhooks

Outbound webhook registration and management

GET /v1/webhooks Tenant key

List webhooks

Returns all registered webhooks for the authenticated tenant. Secrets are never included in list responses.

Responses

200 — List of registered webhooks.
Property Type Required Description
data array of object Yes Registered webhooks. Empty array when none exist; never null.
  id string Yes Unique webhook identifier.
  url string (uri) Yes The registered HTTPS endpoint.
  events array of string Yes Event types this webhook is subscribed to.
  name string Yes Human-readable label for this webhook.
  createdAt string (date-time) Yes ISO 8601 timestamp when the webhook was created.
  active boolean Yes Whether this webhook is active and will receive events. All registered webhooks are active by default. To disable, delete and re-register.
{
  "data": [
    {
      "id": "whk_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
      "url": "https://hooks.example.com/wrl-events",
      "events": [
        "capture.complete",
        "capture.failed"
      ],
      "name": "ci-notifier",
      "createdAt": "2026-03-22T12:00:00.000Z",
      "active": true
    },
    {
      "id": "whk_b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7",
      "url": "https://alerts.example.com/wrl",
      "events": [
        "capture.failed"
      ],
      "name": "failure-alerts",
      "createdAt": "2026-03-22T14:00:00.000Z",
      "active": true
    }
  ]
}
401 — Unauthorized — missing, malformed, or invalid Authorization header.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 401,
  "title": "Unauthorized",
  "detail": "Authorization header is required."
}
429 — Too Many Requests — rate limit or monthly quota exceeded. The response body includes a `limitType` extension field that distinguishes the specific 429 variant: - `limitType` absent: IP-based rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'tenant'`: Per-tenant rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'quota'`: Monthly capture quota exhausted. A `quota` extension object is present with `limit`, `used`, `resource`, `resetsAt`, and (for batch) `requested`. Retrying before `resetsAt` will continue to fail.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 429,
  "title": "Too Many Requests",
  "detail": "Rate limit exceeded. Try again later."
}
POST /v1/webhooks Tenant key

Register a webhook

Registers an HTTPS endpoint to receive outbound event notifications. A signing secret is returned exactly once -- store it immediately. Tenants are limited to 5 active webhooks. All registered webhooks are active immediately upon creation.

Request body required

Property Type Required Description
url string (uri) Yes HTTPS endpoint WRL will POST events to. Must use https scheme. Maximum 2048 characters.
events array of "capture.complete" | "capture.failed" Yes Event types this webhook subscribes to. At least one required. Valid values: capture.complete, capture.failed.
name string Yes Human-readable label for this webhook. 1-128 characters using letters, digits, spaces, and _ . : - characters.
{
  "url": "https://hooks.example.com/wrl-events",
  "events": [
    "capture.complete",
    "capture.failed"
  ],
  "name": "ci-notifier"
}

Responses

201 — Webhook registered. Store the secret -- it is shown only once.
Property Type Required Description
id string Yes Unique webhook identifier.
url string (uri) Yes The HTTPS endpoint registered.
events array of string Yes Event types this webhook is subscribed to.
name string Yes Human-readable label for this webhook.
secret string Yes Webhook signing secret. Prefixed with "wrlsec_" followed by 64 hex characters (32 bytes, hex-encoded). Use this to verify the HMAC-SHA256 signature on incoming webhook requests. Store this value now -- it cannot be retrieved after this response.
createdAt string (date-time) Yes ISO 8601 timestamp when the webhook was created.
warning "Store this secret now. It cannot be retrieved after this response." Yes Always "Store this secret now. It cannot be retrieved after this response."
{
  "id": "whk_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
  "url": "https://hooks.example.com/wrl-events",
  "events": [
    "capture.complete",
    "capture.failed"
  ],
  "name": "ci-notifier",
  "secret": "wrlsec_0000000000000000000000000000000000000000000000000000000000000000",
  "createdAt": "2026-03-22T12:00:00.000Z",
  "warning": "Store this secret now. It cannot be retrieved after this response."
}
400 — Bad request — missing or malformed body, missing `url` field, invalid URL scheme.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 400,
  "title": "Bad Request",
  "detail": "Request body is missing or not valid JSON."
}
401 — Unauthorized — missing, malformed, or invalid Authorization header.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 401,
  "title": "Unauthorized",
  "detail": "Authorization header is required."
}
409 — Conflict -- tenant has reached the 5-webhook limit.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 409,
  "title": "Conflict",
  "detail": "Webhook limit reached. A tenant may have at most 5 active webhooks. Delete an existing webhook to register a new one."
}
429 — Too Many Requests — rate limit or monthly quota exceeded. The response body includes a `limitType` extension field that distinguishes the specific 429 variant: - `limitType` absent: IP-based rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'tenant'`: Per-tenant rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'quota'`: Monthly capture quota exhausted. A `quota` extension object is present with `limit`, `used`, `resource`, `resetsAt`, and (for batch) `requested`. Retrying before `resetsAt` will continue to fail.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 429,
  "title": "Too Many Requests",
  "detail": "Rate limit exceeded. Try again later."
}
503 — Service Unavailable -- required configuration is missing or service is at capacity.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 503,
  "title": "Service Unavailable",
  "detail": "Service is at capacity. Retry in 10 seconds."
}
DELETE /v1/webhooks/{webhookId} Tenant key

Delete a webhook

Permanently removes a registered webhook. Events that were in-flight at deletion time may still be delivered. Deletion is not reversible; the secret is discarded.

Parameters

Name Location Required Type Description
webhookId path Yes string The webhook ID to delete.

Responses

200 — Webhook deleted.
Property Type Required Description
id string Yes Unique webhook identifier.
deleted "true" Yes Always true in this response.
{
  "id": "whk_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
  "deleted": true
}
401 — Unauthorized — missing, malformed, or invalid Authorization header.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 401,
  "title": "Unauthorized",
  "detail": "Authorization header is required."
}
404 — Not found.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 404,
  "title": "Not Found",
  "detail": "Capture not found."
}
429 — Too Many Requests — rate limit or monthly quota exceeded. The response body includes a `limitType` extension field that distinguishes the specific 429 variant: - `limitType` absent: IP-based rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'tenant'`: Per-tenant rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'quota'`: Monthly capture quota exhausted. A `quota` extension object is present with `limit`, `used`, `resource`, `resetsAt`, and (for batch) `requested`. Retrying before `resetsAt` will continue to fail.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 429,
  "title": "Too Many Requests",
  "detail": "Rate limit exceeded. Try again later."
}
POST /v1/webhooks/{webhookId}/ping Tenant key

Send a test event

Delivers a synthetic "ping" event to the registered endpoint and reports the outcome. Use this to verify your endpoint is reachable and correctly configured before relying on live events. The ping payload has type "ping" and is signed with the webhook secret using the same HMAC-SHA256 scheme as live events. Your endpoint should verify the signature even for ping events. A non-2xx response from your endpoint does not prevent future event delivery -- ping results do not affect webhook active status.

Parameters

Name Location Required Type Description
webhookId path Yes string The webhook ID to ping.

Responses

200 — Ping dispatched. Check `success` for the delivery outcome.
Property Type Required Description
success boolean Yes True when the endpoint responded with a 2xx status code.
httpStatus integer Yes HTTP status code returned by the webhook endpoint.
latencyMs integer Yes Round-trip latency in milliseconds from dispatch to response.
detail string No Human-readable description of the outcome. Present on failures (non-2xx response, network error, timeout).
{
  "success": true,
  "httpStatus": 200,
  "latencyMs": 142
}
401 — Unauthorized — missing, malformed, or invalid Authorization header.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 401,
  "title": "Unauthorized",
  "detail": "Authorization header is required."
}
404 — Not found.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 404,
  "title": "Not Found",
  "detail": "Capture not found."
}
429 — Too Many Requests — rate limit or monthly quota exceeded. The response body includes a `limitType` extension field that distinguishes the specific 429 variant: - `limitType` absent: IP-based rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'tenant'`: Per-tenant rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'quota'`: Monthly capture quota exhausted. A `quota` extension object is present with `limit`, `used`, `resource`, `resetsAt`, and (for batch) `requested`. Retrying before `resetsAt` will continue to fail.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 429,
  "title": "Too Many Requests",
  "detail": "Rate limit exceeded. Try again later."
}

Account

Tenant account information and usage

GET /v1/account/usage Tenant key

Get current usage and quota

Returns the authenticated tenant's current-period usage against their effective quotas. Quotas reflect tier defaults unless an admin override is in effect for this tenant. Use this endpoint to build quota-aware UIs or to check remaining capacity before submitting large batches.

Responses

200 — Current-period usage and quota for the authenticated tenant.
Property Type Required Description
tenantId string Yes Tenant identifier.
period string Yes Billing period in YYYY-MM format.
tierDisplay string Yes Human-readable tier name for UI display.
captures object Yes Capture count usage and quota for the current period.
  used integer Yes Units consumed in the current billing period.
  limit integer Yes Maximum units allowed in the current billing period.
  remaining integer Yes Units remaining before the quota is reached.
storageBytes object Yes Storage byte usage and quota for the current period.
  used integer Yes Units consumed in the current billing period.
  limit integer Yes Maximum units allowed in the current billing period.
  remaining integer Yes Units remaining before the quota is reached.
resetsAt string (date-time) Yes ISO 8601 timestamp when the current period quotas reset (first of next month).
{
  "tenantId": "gh-12345",
  "period": "2026-03",
  "tierDisplay": "Starter",
  "captures": {
    "used": 42,
    "limit": 100,
    "remaining": 58
  },
  "storageBytes": {
    "used": 157286400,
    "limit": 1073741824,
    "remaining": 916455424
  },
  "resetsAt": "2026-04-01T00:00:00.000Z"
}
401 — Unauthorized — missing, malformed, or invalid Authorization header.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 401,
  "title": "Unauthorized",
  "detail": "Authorization header is required."
}
429 — Too Many Requests — rate limit or monthly quota exceeded. The response body includes a `limitType` extension field that distinguishes the specific 429 variant: - `limitType` absent: IP-based rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'tenant'`: Per-tenant rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'quota'`: Monthly capture quota exhausted. A `quota` extension object is present with `limit`, `used`, `resource`, `resetsAt`, and (for batch) `requested`. Retrying before `resetsAt` will continue to fail.
Property Type Required Description
type "about:blank" Yes Always "about:blank". Clients should switch on `status`, not `type`.
status integer Yes HTTP status code.
title string Yes Short, human-readable summary of the problem class.
detail string Yes Human-readable explanation of this specific occurrence.
{
  "type": "about:blank",
  "status": 429,
  "title": "Too Many Requests",
  "detail": "Rate limit exceeded. Try again later."
}