How to Reconcile NetSuite Invoices Against Stripe Payments Weekly
Build a Spojit workflow that runs every week, pulls open NetSuite invoices and recent Stripe charges, matches them, and posts any unmatched items to a Slack channel for your finance team to chase.
What This Integration Does
Finance teams often carry a gap between what has been billed in NetSuite and what has actually been collected through Stripe. Invoices sit open in NetSuite while the matching payment has already cleared in Stripe, or a Stripe charge lands with no corresponding invoice on file. Reconciling this by hand every week is slow and error prone. This workflow does the comparison for you on a fixed schedule and surfaces only the exceptions, so your team spends its time resolving discrepancies instead of hunting for them.
The workflow is started by a Schedule trigger, so it runs unattended once a week at a time you choose. On each run it reads the current set of open invoices from NetSuite and the recent charges from Stripe, builds a comparison in a Code step, and sends a summary of unmatched items to Slack. It writes nothing back to NetSuite or Stripe, so it is safe to run repeatedly: every run is a fresh read-and-report against live data, and re-running it simply produces an up-to-date snapshot with no side effects on your financial records.
Prerequisites
- A NetSuite connection in Spojit with permission to read invoice and customer records (used by
list-invoicesandrun-suiteql). - A Stripe connection in Spojit with read access to charges (used by
list-charges). - A Slack connection authorized to post to the channel you want alerts in (used by
send-message), plus the target channel ID or name. - A shared key you can match on across both systems. The most reliable choice is to store the Stripe charge or payment-intent ID on the NetSuite invoice (for example in a custom field or the external ID), or to put the NetSuite invoice number into the Stripe charge
metadata. Decide this before you build so your matching logic in Step 4 has something to join on.
Step 1: Add a Schedule trigger
Create a new workflow in the Workflow Designer and add a Trigger node set to type Schedule. Schedules use a 5-field Unix cron expression plus an IANA timezone. For "every Monday at 7am Sydney time", set the cron to 0 7 * * 1 and the timezone to Australia/Sydney. A single trigger can hold more than one schedule if you want a mid-week run as well. The trigger output is simply { scheduledAt }, which you can reference downstream as {{ scheduledAt }} for stamping the report.
Step 2: Pull open invoices from NetSuite
Add a Connector node in Direct mode, choose the NetSuite connector, and select the list-invoices tool. Use the q field to pass a SuiteTalk filter so you only fetch open invoices, for example:
status IS "Invoice:A"
Name the output variable so later steps can read it, for example nsInvoices. If you need columns that list-invoices does not return, add a second NetSuite Direct-mode node using run-suiteql instead, with a query such as:
SELECT id, tranid, entity, total, status, foreigntotal
FROM transaction
WHERE type = 'CustInvc' AND status = 'A'
Either approach gives you a list of invoices to compare. Keep the result count modest on your first build so the run stays fast while you test.
Step 3: Pull recent charges from Stripe
Add another Connector node in Direct mode, choose the Stripe connector, and select list-charges. Set limit to the maximum useful page size (up to 100) and use starting_after with the last charge ID if you need to page through more than one batch. Name the output variable stripeCharges. Each charge carries an amount in the smallest currency unit (cents), a status of succeeded, pending, or failed, a customer ID, a created Unix timestamp, and any metadata you set at charge time. You will key your match on the field you chose in the prerequisites, typically the charge ID, the payment-intent ID, or a value inside metadata.
Step 4: Match invoices to charges in a Code step
Add a Connector node in Direct mode, choose the code connector, and select execute-javascript. Pass both lists in and return three groups: matched, invoices with no payment, and charges with no invoice. A starting point:
const invoices = {{ nsInvoices }};
const charges = {{ stripeCharges }};
// Build a lookup of succeeded charges by your shared key.
const paidBy = {};
for (const c of charges) {
if (c.status !== 'succeeded') continue;
const key = c.metadata?.invoiceNumber; // or c.id, c.payment_intent
if (key) paidBy[key] = c;
}
const unpaidInvoices = [];
const matched = [];
for (const inv of invoices) {
const match = paidBy[inv.tranid];
if (match) matched.push({ invoice: inv.tranid, charge: match.id });
else unpaidInvoices.push({ invoice: inv.tranid, total: inv.total });
}
const matchedChargeIds = new Set(matched.map(m => m.charge));
const orphanCharges = charges
.filter(c => c.status === 'succeeded' && !matchedChargeIds.has(c.id))
.map(c => ({ charge: c.id, amount: c.amount / 100 }));
return { matched, unpaidInvoices, orphanCharges };
Compare amounts in the same units: Stripe reports cents, so divide by 100 before comparing against a NetSuite invoice total. Name the output variable recon. If you would rather reason about fuzzy matches (for example tolerating small rounding differences or partial payments), use a Connector node in Agent mode with a Response Schema instead, and let Miraxa, the intelligent layer across your automation, return the same three groups as structured JSON.
Step 5: Stop early when everything reconciles
Add a Condition node that checks whether there is anything worth reporting. Set the condition to evaluate true when {{ recon.unpaidInvoices }} or {{ recon.orphanCharges }} contains items (for example, when their combined length is greater than zero). Route the false branch to the end of the workflow so a clean week produces no noise, and route the true branch into the Slack step. This keeps your channel quiet unless a real discrepancy exists.
Step 6: Post unmatched items to Slack
On the true branch, add a Connector node in Direct mode, choose the Slack connector, and select send-message. Set the channel to your finance channel and build the message text from the reconciliation result, for example:
Weekly Stripe/NetSuite reconciliation ({{ scheduledAt }})
Invoices with no matching payment: {{ recon.unpaidInvoices.length }}
Stripe charges with no matching invoice: {{ recon.orphanCharges.length }}
Please review and resolve in NetSuite.
If you want the full line-item detail rather than just counts, format the lists into a readable block in the Step 4 Code step first and reference that formatted string here. You can also send a copy by email using a Send Email node, which sends from Spojit's built-in mail service without needing a connection.
Tips
- Stripe
list-chargescapslimitat100per call. For high volume, loop withstarting_afterset to the last charge ID until no more pages return, or narrow the window so you only fetch charges since the last run. - Filter NetSuite at the source with the
qSuiteTalk filter (or a tightrun-suiteqlWHERE clause) so you only pull open invoices. Comparing smaller lists keeps the Code step fast and cheap. - Keep the join key deterministic. Storing the Stripe charge or payment-intent ID on the invoice gives an exact match and avoids false positives from amount-only matching when two invoices share the same total.
- Use the Condition node to suppress empty reports so the channel only pings when there is something to act on.
Common Pitfalls
- Currency unit mismatch. Stripe amounts are in the smallest unit (cents); NetSuite totals are in major units. Forgetting to divide Stripe amounts by 100 makes every comparison fail.
- Timezone drift in the schedule. Always set the IANA timezone on the Schedule trigger. A cron of
0 7 * * 1with no timezone will not land at 7am locally, which matters when your "weekly" cutoff straddles a day boundary. - Charge status confusion. Treat only
succeededcharges as payments. Countingpendingorfailedcharges as collected money produces false matches and hides genuinely unpaid invoices. - Permission gaps. If
list-invoicesorrun-suiteqlreturns nothing, confirm the NetSuite role behind the connection can read invoice and transaction records. A read-only Stripe key is enough forlist-charges.
Testing
Before turning the schedule on, validate the logic on a small scope. Temporarily swap the Schedule trigger for a Manual trigger (or just press Run) and narrow both reads: set a tight q filter on list-invoices and a low limit on list-charges so you are working with a handful of records you can verify by hand. Run it, open the execution log, and confirm the Code step puts each known invoice in the right bucket and that your Slack message reflects reality. Once the matching is correct, restore the Schedule trigger and let it run for one cycle before relying on it.
Learn More
- Schedule trigger and cron configuration
- NetSuite connector tools including list-invoices and run-suiteql
- Stripe connector and the list-charges tool
- How to Sync Stripe Payments to NetSuite Automatically
- How to Reconcile Shopify Orders with Stripe Charges
- How to Set Up Slack Alerts for Workflow Failures