Screenshot -- Signed URLs

Share API access securely without exposing your API key using HMAC-SHA256 signed URLs.

Base URL: https://api.nodium.io/api/v1/screenshot


Table of Contents

- Step-by-Step Algorithm

- Node.js - Python - PHP


Why Signed URLs?

In many applications, you need to expose screenshot URLs on the client side -- for example, in <img> tags, iframes, or public-facing pages. Using your raw API key in these URLs is a security risk because:

  • Anyone who views the page source can extract your API key
  • The key could be used to make unauthorized requests against your quota
  • API keys in URLs may be logged by proxies, CDNs, or analytics tools

Signed URLs solve this problem. They allow you to pre-authorize specific screenshot requests without revealing your API key. The recipient can use the signed URL to capture a screenshot, but they cannot modify the parameters or use it for other requests.

Use cases include:

  • Client-side <img> tags that request screenshots on load
  • Public embeds on third-party websites
  • Shareable links that capture a specific page with specific settings
  • Webhook integrations where a third party triggers screenshots

How It Works

Signed URLs use HMAC-SHA256 to create a tamper-proof signature of all request parameters. The signature is generated using your signing key (different from your API key) and appended to the URL.

1. You construct the screenshot URL with all desired parameters
         |
         v
2. You sort the parameters alphabetically and concatenate them
         |
         v
3. You compute HMAC-SHA256(signing_key, canonical_string)
         |
         v
4. You append the hex-encoded signature as the `signature` parameter
         |
         v
5. The signed URL can be used publicly without exposing your API key

When the API receives a signed request, it:

  1. Extracts all parameters except signature
  2. Recomputes the signature using your signing key
  3. Compares the computed signature with the provided one
  4. Rejects the request if they do not match

Creating a Signed URL

Step-by-Step Algorithm

1. Gather your parameters

Start with all the query parameters you want in the URL, including access_key:

access_key = YOUR_API_KEY
url = https://example.com
format = png
viewport_width = 1280

2. Sort parameters alphabetically by key

access_key = YOUR_API_KEY
format = png
url = https://example.com
viewport_width = 1280

3. Concatenate into a canonical string

Join all key-value pairs with &:

access_key=YOUR_API_KEY&format=png&url=https://example.com&viewport_width=1280

4. Compute the HMAC-SHA256 signature

Using your signing key (available in the Nodium Dashboard under Settings > Signing Key):

HMAC-SHA256(signing_key, "access_key=YOUR_API_KEY&format=png&url=https://example.com&viewport_width=1280")

5. Append the signature

Add the hex-encoded signature as the signature parameter:

https://api.nodium.io/api/v1/screenshot/take?access_key=YOUR_API_KEY&format=png&url=https://example.com&viewport_width=1280&signature=a1b2c3d4e5f6789012345678abcdef0123456789abcdef0123456789abcdef01
Important: The signature parameter is excluded from the canonical string used to compute the hash. Only include it in the final URL.

The signature Parameter

ParameterTypeDescription
signaturestringHex-encoded HMAC-SHA256 signature of the canonical parameter string, computed with your signing key.

The signature validates that:

  • The parameters have not been modified since signing
  • The request was authorized by someone with access to the signing key
  • The URL is intended for this specific set of parameters

Time-Limited Signatures

Add an expires parameter to create signatures that are only valid until a specific time. This prevents signed URLs from being reused indefinitely.

access_key=YOUR_API_KEY&expires=1709654400&format=png&url=https://example.com

The expires value is a UNIX timestamp (seconds since epoch). The API rejects requests where the current time exceeds the expires value.

ParameterTypeDescriptionExample
expiresinteger (UNIX timestamp)Expiration time for the signed URL. Requests after this time are rejected.1709654400
javascript
// Create a signed URL that expires in 1 hour
const expires = Math.floor(Date.now() / 1000) + 3600;
Recommendation: Always use expires for signed URLs that will be exposed on public pages. A TTL of 1--24 hours is appropriate for most use cases.

Enforcing Signed URLs

You can make signed URLs mandatory for your organization in the Nodium Dashboard under Settings > Signed URLs. When enforcement is enabled:

  • All requests without a valid signature parameter are rejected with error code signature_is_required
  • All requests with an invalid signature are rejected with error code signature_is_not_valid
  • This applies to all endpoints (/take, /animate, /bulk)

This is recommended for production environments where you want to ensure that all API usage is pre-authorized.


Code Examples

Node.js

javascript
const crypto = require("crypto");

const API_KEY = "YOUR_API_KEY";
const SIGNING_KEY = "YOUR_SIGNING_KEY";

function createSignedUrl(params) {
  // 1. Add access_key to params
  const allParams = { access_key: API_KEY, ...params };

  // 2. Sort parameters alphabetically
  const sortedKeys = Object.keys(allParams).sort();

  // 3. Build canonical string
  const canonicalString = sortedKeys
    .map((key) => `${key}=${allParams[key]}`)
    .join("&");

  // 4. Compute HMAC-SHA256 signature
  const signature = crypto
    .createHmac("sha256", SIGNING_KEY)
    .update(canonicalString)
    .digest("hex");

  // 5. Build the final URL
  const queryString = canonicalString + `&signature=${signature}`;
  return `https://api.nodium.io/api/v1/screenshot/take?${queryString}`;
}

// Usage
const signedUrl = createSignedUrl({
  url: "https://example.com",
  format: "png",
  viewport_width: 1280,
  viewport_height: 720,
});

console.log(signedUrl);
// Use in HTML: <img src="${signedUrl}" />

With expiration:

javascript
function createTimeLimitedSignedUrl(params, ttlSeconds = 3600) {
  const expires = Math.floor(Date.now() / 1000) + ttlSeconds;
  return createSignedUrl({ ...params, expires });
}

// URL valid for 1 hour
const signedUrl = createTimeLimitedSignedUrl({
  url: "https://example.com",
  format: "png",
});

Python

python
import hmac
import hashlib
import time
from urllib.parse import urlencode

API_KEY = "YOUR_API_KEY"
SIGNING_KEY = "YOUR_SIGNING_KEY"


def create_signed_url(params: dict) -> str:
    # 1. Add access_key to params
    all_params = {"access_key": API_KEY, **params}

    # 2. Sort parameters alphabetically
    sorted_params = sorted(all_params.items())

    # 3. Build canonical string
    canonical_string = "&".join(f"{k}={v}" for k, v in sorted_params)

    # 4. Compute HMAC-SHA256 signature
    signature = hmac.new(
        SIGNING_KEY.encode(),
        canonical_string.encode(),
        hashlib.sha256,
    ).hexdigest()

    # 5. Build the final URL
    query_string = canonical_string + f"&signature={signature}"
    return f"https://api.nodium.io/api/v1/screenshot/take?{query_string}"


# Usage
signed_url = create_signed_url({
    "url": "https://example.com",
    "format": "png",
    "viewport_width": 1280,
    "viewport_height": 720,
})

print(signed_url)


# With expiration
def create_time_limited_signed_url(params: dict, ttl_seconds: int = 3600) -> str:
    expires = int(time.time()) + ttl_seconds
    return create_signed_url({**params, "expires": expires})


# URL valid for 1 hour
signed_url = create_time_limited_signed_url({
    "url": "https://example.com",
    "format": "png",
})

PHP

php
<?php

$apiKey = 'YOUR_API_KEY';
$signingKey = 'YOUR_SIGNING_KEY';

function createSignedUrl(array $params, string $apiKey, string $signingKey): string {
    // 1. Add access_key to params
    $params['access_key'] = $apiKey;

    // 2. Sort parameters alphabetically
    ksort($params);

    // 3. Build canonical string
    $canonicalString = http_build_query($params, '', '&', PHP_QUERY_RFC3986);

    // 4. Compute HMAC-SHA256 signature
    $signature = hash_hmac('sha256', $canonicalString, $signingKey);

    // 5. Build the final URL
    return "https://api.nodium.io/api/v1/screenshot/take?{$canonicalString}&signature={$signature}";
}

// Usage
$signedUrl = createSignedUrl([
    'url' => 'https://example.com',
    'format' => 'png',
    'viewport_width' => 1280,
    'viewport_height' => 720,
], $apiKey, $signingKey);

echo $signedUrl;

// With expiration
function createTimeLimitedSignedUrl(
    array $params,
    string $apiKey,
    string $signingKey,
    int $ttlSeconds = 3600
): string {
    $params['expires'] = time() + $ttlSeconds;
    return createSignedUrl($params, $apiKey, $signingKey);
}

// URL valid for 1 hour
$signedUrl = createTimeLimitedSignedUrl([
    'url' => 'https://example.com',
    'format' => 'png',
], $apiKey, $signingKey);

Security Best Practices

  • Never expose your signing key on the client side. Signature generation must always happen server-side.
  • Always use expires for signed URLs on public pages. Without expiration, a signed URL remains valid indefinitely.
  • Keep TTLs short. A 1-hour expiration is sufficient for most use cases. Use 24 hours at most for URLs that need to persist across sessions.
  • Use a different signing key than your API key. The signing key is a separate credential available in the Dashboard. Rotate it periodically.
  • Enable enforcement in the Dashboard for production environments. This ensures that unsigned requests are rejected, even if someone obtains your API key.
  • URL-encode parameter values consistently. The canonical string must match exactly between your signing code and the API's verification. Use the same encoding on both sides.
  • Do not include the signature parameter in the canonical string when computing the hash. Only signature is excluded; all other parameters (including access_key and expires) are included.
  • Log and monitor signed URL usage. If you suspect a signing key has been compromised, rotate it immediately in the Dashboard.
  • Avoid putting sensitive data in parameters. Even though the URL is signed, the parameters are visible in the URL. Do not include passwords, tokens, or other secrets as parameter values.