Skip to content

Type an option name like full_page, an error code, or a topic.

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:

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

The parts
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, html or markdown. Everything else is optional and has a default. The Options reference lists them all.

#url

Address of the page to capture.

There is no default. Send it, or send html or markdown 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.
  • It is public. localhost and private network addresses are refused with host_not_allowed.
  • It has no username and password in it. For a page behind a login, use the 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, 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.

Request with POST
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, markdown, styles 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.

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

The page you want
https://news.ycombinator.com/front?day=2024-01-15&p=2

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

Before: not encoded
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 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.

After: encoded
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:

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

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. 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, block_resources, block_requests, headers and cookies. You can write a list in two ways:

Two ways to send a list
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 to get a description of the file and a link to it.

Request
curl "https://curlshot.com/api/v1/screenshot?access_key=YOUR_ACCESS_KEY&url=https://example.com&response_type=json"
Response, status 200
{
  "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.

FieldWhat it holds
idThe id of this screenshot.
urlThe expiring link to the file.
formatpng, jpeg, webp, pdf, mp4, webm or gif.
width, heightSize of the image or the video in pixels.
bytesSize of the file.
render_msHow long the render took, in milliseconds.
cachedtrue when a stored copy was served. See Caching.
expires_atWhen the link and the stored file expire.

#Response headers

Every answer carries X-Reference-Id, including errors. Quote it when you contact us.

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:

HeaderWhat it tells you
X-Quota-LimitScreenshots your plan includes in the current period.
X-Quota-RemainingScreenshots you have left: what remains of the plan, plus any bonus screenshots.
X-Quota-ResetWhen the period resets, as an ISO date.
X-RateLimit-LimitRequests your account may send per minute.
X-RateLimit-RemainingRequests left in the current minute.
X-RateLimit-ResetWhen the minute resets, in Unix seconds.
X-Concurrency-LimitRenders your account may run at the same time.
X-Concurrency-RemainingFree 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:

HeaderWhat it tells you
X-CacheHIT if a stored copy was served, MISS if the page was rendered. See Caching.
X-Render-MsHow long the render took, in milliseconds.
X-Image-Width, X-Image-HeightSize 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 explains the three limits.

#Where to go next