Skip to content

Save PDFs to your own bucket

A storage destination is a bucket of yours that Typefold uploads finished PDFs to. Save it once in your workspace, then name it in a render request. The PDF is in your bucket before the render counts as succeeded.

Any S3-compatible service works, such as Amazon S3, Cloudflare R2, Backblaze B2, Hetzner Object Storage and MinIO.

  • A bucket and an access key for it from your storage provider. The key needs permission to write objects (PutObject); permission to delete them is optional. How to create one: Amazon S3, Cloudflare R2, Backblaze B2, Hetzner Object Storage, MinIO.
  • A workspace owner or admin to save the destination in Studio, or an API key with destination:manage. Rendering to a saved destination needs only render:create.
  • PDF_URL and API_KEY set in your terminal as in the quickstart.

Typefold makes a test upload before it saves the destination: it writes an object named .typefold-connection-test under the prefix, then deletes it. If the key may not delete, the object stays. If the test fails, nothing is saved and the answer says why, so every saved destination is known to work.

Terminal window
curl -X POST "$PDF_URL/v1/storage-destinations" \
-H "x-api-key: $API_KEY" \
-H "content-type: application/json" \
-d '{
"name": "Invoices",
"endpoint": "https://s3.example.com",
"region": "eu-central-1",
"bucket": "example-invoices",
"prefix": "pdf",
"accessKeyId": "<access key ID>",
"secretAccessKey": "<secret access key>"
}'

The response holds the destination, without its secret access key. A failed test upload answers 400 BAD_REQUEST with the reason.

Only owners and admins see Manage → Storage destinations.

Field Default Description
name required What render requests call it, up to 120 characters. Unique in the workspace, ignoring case.
endpoint none The service’s S3 endpoint; see endpoints. Leave it out for Amazon S3.
region us-east-1 The bucket’s region. auto for Cloudflare R2.
bucket required The bucket’s name.
prefix none A folder put in front of every object key, such as pdf. Up to 512 characters.
forcePathStyle false true puts the bucket in the URL’s path instead of its host name. MinIO and some other services need it.
accessKeyId required The access key’s ID.
secretAccessKey required Stored encrypted and never shown or returned again.
Service endpoint region
Amazon S3 Leave it out The bucket’s region
Cloudflare R2 https://<account-id>.r2.cloudflarestorage.com auto
Backblaze B2 https://s3.<region>.backblazeb2.com, such as us-west-004 The same region
Hetzner Object Storage https://<location>.your-objectstorage.com, such as fsn1 The same location
MinIO and others Your server’s S3 address, with "forcePathStyle": true As the service documents

On the hosted service the endpoint must be an https:// address on the public internet. A self-hosted installation can also allow private and http:// addresses; see on your own network.

Add destination to a render request with the destination’s name:

Terminal window
curl "$PDF_URL/v1/renders" \
-H "x-api-key: $API_KEY" \
-H "content-type: application/json" \
-d '{
"templateVersionId": "c7e0b9a4-1f2d-4c3b-9a8e-5d6f7a8b9c0d",
"data": { "invoice": { "number": "INV-2041" } },
"destination": { "name": "Invoices", "path": "2026/{renderId}.pdf" },
"waitMs": 10000
}'
destination field Default Description
name required A saved destination’s name. An unknown name answers 400 BAD_REQUEST.
path {renderId}.pdf The object’s key after the prefix. {renderId} is replaced with the render’s ID. An image defaults to its format’s extension, such as {renderId}.png. Up to 512 characters; the whole key may be at most 1,024 bytes, or the request answers 400 BAD_REQUEST.
keepCopy true false keeps no copy in Typefold; see keep no copy.
  • The object key is the prefix, a slash, then path. Both are slash-separated names without a leading or trailing slash or . and .. parts. With the destination above, the PDF lands at pdf/2026/<render ID>.pdf. Without a prefix it is path alone.
  • An object already at that key is replaced.
  • destination works on every POST /v1/renders request that renders a template version or your own HTML, and on batch items. Previews, in Studio or with a preview body, and the Reports API, which returns the file directly, don’t take it.
  • Webhooks are sent as usual. A render.succeeded event means the PDF is in your bucket.

A succeeded render’s result.destination says where the PDF went:

{
"id": "0b6f0f8e-3c55-4c1e-a3a7-8d1a6f2c9e41",
"state": "succeeded",
"result": {
"downloadUrl": "https://pdf.example.com/v1/storage/...",
"expiresAt": "2026-10-08T09:25:13.480Z",
"destination": {
"name": "Invoices",
"key": "pdf/2026/0b6f0f8e-3c55-4c1e-a3a7-8d1a6f2c9e41.pdf"
},
"pageCount": 2,
"outputBytes": 48213,
"durationMs": 1840
}
}

key is the object’s full key in the bucket. Typefold still keeps its own copy for 30 days, which downloadUrl and the clients’ download methods fetch.

With "keepCopy": false, the PDF goes only to your bucket:

  • The succeeded render has no downloadUrl or expiresAt, so the clients’ download methods raise an error for it, and their render methods that return the PDF refuse keepCopy: false before creating a render. Create the render and wait for it instead, as in step 2.
  • A batch with such items cannot be downloaded as one combined PDF: that request answers 409 CONFLICT.
  • The render itself, with its result and destination, is kept as usual.

The render then fails, with a destination_upload_failed diagnostic whose message names the cause, such as the keys may not write to this bucket or prefix (AccessDenied, HTTP 403).

  • Timeouts, dropped connections, throttling (429, SlowDown) and 5xx answers from the service are retried with the render’s other retries. Wrong keys, permissions, bucket, region or an unknown host are final: fix the destination or the key’s permissions, then render again.
  • A render still queued when its destination is deleted fails the same way. Renaming the destination does not affect renders already created.
  • New keys, a new bucket and other changes apply to renders still queued, because the destination is read when the PDF is uploaded. Only a new prefix applies to new renders alone: the object key is fixed when the render is created.
  • A render cancelled, or failed by its time limit, after its upload has started may still leave its PDF in your bucket.

In Studio, open Manage → Storage destinations to change or delete destinations.

  • A workspace can have up to 10 destinations. An eleventh answers 409 CONFLICT.
  • Saving a change runs the test upload again, except when you only rename the destination.
  • The secret access key is never shown again. Leave the keys out of a change to keep the saved ones. Changing the endpoint, region, bucket or forcePathStyle needs the secret access key again.
  • Deleting a destination deletes its saved keys. Objects already in your bucket stay.

With the API, every operation needs the destination:manage scope. Request and response fields are in the API reference.

Operation What it does
POST /v1/storage-destinations Test and save a destination. Answers 201 Created.
GET /v1/storage-destinations List all destinations, without secret access keys.
GET /v1/storage-destinations/{id} Read one destination, without its secret access key.
PATCH /v1/storage-destinations/{id} Change only the fields you send. null removes endpoint or prefix. Changing endpoint, region, bucket or forcePathStyle needs secretAccessKey.
DELETE /v1/storage-destinations/{id} Delete the destination and its saved keys.

A self-hosted installation can upload to services on private addresses or over plain http://, such as a MinIO server next to it, with DESTINATION_ALLOW_PRIVATE_NETWORKS=true; see configuration.