# Upload to Cloudflare R2

Send every rendered image or PDF to a Cloudflare R2 bucket you control.

HTML/CSS to Image connects to Cloudflare R2 through its S3-compatible API. You provide an R2 access key, secret, Cloudflare account ID, and the bucket’s data jurisdiction.

Plan availability

Storage destinations are available on the **10,000 images/month plan or higher**. To try this on another plan, email [support@htmlcsstoimage.com](mailto:support@htmlcsstoimage.com).

## Before you begin

You need:

*   An existing R2 bucket.
*   The Cloudflare account ID that owns it.
*   The bucket’s data jurisdiction, if it was created with one.
*   An optional key prefix, such as `hcti-renders`.

## Create R2 S3 credentials

In the Cloudflare dashboard, open **R2 Object Storage**, select **Manage API Tokens**, and create an account or user API token.

1.  Choose **Object Read & Write** permission.
2.  Scope the token to the destination bucket when possible.
3.  Create the token.
4.  Copy the **Access Key ID** and **Secret Access Key**. Cloudflare shows the secret only once.

Read and write access lets HCTI upload renders and reload an original for a later resize, crop, or format conversion when **Disable HCTI Storage** is selected. HCTI uses delete access only to clean up connection-test objects; it does not delete rendered images from R2.

## Add Cloudflare R2 to HCTI

Open the [Storage Destinations dashboard](https://htmlcsstoimage.com/dashboard/storage-destinations), click **Add storage destination**, and enter:

| HCTI field | Cloudflare value |
| --- | --- |
| Provider | **Cloudflare R2** |
| Bucket | Existing R2 bucket name |
| Key prefix | Optional folder-like prefix, without leading or trailing slashes |
| Account ID | Cloudflare account ID |
| Access key ID | R2 token’s Access Key ID |
| Secret access key | R2 token’s Secret Access Key |
| Data jurisdiction | The jurisdiction used when the bucket was created |

### Choose the R2 data jurisdiction

The setting determines which R2 endpoint HCTI uses:

| Dashboard option | Endpoint | Use when |
| --- | --- | --- |
| **Default** | `<account-id>.r2.cloudflarestorage.com` | The bucket has no jurisdiction restriction. This is also the correct option for a bucket with only a location hint. |
| **European Union** | `<account-id>.eu.r2.cloudflarestorage.com` | The bucket was created in the European Union jurisdiction. |
| **FedRAMP** | `<account-id>.fedramp.r2.cloudflarestorage.com` | The bucket was created in the FedRAMP jurisdiction. |

The wrong jurisdiction points HCTI at an endpoint that cannot access the bucket. If you change this setting later, run **Test connection** again before saving.

## Test and save the destination

1.  Click **Test connection**.
2.  Enable and save the destination after the test succeeds.
3.  Copy the destination `id`.

The test writes a small object under `<key-prefix>/.hcti/connection-tests/`. Cleanup is best-effort and does not determine whether the connection passes.

## Upload an HCTI image to R2

Pass the destination ID as `storage_destination_id` when you create an image:

```bash
curl -X POST https://hcti.io/v1/image \
  -u 'user-id:api-key' \
  -H 'Content-Type: application/json' \
  -d '{
        "url": "https://example.com",
        "storage_destination_id": "your-storage-destination-id"
      }'
```

Rendering is lazy. With normal HCTI storage enabled, request the returned image URL to render the image and write both copies. With **Disable HCTI Storage** selected, send an authenticated `PUT` request to the returned `/v1/store/...` URL.

The base object is stored as `<key-prefix>/<image-id>.png`, or `.pdf` when the request uses `pdf_options`. R2 bucket access remains under your control; HCTI does not make the object public.

## Troubleshooting Cloudflare R2 uploads

*   **The connection test cannot find the bucket** — Confirm the bucket name, account ID, and data jurisdiction.
*   **Authentication fails** — Use the S3 Access Key ID and Secret Access Key generated for R2, not a general Cloudflare API token string.
*   **The secret is unavailable** — Create a new R2 API token; Cloudflare does not show the secret again after creation.
*   **A later transformation fails with HCTI Storage disabled** — Confirm the token has object read access as well as write access.
*   **The connection worked before the jurisdiction changed** — Test again; changing the jurisdiction changes the R2 endpoint.

Cloudflare documents [creating R2 S3 API credentials](https://developers.cloudflare.com/r2/get-started/s3/) and [jurisdiction-specific R2 endpoints](https://developers.cloudflare.com/r2/reference/data-location/).

Next: use your storage destination

[Continue to the storage destinations guide](/guides/advanced/storage-destinations/) for templates, batches, object storage behavior, and the complete `PUT /v1/store` response.

## Automate destination setup

You can also automate configuration through the [Storage destinations API](/management-api/storage-destinations/) or [MCP tools](/integrations/mcp/tools/#storage-destinations).

## Need help?

Talk to a human. Email [support@htmlcsstoimage.com](mailto:support@htmlcsstoimage.com) and we’ll help you get started.
