Docs

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/json header.
  • There are two exceptions, the endpoints that take a PDF: POST /v1/letters and POST /v1/letters/quote. They also accept multipart/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-Type gets a 400 with unsupported_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_parameter code. That way a typo such as postal_cod never 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 JSONIn 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
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.

PrefixObjectExample
ltr_Letterltr_8Kq2wX4mN7pR
upl_PDF upload (POST /v1/uploads)upl_6Np3Vx8Rk2Tq
ver_Address verificationver_3Hd9Kp2qZ6tW
evt_Eventevt_6Tq1Yz8Wm3Rb
btx_Balance transactionbtx_4Lm8Qw2Zx9Kd
acc_Accountacc_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, formatted YYYY-MM-DD, in Brasília time (America/Sao_Paulo).
  • The created_after and created_before filters 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

MethodUsed forExample
GETRetrieving an object or a list. It never changes anything.GET /v1/letters/{letter_id}
POSTCreating an object or running an action.POST /v1/letters, POST /v1/letters/{letter_id}/cancel

HTTP statuses

StatusWhen
200It worked: a retrieval, a list, an update or an action such as canceling or calculating a price.
201An object was created: a letter, an upload or a verification.
400We could not read the request: malformed JSON, an unsupported Content-Type or an invalid Idempotency-Key.
401The API key is missing, invalid or revoked.
402The balance does not cover the operation.
403The account cannot perform the operation right now, for example because it is suspended.
404The object does not exist in your account, or the endpoint does not exist.
409The current state of the object does not allow the operation, or the Idempotency-Key conflicts with another request.
413The body is larger than 4.5 MB. Send the PDF by URL or with POST /v1/uploads.
422One or more fields have invalid values. error.details lists all of them.
429Too many requests per second. Wait for Retry-After.
500Unexpected error at CartaFlow. Retry with the same Idempotency-Key.
503The API is temporarily unavailable. Retry after Retry-After.

Every error response has the same shape. See Errors.

Headers

What you send:

HeaderRequiredWhat it is for
AuthorizationYesBearer followed by your API key. See Authentication.
Content-TypeWhen there is a bodyapplication/json. On POST /v1/letters and POST /v1/letters/quote, also multipart/form-data.
Idempotency-KeyNo, but recommended on every POSTStops the same object from being created twice when you repeat a request. See Idempotency.
Accept-LanguageNopt-BR (default) or en. Sets the language of error.message.

What the API returns:

HeaderWhat it carries
Request-IdThe request id, such as req_9Vb4Xk2Lq8Mz. Send it to support when you need help.
RateLimit-LimitHow many requests per second the account can make, across all of its keys.
RateLimit-RemainingHow many are left in the current window.
RateLimit-ResetHow many seconds until the window starts over.
Retry-AfterOn 429 and 503 responses: how many seconds to wait before retrying.
Idempotent-Replayedtrue 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 status value as "in progress", and handle an unknown error.code by its error.type;
  • ignore event types it does not use.

Breaking changes only happen in a new version. See Versioning and compatibility.

On this page