How to Send Custom HTTP Headers in Ruby When Using a Screenshot API
Send page headers and API authentication separately in Ruby, with runnable Net::HTTP code, debugging steps, security guidance, and ScreenshotNeo options.
Direct answer: keep the two header sets separate. Ruby sends the screenshot service’s authentication header, usually Authorization: Bearer .... The service sends headers intended for the page being rendered through its target-header option, such as repeated header=Name: value parameters on a GET request or a headers object in a POST body. Do not put the page’s headers into Ruby’s API authentication header.
The example below uses Ruby’s standard Net::HTTP library. It authenticates the API request, sends an X-Preview-Token header to the target page, saves the returned image as binary data, and checks the HTTP result before writing the file.
require "net/http"
require "uri"
api_key = ENV.fetch("SCREENSHOT_API_KEY")
preview_token = ENV.fetch("PREVIEW_TOKEN")
params = {
"url" => "https://example.com",
"header" => ["X-Preview-Token: #{preview_token}"]
}
uri = URI("https://screenshot-api.net/v1/screenshot")
uri.query = URI.encode_www_form(params)
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{api_key}"
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
http.request(request)
end
unless response.is_a?(Net::HTTPSuccess)
raise "Screenshot API request failed: #{response.code} #{response.message}"
end
File.binwrite("shot.png", response.body)
puts "Rendered page status: #{response['X-Page-Status']}"
The screenshot endpoint returns image bytes directly rather than a JSON wrapper. Write the body in binary mode and inspect the response headers before treating it as a successful capture. The target page’s final HTTP status is exposed as X-Page-Status; a 401 or 403 can mean that the captured image is a login or error page even when the screenshot request itself succeeded. See the provider’s documented screenshot endpoint and Ruby’s Net::HTTP reference.
1. Decide which side needs the header
| Header purpose | Where it goes | Example |
|---|---|---|
| Authenticate your API call | Ruby request to the screenshot service | Authorization: Bearer API_KEY |
| Authenticate or configure the page being rendered | Screenshot API’s target-header parameter | X-Preview-Token: value |
| Send a session | Use the provider’s cookie option | Session cookie rather than a forbidden Cookie target header |
| Use HTTP basic authentication | Use the provider’s basic-auth option | Username and password handled by the API |
These are different HTTP hops. Setting request["X-Preview-Token"] in Ruby sends that header to the screenshot service itself; it does not automatically make the rendered browser send it to the target website.
2. Send one or more page headers with GET
The GET form accepts a repeatable header parameter. In Ruby, represent repeated parameters as an array and let URI.encode_www_form perform URL encoding.
require "net/http"
require "uri"
api_key = ENV.fetch("SCREENSHOT_API_KEY")
headers_for_page = [
"X-Preview-Token: #{ENV.fetch('PREVIEW_TOKEN')}",
"Accept-Language: en-US",
"X-Tenant-ID: #{ENV.fetch('TENANT_ID')}"
]
uri = URI("https://screenshot-api.net/v1/screenshot")
uri.query = URI.encode_www_form(
"url" => "https://example.com/account/preview",
"header" => headers_for_page
)
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{api_key}"
request["Accept"] = "image/png"
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(request)
end
abort "API error #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
File.binwrite("preview.png", response.body)
warn "Target status: #{response['X-Page-Status'] || 'not provided'}"
Do not build the query string by concatenating raw values. Tokens can contain characters such as spaces, plus signs, or ampersands; URI.encode_www_form protects the request from malformed parameters.
3. Use POST when credentials are in parameters
Query strings can appear in access logs. When a target credential must be supplied, use the provider’s documented POST form with a headers object, if available, instead of putting the secret in a GET URL. The exact JSON shape is provider-specific; follow that provider’s POST documentation and keep the API bearer token in the request header.
require "net/http"
require "uri"
require "json"
api_key = ENV.fetch("SCREENSHOT_API_KEY")
uri = URI("https://screenshot-api.net/v1/screenshot")
payload = {
url: "https://example.com/account/preview",
headers: {
"X-Preview-Token" => ENV.fetch("PREVIEW_TOKEN"),
"X-Tenant-ID" => ENV.fetch("TENANT_ID")
}
}
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{api_key}"
request["Content-Type"] = "application/json"
request.body = JSON.generate(payload)
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(request)
end
abort "Screenshot request failed: #{response.code} #{response.message}" unless response.is_a?(Net::HTTPSuccess)
File.binwrite("preview.png", response.body)
Use the POST variant when the service documents it and when the request contains credentials. The provider states that target headers are scoped to the target host and are not forwarded to another host after a redirect.
4. Complete cURL, Python, and Node.js examples
cURL
curl -G "https://screenshot-api.net/v1/screenshot" \
-H "Authorization: Bearer $SCREENSHOT_API_KEY" \
--data-urlencode "url=https://example.com/account/preview" \
--data-urlencode "header=X-Preview-Token: $PREVIEW_TOKEN" \
--data-urlencode "header=Accept-Language: en-US" \
-o shot.png
Python
import os
import requests
params = [
("url", "https://example.com/account/preview"),
("header", f"X-Preview-Token: {os.environ['PREVIEW_TOKEN']}"),
("header", "Accept-Language: en-US"),
]
response = requests.get(
"https://screenshot-api.net/v1/screenshot",
params=params,
headers={"Authorization": f"Bearer {os.environ['SCREENSHOT_API_KEY']}"},
timeout=90,
)
response.raise_for_status()
with open("shot.png", "wb") as image:
image.write(response.content)
print("Target status:", response.headers.get("X-Page-Status"))
Node.js
const targetHeaders = [
`X-Preview-Token: ${process.env.PREVIEW_TOKEN}`,
'Accept-Language: en-US'
];
const query = new URLSearchParams({
url: 'https://example.com/account/preview'
});
targetHeaders.forEach((value) => query.append('header', value));
const response = await fetch(`https://screenshot-api.net/v1/screenshot?${query}`, {
headers: { Authorization: `Bearer ${process.env.SCREENSHOT_API_KEY}` }
});
if (!response.ok) {
throw new Error(`Screenshot API request failed: ${response.status} ${response.statusText}`);
}
const bytes = Buffer.from(await response.arrayBuffer());
require('fs').writeFileSync('shot.png', bytes);
console.log('Target status:', response.headers.get('x-page-status'));
5. Header rules and edge cases
- Redirects: target headers apply to the target host and are not sent to a different host after a redirect. This prevents a preview token intended for one origin from being forwarded elsewhere.
- Forbidden names: the target-header mechanism refuses
Host,Cookie, and hop-by-hop headers. Use the provider’s cookie or basic-auth settings instead. - API key in a query string: some services accept a query key for direct image embedding, but query keys can appear in page source and logs. Use a throwaway key for that form and prefer an authorization header for server-side Ruby code.
- Header values containing punctuation: always encode query parameters with a URI helper. Never interpolate an unescaped token into a URL.
- Binary response: do not call JSON parsing on an image response. Save
response.bodyor the equivalent byte buffer. - Successful HTTP request, wrong page: a rendered login page, 401 page, or 403 page can still be returned as an image. Check
X-Page-Status. - Secrets in logs: redact API keys and page tokens from exception messages, request logging, CI output, and persisted URLs.
6. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| 401 or 403 from the screenshot service | Missing, expired, or malformed API bearer token | Set SCREENSHOT_API_KEY, send Authorization: Bearer ..., and verify the key with the provider. |
| Image shows a login page | The target site did not receive the expected header, or the header was rejected | Confirm the target header is in the API’s header parameter, then inspect X-Page-Status. |
| Ruby raises a URI or encoding error | Raw token or header text was concatenated into the query string | Build a parameter hash or array and call URI.encode_www_form. |
| Target says header is missing after a redirect | Redirect changed hosts | Use a final same-host URL, configure authentication for the destination host, or avoid relying on a header that must cross origins. |
API rejects Cookie or Host |
Those names are blocked in the target-header mechanism | Use the provider’s cookie or basic-auth option and leave transport headers to the client. |
| Saved file is corrupted | Response was treated as text or JSON | Write bytes with File.binwrite or a binary stream and check the HTTP status first. |
| Timeouts on large pages | Slow target load or provider render timeout | Use a realistic client timeout, reduce unnecessary resources where supported, and retry only transient failures with backoff. |
7. Reliability, performance, and cost considerations
- Timeouts: choose a client timeout longer than the provider’s documented render window. The researched provider documents a default render timeout of 25 seconds; your Ruby timeout must also cover connection and transfer time.
- Retries: retry network disconnects and temporary 5xx responses with exponential backoff. Do not blindly retry 401, 403, malformed requests, or a deterministic target-page error.
- Connection reuse: for batches, reuse a persistent
Net::HTTPconnection where practical instead of opening a new TLS connection for every URL. - Payload size: custom headers do not make the page load faster. Large pages, slow third-party resources, and full-page rendering dominate capture time.
- Billing: the provider’s documented pricing and billing rules apply to each capture request. Cache identical captures when freshness allows and avoid sending duplicate jobs after an uncertain client timeout until you know whether the first job completed.
- Observability: log request IDs and statuses without logging bearer tokens or page credentials. Record the target URL, elapsed time, HTTP result, and
X-Page-Status.
8. Or skip the browser setup
ScreenshotNeo provides a screenshot API and MCP server. It supports custom headers along with cookies, authorization, user agents, waits, selectors, device presets, full-page captures, PDFs, and other capture controls. Read the ScreenshotNeo API documentation for the header option’s current request format.
For a basic capture, one GET request returns the image bytes:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed as clean shots, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. You get 1,000 screenshots a month free without a card; paid plans start at $5 for 3,000 shots.
Create a free ScreenshotNeo account and start with the 1,000-shot monthly allowance.
9. FAQ
Should the page token be an Authorization header?
Only if the target site requires that exact scheme. The screenshot service’s API bearer token authenticates your API call; a page token belongs in the target-header option or the provider’s cookie/basic-auth mechanism.
Can I send the same custom header on every redirected request?
The researched service scopes target headers to the target host and does not forward them to another host. Design the URL and authentication flow with that boundary in mind.
Why did I receive an image when the page failed?
A screenshot can depict an error or login document. Check the API response status and the target status header before accepting the file.
When should I choose POST?
Use POST when the provider supports it and credentials would otherwise appear in a query string or access logs. Keep the API key in the authorization header.
Do I need a Ruby SDK?
No. Ruby’s standard Net::HTTP and URI libraries are sufficient for the documented GET pattern.


