How to Route High-Value Stripe Refunds Through Human Approval
Build a Spojit workflow where a refund request arrives by webhook, small refunds process instantly, and refunds over a threshold pause for a human to approve before the refund is created in Stripe.
What This Integration Does
Refunds are one of the few automation steps where a wrong number costs real money. This workflow lets your support tool, returns portal, or internal app fire a single webhook to request a refund, then Spojit decides what happens next based on the amount. Anything below your threshold is refunded automatically in Stripe. Anything at or above the threshold stops and waits in the Approvals inbox until a finance approver signs off, so a large or unusual refund never goes out without a human looking at it first. Either way, the requester and the approving team get a Slack message so nothing happens silently.
The workflow is started by a Webhook trigger. The incoming JSON carries the Stripe charge to refund, the amount, and a little context (customer, reason). A Condition node compares the amount against your threshold. The low-value branch goes straight to a Connector node that creates the refund in Stripe. The high-value branch goes to a Human node first; the workflow pauses there and leaves an open approval until someone responds. On approval it continues to the same Stripe refund step; on rejection or timeout the run halts and no refund is created. Re-sending the same webhook starts a fresh run, so treat each request as one-shot and rely on the approval gate rather than retries.
Prerequisites
- A Stripe connection (API key) added under Connections. See the Stripe connector article for setup.
- A Slack connection authorized to post in the channels you want to notify, with the channel IDs or names handy. See the Slack connector article.
- A Webhook signing connection so Spojit can verify the caller. The Custom or Spojit scheme works for an internal portal. See Setting Up a Webhook Connection.
- The refund amount in your requests expressed in the smallest currency unit (cents), since Stripe works in minor units.
- At least one user, role, or team in your workspace that should approve high-value refunds, plus their access to the
/approvalsinbox.
Step 1: Start with a Webhook trigger
On a new workflow, set the Trigger node type to Webhook. Pick your signing connection so the request is verified by HMAC, then copy the generated URL into your refund request source. Have the caller POST a JSON body like this:
{
"chargeId": "ch_3PqExAMPLE012345",
"amount": 24500,
"currency": "usd",
"customerEmail": "buyer@example.com",
"reason": "requested_by_customer",
"requestedBy": "support-agent@yourco.com"
}
The parsed body is available downstream as {{ input }}, so the charge is {{ input.chargeId }} and the amount is {{ input.amount }}. The trigger returns 202 with an executionId immediately; the rest of the run continues in the background, which is important because the Human node can pause for minutes or hours.
Step 2: Branch on the refund amount with a Condition node
Add a Condition node after the trigger. Configure a single test that compares {{ input.amount }} against your threshold. For a 100.00 cutoff in a cents-based currency, check whether {{ input.amount }} is greater than or equal to 10000. The true output is the high-value path that needs approval; the false output is the low-value path that proceeds automatically. Keep the threshold in one place so it is easy to change later. If your requests can arrive in different currencies, branch on currency first or normalize the amount in a Transform node before this comparison.
Step 3: Pause high-value refunds with a Human node
On the true branch, add a Human node. This is the gate that holds the refund. Set the fields:
- Label:
Approve high-value refund - Message:
Refund of {{ input.amount }} {{ input.currency }} for charge {{ input.chargeId }} ({{ input.reason }}) requested by {{ input.requestedBy }}. Approve to create the refund in Stripe. - Urgency:
High - Timeout (minutes): optional; for example
240so a stale request does not sit open forever. A timeout is treated as a rejection. - Approval slots: add one slot containing a finance Role or Team atom (any atom in a slot satisfies it, and every slot must be satisfied to approve). For two-person sign-off, add two slots.
You can also enable Email approvers to email the first ten approvers. Approvers respond in the /approvals inbox. On approval the node outputs { approved: true, approvalId, outcome: "APPROVED" } and the run continues. On rejection or timeout the run halts there, so no refund is created. Note that "on reject do X" branching is not supported: rejection simply stops the workflow.
Step 4: Create the refund in Stripe with a Connector node
Add a Connector node for Stripe in Direct mode. Both the approved high-value branch and the low-value false branch lead here, so you only configure the refund once. Stripe creates a refund against an existing charge, so use the raw-api-request tool to call the refunds endpoint:
method:POSTpath:/refundsbody:
{
"charge": "{{ input.chargeId }}",
"amount": "{{ input.amount }}",
"reason": "{{ input.reason }}"
}
Map the node output to a result variable such as refund so later steps can reference the new refund id as {{ refund.id }} and its status as {{ refund.status }}. Direct mode is deliberate here: a refund must be a single, predictable call with no AI judgment, so it spends no AI credits. If you later need to confirm or look up the original charge, the Stripe connector also offers list-charges.
Step 5: Notify Slack on the result
After the Stripe step, add a Connector node for Slack in Direct mode using the send-message tool. Set channel to your finance or support channel and compose text from upstream variables, for example:
Refund created: {{ refund.id }} for charge {{ input.chargeId }}, amount {{ input.amount }} {{ input.currency }}, status {{ refund.status }}. Requested by {{ input.requestedBy }}.
For the high-value branch you can include that it was approved, since the Human node ran upstream. If you want to reach a specific person by their email rather than a channel, run the Slack lookup-user-by-email tool first to resolve their user id, then send to that id. Keeping the notification on both branches gives finance a single record of every refund Spojit processed.
Step 6: Reply to the original caller (optional)
If the webhook came from a tool that expects a synchronous answer, add a Response node to return a small payload like { "status": "refunded", "refundId": "{{ refund.id }}" }. Because the high-value path can pause for a long time at the Human node, a synchronous response is best suited to the low-value branch only. For requests that may wait on approval, prefer the asynchronous pattern: let the trigger return its 202 and notify the requester later with a Send Email node to {{ input.requestedBy }} once the refund completes.
Tips
- Keep the threshold as a single value in the Condition node so finance can raise or lower it without touching the rest of the workflow.
- Use a finance Role or Team atom in the approval slot rather than a single named user, so coverage does not depend on one person being available.
- Set a sensible Timeout (minutes) on the Human node so abandoned requests resolve as rejected instead of staying open indefinitely.
- Ask Miraxa, the intelligent layer across your automation, to scaffold the branch for you: try "Add a Condition node that checks if
{{ input.amount }}is at or over 10000 and connect the true branch to a Human node," then fine-tune fields in the properties panel.
Common Pitfalls
- Amount units: Stripe expects the smallest currency unit. Sending
100when you mean 100.00 will refund one dollar. Validate the unit at the source or in a Transform node. - Refunding more than the charge: Stripe rejects a refund larger than the remaining refundable amount. The approval gate does not catch this, so confirm the amount against the original charge if requests can be malformed.
- Rejection has no alternate path: a rejected or timed-out Human node halts the run. There is no "on reject" branch, so if you need to tell the requester it was declined, send that notice from a separate workflow or upstream of the refund step.
- Webhook replays: re-sending the same request creates a second run and a second refund. Use the trigger's event-id dedup header, or have the caller send a unique id per request, to avoid double refunds.
- Approval coverage: if a slot's role or team has no eligible members, the workflow can wait forever (or until timeout). Confirm approvers exist before going live.
Testing
Test against a Stripe test-mode connection and a small charge first. Send one webhook with an amount below the threshold and confirm the run skips the Human node and creates the refund, then send one above the threshold and confirm the run pauses, an item appears in the /approvals inbox, and the refund is only created after you approve. Reject a third test request and confirm the run halts with no Stripe call. Watch each run in the execution history to verify which branch fired and that the Slack message matches the outcome before you point production traffic at the webhook URL.