# Reconcile payments and payouts nightly

> A nightly job that pulls a day's payments, refunds and payouts from the Anytime Capital API, writes a CSV, matches payouts to your bank and flags problems.

Source: https://anytime-capital.com/docs/reconcile-payouts · Markdown: https://anytime-capital.com/docs/reconcile-payouts.md · All docs: https://anytime-capital.com/docs/llms-full.txt

Once a day, pull what happened from the API, write it down, and check it against your bank: payments in, refunds out, and payouts to your bank with their trace numbers. The script below does that for one UTC day and exits non-zero when something needs a person, so your scheduler can alert you.

> **A read-only key:** Create a key with **Read only** access, or Custom with `payments`, `refunds` and `payouts` set to read. A reporting job never needs to write. See [Authentication](https://anytime-capital.com/docs/authentication.md#restricted-keys-and-permissions).

## The script

**reconcile.mjs**

```javascript
// reconcile.mjs — Node.js 18+. Run nightly for yesterday (UTC), or: node reconcile.mjs 2026-10-23
import { writeFile } from "node:fs/promises";

const API = "https://anytime-capital.com/api/v1";
const headers = { Authorization: `Bearer ${process.env.ANYTIME_API_KEY}` };
const DAY_MS = 86_400_000;

/** GET with retries on 429 and 5xx (reads are always safe to repeat). */
async function get(path) {
  for (let attempt = 1; ; attempt++) {
    const res = await fetch(`${API}${path}`, { headers });
    const body = await res.json().catch(() => ({}));
    if (res.ok) return body;
    if ((res.status === 429 || res.status >= 500) && attempt < 4) {
      const wait = Number(res.headers.get("Retry-After") ?? 2 ** attempt);
      await new Promise((resolve) => setTimeout(resolve, wait * 1000));
      continue;
    }
    throw new Error(`GET ${path}: ${body.error?.code}: ${body.error?.message}`);
  }
}

/** Every object of a list, newest first, until `stop` says we've gone back far enough. */
async function* walk(path, params = {}, stop = () => false) {
  let cursor = null;
  do {
    const query = new URLSearchParams({ ...params, limit: "100", ...(cursor ? { cursor } : {}) });
    const page = await get(`${path}?${query}`);
    for (const item of page.data) {
      if (stop(item)) return;
      yield item;
    }
    cursor = page.hasMore ? page.nextCursor : null;
  } while (cursor);
}

const day = process.argv[2] ?? new Date(Date.now() - DAY_MS).toISOString().slice(0, 10);
const from = new Date(`${day}T00:00:00Z`);
const to = new Date(from.getTime() + DAY_MS);
const inDay = (iso) => new Date(iso) >= from && new Date(iso) < to;
const before = (item) => new Date(item.createdAt) < from;

// Payments filter by date on the server; refunds and payouts are newest first, so stop past the day.
const payments = [];
for await (const payment of walk("/payments", { createdFrom: day, createdTo: to.toISOString().slice(0, 10) })) {
  payments.push(payment);
}
const refunds = [];
for await (const refund of walk("/refunds", {}, before)) if (inDay(refund.createdAt)) refunds.push(refund);
const payouts = [];
for await (const payout of walk("/payouts", {}, before)) if (inDay(payout.createdAt)) payouts.push(payout);
const balance = await get("/balance");

const paid = payments.filter((payment) => payment.paidAt !== null);
const sum = (items, field) => items.reduce((total, item) => total + (item[field] ?? 0), 0);
const usd = (cents) => (cents / 100).toFixed(2);

const problems = [
  ...paid.filter((payment) => payment.netCents === null).map((payment) => `${payment.id}: net not reported yet`),
  ...refunds.filter((refund) => refund.status === "failed").map((refund) => `${refund.id}: refund failed (${refund.failureReason})`),
  ...payouts
    .filter((payout) => ["failed", "reversed", "denied", "canceled"].includes(payout.status))
    .map((payout) => `${payout.id}: payout ${payout.status}: ${payout.failureMessage ?? ""}`),
];

// One CSV row per record, for your spreadsheet or accounting import.
const rows = [
  ["type", "id", "status", "amount_cents", "net_cents", "reference", "created_at"],
  ...payments.map((p) => ["payment", p.id, p.status, p.amountCents, p.netCents ?? "", p.metadata?.orderId ?? p.clientReference ?? "", p.createdAt]),
  ...refunds.map((r) => ["refund", r.id, r.status, -r.amountCents, "", r.paymentId, r.createdAt]),
  ...payouts.map((p) => ["payout", p.id, p.status, p.amountCents ?? "", p.netCents ?? "", p.traceCode ?? "", p.createdAt]),
];
const csv = rows.map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(",")).join("\n");
await writeFile(`reconciliation-${day}.csv`, `${csv}\n`);

console.log(`${day}: ${paid.length} payments, $${usd(sum(paid, "amountCents"))} gross, $${usd(sum(paid, "netCents"))} net`);
console.log(`refunds: ${refunds.length}, $${usd(sum(refunds.filter((r) => r.status !== "failed"), "amountCents"))}`);
for (const payout of payouts) {
  console.log(`payout ${payout.id} ${payout.status} $${usd(payout.netCents ?? 0)} trace ${payout.traceCode ?? "-"} arrives ${payout.estimatedArrival ?? "-"}`);
}
console.log(`balance now: $${usd(balance.availableCents)} available, $${usd(balance.pendingCents)} pending, $${usd(balance.reserveCents)} reserve`);
if (problems.length > 0) {
  console.error(`Needs a look:\n${problems.join("\n")}`);
  process.exitCode = 1;
}
```

## Run it every night

**crontab (Node.js 20.6+ reads .env with --env-file)**

```bash
# 06:15 UTC every day, for the day before
15 6 * * * cd /srv/reconcile && node --env-file=.env reconcile.mjs >> reconcile.log 2>&1
```

## Match payouts to your bank

- Each payout's `netCents` is what reaches your bank, and `traceCode` is the trace number your bank can find the deposit by: match on the amount, and on the trace number where your bank shows it.
- `estimatedArrival` says when to expect it. A payout that is `completed` but not in your bank a day after that is worth a call to [Support](https://anytime-capital.com/docs/support.md).
- `payout.paid` and `payout.failed` [webhooks](https://anytime-capital.com/docs/webhooks.md) tell you as it happens, if you'd rather not wait for the night.

> **What the API doesn't tell you:** The API doesn't say which payments make up each payout: **Payouts** in the dashboard shows that, payout by payout. To have every payment, card processing charge, refund and payout booked for you, use [Accounting sync](https://anytime-capital.com/docs/accounting-sync.md) with QuickBooks Online or Xero, which posts them through a clearing account so the payouts match your bank feed.

---

Anytime Capital is a fintech company, not a bank. Payment processing, banking and payout services are provided by Anytime Capital's licensed partners. Rates shown are for US card payments; international cards, currency conversion and disputes may carry additional fees shown in your dashboard before you accept a payment.
