---
layout: page
title: Upload to Cloudflare R2
nav_title: Cloudflare R2
permalink: /guides/advanced/storage-destinations/r2/
parent: Storage Destinations
grand_parent: Advanced
great_grand_parent: Guides
nav_order: 2
description: >-
Upload images generated from HTML/CSS, URL screenshots, and PDFs directly to Cloudflare R2 with S3 API credentials and HTML/CSS to Image.
---
# Upload images to Cloudflare R2
{: .no_toc }
{: .fs-9 }
Send every rendered image or PDF to a Cloudflare R2 bucket you control.
{: .fs-6 .fw-300 }
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.
{% include hint.md title="Plan availability" text="Storage destinations are available on the **10,000 images/month plan or higher**. If you'd like to try this on a different plan, email **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** | `.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** | `.eu.r2.cloudflarestorage.com` | The bucket was created in the European Union jurisdiction. |
| **FedRAMP** | `.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 `/.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 `/.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/).
{% include hint.md title="Next: use your storage destination" text="[Continue to the Storage Destinations guide](/guides/advanced/storage-destinations/) for templates, batches, object storage behavior, and the complete `PUT /v1/store` response." %}
{% include code_footer.md version=1 %}