API Reference
This page documents all Web Resource Ledger API endpoints (v0.6.0).
See the Authentication guide for how to obtain and use API keys.
Base URLs
-
Production:
https://api.webresourceledger.com
-
Staging:
https://staging.webresourceledger.com
Authentication: Most endpoints require a Bearer token in the
Authorization header. Tenant endpoints use per-tenant API keys;
admin endpoints use the infrastructure ADMIN_KEY.
See the Authentication guide for details.
Health
Service health
Health check
Responses
200
— Service is running.
| Property |
Type |
Required |
Description |
status |
"ok" |
Yes |
|
legal |
object |
Yes |
|
terms |
string (uri) |
Yes |
|
policy |
string (uri) |
Yes |
|
{
"status": "ok",
"legal": {
"terms": "https://github.com/benpeter/web-resource-ledger/blob/main/TERMS.md",
"policy": "https://github.com/benpeter/web-resource-ledger/blob/main/CONTENT-POLICY.md"
}
}
Captures
Web page capture lifecycle
List captures
Returns a paginated list of captures for the authenticated tenant in descending chronological order (newest first by default). Supports filtering by status, URL prefix, and date range, with configurable sort order.
Pagination is offset-based. Use `offset` and `limit` to navigate pages. The response includes a `total` count for the matched query.
Parameters
| Name |
Location |
Required |
Type |
Description |
limit |
query |
No |
integer |
Number of results per page. Default 20. Values above 100 are silently clamped to 100. Values below 1 or non-integers return 400.
|
offset |
query |
No |
integer |
Zero-based offset for pagination. Default 0 (first page).
|
status |
query |
No |
"pending" | "complete" | "failed" |
Filter results to captures with this status. Omit to return all statuses.
|
url |
query |
No |
string |
Filter by URL prefix match. Minimum 4 characters. Must not contain `%` or `_` wildcard characters. Returns captures whose URL starts with the given string.
|
created_after |
query |
No |
string (date-time) |
Filter to captures created at or after this ISO 8601 timestamp (inclusive). Use with `created_before` for date range queries.
|
created_before |
query |
No |
string (date-time) |
Filter to captures created before this ISO 8601 timestamp (exclusive). Must be after `created_after` if both are provided.
|
sort |
query |
No |
"created_at" | "-created_at" |
Sort order. `-created_at` (default) for newest first, `created_at` for oldest first.
|
Responses
200
— Paginated list of capture summaries. Returns empty data array when no captures exist.
| Property |
Type |
Required |
Description |
data |
array of object |
Yes |
Capture summaries for the current page. Empty array when no results; never null. |
id |
string |
Yes |
Unique capture identifier. Also serves as the access secret for per-capture access. |
status |
"pending" | "complete" | "failed" |
Yes |
Lifecycle state of the capture. |
url |
string (uri) |
Yes |
The URL that was captured. |
createdAt |
string (date-time) |
Yes |
ISO 8601 timestamp when the capture was submitted. |
completedAt |
string (date-time) |
No |
Present when status is "complete". ISO 8601 timestamp when capture completed. |
renderQuality |
"full" | "partial" |
No |
Present when status is "complete". Indicates whether the page fully rendered or was captured after a navigation timeout.
|
failedAt |
string (date-time) |
No |
Present when status is "failed". ISO 8601 timestamp when capture failed. |
error |
string |
No |
Present when status is "failed". Human-readable failure reason. |
retryable |
boolean |
No |
Present when status is "failed". True if submitting a new capture may succeed. |
pagination |
object |
Yes |
Offset-based pagination metadata. |
total |
integer |
Yes |
Total number of results matching the query (before pagination). |
offset |
integer |
Yes |
Zero-based offset of the first result in this page. |
hasMore |
boolean |
Yes |
True when additional results exist beyond the current page. |
limit |
integer |
Yes |
Effective page size used for this response. |
{
"data": [],
"pagination": {
"total": 0,
"offset": 0,
"hasMore": false,
"limit": 20
}
}
400
— Bad request — missing or malformed body, missing `url` field, invalid URL scheme.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 400,
"title": "Bad Request",
"detail": "Request body is missing or not valid JSON."
}
401
— Unauthorized — missing, malformed, or invalid Authorization header.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 401,
"title": "Unauthorized",
"detail": "Authorization header is required."
}
429
— Too Many Requests — rate limit or monthly quota exceeded.
The response body includes a `limitType` extension field that distinguishes the specific 429 variant:
- `limitType` absent: IP-based rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'tenant'`: Per-tenant rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'quota'`: Monthly capture quota exhausted. A `quota` extension object
is present with `limit`, `used`, `resource`, `resetsAt`, and (for batch) `requested`.
Retrying before `resetsAt` will continue to fail.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 429,
"title": "Too Many Requests",
"detail": "Rate limit exceeded. Try again later."
}
500
— Internal Server Error — database read failed.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 500,
"title": "Internal Server Error",
"detail": "Could not list captures"
}
503
— Service Unavailable -- required configuration is missing or service is at capacity.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 503,
"title": "Service Unavailable",
"detail": "Service is at capacity. Retry in 10 seconds."
}
Submit a URL for capture
Enqueues a headless-browser capture of the given URL. Returns immediately with a capture ID; poll the status URL to determine when the capture is complete.
Monthly quota: each accepted capture counts against the tenant's monthly capture quota. When the quota is exhausted the endpoint returns 429 with `limitType: 'quota'` in the response body. Quota headers (`X-Quota-Limit`, `X-Quota-Used`, `X-Quota-Remaining`) are present on successful 202 responses.
CORS: This endpoint supports cross-origin requests from allowed origins. Preflight (OPTIONS) is handled automatically. Configure allowed origins via the CORS_ORIGINS environment variable.
Request body required
| Property |
Type |
Required |
Description |
url |
string (uri) |
Yes |
Public http or https URL to capture. Must not resolve to a private IP. |
{
"url": "https://example.com"
}
Responses
202
— Capture accepted and queued.
| Property |
Type |
Required |
Description |
id |
string |
Yes |
Unique capture identifier. Also serves as the access secret for per-capture access. |
statusUrl |
string (uri) |
Yes |
Absolute URL to poll for capture status. |
note |
string |
Yes |
Advisory message about related API capabilities. |
{
"id": "cap_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"statusUrl": "https://api.webresourceledger.com/v1/captures/cap_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6/status",
"note": "Use GET /v1/captures to list and search your captures."
}
400
— Bad request — missing or malformed body, missing `url` field, invalid URL scheme.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 400,
"title": "Bad Request",
"detail": "Request body is missing or not valid JSON."
}
401
— Unauthorized — missing, malformed, or invalid Authorization header.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 401,
"title": "Unauthorized",
"detail": "Authorization header is required."
}
415
— Unsupported Media Type — Content-Type is not application/json.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 415,
"title": "Unsupported Media Type",
"detail": "Content-Type must be application/json"
}
422
— Unprocessable Content — private IP, embedded credentials, or double-encoded URL.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 422,
"title": "Unprocessable Content",
"detail": "Host resolves to a private IP address."
}
429
— Too Many Requests — rate limit or monthly quota exceeded. See `limitType` in the response body to distinguish rate limiting (`limitType` absent or `'tenant'`) from quota exhaustion (`limitType: 'quota'`).
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 429,
"title": "Too Many Requests",
"detail": "Rate limit exceeded. Try again later."
}
503
— Service Unavailable -- service is at capacity.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 503,
"title": "Service Unavailable",
"detail": "Service is at capacity. Retry in 10 seconds."
}
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 is required — the capture ID acts as the access secret. Poll until status is "complete" or "failed".
Parameters
| Name |
Location |
Required |
Type |
Description |
captureId |
path |
Yes |
string |
Capture ID returned by POST /v1/captures. |
Responses
200
— Capture found. Check `status` field for current state.
| Property |
Type |
Required |
Description |
id |
string |
Yes |
Unique capture identifier. Also serves as the access secret for per-capture access. |
status |
"pending" | "complete" | "failed" |
Yes |
Lifecycle state of the capture. |
captureUrl |
string (uri) |
No |
Present when status is "complete". URL to retrieve capture metadata and artifact links. |
error |
string |
No |
Present when status is "failed". Human-readable failure reason. |
retryable |
boolean |
No |
Present when status is "failed". True if submitting a new capture may succeed. |
{
"id": "cap_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"status": "pending"
}
404
— Not found.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 404,
"title": "Not Found",
"detail": "Capture not found."
}
Retrieve a completed capture
Returns metadata and artifact links for a completed capture. The capture ID acts as the access secret -- no authentication header is required. Returns 404 if the capture does not exist or has not yet completed.
Parameters
| Name |
Location |
Required |
Type |
Description |
captureId |
path |
Yes |
string |
Capture ID returned by POST /v1/captures. |
Responses
200
— Capture found and complete.
| Property |
Type |
Required |
Description |
id |
string |
Yes |
Unique capture identifier. Also serves as the access secret for per-capture access. |
status |
"complete" |
Yes |
Always "complete" -- this endpoint only returns completed captures. |
url |
string (uri) |
Yes |
The URL that was captured. |
createdAt |
string (date-time) |
Yes |
ISO 8601 timestamp when the capture was submitted. |
completedAt |
string (date-time) |
Yes |
ISO 8601 timestamp when the capture completed. |
renderQuality |
"full" | "partial" |
Yes |
Render quality of the capture. 'full' when the page reached the load milestone before capture (followed by an adaptive settle phase, up to 3s). 'partial' when the capture was taken after a navigation timeout but DOMContentLoaded had fired. Defaults to 'full' for captures created before this feature.
|
render |
object |
No |
Rendering process metadata. Present on captures created after this feature. When absent, the page reached the load milestone (consistent with renderQuality: full).
|
waitUntilReached |
"domcontentloaded" | "load" | "networkidle" |
Yes |
Highest browser readiness milestone confirmed before the page was captured. "load" means the load event fired (default for new captures). "networkidle" means fewer than two open network connections for 500ms (used by older captures -- retained in enum for backward compatibility). "domcontentloaded" means the DOM was parsed but some resources may still be loading.
|
timedOut |
boolean |
Yes |
True when the navigation did not reach the target milestone (load) within the timeout window. When true, renderQuality is "partial".
|
durationMs |
integer |
Yes |
Wall-clock milliseconds from navigation start to the point the page was captured (either at load + adaptive settle, or at timeout).
|
settleMs |
integer |
No |
Wall-clock milliseconds spent in the post-load adaptive settle phase. Settle ends when in-flight requests drain (settleReason: idle) or the 3s hard cap is reached (settleReason: cap). Absent on partial captures and captures created before this feature.
|
settleReason |
"idle" | "cap" |
No |
Why the settle phase ended. "idle" means all non-persistent in-flight requests completed within the quiescence window. "cap" means the 3s hard cap was reached before quiescence. Absent on partial captures and captures created before this feature.
|
stages |
object |
No |
Per-stage timing breakdown. Present on captures created after stage-level instrumentation was deployed. Absent on earlier captures.
|
artifacts |
object |
Yes |
Named artifact URLs for a complete capture. All fields present for a complete capture except headers, which is absent if the HTTP header fetch failed or timed out. The html artifact is served as text/plain with Content-Disposition: attachment to prevent XSS -- do not embed in iframes or inject as HTML.
The `screenshot` field always points to the best-available screenshot (backward compatible). `screenshotBefore` is the pre-consent-dismissal screenshot; present only when consent was detected and dismissed.
|
screenshot |
string (uri) |
Yes |
Full-page PNG screenshot (best available). When consent was dismissed, this is the post-dismissal screenshot. Served via the API Worker with Content-Type: image/png and Content-Disposition: attachment.
|
screenshotBefore |
string (uri) |
No |
Pre-consent-dismissal screenshot. Present only when consent was detected and successfully dismissed. Corresponds to the page state before the CMP was closed. Served as image/png with Content-Disposition: attachment.
|
html |
string (uri) |
Yes |
Rendered HTML after JavaScript execution. Served as text/plain with Content-Disposition: attachment -- NOT as text/html -- to prevent stored XSS.
|
headers |
string (uri) |
No |
JSON object of HTTP response headers captured at fetch time. Set-Cookie values are redacted to [redacted]. Absent if the header fetch failed or timed out.
|
wacz |
object |
No |
WACZ bundle metadata. Present when status is "complete" AND a signing key was configured. Absent otherwise -- not an error condition.
|
url |
string (uri) |
Yes |
URL to the signed WACZ bundle. Served via the API Worker with Content-Type: application/wacz+zip and Content-Disposition: attachment.
|
bundleHash |
string |
Yes |
SHA-256 hash of the canonical JSON of the WACZ datapackage.json, in the form "sha256:{hex}". Use to verify bundle integrity.
|
size |
integer |
Yes |
Size of the WACZ bundle in bytes. |
verifyUrl |
string (uri) |
No |
URL to the human-readable verification page for this capture's WACZ bundle. Present only when `wacz` is present. Omitted when the capture has no WACZ bundle (e.g., partial captures from navigation timeouts).
|
captureSettings |
object |
No |
Settings and metadata about how the capture was performed. Present when capture settings were recorded (e.g., consent handling was attempted).
|
version |
integer |
Yes |
Schema version for captureSettings. |
consent |
object |
No |
Consent handling metadata. Present when consent opt-out was attempted. |
{
"id": "cap_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"status": "complete",
"url": "https://example.com",
"createdAt": "2024-01-15T10:30:00.000Z",
"completedAt": "2024-01-15T10:30:45.123Z",
"renderQuality": "full",
"artifacts": {
"screenshot": "https://api.webresourceledger.com/v1/captures/cap_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6/artifacts/screenshot",
"html": "https://api.webresourceledger.com/v1/captures/cap_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6/artifacts/html",
"headers": "https://api.webresourceledger.com/v1/captures/cap_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6/artifacts/headers"
},
"wacz": {
"url": "https://api.webresourceledger.com/v1/captures/cap_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6/artifacts/wacz",
"bundleHash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"size": 204800
},
"verifyUrl": "https://api.webresourceledger.com/v1/verify/cap_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
}
404
— Not found.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 404,
"title": "Not Found",
"detail": "Capture not found."
}
Download a capture artifact
Returns the raw artifact binary or text for a completed capture. The capture ID acts as the access secret -- no authentication header is required. Responses include Content-Disposition: attachment to prevent browser rendering.
Parameters
| Name |
Location |
Required |
Type |
Description |
captureId |
path |
Yes |
string |
Capture ID returned by POST /v1/captures. |
name |
path |
Yes |
"screenshot-before" | "screenshot" | "html" | "headers" | "wacz" |
Artifact name to retrieve. |
Responses
200
— Artifact found. Content-Type depends on artifact name.
404
— Not found.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 404,
"title": "Not Found",
"detail": "Capture not found."
}
Verification
WACZ bundle cryptographic verification
Verify a capture's WACZ bundle
Performs cryptographic verification of the WACZ bundle for a completed capture and returns the result. No authentication required.
Verification runs up to four checks: artifactHashes, bundleHash, signature, and (for v0.2.0 bundles) timestamp. The timestamp check verifies an RFC 3161 independent timestamp against the bundle hash. Legacy v0.1.0 bundles produce three checks; the timestamp check is absent.
Content negotiation: browsers (Accept: text/html) receive a self-contained HTML verification page that fetches the JSON result client-side. API clients receive JSON directly.
**A 200 status does NOT mean the capture is verified -- check the `verified` field.** The endpoint returns 200 for all reachable captures, even when verification fails.
Captures without a WACZ bundle (including partial captures from navigation timeouts) return 404 from this endpoint.
Parameters
| Name |
Location |
Required |
Type |
Description |
captureId |
path |
Yes |
string |
Capture ID to verify. |
Responses
200
— Verification result. Check the `verified` boolean field -- a 200 status does not mean the capture passed verification.
| Property |
Type |
Required |
Description |
verified |
boolean |
Yes |
True only when all checks pass or skip. False for any failure. |
capture |
object |
Yes |
Capture identity and timing metadata, extracted from the database record. |
id |
string |
Yes |
Unique capture identifier. Also serves as the access secret for per-capture access. |
createdAt |
string (date-time) |
Yes |
ISO 8601 timestamp when the capture was submitted. |
completedAt |
string (date-time) |
Yes |
ISO 8601 timestamp when the capture completed. |
renderQuality |
"full" | "partial" |
No |
Render quality of the verified capture. |
signing |
object | null |
Yes |
Cryptographic metadata from the WACZ digest document. Null when the WACZ bundle was not found in storage or the digest document is absent.
|
checks |
array of object |
Yes |
Verification check results, always in order: artifactHashes, bundleHash, signature, and (for v0.2.0 bundles) timestamp. Legacy v0.1.0 bundles produce three checks; v0.2.0 bundles produce up to four.
|
name |
"artifactHashes" | "bundleHash" | "signature" | "timestamp" |
Yes |
Which check this result represents. artifactHashes verifies individual file integrity; bundleHash verifies the overall archive; signature verifies the Ed25519 signature over the bundle hash; timestamp verifies the RFC 3161 independent timestamp when present (v0.2.0 bundles only).
|
status |
"pass" | "fail" | "skip" |
Yes |
Outcome of the check. "skip" means the check was not performed because a prerequisite check failed.
|
detail |
string |
No |
Human-readable failure reason. Present only when status is "fail" or "skip" with context. |
captureSettings |
object |
No |
Settings and metadata about how the capture was performed. Present when capture settings were recorded.
|
version |
integer |
Yes |
Schema version for captureSettings. |
consent |
object |
No |
Consent handling metadata. Present when consent opt-out was attempted. |
{
"verified": true,
"capture": {
"id": "cap_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"createdAt": "2024-01-15T10:30:00.000Z",
"completedAt": "2024-01-15T10:30:45.123Z",
"renderQuality": "full"
},
"signing": {
"bundleHash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"signature": "base64encodedSignatureHere==",
"publicKey": "base64encodedPublicKeyHere=",
"signedAt": "2024-01-15T10:30:44.000Z",
"timestamp": {
"genTime": "2024-01-15T10:30:46.000Z",
"tsa": "https://freetsa.org/tsr"
}
},
"checks": [
{
"name": "artifactHashes",
"status": "pass"
},
{
"name": "bundleHash",
"status": "pass"
},
{
"name": "signature",
"status": "pass"
},
{
"name": "timestamp",
"status": "pass"
}
]
}
404
— Not found.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 404,
"title": "Not Found",
"detail": "Capture not found."
}
422
— Unprocessable Content — WACZ bundle exceeds maximum verifiable size.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 422,
"title": "Unprocessable Content",
"detail": "WACZ bundle exceeds maximum verifiable size"
}
429
— Too Many Requests — rate limit or monthly quota exceeded.
The response body includes a `limitType` extension field that distinguishes the specific 429 variant:
- `limitType` absent: IP-based rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'tenant'`: Per-tenant rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'quota'`: Monthly capture quota exhausted. A `quota` extension object
is present with `limit`, `used`, `resource`, `resetsAt`, and (for batch) `requested`.
Retrying before `resetsAt` will continue to fail.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 429,
"title": "Too Many Requests",
"detail": "Rate limit exceeded. Try again later."
}
503
— Service Unavailable -- required configuration is missing or service is at capacity.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 503,
"title": "Service Unavailable",
"detail": "Service is at capacity. Retry in 10 seconds."
}
Signing
Signing key management
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
API key management (admin-only)
List API keys
Returns all API keys, optionally filtered by tenant. Revoked keys are excluded by default. Raw key values are never included.
Rate limited to 5 requests per 60 seconds per IP.
```bash curl https://api.webresourceledger.com/v1/admin/keys \
-H "Authorization: Bearer $ADMIN_KEY" | jq .
# Filter by tenant curl "https://api.webresourceledger.com/v1/admin/keys?tenant=default" \
-H "Authorization: Bearer $ADMIN_KEY" | jq .
# Include revoked keys curl "https://api.webresourceledger.com/v1/admin/keys?include=revoked" \
-H "Authorization: Bearer $ADMIN_KEY" | jq .
```
Parameters
| Name |
Location |
Required |
Type |
Description |
tenant |
query |
No |
string |
Filter results to keys belonging to this tenant ID. |
include |
query |
No |
"revoked" |
Pass "revoked" to include revoked keys in the response. Omit to return only active keys.
|
Responses
200
— List of API key summaries. Raw key values are never included.
| Property |
Type |
Required |
Description |
data |
array of object |
Yes |
API key summaries. Empty array when no keys exist. |
keyHash |
string |
Yes |
SHA-256 hex digest of the raw key. Used as the identifier for revocation. |
tenantId |
string |
Yes |
Tenant this key belongs to. |
scopes |
array of string |
Yes |
Scopes granted to this key. |
name |
string |
Yes |
Human-readable label for the key. |
createdAt |
string (date-time) |
Yes |
ISO 8601 timestamp when the key was created. |
createdBy |
string |
Yes |
Who created the key. Currently always "admin". |
revoked |
boolean |
No |
Present and true when the key has been revoked. |
revokedAt |
string (date-time) |
No |
ISO 8601 timestamp when the key was revoked. Present only when revoked is true. |
{
"data": [
{
"keyHash": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"tenantId": "default",
"scopes": [
"capture"
],
"name": "ci-pipeline",
"createdAt": "2026-03-17T12:00:00.000Z",
"createdBy": "admin"
}
]
}
401
— Unauthorized -- missing or invalid admin key.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 401,
"title": "Unauthorized",
"detail": "Invalid admin key"
}
429
— Too Many Requests — rate limit or monthly quota exceeded.
The response body includes a `limitType` extension field that distinguishes the specific 429 variant:
- `limitType` absent: IP-based rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'tenant'`: Per-tenant rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'quota'`: Monthly capture quota exhausted. A `quota` extension object
is present with `limit`, `used`, `resource`, `resetsAt`, and (for batch) `requested`.
Retrying before `resetsAt` will continue to fail.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 429,
"title": "Too Many Requests",
"detail": "Rate limit exceeded. Try again later."
}
503
— Service Unavailable -- ADMIN_KEY is not configured.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 503,
"title": "Service Unavailable",
"detail": "Admin API is not configured"
}
Create a new API key
Creates a new per-tenant API key and returns the raw key exactly once. Store the returned key immediately -- it cannot be retrieved after this response.
Rate limited to 5 requests per 60 seconds per IP. Auth check happens after rate limit.
```bash curl -X POST https://api.webresourceledger.com/v1/admin/keys \
-H "Authorization: Bearer $ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"tenantId": "default", "scopes": ["capture"], "name": "ci-pipeline"}' | jq .
```
Save the key: ```bash curl ... | jq -r .key > /tmp/wrl-key-default.txt ```
Request body required
| Property |
Type |
Required |
Description |
tenantId |
string |
Yes |
Tenant this key belongs to. Must match /^[a-z0-9_-]{1,64}$/. Callers may use tenantId as a database key without additional sanitization.
|
scopes |
array of "capture" | "read" | "admin" |
Yes |
Non-empty list of scopes to grant. Valid values: capture, read, admin. 'capture' implies 'read'. 'admin' does NOT imply 'capture' or 'read'. Note: the 'admin' scope is reserved for a future migration from the ADMIN_KEY env var to per-tenant admin keys. It currently has no runtime effect on access control -- admin endpoints use ADMIN_KEY only.
|
name |
string |
Yes |
Human-readable label for the key. 1-128 characters using letters, digits, spaces, and _ . : - characters.
|
{
"tenantId": "default",
"scopes": [
"capture"
],
"name": "ci-pipeline"
}
Responses
201
— Key created. The raw key is returned exactly once. Store it immediately.
| Property |
Type |
Required |
Description |
key |
string |
Yes |
Raw API key prefixed with "wrl_live_". Store this value now -- it cannot be retrieved after this response.
|
keyHash |
string |
Yes |
SHA-256 hex digest of the raw key. Used as the database primary key and for revocation. |
tenantId |
string |
Yes |
Tenant this key belongs to. |
scopes |
array of string |
Yes |
Scopes granted to this key. |
name |
string |
Yes |
Human-readable label for the key. |
createdAt |
string (date-time) |
Yes |
ISO 8601 timestamp when the key was created. |
warning |
"Store this key now. It cannot be retrieved after this response." |
Yes |
Always "Store this key now. It cannot be retrieved after this response." |
{
"key": "wrl_live_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789abcdefghij",
"keyHash": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"tenantId": "default",
"scopes": [
"capture"
],
"name": "ci-pipeline",
"createdAt": "2026-03-17T12:00:00.000Z",
"warning": "Store this key now. It cannot be retrieved after this response."
}
400
— Bad request -- missing or invalid fields. Field validation messages identify the specific problem.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 400,
"title": "Bad Request",
"detail": "Field 'tenantId' is required"
}
401
— Unauthorized -- missing or invalid admin key.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 401,
"title": "Unauthorized",
"detail": "Invalid admin key"
}
415
— Unsupported Media Type -- Content-Type is not application/json.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 415,
"title": "Unsupported Media Type",
"detail": "Content-Type must be application/json"
}
429
— Too Many Requests — rate limit or monthly quota exceeded.
The response body includes a `limitType` extension field that distinguishes the specific 429 variant:
- `limitType` absent: IP-based rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'tenant'`: Per-tenant rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'quota'`: Monthly capture quota exhausted. A `quota` extension object
is present with `limit`, `used`, `resource`, `resetsAt`, and (for batch) `requested`.
Retrying before `resetsAt` will continue to fail.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 429,
"title": "Too Many Requests",
"detail": "Rate limit exceeded. Try again later."
}
503
— Service Unavailable -- ADMIN_KEY is not configured.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 503,
"title": "Service Unavailable",
"detail": "Admin API is not configured"
}
Revoke an API key
Marks an API key as revoked. The key is immediately recorded as revoked in D1. Revocation takes effect within 60 seconds due to distributed edge caching.
This operation is idempotent -- revoking an already-revoked key returns 200 with the existing revocation record.
Rate limited to 5 requests per 60 seconds per IP.
```bash curl -X DELETE "https://api.webresourceledger.com/v1/admin/keys/$KEY_HASH" \
-H "Authorization: Bearer $ADMIN_KEY" | jq .
```
Parameters
| Name |
Location |
Required |
Type |
Description |
keyHash |
path |
Yes |
string |
SHA-256 hex digest of the key to revoke (64 hex chars). |
Responses
200
— Key revoked (or was already revoked -- operation is idempotent).
| Property |
Type |
Required |
Description |
keyHash |
string |
Yes |
SHA-256 hex digest of the raw key. |
tenantId |
string |
Yes |
Tenant this key belonged to. |
scopes |
array of string |
Yes |
Scopes the key had. |
name |
string |
Yes |
Human-readable label for the key. |
createdAt |
string (date-time) |
Yes |
ISO 8601 timestamp when the key was created. |
revoked |
"true" |
Yes |
Always true in this response. |
revokedAt |
string (date-time) |
Yes |
ISO 8601 timestamp when the key was revoked. |
{
"keyHash": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"tenantId": "default",
"scopes": [
"capture"
],
"name": "ci-pipeline",
"createdAt": "2026-03-17T12:00:00.000Z",
"revoked": true,
"revokedAt": "2026-03-17T15:00:00.000Z"
}
401
— Unauthorized -- missing or invalid admin key.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 401,
"title": "Unauthorized",
"detail": "Invalid admin key"
}
404
— Key not found.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 404,
"title": "Not Found",
"detail": "API key not found."
}
429
— Too Many Requests — rate limit or monthly quota exceeded.
The response body includes a `limitType` extension field that distinguishes the specific 429 variant:
- `limitType` absent: IP-based rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'tenant'`: Per-tenant rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'quota'`: Monthly capture quota exhausted. A `quota` extension object
is present with `limit`, `used`, `resource`, `resetsAt`, and (for batch) `requested`.
Retrying before `resetsAt` will continue to fail.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 429,
"title": "Too Many Requests",
"detail": "Rate limit exceeded. Try again later."
}
503
— Service Unavailable -- ADMIN_KEY is not configured.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 503,
"title": "Service Unavailable",
"detail": "Admin API is not configured"
}
Get tenant usage counters
Returns per-tenant usage counters for the specified billing period. Defaults to the current calendar month (UTC) if no period is specified.
Rate limited to 5 requests per 60 seconds per IP. Auth check happens after rate limit.
```bash # Current period usage curl "https://api.webresourceledger.com/v1/admin/usage?tenant=default" \
-H "Authorization: Bearer $ADMIN_KEY" | jq .
# Specific period curl "https://api.webresourceledger.com/v1/admin/usage?tenant=default&period=2026-03" \
-H "Authorization: Bearer $ADMIN_KEY" | jq .
```
Parameters
| Name |
Location |
Required |
Type |
Description |
tenant |
query |
Yes |
string |
Tenant ID to query usage for. |
period |
query |
No |
string |
Billing period in YYYY-MM format. Defaults to the current calendar month (UTC) if omitted.
|
Responses
200
— Usage counters for the specified tenant and period.
| Property |
Type |
Required |
Description |
tenantId |
string |
Yes |
Tenant identifier. |
period |
string |
Yes |
Billing period in YYYY-MM format. |
captureCount |
integer |
Yes |
Number of captures completed in this period. |
storageBytes |
integer |
Yes |
Total R2 storage bytes consumed by captures in this period. |
apiCallCount |
integer |
Yes |
Number of authenticated API calls in this period. |
updatedAt |
string | null |
Yes |
Last time counters were updated. Null if no activity in the period.
|
{
"tenantId": "default",
"period": "2026-03",
"captureCount": 42,
"storageBytes": 157286400,
"apiCallCount": 128,
"updatedAt": "2026-03-22T14:30:00.000Z"
}
400
— Bad request — missing or malformed body, missing `url` field, invalid URL scheme.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 400,
"title": "Bad Request",
"detail": "Request body is missing or not valid JSON."
}
401
— Unauthorized — missing, malformed, or invalid Authorization header.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 401,
"title": "Unauthorized",
"detail": "Authorization header is required."
}
404
— Tenant not found.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 404,
"title": "Not Found",
"detail": "Tenant 'nonexistent' not found"
}
429
— Too Many Requests — rate limit or monthly quota exceeded.
The response body includes a `limitType` extension field that distinguishes the specific 429 variant:
- `limitType` absent: IP-based rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'tenant'`: Per-tenant rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'quota'`: Monthly capture quota exhausted. A `quota` extension object
is present with `limit`, `used`, `resource`, `resetsAt`, and (for batch) `requested`.
Retrying before `resetsAt` will continue to fail.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 429,
"title": "Too Many Requests",
"detail": "Rate limit exceeded. Try again later."
}
Webhooks
Outbound webhook registration and management
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" |
Yes |
Event types this webhook subscribes to. At least one required. Valid values: capture.complete, capture.failed.
|
name |
string |
Yes |
Human-readable label for this webhook. 1-128 characters using letters, digits, spaces, and _ . : - characters.
|
{
"url": "https://hooks.example.com/wrl-events",
"events": [
"capture.complete",
"capture.failed"
],
"name": "ci-notifier"
}
Responses
201
— Webhook registered. Store the secret -- it is shown only once.
| Property |
Type |
Required |
Description |
id |
string |
Yes |
Unique webhook identifier. |
url |
string (uri) |
Yes |
The HTTPS endpoint registered. |
events |
array of string |
Yes |
Event types this webhook is subscribed to. |
name |
string |
Yes |
Human-readable label for this webhook. |
secret |
string |
Yes |
Webhook signing secret. Prefixed with "wrlsec_" followed by 64 hex characters (32 bytes, hex-encoded). Use this to verify the HMAC-SHA256 signature on incoming webhook requests. Store this value now -- it cannot be retrieved after this response.
|
createdAt |
string (date-time) |
Yes |
ISO 8601 timestamp when the webhook was created. |
warning |
"Store this secret now. It cannot be retrieved after this response." |
Yes |
Always "Store this secret now. It cannot be retrieved after this response." |
{
"id": "whk_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"url": "https://hooks.example.com/wrl-events",
"events": [
"capture.complete",
"capture.failed"
],
"name": "ci-notifier",
"secret": "wrlsec_0000000000000000000000000000000000000000000000000000000000000000",
"createdAt": "2026-03-22T12:00:00.000Z",
"warning": "Store this secret now. It cannot be retrieved after this response."
}
400
— Bad request — missing or malformed body, missing `url` field, invalid URL scheme.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 400,
"title": "Bad Request",
"detail": "Request body is missing or not valid JSON."
}
401
— Unauthorized — missing, malformed, or invalid Authorization header.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 401,
"title": "Unauthorized",
"detail": "Authorization header is required."
}
409
— Conflict -- tenant has reached the 5-webhook limit.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 409,
"title": "Conflict",
"detail": "Webhook limit reached. A tenant may have at most 5 active webhooks. Delete an existing webhook to register a new one."
}
429
— Too Many Requests — rate limit or monthly quota exceeded.
The response body includes a `limitType` extension field that distinguishes the specific 429 variant:
- `limitType` absent: IP-based rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'tenant'`: Per-tenant rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'quota'`: Monthly capture quota exhausted. A `quota` extension object
is present with `limit`, `used`, `resource`, `resetsAt`, and (for batch) `requested`.
Retrying before `resetsAt` will continue to fail.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 429,
"title": "Too Many Requests",
"detail": "Rate limit exceeded. Try again later."
}
503
— Service Unavailable -- required configuration is missing or service is at capacity.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 503,
"title": "Service Unavailable",
"detail": "Service is at capacity. Retry in 10 seconds."
}
Delete 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).
|
{
"success": true,
"httpStatus": 200,
"latencyMs": 142
}
401
— Unauthorized — missing, malformed, or invalid Authorization header.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 401,
"title": "Unauthorized",
"detail": "Authorization header is required."
}
404
— Not found.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 404,
"title": "Not Found",
"detail": "Capture not found."
}
429
— Too Many Requests — rate limit or monthly quota exceeded.
The response body includes a `limitType` extension field that distinguishes the specific 429 variant:
- `limitType` absent: IP-based rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'tenant'`: Per-tenant rate limit exceeded. Retry after `Retry-After` seconds. - `limitType: 'quota'`: Monthly capture quota exhausted. A `quota` extension object
is present with `limit`, `used`, `resource`, `resetsAt`, and (for batch) `requested`.
Retrying before `resetsAt` will continue to fail.
| Property |
Type |
Required |
Description |
type |
"about:blank" |
Yes |
Always "about:blank". Clients should switch on `status`, not `type`. |
status |
integer |
Yes |
HTTP status code. |
title |
string |
Yes |
Short, human-readable summary of the problem class. |
detail |
string |
Yes |
Human-readable explanation of this specific occurrence. |
{
"type": "about:blank",
"status": 429,
"title": "Too Many Requests",
"detail": "Rate limit exceeded. Try again later."
}
Account
Tenant account information and usage
Get 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."
}