# 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](https://shotkit.net/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:

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

```text
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](#require-signed-requests). 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.

```js tab="Node.js"
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}`;
```

```python tab="Python"
import hashlib, hmac, os
from urllib.parse import urlencode

query = urlencode({
    "access_key": os.environ["SHOTKIT_ACCESS_KEY"],
    "url": "https://example.com",
    "format": "png",
})
signature = hmac.new(os.environ["SHOTKIT_SECRET_KEY"].encode(), query.encode(), hashlib.sha256).hexdigest()

src = f"https://shotkit.net/api/take?{query}&signature={signature}"
```

```php tab="PHP"
<?php
$query = http_build_query([
    "access_key" => getenv("SHOTKIT_ACCESS_KEY"),
    "url" => "https://example.com",
    "format" => "png",
]);
$signature = hash_hmac("sha256", $query, getenv("SHOTKIT_SECRET_KEY"));

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

```ruby tab="Ruby"
require "openssl"
require "uri"

query = URI.encode_www_form(
  access_key: ENV["SHOTKIT_ACCESS_KEY"],
  url: "https://example.com",
  format: "png",
)
signature = OpenSSL::HMAC.hexdigest("SHA256", ENV["SHOTKIT_SECRET_KEY"], query)

src = "https://shotkit.net/api/take?#{query}&signature=#{signature}"
```

```go tab="Go"
query := url.Values{
	"access_key": {os.Getenv("SHOTKIT_ACCESS_KEY")},
	"url":        {"https://example.com"},
	"format":     {"png"},
}.Encode()
mac := hmac.New(sha256.New, []byte(os.Getenv("SHOTKIT_SECRET_KEY")))
mac.Write([]byte(query))
signature := hex.EncodeToString(mac.Sum(nil))

src := "https://shotkit.net/api/take?" + query + "&signature=" + signature
```

Signed URLs work well with [caching](https://shotkit.net/docs/caching.md): 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:

```js tab="Node.js"
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,
});
```

```python tab="Python"
import hashlib, hmac, json, os
import requests

body = json.dumps({"url": "https://example.com", "format": "png"})
signature = hmac.new(os.environ["SHOTKIT_SECRET_KEY"].encode(), body.encode(), hashlib.sha256).hexdigest()

res = requests.post(
    "https://shotkit.net/api/take",
    headers={
        "X-Access-Key": os.environ["SHOTKIT_ACCESS_KEY"],
        "X-Signature": signature,
        "Content-Type": "application/json",
    },
    data=body,
    timeout=90,
)
```

```bash tab="cURL"
BODY='{"url":"https://example.com","format":"png"}'
SIGNATURE=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SHOTKIT_SECRET_KEY" -hex | sed 's/^.* //')

curl -X POST "https://shotkit.net/api/take" \
  -H "X-Access-Key: $SHOTKIT_ACCESS_KEY" \
  -H "X-Signature: $SIGNATURE" \
  -H "Content-Type: application/json" \
  -d "$BODY" -o shot.png
```

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](https://shotkit.net/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](https://shotkit.net/docs/async-and-webhooks.md#verify-the-signature), so you can check that a POST to your webhook came from shotkit. Keep it on your server.
