Menu

Authentication

Authenticate with an access key, and sign GET URLs and verify webhooks with your secret key.

Each workspace has two keys, both on the API keys page:

Key Looks like Used for
Access key ak_… Identifies your workspace on every request.
Secret key sk_… Signs GET URLs and webhook deliveries. Never send it in a request.

Access key

Send it in the X-Access-Key header:

curl "https://shotkit.net/api/take?url=https://example.com" -H "X-Access-Key: YOUR_ACCESS_KEY" -o shot.jpg

or as the access_key parameter, in the query string or the JSON body:

https://shotkit.net/api/take?access_key=YOUR_ACCESS_KEY&url=https://example.com

A missing or wrong key returns 401 with access_key_invalid. On the free Hobby plan, keys only work once someone in the workspace has verified their email address; until then requests return 403 with email_not_verified.

Keep the access key on your server. Anyone who has it can render screenshots on your quota.

Signed requests

A signature proves a request was made by someone who has your secret key, and that nobody changed it afterwards. It's the hex HMAC-SHA256 of the request, keyed with your secret key:

Request What you sign Where the signature goes
GET The query string, exactly as sent, without signature signature query parameter
POST The raw JSON body, exactly as sent X-Signature header

Signatures are optional unless you require them. A signature that is sent is always checked: a mismatch returns 403 with signature_is_not_valid.

Signed URLs

A GET URL with your access key in it can go straight into an <img src>, an email or a page. Sign it so nobody can change its parameters:

  1. Build the query string with every parameter, including access_key.
  2. Compute HMAC-SHA256(query, secret_key) as lowercase hex.
  3. Append &signature=<hex>.

Sign the string exactly as it appears in the URL, after encoding, and don't reorder or re-encode parameters afterwards.

import { createHmac } from "node:crypto";

const query = new URLSearchParams({
  access_key: process.env.SHOTKIT_ACCESS_KEY,
  url: "https://example.com",
  format: "png",
}).toString();
const signature = createHmac("sha256", process.env.SHOTKIT_SECRET_KEY).update(query).digest("hex");

const src = `https://shotkit.net/api/take?${query}&signature=${signature}`;

Signed URLs work well with caching: the first view renders, and later views of the same URL are redirected to the cached file for free.

Signed POST requests

Serialize the body once, sign that string, and send the same string:

import { createHmac } from "node:crypto";

const body = JSON.stringify({ url: "https://example.com", format: "png" });
const signature = createHmac("sha256", process.env.SHOTKIT_SECRET_KEY).update(body).digest("hex");

const res = await fetch("https://shotkit.net/api/take", {
  method: "POST",
  headers: {
    "X-Access-Key": process.env.SHOTKIT_ACCESS_KEY,
    "X-Signature": signature,
    "Content-Type": "application/json",
  },
  body,
});

Don't let your HTTP client re-serialize the body (in Python, pass data=body, not json=), or the bytes it sends won't match the signature.

Require signed requests

An access key alone is enough to render, so a key that leaks, for example from a signed URL in a public page, can be used for any request on your quota. To prevent that, owners and admins can turn on Require signed requests on the API keys page. Then every request without a signature fails with 403 and signature_is_required, and a leaked key can only replay URLs you've already signed.

Before turning it on, make sure all your code signs its requests. The playground signs its own renders, and its Signed switch generates signed code in every language, plus a signed GET URL. The setting takes up to a minute to apply.

Secret key

The secret key also signs webhook deliveries, so you can check that a POST to your webhook came from shotkit. Keep it on your server.