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
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
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."
}
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."
}
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. |
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."
}
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."
}
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."
}
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."
}
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."
}
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
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
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."
}
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)
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"
}
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"
}
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 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."
}
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. |
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 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 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
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."
}
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 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."
}
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 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 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."
}
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 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."
}
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."
}
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."
}
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)
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."
}
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 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 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."
}