How to Hold High-Value Shipments for Approval Before Booking a Carrier

Build a Spojit workflow that catches a new Shopify order over a value threshold, pauses for a manager to approve in the Approvals inbox, and only then creates the ShipStation order and buys the shipping label.

What This Integration Does

Most orders can flow straight to fulfillment, but high-value orders deserve a second look before you spend money on postage and commit stock. This workflow watches Shopify for new orders, checks whether the order total is over a threshold you set, and for those orders it holds the run at a Human approval node. A manager sees the request in the Approvals inbox with the order number and value, and only after they approve does Spojit create the order in ShipStation and purchase the label. Orders under the threshold skip the hold and are fulfilled immediately.

The workflow is started by a Webhook trigger that Shopify calls whenever an order is created. The trigger output is the parsed order JSON. A Condition node splits the run on the order total. On the high-value branch the run pauses at the Human node until every approval slot is satisfied; an approval continues the run, while a rejection or timeout halts it with no label purchased. When the run continues, a Connector node in Direct mode calls ShipStation create-order, a second calls create-shipment-label to buy postage, and a Slack message confirms dispatch. Because the carrier is only booked after approval, a rejected or timed-out order leaves no label and no spend behind.

Prerequisites

  • A Shopify connection (API key) so Spojit can read order detail if you need to enrich it later, plus access to your Shopify admin to register the order-created webhook.
  • A ShipStation connection (API key) with permission to create orders and purchase labels.
  • A Slack connection (OAuth) and the channel ID where dispatch confirmations should post.
  • A Shopify signing connection for the Webhook trigger so Spojit can verify each incoming request by HMAC. See Setting Up a Webhook Connection.
  • The carrier and service codes you ship with (for example stamps_com / ups), and your origin postal code and warehouse address.
  • The user, role, or team in your workspace that should approve high-value shipments.

Step 1: Start with a Webhook trigger for new Shopify orders

Add a Trigger node and set its type to Webhook. Choose your Shopify signing connection so every request is verified by HMAC and forged calls are rejected. Copy the generated workflow URL, then in Shopify admin create a webhook for the orders/create topic that POSTs to that URL. The trigger output is the parsed order body, available downstream as {{ input }}, with fields such as {{ input.order_number }}, {{ input.total_price }}, {{ input.email }}, and the {{ input.shipping_address }} object. The webhook responds 202 with an executionId immediately, so Shopify is never left waiting on the approval pause.

Step 2: Branch on order value with a Condition node

Add a Condition node after the trigger. Configure a single comparison that checks whether the order total is over your threshold, for example {{ input.total_price }} is greater than 500. Shopify sends total_price as a string, so if your comparison treats it as text, first add a small Transform node (or the math connector) to coerce it to a number and compare on that variable instead. Connect the true output to the approval branch in Step 3. Connect the false output straight to the ShipStation order step in Step 4 so low-value orders fulfill without a hold.

Step 3: Pause for a manager with a Human approval node

On the true branch, add a Human node. Give it a clear Label such as Approve high-value shipment and a Message that names the order and amount, for example Order {{ input.order_number }} totals {{ input.total_price }}. Approve to book the carrier and buy the label.. Set the Notification title and Notification body (both accept variables), pick an Urgency, and optionally set a Timeout (minutes) so a stale request does not block forever. In Approval slots (the only required field) add a slot and drop in the user, role, or team that should sign off; any atom satisfies its slot, and the node completes only when every slot is satisfied. Turn on Email approvers if you want the first ten notified by email in addition to the in-app inbox.

When the manager approves in the Approvals inbox at /approvals, the run continues and the node outputs { approved: true, approvalId, outcome: "APPROVED" }. A rejection or a timeout halts the run, so nothing downstream executes and no label is bought. There is no separate "on reject" branch: reject simply stops the workflow.

Step 4: Create the ShipStation order in Direct mode

After the approval (and joined from the Condition false branch), add a Connector node, choose ShipStation, and set it to Direct mode. Pick the create-order tool and map its single order input to an order object built from the trigger data. Use the order number to keep this idempotent: if an order with the same orderNumber already exists, ShipStation updates it instead of duplicating it. A minimal payload looks like:

{
  "orderNumber": "{{ input.order_number }}",
  "orderDate": "{{ input.created_at }}",
  "orderStatus": "awaiting_shipment",
  "billTo": { "name": "{{ input.shipping_address.name }}" },
  "shipTo": {
    "name": "{{ input.shipping_address.name }}",
    "street1": "{{ input.shipping_address.address1 }}",
    "city": "{{ input.shipping_address.city }}",
    "state": "{{ input.shipping_address.province_code }}",
    "postalCode": "{{ input.shipping_address.zip }}",
    "country": "{{ input.shipping_address.country_code }}"
  },
  "items": []
}

Store the result in an output variable such as ship_order so the label step can reference the new ShipStation order ID via {{ ship_order.data }}.

Step 5: Buy the shipping label with a second ShipStation step

Add another Connector node for ShipStation in Direct mode and select create-shipment-label. This tool purchases postage from the carrier, so it is the step you deliberately kept behind the approval. Map its single shipment input to a shipment object that carries the carrier and service codes, package weight, and the ship-from and ship-to addresses:

{
  "carrierCode": "stamps_com",
  "serviceCode": "usps_priority_mail",
  "packageCode": "package",
  "weight": { "value": 32, "units": "ounces" },
  "shipFrom": { "name": "Warehouse", "street1": "1 Depot Rd", "city": "Austin", "state": "TX", "postalCode": "73301", "country": "US" },
  "shipTo": {
    "name": "{{ input.shipping_address.name }}",
    "street1": "{{ input.shipping_address.address1 }}",
    "city": "{{ input.shipping_address.city }}",
    "state": "{{ input.shipping_address.province_code }}",
    "postalCode": "{{ input.shipping_address.zip }}",
    "country": "{{ input.shipping_address.country_code }}"
  }
}

If you want to compare carrier prices before committing, add a get-rates call first and pick the cheapest service, then feed that into create-shipment-label. Save the label result to a variable such as label so you can surface the tracking number next. If you ever need to undo a purchase, ShipStation also exposes void-label.

Step 6: Confirm dispatch in Slack

Add a Connector node for Slack in Direct mode and select send-message. Set the channel to your fulfillment channel ID and compose the text from the upstream variables, for example Order {{ input.order_number }} ({{ input.total_price }}) approved and labelled. Tracking: {{ label.data }}. This gives your team a single confirmation that the high-value order cleared approval and the carrier is booked. You can also add a Send Email node here to notify the customer from Spojit's built-in mail service, using {{ input.email }} as the recipient.

Tips

  • Use a number variable for the threshold comparison. Shopify delivers total_price as a string, so coerce it once in a Transform or math step and compare on that value to avoid surprising text comparisons.
  • Keep the approval message specific. Putting {{ input.order_number }} and the total in the Message and notification fields lets approvers decide without opening the order.
  • Set a Timeout (minutes) on the Human node so an unanswered request eventually halts rather than holding stock indefinitely. A timeout is treated as a reject, so no label is purchased.
  • Call get-rates before create-shipment-label when you want the cheapest eligible service rather than a fixed one.

Common Pitfalls

  • Skipping the signing connection on the Webhook trigger leaves the URL open to anyone who finds it. Always attach the Shopify signing connection so requests are verified by HMAC.
  • Placing create-shipment-label before the approval defeats the purpose: postage is charged the moment that tool runs. Keep both ShipStation calls strictly after the Human node on the high-value branch.
  • Expecting a reject branch. Rejection and timeout halt the run; there is no downstream "if rejected" path. If you need a notify-on-reject behavior, handle it as a separate review process rather than wiring it after the Human node.
  • Webhook replays and retries from Shopify can fire the same order twice. Because create-order updates an existing order with the same orderNumber, keep the order number as the key, and consider enabling event-id dedup on the trigger.

Testing

Validate on a small scope first. Set a low threshold (for example 1) so a test order takes the high-value branch, then place a low-value test order in Shopify. Confirm the run pauses at the Human node and that the request appears in the Approvals inbox with the right order number and total. Approve it and check the execution history: ShipStation should show a new order and a purchased label, and the Slack message should post. Then reject a second test order and confirm the run halts with no label created. Once both paths behave, raise the threshold to your real value and enable the workflow.

Learn More

Did this answer your question? Thanks for the feedback There was a problem submitting your feedback. Please try again later.