Screenshot -- Async Mode & Webhooks
Run long-running screenshots in the background and receive results via webhook callbacks.
Base URL: https://api.nodium.io/api/v1/screenshot
Table of Contents
- Why Async?
- Enabling Async Mode
- The Async Flow
- Webhook Configuration
- Webhook Payload
- Webhook Security
- Retry Policy
- Polling Alternative
- Parameters Reference
- Code Examples
- Sending an Async Request (cURL) - Webhook Receiver (Node.js Express) - Webhook Receiver (Python Flask)
Why Async?
By default, the Nodium Screenshot API processes requests synchronously -- your HTTP connection stays open until the screenshot is rendered and returned. This works well for fast captures, but some scenarios benefit from asynchronous processing:
- Long-running captures -- pages that take a long time to load, or animated screenshots with long durations
- Batch processing -- when you are submitting many screenshots and do not want to hold connections open
- Timeout avoidance -- synchronous requests have a maximum timeout of 90 seconds; async requests support up to 300 seconds
- Decoupled architectures -- fire-and-forget workflows where the result is processed by a different service
- High concurrency -- submit requests without waiting, then process results as they arrive
Enabling Async Mode
Add async=true to any screenshot request:
curl -X POST "https://api.nodium.io/api/v1/screenshot/take" \
-H "X-Access-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"format": "png",
"async": true,
"webhook_url": "https://hooks.example.com/screenshot-ready"
}'The API immediately validates your credentials and parameters, then returns a 202 Accepted response with a reference ID. The screenshot is processed in the background.
The Async Flow
1. Client sends request with async=true
|
v
2. API validates credentials and parameters
|
v
3. API returns 202 Accepted with reference ID
|
v
4. Screenshot is processed in the background
|
v
5. On completion, API POSTs the result to your webhook_urlStep 3: The 202 Response
When the request is accepted, you receive:
HTTP/1.1 202 Accepted
x-nodium-reference: 8472910
x-nodium-trace-id: abc123-def456The x-nodium-reference header contains the unique reference ID for this screenshot. Use it to track the request or poll for status.
Webhook Configuration
Setting the Webhook URL
Provide the webhook_url parameter with the full URL of your endpoint:
{
"url": "https://example.com",
"format": "png",
"async": true,
"webhook_url": "https://hooks.example.com/screenshot-ready"
}Your webhook endpoint must:
- Accept POST requests
- Be publicly accessible (or accessible from Nodium's servers)
- Respond with a 2xx status code to acknowledge receipt
- Handle the request within 30 seconds
Custom Identifier
Use external_identifier to attach your own reference ID to the webhook delivery. This is useful for correlating webhook callbacks with your internal records:
{
"url": "https://example.com",
"format": "png",
"async": true,
"webhook_url": "https://hooks.example.com/screenshot-ready",
"external_identifier": "order-12345"
}The identifier is returned in the x-nodium-external-identifier header on the webhook delivery.
Webhook Payload
When the screenshot is complete, Nodium sends a POST request to your webhook_url. The request body contains the raw screenshot binary data (image, PDF, or video).
Webhook Headers
| Header | Description | Example |
|---|---|---|
X-Nodium-Signature | HMAC-SHA256 signature of the body (when webhook_sign=true) | a1b2c3d4e5f6... |
x-nodium-reference | Unique screenshot reference ID | 8472910 |
x-nodium-trace-id | Trace ID for debugging | abc123-def456 |
x-nodium-rendering-seconds | Time spent rendering (seconds) | 3.45 |
x-nodium-size-bytes | Screenshot file size in bytes | 245760 |
x-nodium-external-identifier | Your custom identifier (if provided) | order-12345 |
Content-Type | MIME type of the screenshot | image/png |
Error Headers
When webhook_errors=true is set on the original request, error information is included if the screenshot fails:
| Header | Description | Example |
|---|---|---|
x-nodium-error-code | Error code | timeout_error |
x-nodium-error-message | Human-readable error message | Operation timed out |
Webhook Security
HMAC-SHA256 Signature Verification
By default (webhook_sign=true), Nodium signs every webhook payload using HMAC-SHA256 with your signing key. The signature is sent in the X-Nodium-Signature header.
To verify the signature:
- Retrieve your signing key from the Nodium Dashboard under Settings > Signing Key
- Compute the HMAC-SHA256 hash of the raw request body using your signing key
- Compare the computed hash with the value in
X-Nodium-Signature
Node.js verification example:
const crypto = require("crypto");
function verifyWebhookSignature(body, signature, signingKey) {
const computed = crypto
.createHmac("sha256", signingKey)
.update(body)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(computed, "hex"),
Buffer.from(signature, "hex")
);
}Python verification example:
import hmac
import hashlib
def verify_webhook_signature(body: bytes, signature: str, signing_key: str) -> bool:
computed = hmac.new(
signing_key.encode(),
body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(computed, signature)Security recommendation: Always verify the webhook signature in production to ensure the request originates from Nodium and has not been tampered with.
Retry Policy
If your webhook endpoint fails to respond with a 2xx status code, Nodium retries the delivery:
| Attempt | Delay |
|---|---|
| 1st retry | ~10 seconds after initial failure |
| 2nd retry | ~60 seconds after 1st retry |
| 3rd retry | ~300 seconds after 2nd retry |
The retry schedule uses exponential backoff. After 3 failed retries, the webhook delivery is abandoned. You can still retrieve the screenshot using the polling endpoint.
Polling Alternative
If you cannot receive webhooks (e.g. behind a firewall), you can poll for the screenshot status using the reference ID from the 202 response:
GET https://api.nodium.io/api/v1/screenshot/status/{reference}?access_key=YOUR_API_KEYPolling Response
While processing:
{
"status": "processing",
"reference": "8472910"
}When complete:
{
"status": "completed",
"reference": "8472910",
"screenshot_url": "https://cdn.nodium.io/screenshots/8472910.png"
}On error:
{
"status": "failed",
"reference": "8472910",
"error_code": "timeout_error",
"message": "Operation timed out"
}Note: Polling is less efficient than webhooks. If possible, prefer webhooks for production workloads.
Parameters Reference
| Parameter | Type | Default | Description | Example |
|---|---|---|---|---|
async | boolean | false | Run the screenshot asynchronously. Returns 202 Accepted immediately. | true |
webhook_url | string | -- | URL to POST the screenshot result to when processing completes. | https://hooks.example.com/screenshot |
webhook_sign | boolean | true | Sign the webhook body with HMAC-SHA256 using your signing key. | true |
webhook_errors | boolean | false | Include error details in the webhook headers when the screenshot fails. | true |
external_identifier | string | -- | Custom identifier returned in webhook headers as x-nodium-external-identifier. | order-12345 |
timeout | integer (seconds) | 60 | Global timeout. Async mode supports up to 300 seconds. | 120 |
Code Examples
Sending an Async Request (cURL)
# Async screenshot with webhook
curl -X POST "https://api.nodium.io/api/v1/screenshot/take" \
-H "X-Access-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{
"url": "https://heavy-page.example.com",
"format": "png",
"async": true,
"webhook_url": "https://hooks.example.com/screenshot-ready",
"webhook_sign": true,
"webhook_errors": true,
"external_identifier": "job-42",
"timeout": 120
}'
# Poll for status
curl "https://api.nodium.io/api/v1/screenshot/status/8472910?access_key=YOUR_API_KEY"Webhook Receiver (Node.js Express)
const express = require("express");
const crypto = require("crypto");
const fs = require("fs");
const app = express();
const SIGNING_KEY = process.env.NODIUM_SIGNING_KEY;
// Use raw body for signature verification
app.post(
"/webhook/screenshot",
express.raw({ type: "*/*", limit: "50mb" }),
(req, res) => {
// 1. Verify signature
const signature = req.headers["x-nodium-signature"];
if (signature && SIGNING_KEY) {
const computed = crypto
.createHmac("sha256", SIGNING_KEY)
.update(req.body)
.digest("hex");
if (!crypto.timingSafeEqual(
Buffer.from(computed, "hex"),
Buffer.from(signature, "hex")
)) {
console.error("Invalid webhook signature");
return res.status(401).send("Invalid signature");
}
}
// 2. Check for errors
const errorCode = req.headers["x-nodium-error-code"];
if (errorCode) {
console.error(
`Screenshot failed: ${errorCode} - ${req.headers["x-nodium-error-message"]}`
);
return res.status(200).send("OK");
}
// 3. Process the screenshot
const reference = req.headers["x-nodium-reference"];
const externalId = req.headers["x-nodium-external-identifier"];
const renderTime = req.headers["x-nodium-rendering-seconds"];
console.log(
`Screenshot ${reference} (${externalId}) rendered in ${renderTime}s`
);
// Save the screenshot
fs.writeFileSync(`screenshots/${reference}.png`, req.body);
// 4. Acknowledge receipt
res.status(200).send("OK");
}
);
app.listen(3000, () => console.log("Webhook receiver listening on port 3000"));Webhook Receiver (Python Flask)
import hmac
import hashlib
import os
from flask import Flask, request
app = Flask(__name__)
SIGNING_KEY = os.environ.get("NODIUM_SIGNING_KEY", "")
@app.route("/webhook/screenshot", methods=["POST"])
def handle_webhook():
body = request.get_data()
# 1. Verify signature
signature = request.headers.get("X-Nodium-Signature")
if signature and SIGNING_KEY:
computed = hmac.new(
SIGNING_KEY.encode(),
body,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(computed, signature):
return "Invalid signature", 401
# 2. Check for errors
error_code = request.headers.get("x-nodium-error-code")
if error_code:
error_message = request.headers.get("x-nodium-error-message")
print(f"Screenshot failed: {error_code} - {error_message}")
return "OK", 200
# 3. Process the screenshot
reference = request.headers.get("x-nodium-reference")
external_id = request.headers.get("x-nodium-external-identifier")
render_time = request.headers.get("x-nodium-rendering-seconds")
print(f"Screenshot {reference} ({external_id}) rendered in {render_time}s")
# Save the screenshot
with open(f"screenshots/{reference}.png", "wb") as f:
f.write(body)
# 4. Acknowledge receipt
return "OK", 200
if __name__ == "__main__":
app.run(port=3000)Best Practices
- Always set
webhook_errors=trueso you are notified when a screenshot fails, not just when it succeeds. - Use
external_identifierto correlate webhook callbacks with your internal job or order IDs. - Verify webhook signatures in production to prevent spoofed deliveries.
- Respond quickly to webhook deliveries (within 30 seconds). Offload heavy processing to a background queue.
- Implement idempotency -- use the
x-nodium-referenceheader to deduplicate webhook deliveries in case of retries. - Increase
timeoutfor async requests when capturing heavy pages. Async mode supports up to 300 seconds versus 90 seconds for synchronous requests. - Use async mode for animated screenshots with long durations, as video rendering can take significantly longer than static captures.
- Caching is not compatible with webhooks. If you need both, capture with caching in synchronous mode, or use the
screenshot_urlfrom a JSON response.