What you will build, and what you will not need
The goal is a button in a Retool app that turns the selected record into a PDF and opens it in a new tab. Support teams use this for statements, operations teams for job sheets, finance for credit notes. It takes one resource, one query and one event handler.
To be clear from the start: CastPDF has no Retool integration, resource type or listing of its own, and a ready-made CastPDF app for Retool is not available yet. You connect through the generic REST API resource that every Retool organisation already has. Nothing needs installing.
Screens change
Retool moves and renames settings from release to release. The labels here match the editor when this guide was written. If a field has moved, look for the one that does the same job: base URL, header, method, body, timeout.
Create the resource and the query
Use a CastPDF test key while you build. It is free, puts a watermark on each page and allows 20 requests per minute. You also need a template in the CastPDF dashboard; its ID is on the template page.
- Create a REST API resource. Go to Resources, choose Create new, then REST API. Name it "CastPDF".
- Set the base URL. Enter
https://api.castpdf.com/v1as the Base URL. Each query adds its own path. - Add the Authorization header. Under Headers, add
Authorizationwith the valueBearerfollowed by your key. AddContent-Typewithapplication/jsontoo. Save the resource. - Add a query in your app. Open the app, add a new resource query and pick the CastPDF resource. Name it
generatePdf. - Set method, path and body. Choose
POSTand type/pdfafter the base URL. Set the body to Raw and paste the expression from the next section. - Run it only on demand. Make sure the query runs only when triggered, never on page load or input change. Otherwise every visit to the app creates a PDF.
- Raise the timeout. In the query’s advanced settings, set the timeout to at least
60000ms. A render may take up to 30 seconds, and the default is shorter.
Why a resource instead of a header typed into each query? Retool stores resource settings on its server, and resource queries run through its backend. The key never reaches the browser of the person using the app. It also lives in one place, so moving from the test key to a live key is one edit.
If your Retool plan has separate environments, give the resource the test key in staging and the live key in production. Builders then test freely without using up paid PDFs.
The request body, built from your components
Picture a field service company. Its Retool app lists finished jobs in table1, and a second query, partsQuery, loads the parts used on the selected job. The team wants a job sheet PDF for the customer. This raw body builds the whole request in one expression:
{{ JSON.stringify({
template_id: '5b2e8f41-9c3d-4a7e-b610-2d4f8a9c1e37',
data: {
job: table1.selectedRow,
parts: formatDataAsArray(partsQuery.data),
signed_off_by: current_user.fullName
},
filename: 'job-' + table1.selectedRow.job_number + '.pdf',
response: 'url'
}) }}Building the body with JSON.stringify saves you from quoting by hand. A customer note with a quote mark or a line break would otherwise break the JSON. The formatDataAsArray helper matters for SQL queries, which Retool returns column by column. CastPDF templates loop over rows, so the parts need to be an array of objects.
One detail depends on your table component. In the current Table, table1.selectedRow is the row object itself. Older apps with the legacy table nest it under table1.selectedRow.data.
Here is what CastPDF receives for one job once Retool fills in the values. Only real request fields are allowed, and an unknown one is rejected with a 400 error.
{
"template_id": "5b2e8f41-9c3d-4a7e-b610-2d4f8a9c1e37",
"data": {
"job": {
"job_number": "J-88213",
"customer": "Ridgeview Dental Clinic",
"technician": "Tomasz Wilk",
"completed_at": "2026-10-02",
"notes": "Replaced the compressor filter and tested the pressure."
},
"parts": [
{ "sku": "CF-220", "name": "Compressor filter", "qty": 2, "unit_price": 18.5 },
{ "sku": "HS-14", "name": "Hose clamp", "qty": 4, "unit_price": 1.2 }
],
"signed_off_by": "Dana Whitfield"
},
"filename": "job-J-88213.pdf",
"response": "url"
}Open the PDF from a button
With "response": "url", the query returns JSON with url, pages, bytes, id and expires_at. The url is a signed link that works without an API key, so it is safe to open in the browser. There are two easy ways to wire it up.
Event handlers, no code
Add a Button labelled "Job sheet PDF". Give it a Click event handler that triggers generatePdf. On the query, add a Success event handler with the action that opens a URL, and set the URL to {{ generatePdf.data.url }} with the new tab option on.
One JavaScript query
If you prefer code, point the button at a JavaScript query with these two lines. It waits for the PDF, then opens the link with utils.openUrl.
const result = await generatePdf.trigger();
utils.openUrl(result.url, { newTab: true });Set the button to show a loading state while generatePdf.isFetching is true. A render takes a few seconds, and the spinner stops people from clicking twice. You can also turn on the query option that asks for confirmation before it runs.
The link expires at expires_at, which follows your plan’s storage period. Test key files last 1 day. If the PDF should stay attached to the job, save the returned id or url to your database in the same success handler, and fetch a fresh link later with GET /v1/pdf/:id.
When the query fails
CastPDF errors are JSON with a code, a message and a docs_url. Retool shows them in the query’s response panel and marks the run as failed, so a Failure event handler can show a notification with generatePdf.error.
- 401 invalid_api_key. The header on the resource is wrong. It must be named
Authorization, and its value must beBearer, a space and the key, with nothing else. - 422 template_render_error. The template could not render with this data, and
detailspoint to a line and column. OftenselectedRowis empty because no row was selected, so disable the button until one is. - 400 invalid_request. The body is not valid JSON or has a field CastPDF rejects. Build it with
JSON.stringify, and check thattemplate_idis a full ID. - 429 rate_limited. Too many requests in a minute, or too many renders at the same moment. Wait the seconds in the
Retry-Afterheader before running the query again. - Timeouts. A render may run for 30 seconds. If the query gives up first, raise its timeout to 60000 ms or more.
For double clicks and retries, add an Idempotency-Key header to the query with a value such as {{ 'job-' + table1.selectedRow.job_number }}. A repeat with the same key returns the first PDF and is not billed again. If the job data changes, add a version to the key, because the same key with a different body is rejected. The errors reference lists every code.
What it costs
CastPDF bills one PDF per request, whatever the page count, and test keys are free for building. The Free plan includes 100 live PDFs a month; the paid plans are on the pricing page. Retool’s own plan costs apply as usual, so check your plan for limits that affect query runs.
Because the query runs only on a click, you pay for the PDFs people ask for, not for page loads. That is the main reason to keep it on manual trigger.
Next steps
Design the job sheet once in HTML and CSS in the CastPDF dashboard, with a live preview, and start from the report template if you want a table that repeats its header across pages. The templates docs explain versions, and the Liquid docs cover loops and the money filter.
Generating PDFs from an automation instead of an app screen? See the guide for workflow automation nodes or the guide for visual scenario builders.