Automated payment reconciliation: stop closing the books by hand

How to automate reconciliation with the Pagnovo API — externalId, transaction lookup, balance by category, x-trace-id and the routine that closes the day for you.

Pagnovo Team · 2026-08-06

Reconciliation answers a simple question: does what the system says I received match what actually landed? Done by hand in a spreadsheet, the cost shows up in three ways — hours of work, human error, and the late discovery of paid orders that were never delivered.

This guide shows how to automate it.

The mistake that makes reconciliation hard later

The most important decision happens when you create the charge, not at month-end: sending the externalId.

POST /transactions/v2/purchase

{
  "amount": 18990,
  "externalId": "order-1042",
  "description": "Order #1042"
}

The externalId is the order identifier in your system. Without it you depend on storing Pagnovo's ID somewhere — and if that record is lost (a failure mid-request, a timeout, a bad deploy), the transaction becomes an orphan: money received with no matching order.

With externalId, lookups work both ways, and the same endpoint accepts three keys:

GET /transactions/:id

It resolves by Pagnovo's ID, by your externalId, or by the PIX end2End. That covers practically any investigation scenario.

The three sources that must match

Honest reconciliation compares three things:

Source What it answers
Your database What I expected to receive
Pagnovo API What was actually processed
Account balance How much is genuinely available

If you only compare the first two, you miss held amounts. That is why the third one matters.

Balance: not everything that came in is available

GET /accounts/balance

The response splits the balance into categories — and that split is what prevents month-end surprises:

A common mistake is summing everything and treating it as cash. It is not: chargeback reserves and precautionary blocks exist but cannot be used. Your reconciliation should treat each category separately, and your cash flow should look only at the available balance.

The reconciliation routine

A design that works well in practice, running a few times a day:

// 1. Orders I created but have not confirmed
const pending = await db.orders.find({
  status: 'PENDING',
  createdAt: { $gte: yesterday },
});

for (const order of pending) {
  // 2. Ask the source of truth, using MY identifier
  const tx = await pagnovo.get(`/transactions/${order.externalId}`);

  // 3. Reconcile the state
  if (tx.status === 'APPROVED' && order.status !== 'PAID') {
    await releaseOrder(order, tx);            // webhook failed — recovered here
  }
  if (['REJECTED', 'BLOCKED'].includes(tx.status)) {
    await markFailed(order, tx.status);
  }
  if (tx.status === 'INCONSISTENT') {
    await flagForHuman(order, tx);            // do not decide on your own
  }
}

Three principles are baked in:

  1. The API is the source of truth, not your database.
  2. The job repairs what the webhook missed. Webhooks are the fast path; reconciliation is the net.
  3. INCONSISTENT is not resolved automatically. Ambiguous state goes to human review — with money, guessing is expensive.

The states and what to do with each

Status Meaning Action
PENDING Awaiting payment Keep watching, respect expiry
APPROVED Paid Release order (idempotently)
REJECTED Declined Close, offer a retry
INCONSISTENT Discrepancy Manual review
BLOCKED Blocked Review + contact support

Pagination: do not lose records in the middle

When listing, the API uses zero-indexed pagination, with a default limit of 20 and a maximum of 100.

?page=0&limit=100

The classic mistake is reading only the first page and concluding "there were 20 transactions in the period". Always walk to the last page before closing any number.

x-trace-id: what saves your support team

Every API response includes the x-trace-id header. Store it alongside the transaction record.

When a "the customer swears they paid" case appears, you open a ticket with the x-trace-id and the team finds exactly that request in the logs — instead of an approximate investigation using time and amount.

It is one line in your HTTP client that saves hours later.

Testing the reconciliation flow

In the sandbox (sk_test_*), amounts produce deterministic outcomes — including the awkward ones:

Amount State produced
R$ 10.00 Approved
R$ 10.01 Rejected
R$ 10.02 Inconsistent
R$ 10.04 Chargeback
R$ 10.05 Refund
R$ 10.06 Blocked

So you can deliberately exercise the INCONSISTENT and chargeback paths — exactly the ones that break sloppy reconciliation. The test environment allows 30 operations per day.

Checklist


Explore our Billing platform and the API documentation. Need help designing your reconciliation? Talk to our team.