Authentication and API keys
How to send your access key, what the secret key and signatures are for, and how to rotate, revoke and protect your keys.
Send your access key in a header, a named line that travels with the request, and the request is yours.
curl "https://curlshot.com/api/v1/screenshot?url=https://example.com" \
-H "X-Access-Key: YOUR_ACCESS_KEY" \
--output example.pngYou get example.png, a screenshot of example.com. Without a key you get this instead:
{
"error_code": "access_key_required",
"error_message": "An access key is required. Pass `access_key` or the `X-Access-Key` header.",
"documentation_url": "https://curlshot.com/docs/errors#access_key_required"
}Every request to CurlShot needs an access key. It tells us whose quota, the monthly number of screenshots in your plan, the screenshot counts against.
#Where to find your key
- Create an account, or log in.
- Open API keys in the dashboard.
- Copy the access key.
A key works once the email address of your account is confirmed. Until then, requests answer email_not_verified.
You can have more than one key. Give each one a name, such as "production" or "staging", so you can tell them apart later.
#Three ways to send the key
All three do the same thing. Pick the one that fits your code.
| Where | What you send | Good for |
|---|---|---|
| Query string | access_key=YOUR_ACCESS_KEY | Quick tests in a terminal. |
| Header | X-Access-Key: YOUR_ACCESS_KEY | Server code. The key stays out of the URL. |
| Header | Authorization: Bearer YOUR_ACCESS_KEY | HTTP clients and tools that already have a "bearer token" setting. |
If a request carries the key in more than one place, the query string wins.
Here is the header version in each language:
curl "https://curlshot.com/api/v1/screenshot?url=https://example.com" \
-H "X-Access-Key: YOUR_ACCESS_KEY" \
--output example.pngimport { writeFile } from 'node:fs/promises'
const params = new URLSearchParams({ url: 'https://example.com' })
const response = await fetch(`https://curlshot.com/api/v1/screenshot?${params}`, {
headers: { 'X-Access-Key': process.env.SCREENSHOT_ACCESS_KEY },
})
if (!response.ok) throw new Error((await response.json()).error_message)
await writeFile('example.png', Buffer.from(await response.arrayBuffer()))import os
import requests
response = requests.get(
"https://curlshot.com/api/v1/screenshot",
params={"url": "https://example.com"},
headers={"X-Access-Key": os.environ["SCREENSHOT_ACCESS_KEY"]},
)
response.raise_for_status()
with open("example.png", "wb") as f:
f.write(response.content)<?php
$query = http_build_query(['url' => 'https://example.com']);
$context = stream_context_create([
'http' => ['header' => 'X-Access-Key: ' . getenv('SCREENSHOT_ACCESS_KEY')],
]);
$image = file_get_contents("https://curlshot.com/api/v1/screenshot?$query", false, $context);
file_put_contents('example.png', $image);package main
import (
"io"
"net/http"
"net/url"
"os"
)
func main() {
params := url.Values{}
params.Set("url", "https://example.com")
req, _ := http.NewRequest("GET", "https://curlshot.com/api/v1/screenshot?"+params.Encode(), nil)
req.Header.Set("X-Access-Key", os.Getenv("SCREENSHOT_ACCESS_KEY"))
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
file, _ := os.Create("example.png")
defer file.Close()
io.Copy(file, resp.Body)
}require "net/http"
uri = URI("https://curlshot.com/api/v1/screenshot")
uri.query = URI.encode_www_form(url: "https://example.com")
request = Net::HTTP::Get.new(uri)
request["X-Access-Key"] = ENV["SCREENSHOT_ACCESS_KEY"]
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(request) }
File.binwrite("example.png", response.body)The samples read the key from an environment variable. That is a setting stored on your server, outside your code, so the key never ends up in your repository.
#Limits belong to the account, not the key
All your keys share one set of limits: the monthly quota, the number of requests per minute, and the number of renders that may run at the same time.
So a second key does not buy more requests per minute. Ten keys on one account have the same per-minute limit as one key.
Extra keys are for organisation and safety: one per app or environment, each one revocable on its own. Usage and limits explains the three limits.
#Access key and secret key
Each key you create is a pair: an access key and a secret key.
The access key identifies you. It travels with every request. Treat it like a password, even though it is the "public" half of the pair.
The secret key never travels. You use it on your own server to sign things, and we use our copy to check the signature. A signature is a short code computed from the request and the secret, so only someone who holds the secret can produce it.
You need the secret key in two places:
- Signed links, when a screenshot URL has to appear in a web page or an email.
- Webhooks, to check that a message really came from us.
If you only call the API from your server, you can ignore the secret key.
#Keep keys off the browser
Anything in a web page can be read by the person looking at it. That includes JavaScript files, HTML attributes and network requests.
So this is not safe in a public page:
<img src="https://curlshot.com/api/v1/screenshot?access_key=YOUR_ACCESS_KEY&url=https://example.com">A visitor can copy the key and spend your quota on their own screenshots.
You have two safe choices:
- Call the API from your server and send the image to the browser yourself.
- Use a signed link. The signature covers every parameter, so a visitor cannot change the page being captured.
#What a signature covers
A signed link has one more parameter, signature. You compute it on your server from the link's parameters and your secret key. Signed links shows how, with code.
Three rules decide what a signed link can do:
- Nothing in it can be changed. Edit, add or remove a parameter and the answer is
signature_invalidwith status403. - It works for one endpoint. An endpoint is one address of the API, such as
/screenshotor/usage. A signed screenshot link takes that one screenshot. It cannot be reused to read your usage, your jobs or your stored files. - It can have an end date. Add
expiresbefore you sign.
#expires
The moment a link stops working, as a Unix time in seconds. Unix time counts the seconds since 1 January 1970, so 1798761600 is 1 January 2027, 00:00 UTC.
There is no default. Leave it out and the link has no end date.
The link works until that moment and never after. Because expires is part of the signed parameters, nobody can move the date. An expired link answers like this:
{
"error_code": "request_expired",
"error_message": "This request has expired: its `expires` time is in the past. Create a new link with a later `expires`.",
"documentation_url": "https://curlshot.com/docs/errors#request_expired"
}#Accept signed requests only
Each key has a Require signature setting in the dashboard. Turn it on, and that key refuses every request that has no valid signature.
An unsigned request then fails like this:
{
"error_code": "signature_required",
"error_message": "This access key only accepts signed requests. Add a `signature` parameter.",
"documentation_url": "https://curlshot.com/docs/errors#signature_required"
}This is the right setting for a key that appears in public pages. Even if someone copies the access key from a link, they cannot make new requests with it.
Without the setting, someone could delete the signature from your link and send their own parameters with your access key.
#Rotate a key
Rotating means swapping an old key for a new one. Do it on a schedule, or right away if a key may have leaked.
- Create a new key in the dashboard.
- Put the new key in your server settings and deploy.
- Check that requests work with the new key.
- Revoke the old key.
Doing it in this order means there is no moment when your app has no working key.
#Revoke a key
Revoking switches a key off for good. Open API keys, pick the key and revoke it.
From then on, every request with that key fails with access_key_invalid and status 401. Signed links made with that key stop working too.
#Where to go next
- The screenshot URL: how a request is put together.
- Signed links: safe screenshot URLs for public pages.
- Usage and limits: check how much of your quota is left.
- Errors: every error code, including the key and signature ones.