Errors
The error format, what each code means and when to retry a request.
Every error response has the same shape, on every endpoint. The HTTP status gives the category and error.code gives the exact error. When the problem is in the fields, the details list carries every field with a problem at once, so you can fix them all in one go.
The format
{
"error": {
"type": "validation_error",
"code": "invalid_parameters",
"message": "2 fields have problems. See details.",
"request_id": "req_9Vb4Xk2Lq8Mz",
"doc_url": "https://docs.cartaflow.com.br/en/errors#invalid_parameters",
"details": [
{
"param": "to.postal_code",
"code": "postal_code_not_found",
"message": "We could not find postal code 01001-999. Check the 8 digits."
},
{
"param": "document.url",
"code": "address_area_not_blank",
"message": "There is content in the address area of the first page. Leave the area blank or use address_placement: extra_page."
}
]
}
}| Field | What it is |
|---|---|
type | The error category. Each type maps to an HTTP status (table below). |
code | The exact error. Use this field in your code: a code never changes meaning. New codes may be added; handle an unknown code by its type. |
message | An explanation for people. The wording can change, so do not match on messages in your code. |
request_id | The id of the request, the same as the Request-Id header. Send it to support when you need help. |
doc_url | A link to this code on this page. |
details | The fields with problems, each with param (the field, in dot notation), code and message. Only present for validation_error; null for other types. |
When there is an error, nothing is created or charged.
Message language
message is in Portuguese by default. To get messages in English, send the Accept-Language: en header. Codes are the same in every language.
curl "https://api.cartaflow.com.br/v1/letters/ltr_doesnotexist" \
-H "Authorization: Bearer $CARTAFLOW_API_KEY" \
-H "Accept-Language: en"Error types
| HTTP | error.type | What it means |
|---|---|---|
| 400, 413 | invalid_request_error | We could not read the request: the JSON is malformed, a header is wrong or the body is too large. |
| 401 | authentication_error | The API key is missing, invalid or revoked. |
| 402 | billing_error | The account balance does not cover the operation. |
| 403 | permission_error | The account cannot perform this operation right now. |
| 404 | not_found_error | The object or the endpoint does not exist. |
| 409 | conflict_error | The request is valid, but the current state of the object does not allow the operation. |
| 422 | validation_error | We read the request, but one or more fields have invalid values. The details list says which ones and why. |
| 429 | rate_limit_error | The account went over its requests-per-second limit. |
| 500, 503 | api_error | Something failed on CartaFlow’s side. It is not a problem with your request. |
Error codes
The value of error.code. Each error's doc_url points to its row in this table.
| error.code | HTTP | What happened | How to fix it |
|---|---|---|---|
invalid_json | 400 | The request body is not valid JSON. | Check quotes, commas and braces. Build the body with your language’s JSON encoder instead of by hand. |
unsupported_content_type | 400 | The Content-Type header is not accepted on this endpoint. | Send application/json. Only POST /v1/letters and POST /v1/letters/quote also accept multipart/form-data, to upload the PDF with the request. |
invalid_idempotency_key | 400 | The Idempotency-Key header is empty or longer than 255 characters. | Use a short unique identifier, such as the invoice id in your system or a UUID. |
request_too_large | 413 | The request body is larger than 4.5 MB, the most the API accepts in one request. | Send the PDF by URL (document.type = url) or with POST /v1/uploads (document.type = upload). Both take PDFs of up to 10 MB. |
api_key_missing | 401 | The request has no Authorization header. | Send Authorization: Bearer followed by your API key. |
api_key_invalid | 401 | The key does not exist or is mistyped. | Copy the key again from the dashboard, under Desenvolvedores > Chaves de API (Developers > API keys). It starts with cf_live_. |
api_key_revoked | 401 | The key was revoked in the dashboard. | Create a new key in the dashboard and update your system’s environment variable. |
insufficient_balance | 402 | The available balance is lower than the price of the mailing. | Top up your balance in the dashboard. Check GET /v1/balance before large sends. |
account_suspended | 403 | The account is suspended and cannot create mailings. | Contact CartaFlow support from the dashboard. |
resource_not_found | 404 | There is no object with that id in your account. | Check the id, character by character. Each account only sees its own objects: the id of another account’s object also gets a 404. |
endpoint_not_found | 404 | The path or the HTTP method does not exist in the API. | Check the URL, the /v1 prefix and the HTTP method shown in the reference. |
postal_code_not_found | 404 | The postal code (CEP) you looked up does not exist in the Correios database. | Check the 8 digits with the person or the source record. |
letter_not_cancelable | 409 | The letter’s status is no longer scheduled or received: it is already in production, posted or failed. | Retrieve the letter to see its current status. It cannot be canceled after production starts. |
verification_not_cancelable | 409 | The verification ended without a confirmation (failed or expired) and cannot be canceled or revoked. | Retrieve the verification to see its current status. To try again, create a new verification. |
verification_not_awaiting_code | 409 | The verification is not waiting for a code: the letter was not posted yet, or the verification is already finished, canceled or revoked. | Only check codes while the status is awaiting_code. The error message tells you the current status. |
idempotency_key_reused | 409 | This Idempotency-Key was already used with a different body or on another endpoint in the last 24 hours. | Use a new key for each different operation. Reuse a key only to retry the exact same request. |
idempotency_key_in_use | 409 | A request with the same Idempotency-Key is still being processed. | Wait a second and retry. You will get the response of the first request. |
invalid_parameters | 422 | One or more fields have invalid values. | Read the details list: each item has the field (param), a code and a message. |
rate_limit_exceeded | 429 | The account made more requests per second than its limit. | Wait the seconds given in the Retry-After header and retry. Spread out the calls for large sends. |
internal_error | 500 | Unexpected error at CartaFlow. | Retry with the same Idempotency-Key. If it persists, send the request_id to support. |
service_unavailable | 503 | The API is temporarily unavailable, for maintenance or overload. | Retry after the time in the Retry-After header, with the same Idempotency-Key. |
Detail codes
In a validation_error, each item of details has one of these codes, along with the field in param.
| details[].code | What is wrong with the field | How to fix it |
|---|---|---|
required | A required field is missing. | Send the field named in param. |
unknown_parameter | The field does not exist on this endpoint. | Check the spelling. The API rejects unknown fields so a typo never goes unnoticed. |
invalid_type | The value has the wrong type, such as text where a number is expected. | Send the type shown in the reference. Booleans are true or false, without quotes. |
invalid_value | The value is not accepted: it is not in the list of allowed values, or it does not go with another field of the request, such as letter and document together in a verification. | Use one of the values listed in the field reference. The message explains why. |
invalid_format | The format is wrong: URL, date, base64, postal code or code. | Follow the format and the example in the field reference. |
too_short | The text or list is shorter than the minimum. | Check the minimum in the field reference. |
too_long | The text or list is longer than the maximum. | Check the maximum in the field reference, and abbreviate if it is an address. |
out_of_range | The number or date is outside the allowed range, such as a send_date that is not between tomorrow and 180 days ahead. | Check the minimum and maximum in the field reference. |
requires_registered | return_receipt and restricted_delivery only work with registered: the return receipt and restricted delivery are services of a registered letter. | Add registered to postal_services, or remove the services that depend on it. |
postal_code_not_found | The postal code (CEP) does not exist in the Correios database. | Check the 8 digits. You can look it up first with GET /v1/postal-codes/{postal_code}. |
address_incomplete | The address could not be completed: the field was not in the request and the postal code does not provide it. This happens with postal codes that cover a whole city and have no street or neighborhood. | Send the field named in param, such as street and neighborhood. |
document_unreachable | We could not download the PDF from the URL within 10 seconds, or the server answered with an error. | Use a public or signed HTTPS URL (S3, GCS, Azure) that stays valid for at least 10 minutes. |
upload_not_found | There is no upload with that id in your account. | Check the upload_id. Use the id returned by POST /v1/uploads, with the same account as the API key. |
upload_expired | The upload was created more than 24 hours ago and can no longer be used. | Create a new upload with POST /v1/uploads and send the file again. |
upload_already_used | The upload was already used by another letter. Each upload is good for one letter only. | Create a new upload for each letter. To retry the same creation safely, use the same Idempotency-Key. |
upload_file_missing | The file was never sent to the upload’s upload_url. | Send the PDF with a PUT to the upload_url, with the Content-Type: application/pdf header, and wait for the 2xx response before creating the letter. If the link expired (it works for 2 hours), create a new upload. |
document_invalid | The file is not a valid PDF: it is corrupted or in another format. | Open the file in a PDF reader to check it. For base64, send only the content, without the data:application/pdf;base64, prefix. |
document_encrypted | The PDF is password protected. | Generate the PDF without a password or printing restrictions. |
document_too_large | The PDF is larger than its way in allows: 10 MB by URL or with POST /v1/uploads, 4 MB in multipart/form-data and 3 MB in base64 (once decoded). | Bring images down to 300 dpi, the most print makes use of. For a large PDF, use a URL or POST /v1/uploads, which take up to 10 MB. |
document_page_size | A page is not A4 portrait (210 x 297 mm). | Export every page of the PDF as A4, portrait orientation. |
too_many_sheets | The letter would need more than 6 sheets, the most that fits in one envelope. | Use double_sided: true to print on both sides, or split the content into more than one letter. |
address_area_not_blank | There is text or an image in the address area of the first page, where CartaFlow prints the recipient. | Leave the area blank in your layout, or use address_placement: extra_page. |
variable_missing | The PDF has a marker, such as {{name}}, that is missing from variables. | Send a value for every marker in the PDF. To leave one blank, send an empty string. |
variable_not_found | You sent a variable the PDF does not have. | Check the name letter by letter. In a PDF made in Word, open the PDF and check that the marker shows up whole, braces included, as selectable text. |
document_text_unreadable | The PDF has no selectable text: it is an image or a scan. Without text, we cannot find the markers, such as variables or the {{OTP_CODE}} of a verification. | Export the PDF straight from the editor (in Word: File > Save as > PDF) instead of scanning or printing an image to PDF. |
otp_code_missing | The PDF of a verification has no {{OTP_CODE}} marker, the place where CartaFlow prints the code. | Type {{OTP_CODE}}, in capitals and with the braces, where the code should appear, and export the PDF again with selectable text. Or create the verification without document, with the CartaFlow letter template. |
When to retry
Some errors are temporary and worth retrying. Others only go away when you change the request.
| Situation | What to do |
|---|---|
429 (rate_limit_exceeded) | Wait the seconds in the Retry-After header and retry. |
500 (internal_error) and 503 (service_unavailable) | Retry with growing waits: 1, 2, 4, 8 seconds, up to about 5 times. On a 503, respect Retry-After. |
| Network failure or timeout, no response | Retry as for a 500. You do not know whether the first request arrived, and the Idempotency-Key makes sure nothing is created twice. |
409 with idempotency_key_in_use | The first request with the same key is still running. Wait a second and retry. |
Any other 4xx | Do not retry as is. Fix the request according to the code. |
Always retry with the same Idempotency-Key. If the first attempt did create the letter, the retry returns that same letter instead of creating another one. See Idempotency.
An example handler
import { randomUUID } from 'node:crypto';
const RETRYABLE = new Set([429, 500, 503]);
async function cartaflow(path, { method = 'GET', body, idempotencyKey } = {}) {
// One key per operation, created once and reused on every attempt.
const key = idempotencyKey ?? (method === 'POST' ? randomUUID() : undefined);
for (let attempt = 0; attempt < 5; attempt++) {
let response;
try {
response = await fetch(`https://api.cartaflow.com.br/v1${path}`, {
method,
headers: {
Authorization: `Bearer ${process.env.CARTAFLOW_API_KEY}`,
...(body ? { 'Content-Type': 'application/json' } : {}),
...(key ? { 'Idempotency-Key': key } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
} catch {
await sleep(2 ** attempt * 1000); // network failure: retry with the same key
continue;
}
if (response.ok) return response.json();
const { error } = await response.json();
const inUse = response.status === 409 && error.code === 'idempotency_key_in_use';
if (!RETRYABLE.has(response.status) && !inUse) {
// An error that will not go away on its own: log it and show it to whoever must fix it.
throw Object.assign(new Error(error.message), { code: error.code, details: error.details, requestId: error.request_id });
}
const retryAfter = Number(response.headers.get('Retry-After'));
await sleep(retryAfter > 0 ? retryAfter * 1000 : 2 ** attempt * 1000);
}
throw new Error('The CartaFlow API did not answer after 5 attempts.');
}
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));Getting help from support
Every response, success or error, carries the Request-Id header, and errors also carry error.request_id. Log that value. When you contact support, send the request_id: it lets us find the exact request.