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

- 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:

bash
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_url

Step 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-def456

The 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:

json
{
  "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:

json
{
  "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

HeaderDescriptionExample
X-Nodium-SignatureHMAC-SHA256 signature of the body (when webhook_sign=true)a1b2c3d4e5f6...
x-nodium-referenceUnique screenshot reference ID8472910
x-nodium-trace-idTrace ID for debuggingabc123-def456
x-nodium-rendering-secondsTime spent rendering (seconds)3.45
x-nodium-size-bytesScreenshot file size in bytes245760
x-nodium-external-identifierYour custom identifier (if provided)order-12345
Content-TypeMIME type of the screenshotimage/png

Error Headers

When webhook_errors=true is set on the original request, error information is included if the screenshot fails:

HeaderDescriptionExample
x-nodium-error-codeError codetimeout_error
x-nodium-error-messageHuman-readable error messageOperation 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:

  1. Retrieve your signing key from the Nodium Dashboard under Settings > Signing Key
  2. Compute the HMAC-SHA256 hash of the raw request body using your signing key
  3. Compare the computed hash with the value in X-Nodium-Signature

Node.js verification example:

javascript
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:

python
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:

AttemptDelay
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_KEY

Polling Response

While processing:

json
{
  "status": "processing",
  "reference": "8472910"
}

When complete:

json
{
  "status": "completed",
  "reference": "8472910",
  "screenshot_url": "https://cdn.nodium.io/screenshots/8472910.png"
}

On error:

json
{
  "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

ParameterTypeDefaultDescriptionExample
asyncbooleanfalseRun the screenshot asynchronously. Returns 202 Accepted immediately.true
webhook_urlstring--URL to POST the screenshot result to when processing completes.https://hooks.example.com/screenshot
webhook_signbooleantrueSign the webhook body with HMAC-SHA256 using your signing key.true
webhook_errorsbooleanfalseInclude error details in the webhook headers when the screenshot fails.true
external_identifierstring--Custom identifier returned in webhook headers as x-nodium-external-identifier.order-12345
timeoutinteger (seconds)60Global timeout. Async mode supports up to 300 seconds.120

Code Examples

Sending an Async Request (cURL)

bash
# 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)

javascript
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)

python
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=true so you are notified when a screenshot fails, not just when it succeeds.
  • Use external_identifier to 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-reference header to deduplicate webhook deliveries in case of retries.
  • Increase timeout for 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_url from a JSON response.