Document and address area
How to send the PDF of a letter, what it needs, and where CartaFlow prints the address.
The content of a letter is a PDF. It goes either as a file, in a multipart upload, or in the document field of a JSON body. CartaFlow prints it as it is: the layout, colors, logo and text are those of your PDF. CartaFlow only adds the address, in the address area of the first page, and the values of your variables, when you send variables.
Four ways to send the PDF
| Way | When to use it | How | PDF up to |
|---|---|---|---|
| File in the request | You have the PDF on disk or in memory and it is small. The simplest way. | multipart/form-data, with the PDF in the file part. | 4 MB |
| PDF from a URL | The PDF already lives in storage, such as S3, Google Cloud Storage or Azure. | JSON, document.type: url. | 10 MB |
| Two-step upload | You have the PDF on disk or in memory and it is large. | POST /v1/uploads, a PUT of the file, then JSON with document.type: upload. | 10 MB |
| PDF in base64 | The PDF is small and you prefer to send everything as JSON. | JSON, document.type: base64. | 3 MB |
Why the limits differ
A request to the API can carry a body of up to 4.5 MB. A PDF that travels inside the request, in multipart or in base64, has to fit in that limit together with the other fields (base64 makes the file about a third larger). By URL or with the two-step upload, the file does not go through the API request, and the limit is 10 MB.
All four also work with POST /v1/letters/quote, which takes the same body as creating a letter.
File upload (multipart/form-data)
Send the PDF in the file part, up to 4 MB, and the other fields as form parts, with the same names as in the JSON body. Object fields use brackets: to[name], to[postal_code], from[number], metadata[customer_id], variables[nome]. Lists repeat the field with []: postal_services[]=registered. Booleans go as text: true or false. There is no document field in this format: the file is the content.
curl -X POST "https://api.cartaflow.com.br/v1/letters" \
-H "Authorization: Bearer $CARTAFLOW_API_KEY" \
-H "Idempotency-Key: fatura-2026-10-0042" \
-F "file=@fatura-2026-10-0042.pdf" \
-F "to[name]=Destinatário Exemplo" \
-F "to[postal_code]=01001-000" \
-F "to[number]=128" \
-F "to[complement]=Ap. 52" \
-F "color=true" \
-F "reference=fatura-2026-10-0042" \
-F "metadata[customer_id]=1842"The response is the same as when creating with JSON. In document, the letter tells you which way was used: { "type": "upload", "filename": "fatura-2026-10-0042.pdf" }.
PDF from a URL
CartaFlow downloads the file once, when the letter is created. The URL must be HTTPS, public or signed, and stay valid for at least 10 minutes. We wait up to 10 seconds for the download. The file can be up to 10 MB.
"document": {
"type": "url",
"url": "https://arquivos.exemplo.com.br/faturas/2026-10-0042.pdf"
}Two-step upload
For a PDF of up to 10 MB that you have at hand but that does not fit in one request, send the file first and then create the letter:
- Create an upload with POST /v1/uploads, giving the file name (
filename) and its size in bytes (size). The response carries the uploadid, which starts withupl_, and a signedupload_url. - Send the file with a
PUTto theupload_url: the PDF bytes in the body, theContent-Type: application/pdfheader and noAuthorizationheader, because the signature is already in the URL. The link works for 2 hours. A 2xx response confirms the file arrived. - Create the letter with
document: { "type": "upload", "upload_id": "upl_..." }, within 24 hours of creating the upload.
# This example uses jq to read the JSON response.
SIZE=$(wc -c < fatura-2026-10-0042.pdf | tr -d ' ')
# 1. Create the upload with the file name and size.
UPLOAD=$(curl -s -X POST "https://api.cartaflow.com.br/v1/uploads" \
-H "Authorization: Bearer $CARTAFLOW_API_KEY" \
-H "Content-Type: application/json" \
-d "{ \"filename\": \"fatura-2026-10-0042.pdf\", \"size\": $SIZE }")
UPLOAD_ID=$(echo "$UPLOAD" | jq -r .id)
UPLOAD_URL=$(echo "$UPLOAD" | jq -r .upload_url)
# 2. Send the file to the upload_url, without the Authorization header.
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: application/pdf" \
--data-binary "@fatura-2026-10-0042.pdf"
# 3. Create the letter with the upload.
curl -X POST "https://api.cartaflow.com.br/v1/letters" \
-H "Authorization: Bearer $CARTAFLOW_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: fatura-2026-10-0042" \
-d "{
\"to\": { \"name\": \"Destinatário Exemplo\", \"postal_code\": \"01001-000\", \"number\": \"128\" },
\"document\": { \"type\": \"upload\", \"upload_id\": \"$UPLOAD_ID\" },
\"reference\": \"fatura-2026-10-0042\"
}"Upload rules:
- One letter per upload. Once a letter is created with it, the upload cannot be used for another one: the API answers 422 with
upload_already_used. To retry the same creation safely, use the sameIdempotency-Key. - A dry run does not use it up. You can use the same
upload_idwith POST /v1/letters/quote as many times as you like before creating the letter. A refused creation, such as a 422, does not use it up either. - Time limits. The
upload_urlaccepts the file for 2 hours (upload_url_expires_at). The upload can be used for 24 hours (expires_at); after that, the API answersupload_expired. - The file must have arrived. If you create the letter without having sent the file, the API answers
upload_file_missing. - The file is checked at creation, with the same rules as any PDF: A4, no password, blank address area, up to 6 sheets.
- Variables work the same way: send
variablesinsidedocument, next toupload_id. - The upload belongs to your account. An
upload_idfrom another account getsupload_not_found.
On the created letter, document holds { "type": "upload", "filename": "fatura-2026-10-0042.pdf" }, the same as a multipart upload.
PDF in base64
The PDF travels inside the JSON body, in standard base64, without a data:application/pdf;base64, prefix. The file can be up to 3 MB once decoded (about 4 MB in base64), so the whole body fits in the 4.5 MB request limit. For larger PDFs, use a URL or the two-step upload.
PDF=$(base64 < contrato-5531.pdf | tr -d '\n')
curl -X POST "https://api.cartaflow.com.br/v1/letters" \
-H "Authorization: Bearer $CARTAFLOW_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: contrato-5531" \
-d "{
\"to\": { \"name\": \"Destinatário Exemplo\", \"postal_code\": \"01001-000\", \"number\": \"128\" },
\"document\": { \"type\": \"base64\", \"data\": \"$PDF\" },
\"double_sided\": true,
\"reference\": \"contrato-5531\"
}"Variables inside a PDF
A PDF can contain variable markers, such as {{nome}} or {{valor_total}}. It is the simplest way to personalize a document made in Word or another editor: type the marker in the text, export to PDF and send the variables with it.
Olá, {{nome}}.
Sua fatura de {{mes}} vence em {{vencimento}}.CartaFlow writes each value in place of its marker, at the same size and in the same color, and the rest of the line adjusts to the value as if it had been typed there: no gap when the value is short and no overlap when it is long. A centered line stays centered, and text after a tab stop and on other lines stays where it is. Values do not wrap: when a value does not fit, the rest of the line is condensed slightly, down to 85% of its width, and if it still does not fit it runs past the margin. This never blocks sending; check the result in the letter's preview_url or in the dashboard's preview.
- As JSON, send
variablesinsidedocument, withtype: url,type: uploadortype: base64. - As multipart, send one part per variable:
variables[nome],variables[mes]. - Names use letters, digits and underscores. Values are text or numbers.
- Without
variables, the PDF is printed as it is.
"document": {
"type": "url",
"url": "https://arquivos.exemplo.com.br/faturas/modelo-fatura.pdf",
"variables": {
"nome": "Destinatário Exemplo",
"mes": "outubro",
"vencimento": "20/10/2026"
}
}curl -X POST "https://api.cartaflow.com.br/v1/letters" \
-H "Authorization: Bearer $CARTAFLOW_API_KEY" \
-F "file=@modelo-fatura.pdf" \
-F "to[name]=Destinatário Exemplo" \
-F "to[postal_code]=01001-000" \
-F "to[number]=128" \
-F "variables[nome]=Destinatário Exemplo" \
-F "variables[mes]=outubro" \
-F "variables[vencimento]=20/10/2026"The check is strict both ways, so no document goes out with a forgotten marker:
| Situation | Response |
|---|---|
The document has a marker that is missing from variables. | 422 variable_missing |
| You sent a variable the document does not have. | 422 variable_not_found |
| The PDF has no selectable text: it is an image or a scan. | 422 document_text_unreadable |
Export the PDF straight from the editor
In Word, use File > Save as > PDF. Scanning, or printing an image to PDF, produces a PDF without text, and the API cannot find the markers. To check the markers at no cost, run a dry run with POST /v1/letters/quote with the same variables.
In an address verification with your own PDF, {{OTP_CODE}}, {{VERIFICATION_URL}} and {{QR_CODE}} are reserved markers, filled in by CartaFlow with the code and the way to the confirmation page. See Your own PDF.
What the PDF needs
- A4 portrait (210 x 297 mm) on every page. A4 is the standard paper size in Brazil; US Letter is not accepted.
- Within the limit of its way in: 10 MB by URL or two-step upload, 4 MB in multipart and 3 MB in base64. Images at 300 dpi are the most print makes use of.
- Up to 6 sheets per letter, counting the sheet the address sometimes adds. That is what fits in one envelope.
- No password and no printing restrictions.
- Embedded fonts. Export the PDF with its fonts embedded so the text prints exactly as you see it on screen.
- Safe margin. Keep text and important elements at least 5 mm from the edges.
- A blank address area on the first page, when
address_placementisfirst_page.
The address area
The envelope has a window. Through it you see a rectangle of the first page: the address area. CartaFlow prints the sender there, on one line, with the recipient right below it. That is why the area must be blank in your PDF.
If your layout fills that area, for example because the first page of an invoice already has other content there, use address_placement: extra_page.
address_placement | Where the address is printed | Cost |
|---|---|---|
first_page (default) | In the address area of your document’s first page. | No extra charge. |
extra_page | On a side of its own, the first one, which shows through the window, carrying the sender and the recipient. It takes a side that would otherwise be blank, when there is one; if not, one more sheet goes in at the front. Your document follows. | No extra charge, even when a sheet is added. |
When the address area is not blank and address_placement is first_page, the API answers 422 with the code address_area_not_blank, and nothing is created or charged.
Color and double-sided printing
color: trueprints in color. Withfalse, the default, printing is black and white, and a color PDF comes out in grayscale. Color printing costs extra per printed page: each printed side of a sheet counts as a page.double_sided: trueprints on both sides: each sheet carries two pages of the document, so the letter uses fewer sheets and costs less. Withfalse, the default, each page goes on its own sheet.
How many sheets a letter uses
The sheets field says how many sheets of paper go in the envelope. The price counts only the sheets of your document: the address is never charged, even when it adds a sheet.
- Front only: one sheet per page.
- Double-sided: half the pages, rounded up.
- With
extra_page, front only: the same number of sheets. The address goes on the back of the first page, and only that sheet is printed on both sides. - With
extra_page, double-sided, odd number of pages: the same number of sheets. The address takes the spare side. - With
extra_page, double-sided, even number of pages: one more sheet, with the address alone and a blank back.
| PDF pages | double_sided | address_placement | sheets |
|---|---|---|---|
| 1 | false | first_page | 1 |
| 3 | false | first_page | 3 |
| 3 | true | first_page | 2 |
| 2 | false | extra_page | 2 |
| 7 | true | extra_page | 4 |
| 2 | true | extra_page | 2: the address sheet is not charged |
| 7 | false | first_page | 7: over the limit, too_many_sheets |
| 12 | true | extra_page | 7: over the limit, too_many_sheets |
With color: true, the color price line counts pages, not sheets: each printed side is a page. A 3-page PDF printed double-sided uses 2 sheets and 3 color pages. The address side is never printed in color or charged.
When a letter would need more than 6 sheets, counting the sheet the address adds, the API answers 422 with the code too_many_sheets. Use double_sided: true, or split the content into more than one letter.
Check the result before sending
- Dry run. POST /v1/letters/quote takes exactly the same body as creating a letter, validates the document and the variables, counts the pages and returns the price and the complete address. Nothing is created or charged.
- Final PDF. Every letter has a
preview_urllinking to the PDF exactly as it will be printed, address included. The link works for 60 minutes; retrieve the letter again for a fresh one. The file stays available for at least 90 days after creation (or after the send date, for a scheduled letter).
For a black-and-white letter, preview_url shows the original colors of the PDF: the conversion to grayscale happens at printing.
While the letter is received (or scheduled, if it was scheduled), you can still cancel it: nothing is printed and, if it was already charged, the amount goes back to your balance. See Letters and statuses.
Document errors
When the document has a problem, the API answers 422 with validation_error, and each item in details names the field and the reason. Nothing is created or charged.
details[].code | What happened |
|---|---|
document_unreachable | We could not download the PDF from the URL in time, or the server answered with an error. |
upload_not_found | There is no upload with that upload_id in your account. |
upload_expired | The upload was created more than 24 hours ago. |
upload_already_used | The upload was already used by another letter. |
upload_file_missing | The file was never sent to the upload_url. |
document_invalid | The file is not a valid PDF. For base64, check there is no data: prefix. |
document_encrypted | The PDF is password protected. |
document_too_large | The PDF is larger than its way in allows: 10 MB by URL or upload, 4 MB in multipart, 3 MB in base64. |
document_page_size | A page is not A4 portrait. |
too_many_sheets | The letter would need more than 6 sheets. |
address_area_not_blank | There is content in the address area of the first page. |
document_text_unreadable | You sent variables, but the PDF has no selectable text. |
variable_missing | The document has a marker that is missing from variables. |
variable_not_found | You sent a variable the document does not have. |
A body larger than 4.5 MB is not even read: the API answers 413 with request_too_large. The full list of codes is in Errors.