# The screenshot URL

How a screenshot request is built, when to use GET or POST, how to encode the page address, and what the response contains.

A screenshot request is one web address. This one captures example.com as a JPEG, 800 pixels wide:

```bash
curl "https://curlshot.com/api/v1/screenshot?access_key=YOUR_ACCESS_KEY&url=https://example.com&format=jpeg&viewport_width=800" \
  --output example.jpg
```

You get `example.jpg`. The response body is the image itself, with the header `Content-Type: image/jpeg`.

## Anatomy of the request

Here is the same address, split into its parts:

```text
https://curlshot.com/api/v1/screenshot      the endpoint
?access_key=YOUR_ACCESS_KEY  who is asking
&url=https://example.com     what to capture
&format=jpeg                 an option
&viewport_width=800          another option
```

The endpoint is always `https://curlshot.com/api/v1/screenshot`. The address `https://curlshot.com/api/v1/take` is an alias and works the same way.

After the `?` come the options. Each one is `name=value`, and they are joined with `&`. The order does not matter.

You need exactly one source: [`url`](https://curlshot.com/docs/options.md#url), [`html`](https://curlshot.com/docs/options.md#html) or [`markdown`](https://curlshot.com/docs/options.md#markdown). Everything else is optional and has a default. The [Options reference](https://curlshot.com/docs/options.md) lists them all.

### url

Address of the page to capture.

There is no default. Send it, or send [`html` or `markdown`](https://curlshot.com/docs/html-and-markdown.md) in its place.

The address has to follow four rules:

- **It is a full address.** It starts with `http://` or `https://` and is at most 4096 characters long. `example.com` alone returns [`invalid_options`](https://curlshot.com/docs/errors.md#invalid_options).
- **It is public.** `localhost` and private network addresses are refused with [`host_not_allowed`](https://curlshot.com/docs/errors.md#host_not_allowed).
- **It has no username and password in it.** For a page behind a login, use the [`authorization`](https://curlshot.com/docs/options.md#authorization) option.
- **Its port is a web port.** The port is the number after the host name, as in `https://your-app.example:8443`. Most addresses have none and use `80` or `443`.

Ports `80` and `443` always work. So do most ports from `1024` up, such as `3000`, `8080` or `8443`.

Every other port below `1024` is refused. So are the ports of well-known services that are not websites: databases, caches, message queues, remote desktops and the like. Examples are `3306`, `5432`, `6379`, `9200`, `11211` and `27017`. A refused port answers `host_not_allowed`.

## GET or POST

You can send the same options in two ways.

**GET** puts the options in the address, as above. It works anywhere a URL works: a terminal, an `<img>` tag with a [signed link](https://curlshot.com/docs/signed-links.md), a browser tab.

**POST** puts the options in a JSON body. JSON is a text format for structured data. Set the header `Content-Type: application/json`.

```bash
curl -X POST "https://curlshot.com/api/v1/screenshot" \
  -H "X-Access-Key: YOUR_ACCESS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com",
    "format": "jpeg",
    "viewport_width": 800
  }' \
  --output example.jpg
```

The result is the same `example.jpg`.

Use POST when:

- You send [`html`](https://curlshot.com/docs/html-and-markdown.md), `markdown`, [`styles`](https://curlshot.com/docs/customize.md) or `scripts`. These are long and full of characters that are awkward in a URL.
- A value is long, such as a big cookie or a list of headers.
- You want the access key out of the URL. Addresses often end up in server logs.

In JSON you can use real types: `true` instead of `"true"`, `800` instead of `"800"`, and arrays for lists.

A body that is not valid JSON, or is not a JSON object, returns [`invalid_request`](https://curlshot.com/docs/errors.md#invalid_request).

## Encode the URL

The page address you want to capture is itself a URL, and it sits inside another URL. If it contains `&`, `?`, `=`, `#` or a space, the two get mixed up.

Take this page address:

```text
https://news.ycombinator.com/front?day=2024-01-15&p=2
```

Paste it in as it is, and the request breaks:

```text
https://curlshot.com/api/v1/screenshot?access_key=YOUR_ACCESS_KEY&url=https://news.ycombinator.com/front?day=2024-01-15&p=2
```

The `&` ends the `url` value early. We receive `url=https://news.ycombinator.com/front?day=2024-01-15` and a separate option called `p`. There is no option called `p`, so the request fails with [`invalid_options`](https://curlshot.com/docs/errors.md#invalid_options) and the message `p: is not a known option`.

The fix is percent-encoding. Each special character is replaced by `%` and a two-character code, so it can no longer be mistaken for part of the outer address.

```text
https://curlshot.com/api/v1/screenshot?access_key=YOUR_ACCESS_KEY&url=https%3A%2F%2Fnews.ycombinator.com%2Ffront%3Fday%3D2024-01-15%26p%3D2
```

Now `&` has become `%26` and `?` has become `%3F`. The whole page address arrives as one value.

You do not need to do this by hand. Every language has a function for it:

**cURL**

```bash
# -G sends a GET request; --data-urlencode encodes each value for you.
curl -G "https://curlshot.com/api/v1/screenshot" \
  --data-urlencode "access_key=YOUR_ACCESS_KEY" \
  --data-urlencode "url=https://news.ycombinator.com/front?day=2024-01-15&p=2" \
  --output hn.png
```

**Node.js**

```javascript
// URLSearchParams encodes every value.
const params = new URLSearchParams({
  access_key: 'YOUR_ACCESS_KEY',
  url: 'https://news.ycombinator.com/front?day=2024-01-15&p=2',
})

const requestUrl = `https://curlshot.com/api/v1/screenshot?${params}`
```

**Python**

```python
from urllib.parse import urlencode

# urlencode encodes every value.
query = urlencode({
    "access_key": "YOUR_ACCESS_KEY",
    "url": "https://news.ycombinator.com/front?day=2024-01-15&p=2",
})

request_url = f"https://curlshot.com/api/v1/screenshot?{query}"
```

**PHP**

```php
<?php
// http_build_query encodes every value.
$query = http_build_query([
    'access_key' => 'YOUR_ACCESS_KEY',
    'url' => 'https://news.ycombinator.com/front?day=2024-01-15&p=2',
]);

$requestUrl = "https://curlshot.com/api/v1/screenshot?$query";
```

**Go**

```go
package main

import (
	"fmt"
	"net/url"
)

func main() {
	// url.Values encodes every value.
	params := url.Values{}
	params.Set("access_key", "YOUR_ACCESS_KEY")
	params.Set("url", "https://news.ycombinator.com/front?day=2024-01-15&p=2")

	fmt.Println("https://curlshot.com/api/v1/screenshot?" + params.Encode())
}
```

**Ruby**

```ruby
require "uri"

# encode_www_form encodes every value.
query = URI.encode_www_form(
  access_key: "YOUR_ACCESS_KEY",
  url: "https://news.ycombinator.com/front?day=2024-01-15&p=2"
)

request_url = "https://curlshot.com/api/v1/screenshot?#{query}"
```

> **Note**
>
> A plain address such as `https://example.com` has no special characters, so it works without encoding. Encode anyway. It costs nothing and it keeps working when the address changes.

With POST there is nothing to encode. The address goes into the JSON body as an ordinary string.

## Booleans and lists

A boolean is an on or off option, such as [`full_page`](https://curlshot.com/docs/options.md#full_page). In a query string, these all mean on: `true`, `1`, `yes`, `on`. These all mean off: `false`, `0`, `no`, `off`.

Some options take a list: [`hide_selectors`](https://curlshot.com/docs/options.md#hide_selectors), [`block_resources`](https://curlshot.com/docs/options.md#block_resources), [`block_requests`](https://curlshot.com/docs/options.md#block_requests), [`headers`](https://curlshot.com/docs/options.md#headers) and [`cookies`](https://curlshot.com/docs/options.md#cookies). You can write a list in two ways:

```text
block_resources=font,media
block_resources=font&block_resources=media
```

In a POST body, send a JSON array: `"block_resources": ["font", "media"]`.

Any other option may appear only once. An empty value, such as `full_page=`, counts as not set, so the default applies.

## Get JSON instead of the file

By default the response is the file. Add [`response_type=json`](https://curlshot.com/docs/options.md#response_type) to get a description of the file and a link to it.

```bash
curl "https://curlshot.com/api/v1/screenshot?access_key=YOUR_ACCESS_KEY&url=https://example.com&response_type=json"
```

```json
{
  "id": "a187741109b34d6c991bdab9b2c5da03",
  "url": "https://curlshot.com/api/v1/files/a187741109b34d6c991bdab9b2c5da03.png?expires=1791155483&token=Zk3vQ8sT1nY6bW2xLr9cHd4JmPq7uAe0GfKoIiNtVyE",
  "format": "png",
  "width": 1280,
  "height": 1024,
  "bytes": 48213,
  "render_ms": 1240,
  "cached": false,
  "expires_at": "2026-10-04T23:11:23.563Z"
}
```

This suits code that wants to pass a link along, not the bytes.

The `url` is a link to the stored file. It carries its own token, so it opens without an access key and you can hand it to a browser or another service. It stops working at `expires_at`. Download the file if you need it for longer.

| Field | What it holds |
| --- | --- |
| `id` | The id of this screenshot. |
| `url` | The expiring link to the file. |
| `format` | `png`, `jpeg`, `webp`, `pdf`, `mp4`, `webm` or `gif`. |
| `width`, `height` | Size of the image or the video in pixels. |
| `bytes` | Size of the file. |
| `render_ms` | How long the render took, in milliseconds. |
| `cached` | `true` when a stored copy was served. See [Caching](https://curlshot.com/docs/caching.md). |
| `expires_at` | When the link and the stored file expire. |

## Response headers

Every answer carries `X-Reference-Id`, including errors. Quote it when you [contact us](https://curlshot.com/contact).

A request that gets as far as a render also tells you where your limits stand, whether the answer is a file, JSON or a render error:

| Header | What it tells you |
| --- | --- |
| `X-Quota-Limit` | Screenshots your plan includes in the current period. |
| `X-Quota-Remaining` | Screenshots you have left: what remains of the plan, plus any bonus screenshots. |
| `X-Quota-Reset` | When the period resets, as an ISO date. |
| `X-RateLimit-Limit` | Requests your account may send per minute. |
| `X-RateLimit-Remaining` | Requests left in the current minute. |
| `X-RateLimit-Reset` | When the minute resets, in Unix seconds. |
| `X-Concurrency-Limit` | Renders your account may run at the same time. |
| `X-Concurrency-Remaining` | Free render slots right now. |

A request that is refused before that point, for example with `invalid_options` or a wrong key, has only `X-Reference-Id`.

A successful answer adds these:

| Header | What it tells you |
| --- | --- |
| `X-Cache` | `HIT` if a stored copy was served, `MISS` if the page was rendered. See [Caching](https://curlshot.com/docs/caching.md). |
| `X-Render-Ms` | How long the render took, in milliseconds. |
| `X-Image-Width`, `X-Image-Height` | Size of the image in pixels. Sent when the answer is the image itself. |

A `429` answer also has `Retry-After`: the number of seconds to wait before you try again. [Usage and limits](https://curlshot.com/docs/usage-and-limits.md) explains the three limits.

> **Common mistakes**
>
> - **A misspelled option.** Unknown options are not ignored. `fullpage=true` returns `invalid_options` with a hint: did you mean `full_page`?
> - **The same option twice.** Only list options may repeat. Two `format` values return `invalid_options`.
> - **A username and password in `url`.** An address like `https://user:pass@example.com` returns `invalid_options`. Use the [`authorization`](https://curlshot.com/docs/options.md#authorization) option.
> - **A local address or an unusual port.** Both return `host_not_allowed`. See the [`url`](https://curlshot.com/docs/screenshot-url.md#url) rules above.
> - **Saving an error as an image.** Check the HTTP status before you write the file. Errors are JSON, as shown on the [errors page](https://curlshot.com/docs/errors.md).

## Where to go next

- [Options reference](https://curlshot.com/docs/options.md): every option with its default and limits.
- [Authentication and API keys](https://curlshot.com/docs/authentication.md): the three ways to send your key.
- [HTML and Markdown input](https://curlshot.com/docs/html-and-markdown.md): render your own markup with POST.
- [Signed links](https://curlshot.com/docs/signed-links.md): put a screenshot URL in a public page safely.
