Skip to content

Settlement Flow

This guide describes the complete boleto settlement process on the FluxiQ NPC platform, including automatic processing of ACMP615 files and payment reconciliation.

Settlement Cycles

Nuclea processes settlements in two daily cycles:

CycleNuclea TimeFluxiQ NPC ProcessingDescription
Morning08:3008:35Payments from previous day (afternoon)
Afternoon16:3016:35Payments from current day (morning)

Business Days

Settlement occurs only on business days. On weekends and bank holidays, payments accumulate for the next business day.

Automatic Flow

The diagram below illustrates the complete settlement processing flow:

+-----------------------------------------------------------------------------+
|                       Automatic Settlement Flow                              |
+-----------------------------------------------------------------------------+

  +----------+      +----------+      +----------+      +----------+
  |  TRIGGER |----->| DOWNLOAD |----->|  PARSE   |----->|  MATCH   |
  |  08:35   |      |   FTP    |      | ACMP615  |      | BOLETOS  |
  |  16:35   |      |          |      |          |      |          |
  +----------+      +----------+      +----------+      +----------+
                                                              |
                                                              v
  +----------+      +----------+      +----------+      +----------+
  | WEBHOOK  |<-----| UPDATE   |<-----| RECONCILE|<-----|  NOTIFY  |
  | CENTRAL  |      | STATUS   |      | AMOUNTS  |      | PAYMENTS |
  +----------+      +----------+      +----------+      +----------+

  Events generated:
  - payment_received (for each boleto)
  - settlement_completed (at end of cycle)

Flow Details

  1. Trigger (08:35 / 16:35): Oban job triggered automatically
  2. FTP Download: Connect to Nuclea FTP server to download ACMP615 file
  3. Parse ACMP615: Read and interpret file records
  4. Match Boletos: Associate payments with registered boletos
  5. Notify Payments: Generate individual events for each payment
  6. Reconcile Amounts: Verify values (original vs paid)
  7. Update Status: Update boleto status to settled
  8. Webhook Central: Send webhooks to Central

Manual Trigger

For special situations (reprocessing, testing), you can trigger manually:

bash
curl -X POST "https://api.pixconnect.com.br/api/v1/central/settlements/trigger" \
  -H "X-API-Key: pk_live_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{
    "cycle": 1,
    "date": "2026-02-15"
  }'
javascript
async function triggerSettlement(cycle = 1, date = null) {
  const payload = { cycle };
  if (date) payload.date = date;

  const response = await fetch(
    "https://api.pixconnect.com.br/api/v1/central/settlements/trigger",
    {
      method: "POST",
      headers: {
        "X-API-Key": "pk_live_abc123def456",
        "Content-Type": "application/json",
      },
      body: JSON.stringify(payload),
    }
  );

  const data = await response.json();
  console.log("Job enqueued:", data.data.job_id);
  return data;
}

// Trigger morning cycle for specific date
triggerSettlement(1, "2026-02-15");
python
import requests

def trigger_settlement(cycle=1, date=None):
    url = "https://api.pixconnect.com.br/api/v1/central/settlements/trigger"
    headers = {
        "X-API-Key": "pk_live_abc123def456",
        "Content-Type": "application/json"
    }
    payload = {"cycle": cycle}
    if date:
        payload["date"] = date

    response = requests.post(url, headers=headers, json=payload)
    data = response.json()
    print(f"Job enqueued: {data['data']['job_id']}")
    return data

# Trigger morning cycle for specific date
trigger_settlement(1, "2026-02-15")

Response:

json
{
  "success": true,
  "data": {
    "job_id": 12345,
    "date": "2026-02-15",
    "cycle": 1,
    "status": "queued"
  }
}

Manual Trigger Usage

Use manual trigger only for:

  • Reprocessing files with errors
  • Testing in sandbox environment
  • Recovering historical data

Automatic processing is the recommended method for production.

Receiving Notifications

settlement_completed Event

Sent when a settlement cycle is completed:

javascript
app.post("/webhooks/pixconnect", async (req, res) => {
  const { event, data } = req.body;

  if (!validateSignature(req)) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  if (event === "settlement_completed") {
    console.log("Settlement cycle completed!");
    console.log("Settlement ID:", data.settlement_id);
    console.log("Date:", data.cycle_date);
    console.log("Cycle:", data.cycle_type);
    console.log("Total boletos:", data.total_boletos);
    console.log("Total value:", data.total_valor / 100);

    // Process summary
    const { summary } = data;
    console.log("Paid:", summary.pagos, "- R$", summary.valor_pagos / 100);
    console.log("Rejected:", summary.rejeitados, "- R$", summary.valor_rejeitados / 100);

    // Save cycle record
    await saveSettlementCycle({
      id: data.settlement_id,
      date: data.cycle_date,
      type: data.cycle_type,
      total_boletos: data.total_boletos,
      total_valor: data.total_valor,
      summary: summary,
      arquivo: data.arquivo_acmp615,
      completed_at: data.completed_at
    });

    // Notify finance team
    await notifyFinanceTeam(data);
  }

  res.status(200).json({ received: true });
});

Event payload:

json
{
  "event": "settlement_completed",
  "timestamp": "2026-02-04T08:45:00Z",
  "data": {
    "settlement_id": "stl_abc123def456",
    "cycle_date": "2026-02-04",
    "cycle_type": "morning",
    "total_boletos": 47,
    "total_valor": 1523450,
    "arquivo_acmp615": "ACMP615.20260204.083000",
    "status": "completed",
    "started_at": "2026-02-04T08:35:00Z",
    "completed_at": "2026-02-04T08:45:00Z",
    "summary": {
      "pagos": 45,
      "valor_pagos": 1485000,
      "rejeitados": 2,
      "valor_rejeitados": 38450
    }
  }
}

payment_received Event

Sent for each individual payment processed:

javascript
app.post("/webhooks/pixconnect", async (req, res) => {
  const { event, data } = req.body;

  if (!validateSignature(req)) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  if (event === "payment_received") {
    console.log("Payment processed:", data.nosso_numero);
    console.log("Value:", data.valor_pago / 100);
    console.log("Credit date:", data.data_credito);

    // Update boleto in database
    await updateBoletoSettlement(data.nosso_numero, {
      status: "settled",
      valor_pago: data.valor_pago,
      data_credito: data.data_credito,
      settlement_id: data.settlement_id,
      banco_recebedor: data.banco_recebedor,
      sequencial_arquivo: data.sequencial_arquivo
    });

    // Create accounting entry
    await createAccountingEntry({
      type: "credit",
      amount: data.valor_pago,
      reference: data.nosso_numero,
      settlement_id: data.settlement_id,
      description: `Boleto settlement ${data.nosso_numero}`
    });
  }

  res.status(200).json({ received: true });
});

Accounting Reconciliation

ACMP615 File Structure

The ACMP615 file follows the FEBRABAN standard with three record types:

TypeNameDescription
00HeaderFile information (date, time, sender)
20DetailIndividual payment records
99TrailerTotals and record count

Header Record (Type 00)

PositionSizeFieldDescription
1-22tipo_registro"00"
3-108data_geracaoGeneration date (YYYYMMDD)
11-166hora_geracaoGeneration time (HHMMSS)
17-248ispb_remetenteSender ISPB
25-306sequencial_arquivoFile sequence number

Detail Record (Type 20)

PositionSizeFieldDescription
1-22tipo_registro"20"
3-1311nosso_numeroBoleto identifier
14-3017valor_pagoAmount paid (cents, left-padded zeros)
31-388data_pagamentoPayment date (YYYYMMDD)
39-468data_creditoCredit date (YYYYMMDD)
47-493banco_recebedorBank COMPE code
50-534agencia_recebedoraReceiving branch
54-552codigo_movimentoMovement code

Trailer Record (Type 99)

PositionSizeFieldDescription
1-22tipo_registro"99"
3-86qtd_registrosCount of type 20 records
9-2618valor_totalSum of values (cents)

Reconciliation Code

Example of reconciling payments with their records:

javascript
class SettlementReconciliation {
  constructor(settlementId) {
    this.settlementId = settlementId;
    this.matched = [];
    this.unmatched = [];
    this.discrepancies = [];
  }

  async reconcile(payments, boletos) {
    const boletoMap = new Map(
      boletos.map(b => [b.nosso_numero, b])
    );

    for (const payment of payments) {
      const boleto = boletoMap.get(payment.nosso_numero);

      if (!boleto) {
        // Payment without corresponding boleto
        this.unmatched.push({
          payment,
          reason: "BOLETO_NOT_FOUND"
        });
        continue;
      }

      // Verify values
      const valorOriginal = boleto.amount_cents;
      const valorPago = payment.valor_pago;

      if (valorPago !== valorOriginal) {
        // Value discrepancy
        this.discrepancies.push({
          nosso_numero: payment.nosso_numero,
          valor_original: valorOriginal,
          valor_pago: valorPago,
          diferenca: valorPago - valorOriginal,
          reason: this.classifyDiscrepancy(valorOriginal, valorPago, boleto)
        });
      }

      this.matched.push({
        nosso_numero: payment.nosso_numero,
        boleto_id: boleto.id,
        valor_original: valorOriginal,
        valor_pago: valorPago,
        data_pagamento: payment.data_pagamento,
        data_credito: payment.data_credito
      });
    }

    return this.generateReport();
  }

  classifyDiscrepancy(valorOriginal, valorPago, boleto) {
    const diferenca = valorPago - valorOriginal;

    if (diferenca > 0) {
      // Higher payment - could be interest/penalty
      return "INTEREST_PENALTY";
    } else if (diferenca < 0) {
      // Lower payment - could be discount
      const percentual = Math.abs(diferenca / valorOriginal) * 100;
      if (percentual <= 10) {
        return "EARLY_PAYMENT_DISCOUNT";
      }
      return "PARTIAL_PAYMENT";
    }

    return "EXACT_VALUE";
  }

  generateReport() {
    const totalMatched = this.matched.reduce((sum, m) => sum + m.valor_pago, 0);
    const totalUnmatched = this.unmatched.reduce((sum, u) => sum + u.payment.valor_pago, 0);

    return {
      settlement_id: this.settlementId,
      generated_at: new Date().toISOString(),
      summary: {
        total_payments: this.matched.length + this.unmatched.length,
        matched: this.matched.length,
        unmatched: this.unmatched.length,
        with_discrepancy: this.discrepancies.length,
        valor_matched: totalMatched,
        valor_unmatched: totalUnmatched,
        match_rate: (this.matched.length / (this.matched.length + this.unmatched.length) * 100).toFixed(2) + "%"
      },
      matched: this.matched,
      unmatched: this.unmatched,
      discrepancies: this.discrepancies
    };
  }
}

// Usage
async function processSettlement(settlementData) {
  const payments = settlementData.payments;
  const boletos = await fetchBoletosFromDatabase(
    payments.map(p => p.nosso_numero)
  );

  const reconciliation = new SettlementReconciliation(settlementData.settlement_id);
  const report = await reconciliation.reconcile(payments, boletos);

  console.log("Reconciliation completed:");
  console.log("Match rate:", report.summary.match_rate);

  return report;
}

Handling Unmatched Records

When a payment doesn't find a corresponding boleto:

javascript
async function handleUnmatchedPayments(unmatchedPayments) {
  for (const { payment, reason } of unmatchedPayments) {
    console.warn(`Unmatched payment: ${payment.nosso_numero}`);
    console.warn(`Reason: ${reason}`);
    console.warn(`Value: R$ ${payment.valor_pago / 100}`);

    // Create investigation ticket
    await createInvestigationTicket({
      type: "UNMATCHED_PAYMENT",
      nosso_numero: payment.nosso_numero,
      valor: payment.valor_pago,
      data_pagamento: payment.data_pagamento,
      settlement_id: payment.settlement_id,
      raw_record: payment.acmp615_record
    });

    // Try alternative search
    const possibleMatch = await searchAlternativeMatches(payment);

    if (possibleMatch) {
      console.log(`Possible match found: ${possibleMatch.nosso_numero}`);
      await flagForManualReview(payment, possibleMatch);
    }
  }
}

async function searchAlternativeMatches(payment) {
  // Search by similar value
  const candidates = await db.query(`
    SELECT * FROM boletos
    WHERE amount_cents = $1
    AND status = 'registered'
    AND due_date >= $2
    ORDER BY due_date ASC
    LIMIT 5
  `, [payment.valor_pago, payment.data_pagamento]);

  return candidates.length > 0 ? candidates[0] : null;
}

Daily Report Generation

Example of consolidated daily report:

javascript
async function generateDailySettlementReport(date) {
  // Fetch day's cycles
  const cycles = await db.query(`
    SELECT * FROM settlement_cycles
    WHERE cycle_date = $1
    ORDER BY cycle_type ASC
  `, [date]);

  // Fetch payments
  const payments = await db.query(`
    SELECT * FROM payments
    WHERE DATE(paid_at) = $1
  `, [date]);

  // Consolidate data
  const report = {
    date: date,
    generated_at: new Date().toISOString(),
    cycles: cycles.map(c => ({
      type: c.cycle_type,
      started_at: c.started_at,
      completed_at: c.completed_at,
      total_boletos: c.total_boletos,
      total_valor: c.total_valor,
      status: c.status
    })),
    totals: {
      total_boletos: payments.length,
      total_valor: payments.reduce((sum, p) => sum + p.valor_pago, 0),
      by_channel: groupByChannel(payments),
      by_bank: groupByBank(payments)
    },
    reconciliation: {
      matched: payments.filter(p => p.reconciliation_status === "matched").length,
      unmatched: payments.filter(p => p.reconciliation_status === "unmatched").length,
      discrepancies: payments.filter(p => p.has_discrepancy).length
    }
  };

  // Format values in currency
  report.totals.total_valor_formatted = formatCurrency(report.totals.total_valor);

  // Save report
  await saveReport("settlement_daily", date, report);

  // Send to finance team
  await sendReportEmail(report, ["finance@company.com"]);

  return report;
}

function groupByChannel(payments) {
  return payments.reduce((acc, p) => {
    const channel = p.canal_pagamento || "unknown";
    if (!acc[channel]) {
      acc[channel] = { count: 0, valor: 0 };
    }
    acc[channel].count++;
    acc[channel].valor += p.valor_pago;
    return acc;
  }, {});
}

function groupByBank(payments) {
  return payments.reduce((acc, p) => {
    const bank = p.banco_recebedor || "unknown";
    if (!acc[bank]) {
      acc[bank] = { count: 0, valor: 0 };
    }
    acc[bank].count++;
    acc[bank].valor += p.valor_pago;
    return acc;
  }, {});
}

function formatCurrency(cents) {
  return (cents / 100).toLocaleString("pt-BR", {
    style: "currency",
    currency: "BRL"
  });
}

Monitoring and Alerts

Configure alerts for the following situations:

MetricThresholdAction
Match rate< 95%Yellow alert - investigate
Match rate< 90%Red alert - immediate action
Processing time> 15 minCheck FTP performance
File not found30 min after cycleContact Nuclea
Value discrepancies> 5% of boletosReview interest/discount rules

Monitoring Example

javascript
class SettlementMonitor {
  constructor() {
    this.thresholds = {
      matchRate: { warning: 95, critical: 90 },
      processingTime: { warning: 10, critical: 15 }, // minutes
      discrepancyRate: { warning: 3, critical: 5 } // percentage
    };
  }

  async checkSettlementHealth(settlementId) {
    const settlement = await getSettlement(settlementId);
    const alerts = [];

    // Check match rate
    const matchRate = (settlement.matched / settlement.total_boletos) * 100;
    if (matchRate < this.thresholds.matchRate.critical) {
      alerts.push({
        severity: "critical",
        metric: "match_rate",
        value: matchRate,
        message: `Critical match rate: ${matchRate.toFixed(2)}%`
      });
    } else if (matchRate < this.thresholds.matchRate.warning) {
      alerts.push({
        severity: "warning",
        metric: "match_rate",
        value: matchRate,
        message: `Match rate below expected: ${matchRate.toFixed(2)}%`
      });
    }

    // Check processing time
    const processingTime = (
      new Date(settlement.completed_at) - new Date(settlement.started_at)
    ) / 60000; // minutes

    if (processingTime > this.thresholds.processingTime.critical) {
      alerts.push({
        severity: "critical",
        metric: "processing_time",
        value: processingTime,
        message: `Critical processing time: ${processingTime.toFixed(0)} min`
      });
    }

    // Check discrepancies
    const discrepancyRate = (settlement.discrepancies / settlement.total_boletos) * 100;
    if (discrepancyRate > this.thresholds.discrepancyRate.warning) {
      alerts.push({
        severity: "warning",
        metric: "discrepancy_rate",
        value: discrepancyRate,
        message: `Discrepancy rate: ${discrepancyRate.toFixed(2)}%`
      });
    }

    // Send alerts
    for (const alert of alerts) {
      await this.sendAlert(alert, settlementId);
    }

    return {
      settlement_id: settlementId,
      status: alerts.length === 0 ? "healthy" : "needs_attention",
      metrics: {
        match_rate: matchRate,
        processing_time: processingTime,
        discrepancy_rate: discrepancyRate
      },
      alerts
    };
  }

  async sendAlert(alert, settlementId) {
    console.log(`[${alert.severity.toUpperCase()}] ${alert.message}`);

    if (alert.severity === "critical") {
      // Send SMS/call to on-call
      await sendCriticalAlert({
        settlement_id: settlementId,
        alert
      });
    }

    // Log in monitoring system
    await logAlert(alert, settlementId);
  }
}

// Usage after each cycle
async function postSettlementCheck(settlementId) {
  const monitor = new SettlementMonitor();
  const health = await monitor.checkSettlementHealth(settlementId);

  console.log("Settlement status:", health.status);

  if (health.alerts.length > 0) {
    console.log("Alerts:");
    health.alerts.forEach(a => console.log(`  - ${a.message}`));
  }

  return health;
}

Implementation Checklist

Initial Configuration

  • [ ] Nuclea FTP credentials configured
  • [ ] Processing times defined (08:35, 16:35)
  • [ ] Webhook endpoint configured with Central
  • [ ] Database prepared to store cycles

Webhooks

  • [ ] Handler for settlement_completed
  • [ ] Handler for payment_received
  • [ ] HMAC signature validation
  • [ ] Response within 30 seconds

Reconciliation

  • [ ] ACMP615 file parser implemented
  • [ ] Match logic by nosso_numero
  • [ ] Value discrepancy handling
  • [ ] Queue for unmatched payments

Accounting

  • [ ] Accounting system integration
  • [ ] Automatic credit entries
  • [ ] Daily reports configured
  • [ ] Value auditing

Monitoring

  • [ ] Match rate alerts
  • [ ] Processing time alerts
  • [ ] Monitoring dashboard
  • [ ] Debug logs

Operations

  • [ ] Manual reprocessing procedure
  • [ ] Unmatched payment procedure
  • [ ] Nuclea support contact configured
  • [ ] Internal documentation updated

Next Steps

Documentação da API FluxiQ NPC