How to Draft Monthly Client Invoices in Stripe from NetSuite Sales Orders with Approval
Run a monthly workflow in Spojit that pulls open NetSuite sales orders, drafts a Stripe invoice for each client, and pauses on a Human approval node so a finance approver signs off before anyone is billed.
What This Integration Does
Professional-services firms often track billable work as open sales orders in NetSuite, then bill clients through Stripe at the end of each month. Doing that by hand means exporting orders, matching each one to the right Stripe customer, and re-keying amounts, which is slow and easy to get wrong. This Spojit workflow reads the open NetSuite sales orders on a fixed schedule, loops over them to draft a Stripe invoice per client, and holds every draft behind a single human sign-off so a finance lead reviews the full batch before a single client is charged.
The workflow runs on a monthly Schedule trigger. When it fires, a Connector node in Direct mode lists the open NetSuite sales orders, a Loop node walks each order, and inside the loop a Direct-mode Stripe create-invoice call drafts an invoice (with auto_advance left off so nothing finalizes automatically). Before any of that billing happens, a Human node pauses the run for approval. Drafts are created in Stripe in draft state, so re-running the workflow does not double-charge a client as long as you finalize or void drafts in Stripe between runs. A Slack message at the end posts the batch summary to your finance channel.
Prerequisites
- A NetSuite connection (token-based auth) added under Connections, with read access to sales orders. See the NetSuite connector article.
- A Stripe connection (API key) added under Connections, with permission to create customers and invoices. See the Stripe connector article.
- A Slack connection if you want the end-of-run summary posted to a channel. See the Slack connector article.
- A way to map each NetSuite customer to a Stripe customer. The simplest path is to store the Stripe customer ID (for example in a NetSuite customer field) or to match on email so the workflow can look the customer up by
emailin Stripe. - At least one Spojit user, role, or team to assign as the finance approver in the Human node.
Step 1: Start with a monthly Schedule trigger
Add a Trigger node and set its type to Schedule. Schedule triggers use a 5-field Unix cron expression plus an IANA timezone. To run at 9:00 AM on the first day of every month in Sydney, use:
Cron: 0 9 1 * *
Timezone: Australia/Sydney
The trigger output is { scheduledAt }, which you can reference downstream as {{ input.scheduledAt }} when you build the summary message. A single Schedule trigger can hold more than one schedule if you bill on multiple cycles.
Step 2: List open NetSuite sales orders
Add a Connector node on the NetSuite connector in Direct mode and pick the list-sales-orders tool. Use the q field to pass a SuiteTalk filter that limits results to open orders for the period you want, and set limit to cap the page size. For example, to fetch orders dated on or after the start of last month:
q: status ANYOF "SalesOrd:B" AND tranDate ON_OR_AFTER "01-05-2026"
limit: 200
Save the result to a variable such as orders. Direct mode is the right choice here because this is a single, predictable read with no AI cost. If you have more orders than one page, see the pagination note under Common Pitfalls.
Step 3: Loop over each sales order
Add a Loop node set to ForEach and point it at the list of orders, for example {{ orders.data.items }} (match the path to the shape your NetSuite list returns). Each iteration exposes the current order, which you can reference as {{ item }}. Everything you add inside the loop body runs once per order, so this is where you resolve the client and draft the invoice.
If a single order does not carry the customer email or Stripe ID you need, add a NetSuite get-sales-order call inside the loop (Direct mode, pass the order's internal ID as id and set expandSubResources to true to inline item lines) and a get-customer call to fetch the client's details.
Step 4: Resolve the Stripe customer
Inside the loop, add a Connector node on the Stripe connector in Direct mode using list-customers and filter by the client's email so you find an existing customer instead of creating duplicates:
email: {{ item.customerEmail }}
limit: 1
Add a Condition node that checks whether a customer was returned. On the branch where none exists, call create-customer with email and name to create one. Capture the resulting Stripe customer ID (for example cus_...) into a variable such as stripeCustomerId so the next step can reference it. A Transform node is useful here to normalize the order into a clean shape, for example:
{
"stripeCustomerId": "{{ stripeCustomerId }}",
"clientName": "{{ item.entity.refName }}",
"orderNumber": "{{ item.tranId }}",
"description": "Services for {{ item.tranId }}"
}
Step 5: Pause for finance approval (Human node)
Before any invoice is drafted, add a Human node so a finance approver reviews the batch. Place it after the listing step and before the billing work, or at the start of the loop, depending on whether you want one approval for the whole batch or per client (one approval per batch is usually what finance teams want). Configure:
- Label:
Approve monthly client invoices - Message: a short summary, for example
{{ orders.data.count }} open sales orders are ready to bill for {{ input.scheduledAt }}. - Approval slots: add a slot and assign your finance user, role, or team as an atom. Approval completes when every slot is satisfied.
- Timeout (minutes): optional. Leave blank for no timeout; a timeout is treated as a reject and halts the run.
- Email approvers: turn on to email the first ten approvers, or leave off and rely on the Approvals inbox at
/approvals.
When the approver approves, the node outputs { approved: true, approvalId, outcome: "APPROVED" } and the run continues. If it is rejected or times out, the run halts and no invoices are drafted. Branching on rejection is not supported, so a rejection simply stops the batch.
Step 6: Draft the Stripe invoice for each client
Inside the loop, after approval, add a Connector node on the Stripe connector in Direct mode using create-invoice. Pass the resolved customer and leave the invoice as a draft so nothing finalizes on its own:
customer: {{ stripeCustomerId }}
auto_advance: false
collection_method: send_invoice
description: Services for {{ item.tranId }}
Setting auto_advance to false keeps the invoice in draft so your finance team can add line items, review, and finalize each one in Stripe. Save each result so you can include the new invoice IDs in the summary. If you prefer Stripe to finalize automatically after approval, set auto_advance to true, but only do this once your line-item logic is solid.
Step 7: Post a summary to Slack
After the loop completes, add a Connector node on the Slack connector in Direct mode using send-message to post the batch result to your finance channel:
channel: #finance-billing
text: Drafted {{ orders.data.count }} Stripe invoices for {{ input.scheduledAt }}. Review and finalize in Stripe.
You can also add a Send Email node to email the same summary to the workflow owner without configuring a connection. If you want Miraxa, the intelligent layer across your automation, to wire any of these steps together for you, open the chat in the Workflow Designer and describe what you want, naming the real node types and tools.
Tips
- Keep the Stripe
create-invoicecalls in Direct mode so each call is deterministic and costs no AI credits. Reserve Agent mode for steps that genuinely need judgment. - Use a Transform node to shape each order into a tidy object before the Stripe call. Clean inputs make the loop body easy to read and debug.
- Match Stripe customers by email with
list-customersbefore creating one, so re-runs and new orders do not produce duplicate customers. - Set the Human node Urgency to High and turn on Email approvers at month-end so the approval is not missed and billing is not delayed.
Common Pitfalls
- Pagination.
list-sales-ordersreturns one page at a time. If you have more orders than yourlimit, page through usingoffset(or narrow theqfilter) so no client is skipped. - Timezones. The Schedule trigger uses an IANA timezone. Set it to your billing timezone so the monthly run fires on the day you expect and your
tranDatefilter lines up with the period. - Double billing on re-run. Re-running the workflow drafts a fresh invoice per order. Because drafts stay in draft state, this is recoverable, but finalize or void existing drafts in Stripe before re-running, and filter your NetSuite query to the correct period.
- Rejection halts the run. A rejected or timed-out Human node stops the workflow; downstream "on reject do X" branching is not supported. If you need a follow-up path, handle it as a separate workflow.
Testing
Before scheduling it live, validate on a tiny scope. Tighten the q filter in list-sales-orders to a single test customer or a narrow date range so the loop only touches one or two orders. Run the workflow manually (temporarily switch the trigger to Manual, or use the Run button) and confirm the Human node appears in the Approvals inbox. Approve it, then check Stripe to confirm a draft invoice was created against the correct customer with auto_advance off. Inspect the execution logs in Spojit to verify the loop ran the expected number of times and the summary posted to Slack. Once the small batch looks right, widen the filter and turn the Schedule trigger on.