# Upload to Amazon S3

Send every rendered image or PDF to an Amazon S3 bucket you control.

HTML/CSS to Image connects to Amazon S3 by assuming an IAM role in your AWS account. HCTI receives temporary credentials when it needs to write an object, so you do not need to create or share a long-lived AWS access key.

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 S3 bucket and its AWS region.
*   Permission to create an IAM role and attach a permissions policy.
*   An optional key prefix, such as `hcti-renders`, if HCTI objects should stay within one part of the bucket.

Open the [Storage Destinations dashboard](https://htmlcsstoimage.com/dashboard/storage-destinations), click **Add storage destination**, and choose **Amazon S3**. Enter the bucket, region, and optional key prefix first; HCTI uses those values to generate scoped example policies.

## Create the IAM role

The dashboard generates a trust policy and a role permissions policy. They answer different questions and belong in different parts of the IAM role.

### Add the trust policy

The trust policy controls **who can assume the role**. It contains:

*   The HTML/CSS to Image storage writer role as the principal.
*   `sts:AssumeRole` as the allowed action.
*   An `sts:ExternalId` condition unique to your HCTI organization.

In AWS IAM, go to **Roles**, choose **Create role**, select **Custom trust policy**, and paste the dashboard-generated trust policy. The external ID prevents the HCTI writer role from using this role for a different organization. Use the generated value without modifying or reusing it.

The trust policy only allows HCTI to assume the role. It does not grant access to your S3 bucket.

### Attach the permissions policy

Attach the dashboard-generated role permissions policy as an inline or managed policy. It scopes access to the bucket and optional prefix entered in HCTI.

| Permission | Required? | Purpose |
| --- | --- | --- |
| `s3:PutObject` | **Required** | Writes rendered images and the connection-test object. |
| `s3:GetObject` | Optional | Reloads an original for a later resize, crop, or format conversion when **Disable HCTI Storage** is selected. |
| `s3:DeleteObject` | Optional | Removes the small object created by **Test connection**. It is not used to delete rendered images. |

The generated resources cover rendered objects under `arn:aws:s3:::<bucket>/<key-prefix>/*`, including connection-test objects under `<key-prefix>/.hcti/connection-tests/`.

Write access is enough to connect and upload base renders. Include read access if HCTI may need to create a transformation after the initial render while HCTI Storage is disabled. Include delete access if you want connection tests to clean up after themselves.

## Finish the destination in HCTI

1.  Finish creating the role and copy its ARN.
2.  Return to the storage destination form and paste the ARN into **IAM role ARN**.
3.  Confirm the bucket, AWS region, and optional key prefix.
4.  Click **Test connection**.
5.  Enable and save the destination after the test succeeds.
6.  Copy the destination `id`.

The connection test assumes the role and writes a small text object. Cleanup is best-effort, so a missing `s3:DeleteObject` permission does not make a successful write test fail.

## Upload an HCTI image to S3

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 image URL returned by the API to render the image and write both the HCTI copy and the S3 copy. With **Disable HCTI Storage** selected, send an authenticated `PUT` request to the `/v1/store/...` URL returned by the create request.

The base object is stored as `<key-prefix>/<image-id>.png`, or `.pdf` when the request uses `pdf_options`. Resized, cropped, and reformatted versions use separate object keys.

## Troubleshooting Amazon S3 uploads

*   **HCTI cannot assume the role** — Confirm that the trust relationship contains the generated HCTI principal and the exact organization-specific external ID.
*   **The connection test returns Access Denied** — Confirm that the role permissions policy allows `s3:PutObject` on the bucket and configured prefix, and that a bucket policy does not explicitly deny the request.
*   **The connection test uses the wrong endpoint** — Confirm that the AWS region in HCTI matches the bucket’s region.
*   **A later transformation fails with HCTI Storage disabled** — Add `s3:GetObject` for the configured object prefix.
*   **Connection-test objects remain** — Add `s3:DeleteObject` for the `.hcti/connection-tests/` path.

AWS documents [creating a role with a custom trust policy](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-custom.html) and [using external IDs for third-party access](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_common-scenarios_third-party.html).

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). `GET /v1/storage-destinations/aws-external-id` and `get_aws_storage_external_id` return both `external_id` and `writer_role_arn`; both operations require `storage_destinations:create_update` and take no destination ID.

Use `writer_role_arn` as `Principal.AWS` and `external_id` in a `StringEquals` condition on `sts:ExternalId` when creating your role’s trust policy. This supplies both values programmatically, without copying the principal from the dashboard. Attach the bucket permissions described above, then pass **your role’s ARN** as the destination’s `connection_info.role_arn`. The returned `writer_role_arn` identifies HCTI’s existing role, which assumes your role; it is not the destination’s role ARN.

## Need help?

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