# 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.

```bash
curl "https://curlshot.com/api/v1/screenshot?url=https://example.com" \
  -H "X-Access-Key: YOUR_ACCESS_KEY" \
  --output example.png
```

You get `example.png`, a screenshot of example.com. Without a key you get this instead:

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

1. [Create an account](https://curlshot.com/register), or [log in](https://curlshot.com/login).
2. Open **API keys** in the dashboard.
3. Copy the access key.

A key works once the email address of your account is confirmed. Until then, requests answer [`email_not_verified`](https://curlshot.com/docs/errors.md#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**

```bash
curl "https://curlshot.com/api/v1/screenshot?url=https://example.com" \
  -H "X-Access-Key: YOUR_ACCESS_KEY" \
  --output example.png
```

**Node.js**

```javascript
import { 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()))
```

**Python**

```python
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**

```php
<?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);
```

**Go**

```go
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)
}
```

**Ruby**

```ruby
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](https://curlshot.com/docs/usage-and-limits.md) 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](https://curlshot.com/docs/signed-links.md), when a screenshot URL has to appear in a web page or an email.
- [Webhooks](https://curlshot.com/docs/async-and-webhooks.md), 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:

```html
<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](https://curlshot.com/docs/signed-links.md). 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](https://curlshot.com/docs/signed-links.md) 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_invalid`](https://curlshot.com/docs/errors.md#signature_invalid) with status `403`.
- **It works for one endpoint.** An endpoint is one address of the API, such as `/screenshot` or `/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 [`expires`](https://curlshot.com/docs/options.md#expires) before 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:

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

> **Note**
>
> The other endpoints (`/usage`, `/jobs/:id`, `/batches/:id`, `/files/:id` and `/bulk`) can be signed too, each with its own short set of signed parameters. [Signed links](https://curlshot.com/docs/signed-links.md) has the details.

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

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

> **Tip**
>
> Use two keys. Keep one for your server, with no signature needed. Use a second one with **Require signature** on for links that end up in a browser.

## 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.

1. Create a new key in the dashboard.
2. Put the new key in your server settings and deploy.
3. Check that requests work with the new key.
4. 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`](https://curlshot.com/docs/errors.md#access_key_invalid) and status `401`. Signed links made with that key stop working too.

> **Common mistakes**
>
> - **The key is in a public repository.** Revoke it and create a new one. Deleting the commit is not enough, because the old version can still be found.
> - **The key is in front-end code.** Move the call to your server, or switch to [signed links](https://curlshot.com/docs/signed-links.md).
> - **You mixed up `Authorization` and `authorization`.** The `Authorization: Bearer` header on your request carries your access key to us. The [`authorization`](https://curlshot.com/docs/options.md#authorization) option is something else: a login for the site being captured. It goes only to the site in `url`, and so do `Cookie`, `Authorization` and API-key style entries in [`headers`](https://curlshot.com/docs/options.md#headers). Other hosts the page loads files from never see them.
> - **You created more keys to get a higher rate limit.** The limits are per account. See [Usage and limits](https://curlshot.com/docs/usage-and-limits.md) for what each plan allows.
> - **You signed with the wrong secret.** The secret must belong to the same access key that is in the request. Otherwise you get [`signature_invalid`](https://curlshot.com/docs/errors.md#signature_invalid).

## Where to go next

- [The screenshot URL](https://curlshot.com/docs/screenshot-url.md): how a request is put together.
- [Signed links](https://curlshot.com/docs/signed-links.md): safe screenshot URLs for public pages.
- [Usage and limits](https://curlshot.com/docs/usage-and-limits.md): check how much of your quota is left.
- [Errors](https://curlshot.com/docs/errors.md): every error code, including the key and signature ones.
