API Reference

This page documents all Web Resource Ledger API endpoints (v1.0.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
build object No Build identity metadata. Present only when deployed via CI with --define flags.
  commit string Yes Full git commit SHA of the deployed code.
  version string Yes Semantic version from package.json.
  env "production" | "staging" Yes Deployment environment.
  deployedAt string (date-time) Yes ISO 8601 UTC timestamp of when the deploy job ran.
{
  "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" | "quarantined" 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.
schedule_id query No string Filter results to captures triggered by this schedule. Omit to return captures from all sources (manual and scheduled).

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 (128-bit, unguessable). Knowing the capture ID grants read access.
  status "pending" | "complete" | "failed" | "quarantined" 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.
  threatCheck string | null No Result of URL threat screening. null for captures created before this feature was deployed.
  quarantineReason string | null No Human-readable reason why the capture was quarantined. Present only when status is "quarantined".
  quarantinedAt string | null No ISO 8601 timestamp when the capture was quarantined. Present only when status is "quarantined".
  scheduleId string | null No ID of the schedule that triggered this capture, if any. Null for manually submitted captures.
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 (128-bit, unguessable). Knowing the capture ID grants read 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 required. 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 (128-bit, unguessable). Knowing the capture ID grants read access.
status "pending" | "complete" | "failed" | "quarantined" 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. No authentication 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 (128-bit, unguessable). Knowing the capture ID grants read 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).
certificateUrl string (uri) No URL to the FRE 902(13) certification PDF for this capture. Present only when `wacz` is present. The PDF is generated deterministically from the capture record and signed with the operator's Ed25519 key. Omitted when the capture has no WACZ bundle.
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.
threatCheck string | null No Result of URL threat screening. null for captures created before this feature was deployed.
quarantineReason string | null No Human-readable reason why the capture was quarantined. Present only when status is "quarantined".
quarantinedAt string | null No ISO 8601 timestamp when the capture was quarantined. Present only when status is "quarantined".
scheduleId string | null No ID of the schedule that triggered this capture, if any. Null for manually submitted captures.
changeSummary object No Summary of what changed compared to the previous scheduled capture of the same URL. Present only on captures produced by a schedule that has a prior capture to compare against.
  changed boolean No True if any artifact changed.
  previousCaptureId string No Unique capture identifier (128-bit, unguessable). Knowing the capture ID grants read access.
  html object No
  screenshot object No
  headers object No
{
  "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",
  "certificateUrl": "https://api.webresourceledger.com/v1/captures/cap_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6/certificate"
}
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. No authentication 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."
}
451 — Unavailable For Legal Reasons — this capture has been quarantined due to content security screening. Capture metadata (GET /v1/captures/{captureId}) is still accessible; artifact downloads are restricted.
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": 451,
  "title": "Unavailable For Legal Reasons",
  "detail": "This capture has been quarantined and its artifacts are restricted."
}
GET /v1/captures/{captureId}/certificate No auth

Download FRE 902(13) certification PDF

Generates a deterministic PDF certification document for a completed capture. The document describes the automated capture process, integrity evidence, and operator identity in a format designed to support Federal Rules of Evidence 902(13) self-authentication. The PDF is signed with the operator's Ed25519 key; the signature is delivered in the `X-Signature-Ed25519` response header. No authentication required. The PDF is generated on demand and cached at the edge. A qualified person must still adopt and sign the certification statement when introducing the document as evidence.

Parameters

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

Responses

200 — Certification PDF. Response headers include: - `Content-Disposition: attachment; filename="certificate-{captureId}.pdf"` -- triggers download. - `Cache-Control: public, max-age=31536000, immutable` -- deterministic output; cached aggressively. - `X-Signature-Ed25519` -- base64-encoded Ed25519 signature over the PDF bytes, using the operator signing key. - `X-Signature-Key-Id` -- 8-character key fingerprint identifying which signing key produced the signature. - `Access-Control-Expose-Headers: X-Signature-Ed25519, X-Signature-Key-Id` -- exposes signature headers to browser clients.
404 — Capture not found, not yet complete, or has no WACZ bundle.
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 or not complete."
}
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."
}
451 — Unavailable For Legal Reasons -- this capture has been quarantined due to content security screening. Certification is not available for quarantined captures.
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": 451,
  "title": "Unavailable For Legal Reasons",
  "detail": "This capture has been quarantined and certification is not available."
}
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 /v1/captures/{baseId}/diff/{targetId} Tenant key

Compare two captures

Returns structured differences between two captures of the same URL. Compares HTML content (text-level diff with line context), screenshots (hash-based similarity), and HTTP response headers (added, removed, modified). Both captures must belong to the requesting tenant. The `include` query parameter controls which diff sections are computed; omitted sections still appear in the summary with cached or default values.

Parameters

Name Location Required Type Description
baseId path Yes string ID of the base (older) capture.
targetId path Yes string ID of the target (newer) capture.
include query No string Comma-separated list of diff sections to compute. Valid values: `html`, `screenshot`, `headers`. Sections not listed are omitted from the response body but still reflected in the summary.

Responses

200 — Structured diff response.
Property Type Required Description
base object Yes
  id string Yes Unique capture identifier (128-bit, unguessable). Knowing the capture ID grants read access.
  url string (uri) Yes
  createdAt string (date-time) Yes
target object Yes
  id string Yes Unique capture identifier (128-bit, unguessable). Knowing the capture ID grants read access.
  url string (uri) Yes
  createdAt string (date-time) Yes
summary object Yes
  changed boolean Yes True if any section detected changes.
  sections object Yes
html object No Present only when `html` is in the `include` parameter.
  changed boolean No
  truncated boolean No True if HTML was too large and diff was skipped.
  stats object No
  hunks array of object No
screenshot object No Present only when `screenshot` is in the `include` parameter.
  changed boolean No
  method "hash" No Always `hash` — server-side etag comparison.
headers object No Present only when `headers` is in the `include` parameter.
  changed boolean No
  added array of object No
  removed array of object No
  modified array of object No
  unchanged integer No Count of headers present in both captures with identical values.
  statusChanged boolean No
{
  "base": {
    "id": "cap_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
    "url": "https://example.com",
    "createdAt": "2025-01-01T00:00:00.000Z"
  },
  "target": {
    "id": "cap_b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7",
    "url": "https://example.com",
    "createdAt": "2025-01-02T00:00:00.000Z"
  },
  "summary": {
    "changed": true,
    "sections": {
      "html": {
        "changed": true,
        "additions": 3,
        "deletions": 1
      },
      "screenshot": {
        "changed": true
      },
      "headers": {
        "changed": false,
        "added": 0,
        "removed": 0,
        "modified": 0
      }
    }
  },
  "html": {
    "changed": true,
    "truncated": false,
    "stats": {
      "additions": 3,
      "deletions": 1
    },
    "hunks": [
      {
        "type": "addition",
        "value": "<p>New paragraph</p>",
        "context": {
          "before": "<div>",
          "after": "</div>"
        }
      }
    ]
  },
  "screenshot": {
    "changed": true,
    "method": "hash"
  },
  "headers": {
    "changed": false,
    "added": [],
    "removed": [],
    "modified": [],
    "unchanged": 12,
    "statusChanged": false
  }
}
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 — One or both captures not found, not yet complete, or not owned by the requesting tenant.
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": "One or both captures 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 -- 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."
}

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 (128-bit, unguessable). Knowing the capture ID grants read 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

Administration endpoints (key management, tenant overview, usage statistics)

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 30 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 30 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 30 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 30 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."
}
POST /v1/admin/cache/purge Admin key

Purge CDN cache for verification endpoints

Purges cached verification responses at the CDN edge. Uses Cloudflare's zone-level purge-by-URL API (works on all plans). Targets are semantic names that expand to actual URLs: - `signing-keys` — purges both `/.well-known/signing-key` and `/.well-known/signing-keys` - `capture:cap_{id}` — purges both JSON and HTML variants of a specific capture verification - `all` — purges all cached content (use sparingly) Requires `CLOUDFLARE_ZONE_ID` and `CLOUDFLARE_CACHE_PURGE_TOKEN` environment variables to be configured. Returns 503 if not configured. Rate limited to 30 requests per 60 seconds per IP. Auth check happens after rate limit. ```bash # Purge signing key cache (e.g., after key rotation) curl -X POST "https://api.webresourceledger.com/v1/admin/cache/purge" \ -H "Authorization: Bearer $ADMIN_KEY" \ -H "Content-Type: application/json" \ -d '{"targets": ["signing-keys"]}' | jq . # Purge a specific capture curl -X POST "https://api.webresourceledger.com/v1/admin/cache/purge" \ -H "Authorization: Bearer $ADMIN_KEY" \ -H "Content-Type: application/json" \ -d '{"targets": ["capture:cap_abcdef0123456789abcdef0123456789"]}' | jq . ```

Request body required

Property Type Required Description
targets array of string Yes Semantic purge targets. Each target expands to one or more cache key URLs.
{
  "targets": [
    "signing-keys"
  ]
}

Responses

200 — Cache purge completed successfully.
Property Type Required Description
purged boolean No
targets array of string No
urls array | null No Resolved URLs that were purged. Null when target is "all" (purge_everything).
purgeId string | null No Cloudflare purge request ID for audit trail.
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 must be 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.
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."
}
502 — Cloudflare API returned an error.
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.
503 — Cache purge not configured (missing API token or zone ID).
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.
GET /v1/admin/tenants Admin key

List all tenants with usage

Returns a list of all tenants with their current-period usage counters, quota information, and active key count. Designed for the admin dashboard tenant overview. Rate limited to 30 requests per 60 seconds per IP. ```bash curl "https://api.webresourceledger.com/v1/admin/tenants" \ -H "Authorization: Bearer $ADMIN_KEY" | jq . # Specific billing period curl "https://api.webresourceledger.com/v1/admin/tenants?period=2026-02" \ -H "Authorization: Bearer $ADMIN_KEY" | jq . ```

Parameters

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

Responses

200 — Tenant list with usage data.
Property Type Required Description
data array of object Yes
  tenantId string Yes
  tier "free" | "pro" Yes
  billingStatus "active" | "grace_period" | "blocked" Yes
  hasPaymentMethod boolean Yes
  eidasQualified boolean No
  createdAt string (date-time) No
  currentPeriod object Yes
  quota object Yes
  keyCount integer Yes Number of active (non-revoked) API keys.
meta object Yes
  totalTenants integer No
  period string No
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."
}
GET /v1/admin/tenants/{tenantId} Admin key

Get tenant detail

Returns detailed information for a single tenant including billing state, usage history across multiple periods, active API keys, and configuration. Rate limited to 30 requests per 60 seconds per IP. ```bash curl "https://api.webresourceledger.com/v1/admin/tenants/default" \ -H "Authorization: Bearer $ADMIN_KEY" | jq . # More history (up to 24 periods) curl "https://api.webresourceledger.com/v1/admin/tenants/default?periods=12" \ -H "Authorization: Bearer $ADMIN_KEY" | jq . ```

Parameters

Name Location Required Type Description
tenantId path Yes string Tenant ID.
periods query No integer Number of billing periods of history to return. Positive integer, default 6, maximum 24.

Responses

200 — Tenant detail with usage history and keys.
Property Type Required Description
tenantId string Yes
tier "free" | "pro" Yes
billingStatus "active" | "grace_period" | "blocked" Yes
gracePeriodEnd string | null No
hasPaymentMethod boolean Yes
paymentMethodAddedAt string | null No
stripeCustomerId string | null No
eidasQualified boolean No
config object No Tenant configuration overrides (admin-set).
createdAt string (date-time) No
updatedAt string (date-time) No
quota object Yes
  capturesPerMonth integer | null No
  storageBytes integer | null No
keys array of object Yes
  keyHash string No SHA-256 hex hash of the API key.
  name string | null No
  scopes array of string No
  createdAt string (date-time) No
  createdBy string | null No
usageHistory array of object Yes Usage counters per billing period, most recent first.
  period string No
  captureCount integer No
  storageBytes integer No
  apiCallCount integer No
  eidasCaptureCount integer No
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."
}
GET /v1/admin/overview Admin key

Get platform overview statistics

Returns platform-wide aggregate statistics: tenant counts by tier and billing status, total captures (current period and all-time), storage, and active API key count. Designed for the admin dashboard overview cards. Rate limited to 30 requests per 60 seconds per IP. ```bash curl "https://api.webresourceledger.com/v1/admin/overview" \ -H "Authorization: Bearer $ADMIN_KEY" | jq . ```

Parameters

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

Responses

200 — Platform-wide aggregate statistics.
Property Type Required Description
totalTenants integer Yes
totalCapturesCurrentPeriod integer Yes
totalCapturesAllTime integer Yes
currentPeriodStorageBytes integer Yes
totalEidasCaptures integer No
tenantsByTier object Yes
  free integer No
  pro integer No
tenantsByBillingStatus object Yes
  active integer No
  gracePeriod integer No
  blocked integer No
activeApiKeys integer Yes
period string Yes
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."
}

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" | "capture.quarantined" Yes Event types this webhook subscribes to. At least one required. Valid values: capture.complete, capture.failed, capture.quarantined.
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).
signatureHeader string Yes The X-WRL-Signature-256 header value sent to the webhook endpoint. Use this to test your signature verification logic without inspecting server logs. Format: t={unix_timestamp},v1={hex_hmac_sha256}.
timestampHeader string Yes The X-WRL-Timestamp header value sent to the webhook endpoint (Unix seconds). Use this alongside signatureHeader to verify your timestamp tolerance logic.
sentPayload string Yes The exact JSON string sent as the request body to the webhook endpoint. Use this as the input to your HMAC-SHA256 verification to confirm you are signing the correct bytes.
{
  "success": true,
  "httpStatus": 200,
  "latencyMs": 142,
  "signatureHeader": "t=1711108800,v1=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
  "timestampHeader": "1711108800",
  "sentPayload": "{\"id\":\"evt_00000000000000000000000000000000\",\"type\":\"ping\",\"createdAt\":\"2026-03-22T12:05:00.000Z\",\"data\":{\"webhookId\":\"whk_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6\"}}"
}
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."
}
GET /v1/account/notifications

Get email notification preferences

Returns the authenticated tenant's email notification preferences. If no preferences have been configured, synthesised defaults are returned (all notification types enabled, email null, updatedAt null).

Responses

200 — Current notification preferences for the authenticated tenant.
Property Type Required Description
email string | null Yes Email address for notifications. Null if none is configured. Set to null to clear the configured address.
emailVerified boolean Yes Whether the email address has been verified by a confirmation email. Automatically reset to false when email changes.
emailSource "github" | "manual" Yes How the email address was obtained. 'github' means it was pulled from the GitHub OAuth profile. 'manual' means it was explicitly set via PUT /v1/account/notifications.
notifications object Yes Per-event-type subscription flags. All default to true (subscribed). Set a value to false to suppress that category of email.
  capture_failure boolean Yes Alerts when a scheduled or API-triggered capture fails.
  approaching_limit boolean Yes Warnings when the tenant is approaching their monthly quota.
  limit_reached boolean Yes Alerts when the monthly capture quota is exhausted.
  invoice_generated boolean Yes Notifications when a new invoice is generated.
  payment_failure boolean Yes Alerts when a payment attempt fails.
  weekly_digest boolean Yes Weekly summary of capture activity and usage.
updatedAt string | null Yes ISO 8601 timestamp of the last update. Null if defaults have never been overridden.
{
  "email": null,
  "emailVerified": false,
  "emailSource": "github",
  "notifications": {
    "capture_failure": true,
    "approaching_limit": true,
    "limit_reached": true,
    "invoice_generated": true,
    "payment_failure": true,
    "weekly_digest": true
  },
  "updatedAt": null
}
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."
}
PUT /v1/account/notifications

Update email notification preferences

Partially updates the authenticated tenant's notification preferences. Both `email` and `notifications` are optional. The `notifications` object uses merge semantics -- only keys present in the request are changed. Changing `email` automatically resets `emailVerified` to false and sets `emailSource` to 'manual'. Setting `email` to null clears the address. Requires the `X-WRL-CSRF` header.

Request body required

Property Type Required Description
email string | null No Email address to use for notifications. Set to null to clear. Changing this field resets emailVerified to false and emailSource to 'manual'.
notifications object No Merge-patch for notification subscriptions. Only keys present in this object are changed; absent keys retain their current value.
  capture_failure boolean No
  approaching_limit boolean No
  limit_reached boolean No
  invoice_generated boolean No
  payment_failure boolean No
  weekly_digest boolean No
{
  "email": "user@example.com"
}

Responses

200 — Updated notification preferences.
Property Type Required Description
email string | null Yes Email address for notifications. Null if none is configured. Set to null to clear the configured address.
emailVerified boolean Yes Whether the email address has been verified by a confirmation email. Automatically reset to false when email changes.
emailSource "github" | "manual" Yes How the email address was obtained. 'github' means it was pulled from the GitHub OAuth profile. 'manual' means it was explicitly set via PUT /v1/account/notifications.
notifications object Yes Per-event-type subscription flags. All default to true (subscribed). Set a value to false to suppress that category of email.
  capture_failure boolean Yes Alerts when a scheduled or API-triggered capture fails.
  approaching_limit boolean Yes Warnings when the tenant is approaching their monthly quota.
  limit_reached boolean Yes Alerts when the monthly capture quota is exhausted.
  invoice_generated boolean Yes Notifications when a new invoice is generated.
  payment_failure boolean Yes Alerts when a payment attempt fails.
  weekly_digest boolean Yes Weekly summary of capture activity and usage.
updatedAt string | null Yes ISO 8601 timestamp of the last update. Null if defaults have never been overridden.
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."
}
403 — Forbidden — authenticated but insufficient permissions for this operation.
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": 403,
  "title": "Forbidden",
  "detail": "You do not have permission to perform this action."
}
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."
}

Notifications

Email notification preferences and one-click unsubscribe

GET /v1/account/notifications

Get email notification preferences

Returns the authenticated tenant's email notification preferences. If no preferences have been configured, synthesised defaults are returned (all notification types enabled, email null, updatedAt null).

Responses

200 — Current notification preferences for the authenticated tenant.
Property Type Required Description
email string | null Yes Email address for notifications. Null if none is configured. Set to null to clear the configured address.
emailVerified boolean Yes Whether the email address has been verified by a confirmation email. Automatically reset to false when email changes.
emailSource "github" | "manual" Yes How the email address was obtained. 'github' means it was pulled from the GitHub OAuth profile. 'manual' means it was explicitly set via PUT /v1/account/notifications.
notifications object Yes Per-event-type subscription flags. All default to true (subscribed). Set a value to false to suppress that category of email.
  capture_failure boolean Yes Alerts when a scheduled or API-triggered capture fails.
  approaching_limit boolean Yes Warnings when the tenant is approaching their monthly quota.
  limit_reached boolean Yes Alerts when the monthly capture quota is exhausted.
  invoice_generated boolean Yes Notifications when a new invoice is generated.
  payment_failure boolean Yes Alerts when a payment attempt fails.
  weekly_digest boolean Yes Weekly summary of capture activity and usage.
updatedAt string | null Yes ISO 8601 timestamp of the last update. Null if defaults have never been overridden.
{
  "email": null,
  "emailVerified": false,
  "emailSource": "github",
  "notifications": {
    "capture_failure": true,
    "approaching_limit": true,
    "limit_reached": true,
    "invoice_generated": true,
    "payment_failure": true,
    "weekly_digest": true
  },
  "updatedAt": null
}
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."
}
PUT /v1/account/notifications

Update email notification preferences

Partially updates the authenticated tenant's notification preferences. Both `email` and `notifications` are optional. The `notifications` object uses merge semantics -- only keys present in the request are changed. Changing `email` automatically resets `emailVerified` to false and sets `emailSource` to 'manual'. Setting `email` to null clears the address. Requires the `X-WRL-CSRF` header.

Request body required

Property Type Required Description
email string | null No Email address to use for notifications. Set to null to clear. Changing this field resets emailVerified to false and emailSource to 'manual'.
notifications object No Merge-patch for notification subscriptions. Only keys present in this object are changed; absent keys retain their current value.
  capture_failure boolean No
  approaching_limit boolean No
  limit_reached boolean No
  invoice_generated boolean No
  payment_failure boolean No
  weekly_digest boolean No
{
  "email": "user@example.com"
}

Responses

200 — Updated notification preferences.
Property Type Required Description
email string | null Yes Email address for notifications. Null if none is configured. Set to null to clear the configured address.
emailVerified boolean Yes Whether the email address has been verified by a confirmation email. Automatically reset to false when email changes.
emailSource "github" | "manual" Yes How the email address was obtained. 'github' means it was pulled from the GitHub OAuth profile. 'manual' means it was explicitly set via PUT /v1/account/notifications.
notifications object Yes Per-event-type subscription flags. All default to true (subscribed). Set a value to false to suppress that category of email.
  capture_failure boolean Yes Alerts when a scheduled or API-triggered capture fails.
  approaching_limit boolean Yes Warnings when the tenant is approaching their monthly quota.
  limit_reached boolean Yes Alerts when the monthly capture quota is exhausted.
  invoice_generated boolean Yes Notifications when a new invoice is generated.
  payment_failure boolean Yes Alerts when a payment attempt fails.
  weekly_digest boolean Yes Weekly summary of capture activity and usage.
updatedAt string | null Yes ISO 8601 timestamp of the last update. Null if defaults have never been overridden.
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."
}
403 — Forbidden — authenticated but insufficient permissions for this operation.
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": 403,
  "title": "Forbidden",
  "detail": "You do not have permission to perform this action."
}
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."
}
GET /v1/notifications/unsubscribe No auth

Render email unsubscribe confirmation page

Renders an HTML confirmation page for the given unsubscribe token. The page contains a form that the user must submit (POST) to complete the unsubscribe action. Does NOT auto-unsubscribe: email security scanners pre-fetch GET URLs, so the actual unsubscribe requires an explicit form submission. Returns 200 for both valid and invalid tokens. Invalid tokens display an error message without leaking whether the token is known. No authentication required. Rate-limited by IP.

Parameters

Name Location Required Type Description
token query No string HMAC-signed unsubscribe token from the email link.

Responses

200 — HTML confirmation page (always 200, valid or invalid token).
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/notifications/unsubscribe No auth

Process email unsubscribe (RFC 8058 One-Click)

Processes the unsubscribe action for a valid token. Accepts `application/x-www-form-urlencoded` with `List-Unsubscribe=One-Click` (RFC 8058) or the `token` query parameter (form submission from the GET page). Idempotent: unsubscribing twice is a no-op. Returns 200 HTML for both valid and invalid tokens (no information leakage). No authentication required. Rate-limited by IP.

Parameters

Name Location Required Type Description
token query No string HMAC-signed unsubscribe token. Preferred source for form submissions.

Request body

Property Type Required Description
List-Unsubscribe "One-Click" No RFC 8058 One-Click header value.

Responses

200 — HTML confirmation page (always 200, valid or invalid token).
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."
}

Schedules

Scheduled recurring captures (cron-style)

GET /v1/schedules Tenant key

List schedules

Returns all schedules for the authenticated tenant.

Responses

200 — List of schedules for the tenant.
Property Type Required Description
data array of object Yes Schedules for the tenant. Empty array when none exist; never null.
  id string Yes Unique schedule identifier.
  url string (uri) Yes URL to capture on each scheduled run.
  name string Yes Human-readable label for this schedule.
  cron string Yes Standard 5-field cron expression. Minimum interval: 1 hour.
  paused boolean Yes When true, the schedule is not executed at its next run time.
  nextRunAt string | null Yes ISO 8601 timestamp of the next scheduled run. Null when the schedule is paused.
  lastRunAt string | null No ISO 8601 timestamp of the most recent run. Null when the schedule has not run yet.
  lastCaptureId string | null No Capture ID from the most recent run. Null when the schedule has not run yet.
  lastCaptureStatus string | null No Status of the most recent capture. Null when the schedule has not run yet.
  createdAt string (date-time) Yes ISO 8601 timestamp when this schedule was created.
{
  "data": []
}
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/schedules Tenant key

Create a scheduled capture

Creates a new scheduled recurring capture. The schedule runs automatically at the cadence defined by the cron expression. Each run submits a new capture for the given URL and counts against the tenant's monthly quota. Minimum interval is 1 hour. Schedules are active immediately after creation.

Request body required

Property Type Required Description
url string (uri) Yes Public http or https URL to capture on each run. Must not resolve to a private IP.
name string Yes Human-readable label for this schedule. Maximum 128 characters.
cron string Yes Standard 5-field cron expression. Minimum interval: 1 hour.
{
  "url": "https://example.com",
  "name": "Daily homepage capture",
  "cron": "0 0 * * *"
}

Responses

201 — Schedule created.
Property Type Required Description
id string Yes Unique schedule identifier.
url string (uri) Yes URL to capture on each scheduled run.
name string Yes Human-readable label for this schedule.
cron string Yes Standard 5-field cron expression. Minimum interval: 1 hour.
paused boolean Yes When true, the schedule is not executed at its next run time.
nextRunAt string | null Yes ISO 8601 timestamp of the next scheduled run. Null when the schedule is paused.
lastRunAt string | null No ISO 8601 timestamp of the most recent run. Null when the schedule has not run yet.
lastCaptureId string | null No Capture ID from the most recent run. Null when the schedule has not run yet.
lastCaptureStatus string | null No Status of the most recent capture. Null when the schedule has not run yet.
createdAt string (date-time) Yes ISO 8601 timestamp when this schedule was created.
{
  "id": "sch_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
  "url": "https://example.com",
  "name": "Daily homepage capture",
  "cron": "0 0 * * *",
  "paused": false,
  "nextRunAt": "2026-04-01T00:00:00.000Z",
  "lastRunAt": null,
  "lastCaptureId": null,
  "lastCaptureStatus": null,
  "createdAt": "2026-03-23T10:00: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."
}
429 — Rate limit exceeded or schedule limit reached. The response body includes a `limitType` field: `'rate'` for burst rate limiting, `'schedules'` when the tenant's maximum schedule count is reached.
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": "Schedule limit reached. Delete an existing schedule before creating a new one.",
  "limitType": "schedules"
}
GET /v1/schedules/{scheduleId} Tenant key

Get a schedule

Returns the schedule with the given ID.

Responses

200 — Schedule record.
Property Type Required Description
id string Yes Unique schedule identifier.
url string (uri) Yes URL to capture on each scheduled run.
name string Yes Human-readable label for this schedule.
cron string Yes Standard 5-field cron expression. Minimum interval: 1 hour.
paused boolean Yes When true, the schedule is not executed at its next run time.
nextRunAt string | null Yes ISO 8601 timestamp of the next scheduled run. Null when the schedule is paused.
lastRunAt string | null No ISO 8601 timestamp of the most recent run. Null when the schedule has not run yet.
lastCaptureId string | null No Capture ID from the most recent run. Null when the schedule has not run yet.
lastCaptureStatus string | null No Status of the most recent capture. Null when the schedule has not run yet.
createdAt string (date-time) Yes ISO 8601 timestamp when this schedule was created.
{
  "id": "sch_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
  "url": "https://example.com",
  "name": "Daily homepage capture",
  "cron": "0 0 * * *",
  "paused": false,
  "nextRunAt": "2026-04-01T00:00:00.000Z",
  "lastRunAt": "2026-03-31T00:00:00.000Z",
  "lastCaptureId": "cap_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
  "lastCaptureStatus": "complete",
  "createdAt": "2026-03-01T10: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."
}
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."
}
DELETE /v1/schedules/{scheduleId} Tenant key

Delete a schedule

Permanently deletes the schedule. Any capture currently in progress from the schedule's last run is not affected. The schedule will not trigger any further captures after deletion.

Responses

200 — Schedule deleted.
Property Type Required Description
id string Yes ID of the deleted schedule.
deleted "true" Yes Always true in this response.
{
  "id": "sch_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
  "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."
}