Conventions
The rules that apply to every endpoint, so you never have to guess.
Every endpoint of the API follows the same rules. Once you have read this page, you know the shape of any request and any response.
Requests
- The base URL is
https://api.cartaflow.com.br/v1. HTTPS only. - Bodies are JSON in UTF-8, with the
Content-Type: application/jsonheader. - There are two exceptions, the endpoints that take a PDF:
POST /v1/lettersandPOST /v1/letters/quote. They also acceptmultipart/form-data, so you can upload the file together with the data, without converting it to base64. See Upload a PDF with multipart. - Any other
Content-Typegets a 400 withunsupported_content_type. - A body can be up to 4.5 MB. A PDF larger than 4 MB goes by URL or through the two-step upload (
POST /v1/uploads), which take up to 10 MB. See Document and address area. - Unknown fields are rejected with a 422 and the
unknown_parametercode. That way a typo such aspostal_codnever goes unnoticed.
Upload a PDF with multipart
On POST /v1/letters and POST /v1/letters/quote, you can send the body as multipart/form-data. The PDF goes in the file part, up to 4 MB. The other fields are the same as in JSON, one per part, with fields inside objects written in brackets and lists repeating the field with []:
| In JSON | In multipart |
|---|---|
{ "to": { "name": "Destinatário Exemplo" } } | to[name]=Destinatário Exemplo |
{ "to": { "postal_code": "01001-000" } } | to[postal_code]=01001-000 |
{ "variables": { "nome": "Destinatário Exemplo" } } | variables[nome]=Destinatário Exemplo |
{ "metadata": { "customer_id": "1842" } } | metadata[customer_id]=1842 |
{ "color": true } | color=true |
{ "postal_services": ["registered", "return_receipt"] } | postal_services[]=registered and postal_services[]=return_receipt |
Booleans are sent as the text true or false. Validation rules, responses and errors are the same as with JSON.
curl -X POST https://api.cartaflow.com.br/v1/letters \
-H "Authorization: Bearer $CARTAFLOW_API_KEY" \
-F "file=@exemplo-a4.pdf" \
-F "to[name]=Destinatário Exemplo" \
-F "to[postal_code]=01001-000" \
-F "to[number]=128" \
-F "color=true"With curl -F, the Content-Type: multipart/form-data header is set for you.
Names in English, messages in your language
Field names, values, events and error codes are in English and snake_case: postal_code, in_production, letter.posted, insufficient_balance. A few Brazilian concepts keep a plain English name: the CEP (postal code) is postal_code and the UF (state code) is state.
Text written for people, such as error.message, comes in Portuguese or English according to the Accept-Language header. Without that header the API answers in Portuguese, so send Accept-Language: en to get English messages.
IDs
Every object has an id with a prefix that tells its type. Store the whole id, as a string.
| Prefix | Object | Example |
|---|---|---|
ltr_ | Letter | ltr_8Kq2wX4mN7pR |
upl_ | PDF upload (POST /v1/uploads) | upl_6Np3Vx8Rk2Tq |
ver_ | Address verification | ver_3Hd9Kp2qZ6tW |
evt_ | Event | evt_6Tq1Yz8Wm3Rb |
btx_ | Balance transaction | btx_4Lm8Qw2Zx9Kd |
acc_ | Account | acc_7Jw3Nf8Xq2Lp |
req_ | Request (the Request-Id header and error.request_id) | req_9Vb4Xk2Lq8Mz |
Every object also has an object field with its type spelled out: letter, upload, verification, event, balance_transaction, account. Lists have object: "list".
Dates
- Dates with a time follow ISO 8601, in UTC:
2026-10-13T14:20:00Z. Convert them to your user's time zone when you display them. send_date, the date a letter is scheduled for, is a date only:2026-10-20, formattedYYYY-MM-DD, in Brasília time (America/Sao_Paulo).- The
created_afterandcreated_beforefilters take an ISO 8601 date and time.
Money
Amounts are integers in centavos (1/100 of a real), always with a currency field set to BRL (Brazilian real). That rules out rounding errors.
{ "currency": "BRL", "total": 1690 }1690 is R$ 16.90. To display it, divide by 100 and format it as reais.
Every field is always there
A response contains every documented field of the object, every time. When a field does not apply, it is null; it never goes missing. For example, send_date is null on a letter sent right away, and cancellation_reason is null until the letter is canceled.
An empty metadata comes back as an empty object, {}. Booleans are always true or false, without quotes.
HTTP methods
| Method | Used for | Example |
|---|---|---|
GET | Retrieving an object or a list. It never changes anything. | GET /v1/letters/{letter_id} |
POST | Creating an object or running an action. | POST /v1/letters, POST /v1/letters/{letter_id}/cancel |
HTTP statuses
| Status | When |
|---|---|
200 | It worked: a retrieval, a list, an update or an action such as canceling or calculating a price. |
201 | An object was created: a letter, an upload or a verification. |
400 | We could not read the request: malformed JSON, an unsupported Content-Type or an invalid Idempotency-Key. |
401 | The API key is missing, invalid or revoked. |
402 | The balance does not cover the operation. |
403 | The account cannot perform the operation right now, for example because it is suspended. |
404 | The object does not exist in your account, or the endpoint does not exist. |
409 | The current state of the object does not allow the operation, or the Idempotency-Key conflicts with another request. |
413 | The body is larger than 4.5 MB. Send the PDF by URL or with POST /v1/uploads. |
422 | One or more fields have invalid values. error.details lists all of them. |
429 | Too many requests per second. Wait for Retry-After. |
500 | Unexpected error at CartaFlow. Retry with the same Idempotency-Key. |
503 | The API is temporarily unavailable. Retry after Retry-After. |
Every error response has the same shape. See Errors.
Headers
What you send:
| Header | Required | What it is for |
|---|---|---|
Authorization | Yes | Bearer followed by your API key. See Authentication. |
Content-Type | When there is a body | application/json. On POST /v1/letters and POST /v1/letters/quote, also multipart/form-data. |
Idempotency-Key | No, but recommended on every POST | Stops the same object from being created twice when you repeat a request. See Idempotency. |
Accept-Language | No | pt-BR (default) or en. Sets the language of error.message. |
What the API returns:
| Header | What it carries |
|---|---|
Request-Id | The request id, such as req_9Vb4Xk2Lq8Mz. Send it to support when you need help. |
RateLimit-Limit | How many requests per second the account can make, across all of its keys. |
RateLimit-Remaining | How many are left in the current window. |
RateLimit-Reset | How many seconds until the window starts over. |
Retry-After | On 429 and 503 responses: how many seconds to wait before retrying. |
Idempotent-Replayed | true when the response replays an earlier request with the same Idempotency-Key. |
The API grows without breaking your integration
Version 1 gains new fields, new enum values, new events and new error codes without prior notice, because none of that breaks a well-built integration. For that, your code needs to:
- ignore fields it does not know in responses and webhooks;
- treat an unknown
statusvalue as "in progress", and handle an unknownerror.codeby itserror.type; - ignore event types it does not use.
Breaking changes only happen in a new version. See Versioning and compatibility.