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:
| Cycle | Nuclea Time | FluxiQ NPC Processing | Description |
|---|---|---|---|
| Morning | 08:30 | 08:35 | Payments from previous day (afternoon) |
| Afternoon | 16:30 | 16:35 | Payments 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
- Trigger (08:35 / 16:35): Oban job triggered automatically
- FTP Download: Connect to Nuclea FTP server to download ACMP615 file
- Parse ACMP615: Read and interpret file records
- Match Boletos: Associate payments with registered boletos
- Notify Payments: Generate individual events for each payment
- Reconcile Amounts: Verify values (original vs paid)
- Update Status: Update boleto status to
settled - Webhook Central: Send webhooks to Central
Manual Trigger
For special situations (reprocessing, testing), you can trigger manually:
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"
}'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");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:
{
"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:
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:
{
"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:
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:
| Type | Name | Description |
|---|---|---|
00 | Header | File information (date, time, sender) |
20 | Detail | Individual payment records |
99 | Trailer | Totals and record count |
Header Record (Type 00)
| Position | Size | Field | Description |
|---|---|---|---|
| 1-2 | 2 | tipo_registro | "00" |
| 3-10 | 8 | data_geracao | Generation date (YYYYMMDD) |
| 11-16 | 6 | hora_geracao | Generation time (HHMMSS) |
| 17-24 | 8 | ispb_remetente | Sender ISPB |
| 25-30 | 6 | sequencial_arquivo | File sequence number |
Detail Record (Type 20)
| Position | Size | Field | Description |
|---|---|---|---|
| 1-2 | 2 | tipo_registro | "20" |
| 3-13 | 11 | nosso_numero | Boleto identifier |
| 14-30 | 17 | valor_pago | Amount paid (cents, left-padded zeros) |
| 31-38 | 8 | data_pagamento | Payment date (YYYYMMDD) |
| 39-46 | 8 | data_credito | Credit date (YYYYMMDD) |
| 47-49 | 3 | banco_recebedor | Bank COMPE code |
| 50-53 | 4 | agencia_recebedora | Receiving branch |
| 54-55 | 2 | codigo_movimento | Movement code |
Trailer Record (Type 99)
| Position | Size | Field | Description |
|---|---|---|---|
| 1-2 | 2 | tipo_registro | "99" |
| 3-8 | 6 | qtd_registros | Count of type 20 records |
| 9-26 | 18 | valor_total | Sum of values (cents) |
Reconciliation Code
Example of reconciling payments with their records:
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:
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:
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
Recommended Metrics
Configure alerts for the following situations:
| Metric | Threshold | Action |
|---|---|---|
| Match rate | < 95% | Yellow alert - investigate |
| Match rate | < 90% | Red alert - immediate action |
| Processing time | > 15 min | Check FTP performance |
| File not found | 30 min after cycle | Contact Nuclea |
| Value discrepancies | > 5% of boletos | Review interest/discount rules |
Monitoring Example
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
- Boleto Flow - Complete boleto lifecycle
- Webhook Events - Event reference
- Settlements API - API reference