Skip to main content

Reconcile Transactions

NextAPI's ledger records every peso movement as a posting — a timestamped, double-entry ledger entry. Use the postings API to reconcile your platform's records against NextAPI without manual bank statement matching.

This guide shows you how to pull postings, match them to your internal records, and identify discrepancies.

How the NextAPI ledger works

Every balance change on an account creates one or more postings:

  • Collection received → credit posting on the account
  • Payout sent → debit posting on the account
  • Internal transfer → debit on source, credit on destination

Postings are grouped into transactions (atomic operations). A payout transaction might have a debit posting on the merchant account and a credit posting on a clearing account.

You never need to cross-reference bank statements — the NextAPI ledger is the authoritative record.

Step 1: Fetch account postings

curl -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
"https://api.partners.nextpay.world/v2/accounts/acct_01HXYZ/postings?limit=100"

Response

{
"data": [
{
"id": "post_01HABC",
"account_id": "acct_01HXYZ",
"transaction_id": "txn_01HDEF",
"direction": "credit",
"amount": 150000,
"running_balance": 150000,
"description": "Payment received - Order #98765",
"reference": "order-98765",
"created_at": "2025-11-15T10:35:22Z"
},
{
"id": "post_01HXYZ",
"account_id": "acct_01HXYZ",
"transaction_id": "txn_01HGHI",
"direction": "debit",
"amount": 100000,
"running_balance": 50000,
"description": "Payout to BPI ****7890",
"reference": "payout-nov-2025-EMP001",
"created_at": "2025-11-15T18:00:00Z"
}
],
"meta": {
"total": 247,
"has_more": true,
"next_cursor": "post_01HAAA"
}
}

Key fields:

  • directioncredit (money in) or debit (money out)
  • amount — always positive, in centavos
  • running_balance — account balance after this posting
  • reference — the value you set when creating the payout or payment intent

Step 2: Page through all postings

The API is paginated. Use cursor-based pagination to fetch all postings for a period:

async function getAllPostings(accountId, startDate, endDate) {
const postings = [];
let cursor = null;

do {
const params = new URLSearchParams({
limit: 100,
...(cursor && { after: cursor }),
...(startDate && { created_after: startDate }),
...(endDate && { created_before: endDate }),
});

const response = await fetch(
`https://api.partners.nextpay.world/v2/accounts/${accountId}/postings?${params}`,
{ headers: { 'Authorization': 'Basic ...' } }
);
const page = await response.json();

postings.push(...page.data);
cursor = page.meta.has_more ? page.meta.next_cursor : null;

} while (cursor);

return postings;
}

Step 3: Match against internal records

Compare NextAPI postings to your internal transaction records:

async function reconcileDay(accountId, date, internalRecords) {
const startOfDay = `${date}T00:00:00Z`;
const endOfDay = `${date}T23:59:59Z`;

const postings = await getAllPostings(accountId, startOfDay, endOfDay);

const discrepancies = [];

for (const posting of postings) {
// Skip internal platform postings without a user-facing reference
if (!posting.reference) continue;

const internalRecord = internalRecords.find(r => r.reference === posting.reference);

if (!internalRecord) {
discrepancies.push({
type: 'MISSING_INTERNAL_RECORD',
posting,
message: `Posting ${posting.id} (ref: ${posting.reference}) has no matching internal record`,
});
continue;
}

if (internalRecord.amount !== posting.amount) {
discrepancies.push({
type: 'AMOUNT_MISMATCH',
posting,
internalRecord,
message: `Amount mismatch: NextAPI ${posting.amount} vs internal ${internalRecord.amount}`,
});
}
}

// Check for internal records with no corresponding posting
for (const record of internalRecords) {
const posting = postings.find(p => p.reference === record.reference);
if (!posting) {
discrepancies.push({
type: 'MISSING_NEXTAPI_POSTING',
internalRecord: record,
message: `Internal record ${record.reference} has no corresponding NextAPI posting`,
});
}
}

return { postings, discrepancies };
}

Step 4: Verify balance integrity

Cross-check the running balance on the last posting against the current account balance:

async function verifyBalance(accountId) {
const [postingsRes, balanceRes] = await Promise.all([
fetch(`https://api.partners.nextpay.world/v2/accounts/${accountId}/postings?limit=1`, {
headers: { 'Authorization': 'Basic ...' }
}),
fetch(`https://api.partners.nextpay.world/v2/accounts/${accountId}/balances`, {
headers: { 'Authorization': 'Basic ...' }
}),
]);

const { data: postings } = await postingsRes.json();
const balance = await balanceRes.json();

const lastPosting = postings[0];

if (lastPosting && lastPosting.running_balance !== balance.available) {
console.warn(
`Balance discrepancy: last posting running_balance=${lastPosting.running_balance}, ` +
`current available=${balance.available}`
);
// This can happen if there are pending postings — check balance.pending
}

return balance;
}
Pending postings

The available balance excludes pending funds. If running_balance on the last posting doesn't match available, check balance.pending — funds in transit may not yet be settled.

Webhook-based reconciliation

For real-time reconciliation (rather than end-of-day batch), use webhooks. Each significant event fires a webhook with the relevant reference:

EventWhat it means
payment_intent.paidA collection landed — credit will appear in postings
payout_request.completedA payout settled — debit will appear in postings
payout_request.failedA payout failed — no debit posted

Match webhook events to your internal records immediately. Use the ledger as your audit trail for any disputes.

Key fields for reconciliation

FieldDescriptionUse for
referenceYour reference stringPrimary match key to internal records
transaction_idNextAPI's transaction IDGrouping related postings
directioncredit or debitCategorizing money in vs money out
amountAmount in centavosAmount verification
running_balanceBalance after this postingIntegrity checks
created_atTimestampDate-range filtering