REST API
For your backend, with a secret key. Never from a browser.
REST API
Everything the SDK does over the network is a plain HTTP API you can call yourself. Use a secret key from a server; these endpoints are not origin-restricted and a sk_ key in a browser is a compromised key.
| Option | Type | Default | Description |
|---|---|---|---|
POST /api/v1/captures | pk or sk | — | Create a capture and receive signed upload URLs for each format. This is what the SDK calls before it uploads. |
POST /api/v1/captures/:id/complete | pk or sk | — | Mark the uploads finished. This is what queues the webhook — a capture never completed is never delivered. |
GET /api/v1/captures/:id | sk | — | Fetch a capture with freshly signed URLs. Use this rather than storing the webhook URLs, which expire. |
GET /api/v1/config | pk | — | The project’s server-side defaults — formats, editor config, redaction selectors, max scale. The SDK fetches this once per key. |
POST /api/v1/files | sk | — | Open a document with the file API. Returns an id and page count. |
GET /api/v1/files/:id | sk | — | Metadata, page count and status for an opened file. |
GET /api/v1/files/:id/targets | sk | — | The redactable regions we found — form fields, text matches — with their coordinates. |
GET /api/v1/files/:id/preview | sk | — | A rendered page image, for showing the user what they are about to export. |
POST /api/v1/files/:id/export | sk | — | Export with redactions applied. Returns a signed URL and the hashes for both input and output. |
DELETE /api/v1/files/:id | sk | — | Delete the file and its exports immediately, rather than waiting for the TTL. |
curl -X POST https://api.screen2api.com/api/v1/captures \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"formats": ["png"],
"params": { "invoiceId": "inv_1042" },
"width": 1200,
"height": 1600
}'Send an Idempotency-Key on anything that creates something. Retrying with the same key returns the original result instead of making a second capture, which matters when a request times out and you cannot tell whether it landed.
Error shape
Every failure has the same body, and the code is stable:
{ "error": "rate_limited", "message": "Monthly capture limit reached." }| Option | Type | Default | Description |
|---|---|---|---|
400 | invalid_request | — | Malformed body or missing field. |
401 | unauthorized | — | Missing, malformed or revoked key. |
403 | forbidden_origin | — | Live publishable key used from an unlisted origin. |
404 | not_found | — | No such id in this project. Cross-project ids are 404, never 403. |
429 | rate_limited | — | Rate limit or monthly quota. Honour Retry-After. |
5xx | server_error | — | Ours. Safe to retry with the same idempotency key. |
Verifying a webhook
The screen2api-signature header is t=<unix>,v1=<hex>, an HMAC-SHA256 over <t>.<raw body>. Verify against the raw body — parsing and re-serializing changes the bytes and the signature will never match. Compare in constant time, and reject anything with more than five minutes of skew or a replayed request is a valid one.
import { createHmac, timingSafeEqual } from 'node:crypto'
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(',').map((p) => p.split('=')),
)
const timestamp = Number(parts.t)
// Five minutes of tolerance. Without this check the signature stays
// valid forever and a captured request can be replayed at leisure.
if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > 300) return false
const expected = createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest('hex')
const a = Buffer.from(expected)
const b = Buffer.from(parts.v1 ?? '')
return a.length === b.length && timingSafeEqual(a, b)
}Answer 2xx quickly — we treat anything else as a failure and retry six times with backoff. Do the slow work after you have replied, or you will get duplicates. Every attempt, with its status and body, is on the Webhooks page.