> ## Documentation Index
> Fetch the complete documentation index at: https://docs.context.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Download resource bytes

> Fetch an image, PDF, or page as base64 and decode it into a local file.

Use [Scrape Bytes](/api-reference/web-scraping/bytes) to retrieve a resource without converting it to Markdown or rendering JavaScript. It returns a JSON response with base64-encoded bytes and download metadata.

A successful download costs **1 credit** and can contain up to **20 MiB** of decoded resource data. The endpoint does not cache responses. A restricted API key needs `data:execute`; see [API key permissions](/guides/manage-api-keys).

## Download and decode a resource

Set `CONTEXT_DEV_API_KEY` on your server and [install an SDK](/sdks) if needed. This example downloads the HTML source of `example.com`; replace the URL and output filename for your own image, PDF, or other resource.

<Note>
  These examples use the SDKs' low-level request methods to send the current API contract, including fields not yet exposed by the 2.14.0 typed helpers. Authentication, retries, and error handling still come from the SDK. See [SDK compatibility](/optimization/api-stability#sdks-can-lag-the-server-contract).
</Note>

<CodeGroup>
  ```bash cURL theme={null}
  curl --get https://api.context.dev/v1/web/scrape/bytes \
    --header "Authorization: Bearer $CONTEXT_DEV_API_KEY" \
    --data-urlencode "url=https://example.com" \
    --data-urlencode "timeoutOpts[milliseconds]=30000" \
    --data-urlencode "timeoutOpts[behavior]=fail"
  ```

  ```typescript TypeScript theme={null}
  import ContextDev from "context.dev";
  import { writeFile } from "node:fs/promises";

  const client = new ContextDev({ apiKey: process.env.CONTEXT_DEV_API_KEY });

  const response = await client.get<{ bytes: string; contentType: string; contentLength: number }>("/web/scrape/bytes", {
    query: {
      "url": "https://example.com",
      "timeoutOpts[milliseconds]": 30000,
      "timeoutOpts[behavior]": "fail"
    }
  });

  const bytes = Buffer.from(response.bytes, "base64");
  await writeFile("resource.html", bytes);
  console.log(response.contentType, bytes.length);
  ```

  ```python Python theme={null}
  import os
  from context.dev import ContextDev
  import base64
  from pathlib import Path

  client = ContextDev(api_key=os.environ["CONTEXT_DEV_API_KEY"])

  response = client.get(
      "/web/scrape/bytes",
      cast_to=dict[str, object],
      options={"params": {'url': 'https://example.com',
       'timeoutOpts[milliseconds]': 30000,
       'timeoutOpts[behavior]': 'fail'}},
  )

  data = base64.b64decode(response["bytes"], validate=True)
  Path("resource.html").write_bytes(data)
  print(response["contentType"], len(data))
  ```

  ```ruby Ruby theme={null}
  require "cgi/core"
  require "context_dev"
  require "base64"

  client = ContextDev::Client.new(api_key: ENV.fetch("CONTEXT_DEV_API_KEY"))

  response = client.request(
    method: :get,
    path: "web/scrape/bytes",
    query: {
      "url": "https://example.com",
      "timeoutOpts[milliseconds]": 30000,
      "timeoutOpts[behavior]": "fail"
    }
  )

  data = Base64.strict_decode64(response.fetch(:bytes))
  File.binwrite("resource.html", data)
  puts response[:contentType], data.bytesize
  ```

  ```go Go theme={null}
  package main

  import (
  	"context"
  	"encoding/base64"
  	"fmt"
  	"os"

  	contextdev "github.com/context-dot-dev/context-go-sdk/v2"
  	"github.com/context-dot-dev/context-go-sdk/v2/option"
  )

  func main() {
  	client := contextdev.NewClient(option.WithAPIKey(os.Getenv("CONTEXT_DEV_API_KEY")))
  	var response struct {
  		Bytes       string `json:"bytes"`
  		ContentType string `json:"contentType"`
  	}
  	err := client.Get(context.Background(), "/web/scrape/bytes", nil, &response,
  		option.WithQuery("url", "https://example.com"),
  		option.WithQuery("timeoutOpts[milliseconds]", "30000"),
  		option.WithQuery("timeoutOpts[behavior]", "fail"),
  	)
  	if err != nil {
  		panic(err)
  	}
  	data, err := base64.StdEncoding.DecodeString(response.Bytes)
  	if err != nil {
  		panic(err)
  	}
  	if err := os.WriteFile("resource.html", data, 0600); err != nil {
  		panic(err)
  	}
  	fmt.Println(response.ContentType, len(data))
  }
  ```

  ```php PHP theme={null}
  <?php
  require __DIR__.'/vendor/autoload.php';

  use ContextDev\Client;

  $client = new Client(apiKey: getenv('CONTEXT_DEV_API_KEY'));

  $raw = $client->request(
      method: 'get',
      path: 'web/scrape/bytes',
      query: [
          "url" => "https://example.com",
          "timeoutOpts[milliseconds]" => 30000,
          "timeoutOpts[behavior]" => "fail",
      ],
  );

  $response = json_decode((string) $raw->getBody(), true, flags: JSON_THROW_ON_ERROR);
  $bytes = base64_decode($response['bytes'], true);
  if ($bytes === false || file_put_contents('resource.html', $bytes) === false) {
      throw new RuntimeException('Could not decode or save the resource');
  }
  echo $response['contentType'], ' ', strlen($bytes), PHP_EOL;
  ```
</CodeGroup>

The cURL example prints the JSON response. Decode its `bytes` field with a base64 decoder to recover the file, as the SDK examples do. The value has no `data:` URI prefix.

## Read download metadata

| Field               | Meaning                                                                                         |
| ------------------- | ----------------------------------------------------------------------------------------------- |
| `bytes`, `encoding` | Base64-encoded resource content; `encoding` is `base64`.                                        |
| `url`, `finalUrl`   | Requested URL and URL after redirects.                                                          |
| `contentType`       | Origin Content-Type, including a charset when supplied; defaults to `application/octet-stream`. |
| `contentLength`     | Decoded byte count, before base64 encoding, at most 20,971,520.                                 |
| `statusCode`        | Successful HTTP status from the origin, in the 200–299 range.                                   |
| `request_id`        | ID to use when investigating the API call in [request logs](/api-reference/logs/retrieve).      |

HTTP content compression is decoded before the bytes are returned. `contentLength` measures the resulting resource, not the larger JSON/base64 response body.

## Headers, location, and privacy

Use `country` for supported country routing. Supply target request headers as a JSON-encoded `headers` query value or deep-object keys such as `headers[Referer]`.

The API bearer key goes in the API request's Authorization header. A target site's own credential, when needed and authorized, goes inside `headers`. Do not forward your Context.dev API key to the target. Host, Content-Length, and hop-by-hop headers are rejected; target authorization and cookies are removed when a redirect changes origin.

Set `zdr=enabled` when [Zero Data Retention](/optimization/zero-data-retention) is enabled for your organization, and check `X-Context-ZDR: true`. The endpoint requires a public HTTP(S) destination and validates redirects.

## Deadlines and limits

`timeoutOpts.milliseconds` accepts 1–300,000. Bytes supports only `behavior: "fail"`: the entire download must complete, so partial-result mode is rejected. Failed downloads are not charged. See [timeouts](/optimization/timeouts) and [troubleshooting](/optimization/troubleshooting) for shared errors.

Use [Parse documents](/guides/parse-documents) to convert a document to Markdown; Parse accepts uploads up to 50 MiB. Use [Extract page images](/guides/extract-page-images) to discover images referenced by a page, or [Scrape a webpage](/guides/scrape-websites-to-markdown) when you need rendered page text.
