Docs

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

WayWhen to use itHowPDF up to
File in the requestYou 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 URLThe PDF already lives in storage, such as S3, Google Cloud Storage or Azure.JSON, document.type: url.10 MB
Two-step uploadYou 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 base64The 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:

  1. Create an upload with POST /v1/uploads, giving the file name (filename) and its size in bytes (size). The response carries the upload id, which starts with upl_, and a signed upload_url.
  2. Send the file with a PUT to the upload_url: the PDF bytes in the body, the Content-Type: application/pdf header and no Authorization header, because the signature is already in the URL. The link works for 2 hours. A 2xx response confirms the file arrived.
  3. 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 same Idempotency-Key.
  • A dry run does not use it up. You can use the same upload_id with 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_url accepts the file for 2 hours (upload_url_expires_at). The upload can be used for 24 hours (expires_at); after that, the API answers upload_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 variables inside document, next to upload_id.
  • The upload belongs to your account. An upload_id from another account gets upload_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.

Excerpt of the document in Word
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 variables inside document, with type: url, type: upload or type: 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.
JSON
"document": {
  "type": "url",
  "url": "https://arquivos.exemplo.com.br/faturas/modelo-fatura.pdf",
  "variables": {
    "nome": "Destinatário Exemplo",
    "mes": "outubro",
    "vencimento": "20/10/2026"
  }
}
Multipart
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:

SituationResponse
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_placement is first_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.

Address area on the first pageAn A4 page, 210 by 297 mm. The address area is 90 by 45 mm, 20 mm from the left edge and 45 mm from the top edge.Address areaKeep it blank: CartaFlowprints the sender and therecipient here.20 mm45 mm90 mm45 mmA4 · 210 x 297 mm
The dashed line is the safe margin: keep text and important elements at least 5 mm from the edges.

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_placementWhere the address is printedCost
first_page (default)In the address area of your document’s first page.No extra charge.
extra_pageOn 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: true prints in color. With false, 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: true prints on both sides: each sheet carries two pages of the document, so the letter uses fewer sheets and costs less. With false, 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 pagesdouble_sidedaddress_placementsheets
1falsefirst_page1
3falsefirst_page3
3truefirst_page2
2falseextra_page2
7trueextra_page4
2trueextra_page2: the address sheet is not charged
7falsefirst_page7: over the limit, too_many_sheets
12trueextra_page7: 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_url linking 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[].codeWhat happened
document_unreachableWe could not download the PDF from the URL in time, or the server answered with an error.
upload_not_foundThere is no upload with that upload_id in your account.
upload_expiredThe upload was created more than 24 hours ago.
upload_already_usedThe upload was already used by another letter.
upload_file_missingThe file was never sent to the upload_url.
document_invalidThe file is not a valid PDF. For base64, check there is no data: prefix.
document_encryptedThe PDF is password protected.
document_too_largeThe PDF is larger than its way in allows: 10 MB by URL or upload, 4 MB in multipart, 3 MB in base64.
document_page_sizeA page is not A4 portrait.
too_many_sheetsThe letter would need more than 6 sheets.
address_area_not_blankThere is content in the address area of the first page.
document_text_unreadableYou sent variables, but the PDF has no selectable text.
variable_missingThe document has a marker that is missing from variables.
variable_not_foundYou 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.

On this page