Docs

Webhooks

Get a call on your server every time a letter or a verification changes status.

A webhook is a URL on your server that CartaFlow calls when something happens in your account. Every time a letter or a verification changes status, we send a POST with the event in the body. Your system learns that a letter was posted, or that an address was verified, without polling the API.

Set up

Create an endpoint on your server that accepts a POST with a JSON body and answers quickly. The URL must use HTTPS.

Add the URL in the dashboard, under Desenvolvedores > Webhooks (Developers > Webhooks), and pick the events you want.

Copy the signing secret. Each webhook has a secret starting with whsec_, shown in the dashboard. Store it in an environment variable such as CARTAFLOW_WEBHOOK_SECRET and use it to verify the signature of every delivery.

What your server receives

The body is an event object. data carries the full object as it was right after the event, so you do not need to call the API afterwards:

Body of a letter.posted event
{
  "id": "evt_6Tq1Yz8Wm3Rb",
  "object": "event",
  "type": "letter.posted",
  "data": {
    "id": "ltr_8Kq2wX4mN7pR",
    "object": "letter",
    "status": "posted",
    "to": {
      "name": "Destinatário Exemplo",
      "street": "Rua Exemplo",
      "number": "128",
      "complement": "Ap. 52",
      "neighborhood": "Centro",
      "city": "São Paulo",
      "state": "SP",
      "postal_code": "01001-000"
    },
    "reference": "fatura-2026-10-0042",
    "metadata": { "customer_id": "1842" },
    "status_transitions": {
      "received_at": "2026-10-13T14:20:00Z",
      "in_production_at": "2026-10-13T21:00:00Z",
      "printed_at": "2026-10-14T10:42:00Z",
      "posted_at": "2026-10-14T16:05:00Z",
      "canceled_at": null,
      "failed_at": null
    },
    "created_at": "2026-10-13T14:20:00Z"
  },
  "created_at": "2026-10-14T16:05:00Z"
}

To fit on the page, the example shows only some fields of data. In a real delivery, data holds the complete letter, the same as the response of GET /v1/letters/{letter_id}.

Each delivery comes with three headers:

HeaderWhat it is
webhook-idThe event id, the same as id in the body. It stays the same on every delivery attempt of an event.
webhook-timestampWhen it was sent, in Unix seconds.
webhook-signatureThe signature of the body, formatted v1,<signature>. It can hold more than one, separated by spaces, while a secret is being rotated.

Verify the signature

Verify the signature before trusting the content. It proves the delivery came from CartaFlow and that the body was not changed. CartaFlow follows the Standard Webhooks specification, which has official libraries for many languages.

Use the raw body, exactly as it arrived. If your framework parses the JSON and you serialize it again, the signature will not match.

// npm install standardwebhooks express
import express from 'express';
import { Webhook } from 'standardwebhooks';

const app = express();
const webhook = new Webhook(process.env.CARTAFLOW_WEBHOOK_SECRET);

// express.raw keeps the body exactly as it arrived.
app.post('/webhooks/cartaflow', express.raw({ type: 'application/json' }), (req, res) => {
  let event;
  try {
    event = webhook.verify(req.body, req.headers);
  } catch {
    return res.status(400).send('Invalid signature');
  }

  res.status(200).end(); // answer first
  queue.add(event); // process later, outside the request
});

Answer fast, process later

Answer with any 2xx status within 15 seconds. That tells us you received the event. Do slow work, such as updating records or sending an email, after answering, for example on a queue.

Any other answer, or no answer in time, makes CartaFlow try again:

AttemptWhen
1Right when the event happens
25 seconds after the previous one
35 minutes later
430 minutes later
52 hours later
65 hours later
710 hours later
810 hours later

After the last attempt, the event is not sent again. It stays available at GET /v1/events for 30 days.

Repeats and ordering

  • The same event can arrive more than once. A delivery can be repeated even if your server got it, for example when the response is lost on the way back. Store the webhook-id of the events you processed and skip repeats.
  • Events can arrive out of order. A letter.printed may arrive after a letter.posted. Do not infer the status from arrival order: use status and status_transitions of the object in data, which are a snapshot from the moment of the event. To be sure of the current state, retrieve the object from the API.

Recover missed events

If your server was down, fetch what you missed from GET /v1/events, filtering by created_after. To see the history of one letter, use object_id. Events are kept for 30 days and are the same ones webhooks deliver.

Test event

In the dashboard, under Desenvolvedores > Webhooks, the Enviar evento de teste (Send test event) button sends one delivery of type webhook.test to your endpoint. Use it to check that the URL answers, that the signature verifies and that your server answers 2xx in time.

  • The delivery has the same headers and signature as any other, made with the webhook's whsec_ secret.
  • The body has the shape of any event, with type: "webhook.test" and a sample object in data. That object does not exist in your account: do not store anything from it.
  • The test event is sent once, with no retries, and never appears in GET /v1/events.

During development, CartaFlow has to reach your URL over the internet. Use a tunnel such as ngrok or Cloudflare Tunnel to expose your local server with an HTTPS URL, add that URL in the dashboard and send a test event.

Events

Every status change creates an event named object.new_status. When a letter is changed in the dashboard before production without changing status (its send date or address, for example), the event is letter.updated, with the letter as it is now.

EventWhen it is sent
letter.scheduledA letter was scheduled with send_date. It has not been charged yet.
letter.receivedA letter was charged and joined the production queue: right after creation or, if it was scheduled, on its send date. Covers letters created through the API and the dashboard.
letter.in_productionThe letter joined a print batch and can no longer be canceled.
letter.printedThe letter was printed and enveloped.
letter.postedThe letter was handed to Correios. It is the last event of a letter that was sent.
letter.canceledThe letter was canceled. Check cancellation_reason: requested when you canceled it, insufficient_balance when a scheduled letter could not be charged on its date.
letter.failedThe letter failed in production and will not be posted. The amount went back to the balance automatically. Check failure_reason.
letter.updatedThe letter was changed in the dashboard before production, without changing status: send date, address, file, variables, printing, Correios services or reference. data carries the letter as it is now. When the change altered the price of a scheduled letter, price has the new amount.
verification.receivedA verification was created, through the API or the dashboard (see source), and the letter with the code is in the production queue.
verification.in_productionThe letter with the code is being printed. From here on, canceling revokes the verification, with no refund.
verification.awaiting_codeThe letter with the code was posted. The person can confirm once it arrives.
verification.verifiedThe person confirmed the right code. The address is verified.
verification.failed5 wrong codes were entered and the verification was locked.
verification.expiredThe person did not confirm the code within 60 days of posting.
verification.canceledThe verification was canceled before posting, by you or by CartaFlow, and the amount went back to the balance.
verification.revokedYou canceled the verification after production started. The code stopped working and the amount does not go back to the balance. If it was verified, treat the address as not verified.

Handle unknown types without failing

Answer 2xx to any delivery with a valid signature, even of a type your system does not use, such as webhook.test, and ignore what you do not recognize. New event types may appear without notice. See Versioning and compatibility.

On this page