How to Use Urlbox Webhooks for Screenshot Jobs
Queue a Urlbox screenshot, handle success and failure callbacks, verify webhook signatures, and retain results beyond the default hosting period.
To use Urlbox webhooks for screenshot jobs, submit an asynchronous render request with a webhook_url. Urlbox returns a render ID when it queues the job, then sends a POST callback when rendering succeeds or fails. Your application should correlate the callback by renderId, verify the X-Urlbox-Signature using your project webhook secret, and update the job state only after verification.
A webhook is the notification path; it does not determine how long the resulting screenshot is retained. Urlbox says its default hosted render expires after 30 days. Configure storage in your own bucket if you need longer retention.
1. The request and callback lifecycle
- Your server submits a render request to
https://api.urlbox.com/v1/render, including the target URL and a reachablewebhook_url. - Urlbox responds to the creation request with the queued job information, including a
renderId. Keep that ID with your own job record. - After rendering, Urlbox POSTs a success or failure event to your callback endpoint.
- Your endpoint verifies the signature, associates the event with the queued job by
renderId, and records the final state. - For long-term retention, save the result to configured storage. See the [Urlbox webhook guide](https://urlbox.com/webhooks) and [async render API reference](https://urlbox.com/docs/api).
These are separate HTTP exchanges: a successful render-creation response means the job was queued, not that the screenshot is ready. The API reference documents 201 for creation through /v1/render/async; common creation errors include 400 for invalid input, 401 for a wrong key, and 429 for rate limiting.
2. Queue an asynchronous render
Use your Urlbox secret only from a server. Set webhook_url to an HTTPS endpoint your application can receive. Do not expose the secret in browser code or logs.
curl -X POST "https://api.urlbox.com/v1/render" \
-H "Authorization: Bearer YOUR_URLBOX_SECRET" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"webhook_url": "https://app.example.com/webhooks/urlbox"
}'
Store the returned renderId against your internal job before processing the later callback. Validate the actual response shape against the current API reference; do not assume fields beyond those documented by Urlbox.
3. Receive both event types and verify the signature
Urlbox documents two event names: render.succeeded and render.failed. A success sample includes a result object with renderUrl and render metadata; a failure sample includes error.message and timing metadata. Treat sample fields as examples unless the current schema guarantees them.
The signature header is documented in the form t={timestamp},sha256={token}. The signed text is the timestamp, a period, and the JSON-stringified webhook payload: {timestamp}.{JSON stringified webhook payload}. Use the webhook secret from the project dashboard settings. Verify before acting on the event or trusting its result URL.
Serialization matters: parsing JSON and stringifying it again can change whitespace, escaping, or key representation. Preserve the incoming body and implement the exact serialization procedure Urlbox documents. The example below captures the raw body and uses it as the payload text; confirm that this matches the current Urlbox signing procedure for your integration before deploying it.
import express from 'express';
import crypto from 'node:crypto';
const app = express();
const webhookSecret = process.env.URLBOX_WEBHOOK_SECRET;
if (!webhookSecret) throw new Error('Set URLBOX_WEBHOOK_SECRET');
app.post('/webhooks/urlbox', express.raw({ type: 'application/json' }), async (req, res) => {
const header = req.get('X-Urlbox-Signature') || '';
const match = /^t=(\d+),sha256=([a-f0-9]+)$/i.exec(header);
if (!match || !Buffer.isBuffer(req.body)) return res.sendStatus(400);
const [, timestamp, receivedHex] = match;
// Keep these bytes unchanged. Apply Urlbox's documented payload serialization
// exactly; do not parse and re-stringify before calculating the HMAC.
const payloadText = req.body.toString('utf8');
const signedText = `${timestamp}.${payloadText}`;
const expected = crypto
.createHmac('sha256', webhookSecret)
.update(signedText, 'utf8')
.digest();
let received;
try {
received = Buffer.from(receivedHex, 'hex');
} catch {
return res.sendStatus(401);
}
if (received.length !== expected.length || !crypto.timingSafeEqual(received, expected)) {
return res.sendStatus(401);
}
let event;
try {
event = JSON.parse(payloadText);
} catch {
return res.sendStatus(400);
}
if (!event || typeof event.renderId !== 'string') return res.sendStatus(400);
// Replace these placeholders with durable, idempotent application logic.
if (event.event === 'render.succeeded') {
await markJobSucceeded(event.renderId, event.result);
} else if (event.event === 'render.failed') {
await markJobFailed(event.renderId, event.error?.message || 'Render failed');
} else {
// Decide whether unknown events should be ignored or recorded for investigation.
await recordUnknownEvent(event);
}
return res.sendStatus(200);
});
app.listen(3000);
// Implement these functions using your database or job system.
async function markJobSucceeded(renderId, result) { /* persist success */ }
async function markJobFailed(renderId, message) { /* persist failure */ }
async function recordUnknownEvent(event) { /* persist or log safely */ }
This is an Express handler skeleton; the three persistence functions are application-specific. Install Express in your Node project and run the service behind a public HTTPS endpoint. Configure raw-body handling before any JSON middleware that would consume or transform the request body. Do not log the secret, signature token, or sensitive callback contents.
Callback handler checklist
- Read the exact signature header and reject missing or malformed values.
- Verify the HMAC before trusting payload fields.
- Parse the event only after signature verification.
- Handle both documented event types and associate each with
renderId. - Make state updates idempotent so a repeated delivery cannot create duplicate work.
- Return a success response only after the event is safely recorded; otherwise return an error so delivery can be retried if Urlbox supports retries under its current policy.
- Keep callback secrets in server-side configuration and rotate them through the project’s supported settings when needed.
4. Handle completion, failures, and duplicates
For render.succeeded, record the render ID and the available result details, then fetch or copy the result to your storage if your workflow requires durable ownership. For render.failed, record the provided error message when present and mark the job failed. Do not wait for a success-only callback: failures are part of the documented event flow.
Use a unique constraint or equivalent idempotency key based on the render ID and event state. A callback can arrive after a client timeout or be delivered again; processing the same final state twice should not duplicate downstream work. If an event references an unknown render ID, record it for investigation rather than silently attaching it to an unrelated job.
Keep the callback endpoint fast: validate, persist the event or enqueue internal work, and acknowledge it. Perform expensive downloads or follow-up processing in a worker. The documented event samples include timing fields, but your handler should tolerate optional or absent fields.
5. Choose webhooks or polling
Use queued async rendering for renders that routinely take more than a few seconds, large full-page captures, slow sites, or a list of URLs that should not be processed one at a time while a client waits. A webhook suits systems that can expose a receiver. If you do not want a callback endpoint, Urlbox’s CLI guide documents polling by renderId as an alternative.
| Approach | Useful when | Trade-off |
|---|---|---|
| Webhook | Your application can receive POST requests and you want completion notifications. | You must operate and secure a public callback endpoint. |
| Polling | You cannot receive callbacks or prefer a simple worker loop. | Your worker must check status repeatedly and manage its polling schedule. |
The CLI guide warns that a render exceeding its timeout fails rather than retrying, and that retries rarely help for genuinely long renders. Queue long-running work asynchronously instead of repeatedly retrying a synchronous request.
6. Set screenshot options and retain the result
Webhook delivery does not change screenshot options; it reports the outcome of the render you requested. Two relevant capture choices are full-page and selector capture:
full_page: truerequests a full-page screenshot. The documented default mode isstitch;nativeis a faster alternative that may work less well on some sites. Compare page behavior and output for your target pages.selectorcan target one CSS-selected element when the whole page is unnecessary.
Large captures can run into output dimension limits. Urlbox’s screenshot guide states maximum dimensions of 65,535 × 65,535 pixels for JPEG and 16,383 × 16,383 pixels for WebP. It recommends PNG for full-page captures when those size limits matter. Select the format and capture mode based on accuracy, speed, page behavior, and output constraints; no mode is best for every site. See the [Urlbox screenshot guide](https://urlbox.com/docs/screenshots).
Urlbox says its default hosted render expires after 30 days. A webhook tells your application that the render is ready; storage configuration decides where it is kept. For longer retention, configure bucket storage using the documented use_s3 and s3_path settings, or the relevant guide for your supported storage provider. Verify current option names and provider instructions in the [Urlbox storage guide](https://urlbox.com/docs/storage).
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Render creation returns 400 | Malformed JSON, invalid option, or missing required input. | Check the request body and current API reference; test with the smallest valid request first. |
| Render creation returns 401 | Wrong or missing Urlbox secret. | Use the server-side secret in the documented Bearer authorization header and check for accidental whitespace or key rotation. |
| Render creation returns 429 | Rate limit reached. | Reduce submission rate and schedule work through a queue. Follow current API guidance for when to retry. |
| No callback arrives | The callback URL is unreachable from Urlbox, rejects POST, or has TLS/routing problems. | Make the endpoint publicly reachable over HTTPS, accept POST at the exact path, and inspect server and proxy logs. Use polling by render ID if a callback receiver is unsuitable. |
| Signature verification fails | Wrong webhook secret, altered body, wrong signed-text construction, or malformed header parsing. | Use the project webhook secret; capture the raw body before JSON middleware; follow the documented timestamp-plus-payload format exactly; compare digests in constant time. |
| Callback parses but cannot find a job | The render ID was not persisted or the event arrived before the job record was committed. | Persist the returned ID before relying on callbacks, and handle unknown IDs with a durable retry or investigation path. |
| Screenshot link no longer works | The default hosted render retention period elapsed. | Configure bucket storage and save the result where your application controls retention. |
| Full-page output is missing or fails at large dimensions | Capture mode or output format dimensions may not suit the page. | Compare stitch and native behavior, and consider PNG where the documented JPEG or WebP limits matter. |
| Long render repeatedly times out | The render exceeds the configured timeout; retrying does not make a genuinely long render shorter. | Queue it asynchronously and use callback or polling to observe completion. |
8. Performance, reliability, and cost considerations
Async jobs keep slow page loads and large captures out of a request that must wait for completion. Webhooks avoid a tight polling loop, while polling can be simpler where inbound callbacks are not possible. Keep concurrency and submission rate within the service’s current limits, and use a worker queue for batches.
For reliable processing, durably record the render ID and callback outcome, verify every signature, make updates idempotent, and separate callback acknowledgement from heavier work. Plan for failures at both stages: the render request can be rejected before a job exists, and a queued render can later fail and send a failure event. Retention also has a cost and ownership dimension: default hosted results expire after 30 days, while longer retention requires configuring your own storage.
Check current Urlbox pricing and account limits before estimating a batch’s cost. The documentation in this research pass does not establish a price per render, so this guide does not quote one.
9. Or skip the browser setup
If you need screenshots without building and maintaining a browser capture pipeline, [ScreenshotNeo](https://screenshotneo.com) is a website screenshot API and MCP server from Yorker Media. One GET request returns a PNG, JPEG, WebP, or PDF. Its capture flow accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and responses identify the page verdict and billing status in headers. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.
cURL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
See the [ScreenshotNeo API docs](https://screenshotneo.com/docs/). All features are on every plan: 1,000 screenshots per month free with no card; Starter is $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000. Yearly billing gives two months free. [Create a free ScreenshotNeo account](https://screenshotneo.com/account/sign-up/) to start with 1,000 screenshots a month and no card.
10. FAQ
Does the initial render response contain the finished screenshot?
It confirms creation of the asynchronous render job. Use the later callback or polling flow to determine its outcome.
Can a webhook report a failed render?
Yes. The documented failure event is render.failed; store its message when provided and associate it with the render ID.
Will Urlbox keep the result indefinitely?
No. Urlbox says its default hosted render expires after 30 days. Configure storage in your own bucket for longer retention.
Do I need to accept both event types?
Yes. Handle success and failure so jobs do not remain indefinitely in a queued or pending state.


