--- 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 %}