How to Void an Unused ShipStation Label and Refund the Customer
When a Shopify order is cancelled before it ships, this Spojit workflow voids the unused ShipStation label, refunds the Stripe charge, and posts a cancellation summary to Slack so nobody pays for a label or a shipment that will never go out.
What This Integration Does
Labels and charges pile up fast. A picker buys a ShipStation label the moment an order drops, and the payment is captured at checkout. When a customer cancels minutes later, two things are now wrong: you are holding a shipping label you will never use (and may be billed for at the carrier's manifest cutoff), and the customer's money is still sitting on a captured Stripe charge. This workflow closes both loops automatically. The instant Shopify reports a cancellation, Spojit voids the matching ShipStation label, issues the Stripe refund, and drops a tidy summary into a Slack channel so your operations team has a paper trail without lifting a finger.
The workflow is driven by a Webhook trigger wired to Shopify's orders/cancelled topic. Each cancellation delivers the order payload as JSON; Spojit verifies it with a Shopify signing connection, looks up the order's open shipment in ShipStation, voids the label, refunds the captured charge in Stripe, and notifies Slack. The run is one-shot and idempotent in spirit: it only acts on orders that have an unshipped label and a captured charge, so a replayed webhook or a manual re-run against an already-voided order is safe to short-circuit with a Condition node. Nothing is left half-done because the void and the refund each run as their own deterministic Connector step.
Prerequisites
- A Shopify connection (Admin API access scopes for reading orders and issuing cancellations) plus a Shopify signing connection used to verify the inbound webhook HMAC.
- A ShipStation connection (API key and secret) for the store whose labels you create.
- A Stripe connection (secret key) on the account that captured the original payment.
- A Slack connection and the target channel ID (for example
C0123456789) where the cancellation summary should land. - A reliable way to match a Shopify order to its ShipStation shipment, usually the Shopify order number or order ID stored in the ShipStation order's
orderNumberfield. The Stripe charge ID is read from the Shopify order's transactions or stored on the order at checkout.
Step 1: Receive the cancellation with a Webhook trigger
Add a Trigger node and set its type to Webhook. Copy the generated URL and register it in Shopify under Settings -> Notifications -> Webhooks for the orders/cancelled event. Choose a Shopify signing connection on the trigger so Spojit validates the HMAC signature on every delivery and rejects forged calls. The trigger returns 202 with an executionId immediately, then runs the rest of the workflow asynchronously. The parsed order body is available downstream as {{ trigger }}, including {{ trigger.id }}, {{ trigger.name }} (the order number such as #1054), and {{ trigger.cancelled_at }}.
Step 2: Find the ShipStation shipment for the order
Add a Connector node in Direct mode, select ShipStation, and pick the list-shipments tool. Filter to the order so you get the shipment that holds the label. Map the order number from the trigger into the lookup, for example searching by the Shopify order name. A typical input looks like:
{
"orderNumber": "{{ trigger.name }}",
"includeShipmentItems": false
}
Bind the result to a variable such as shipment_lookup. The first matching shipment exposes the numeric shipmentId you will void in the next step and a voided flag you can inspect to avoid double work.
Step 3: Skip orders that already shipped or were already handled
Add a Condition node so you only void unused labels. Branch on whether a void-able shipment exists and has not already been voided. For example, set the condition to continue when {{ shipment_lookup.data.shipments[0].voided }} is false and a shipmentId is present. Route the false branch to a Send Email node or a short Slack message that records "order cancelled, no open label to void" so replays and edge cases are visible rather than silent. The true branch carries on to the void and refund steps. This guard is what makes a replayed Shopify webhook harmless.
Step 4: Void the unused ShipStation label
On the true branch, add a Connector node in Direct mode, select ShipStation, and pick the void-label tool. It takes a single field, the numeric shipmentId from the lookup:
{
"shipmentId": {{ shipment_lookup.data.shipments[0].shipmentId }}
}
Bind the result to void_result. ShipStation returns a success flag in {{ void_result.success }}. Voiding releases the label so it is excluded from your next carrier manifest, which is the action that actually prevents the charge from the carrier.
Step 5: Refund the Stripe charge
Add a Connector node in Direct mode, select Stripe, and use the raw-api-request tool to create a refund against the original charge (the Stripe connector exposes charges and customers directly, and refunds are created through the raw request path). Set method to POST, path to /refunds, and pass the captured charge ID in the body. The charge ID comes from the Shopify order payload (for example a transaction's gateway_reference or a stored charge_id on the order):
{
"method": "POST",
"path": "/refunds",
"body": {
"charge": "{{ trigger.charge_id }}",
"reason": "requested_by_customer"
}
}
Omit amount for a full refund, or include it (in the smallest currency unit, for example 1000 for $10.00) to refund partially. Bind the result to refund_result. If you would rather Shopify issue the refund itself, you can instead call the Shopify cancel-order tool with refund set to true; this article keeps the money movement in Stripe so the refund lands on the exact charge you captured.
Step 6: Post a cancellation summary to Slack
Add a Connector node in Direct mode, select Slack, and pick the send-message tool. Set channel to your operations channel ID and compose a text summary from the upstream variables so the team sees the order, the voided label, and the refund in one line:
{
"channel": "C0123456789",
"text": "Order {{ trigger.name }} cancelled. ShipStation label voided (success: {{ void_result.success }}). Stripe refund issued for charge {{ trigger.charge_id }}."
}
For a richer card you can supply Block Kit blocks instead, but plain text is enough for an audit trail. Bind the result to slack_result if you want the message timestamp for later threading.
Step 7: Confirm and record the outcome
End the run with a short record of what happened. Add a Send Email node to notify the fulfillment lead, or write a row to your records system with another Connector node. Reference {{ void_result.success }} and {{ refund_result }} so the final notification states clearly that both the label and the charge were reversed. If you want the void and the refund to run side by side rather than in sequence, wrap Step 4 and Step 5 in a Parallel node; just make sure the Slack summary in Step 6 sits after the parallel branches rejoin so it can reference both results.
Tips
- Match orders precisely. ShipStation's
orderNumbermust equal what you send from Shopify; if you push the numeric order ID into ShipStation at fulfillment time, search by that instead of the display name to avoid the leading#. - Void before the manifest cutoff. A label only stops costing you money once it is voided and excluded from the carrier manifest, so keep this workflow fast and avoid long Human approval pauses on the void branch.
- Ask Miraxa to scaffold the canvas. Miraxa, the intelligent layer across your automation, can build this from a sentence: "Build a workflow with a Webhook trigger that voids a ShipStation label with void-label, refunds the Stripe charge, and posts a summary to Slack." Then fine-tune each node in the properties panel.
- Keep amounts in the smallest currency unit. Stripe expects cents, so a partial refund of $10.00 is
1000, not10.
Common Pitfalls
- Webhook replays. Shopify can redeliver
orders/cancelled. Without the Step 3 Condition guard you risk a double refund; always check thevoidedflag and short-circuit if there is nothing to void. - Already-shipped orders. If a label was used before the cancellation, voiding it can fail at the carrier. Inspect
{{ void_result.success }}and branch to a notification rather than blindly issuing the refund. - Wrong charge ID. Refunding the order ID instead of the Stripe charge ID returns an error. Confirm the value you pass to
chargebegins withch_(a charge) and is the one captured at checkout. - Signing connection missing. A Webhook trigger without a Shopify signing connection accepts unverified calls. Always attach the signing connection so only genuine Shopify deliveries run the workflow.
Testing
Test on a throwaway order before turning this on for live traffic. Create a low-value test order in Shopify, generate its ShipStation label, then cancel the order so the webhook fires. Watch the run in your execution history: confirm list-shipments found the shipment, void-label returned success: true, the Stripe raw-api-request created a refund, and the Slack message arrived. Temporarily point the Slack step at a private test channel and use a partial refund amount while validating, then switch to your real operations channel and full refunds once each step checks out.