Automate Shopify order fulfillment by listening for `orders/paid` webhooks and calling the GraphQL `fulfillmentCreate` mutation with tracking numbers. The critical risk: Shopify silently deletes your webhook subscription after 19 consecutive failed deliveries (~48 hours). Return a 2xx response within 5 seconds or the delivery counts as a failure. Use the modern `fulfillmentOrders` query — the legacy REST `POST /fulfillments.json` endpoint is deprecated.
API Quick Reference
OAuth 2.0 / Access Token
1,000-point bucket, 50 pts/sec restore; 5s webhook timeout
JSON
Available
Understanding the Shopify Fulfillment API
Shopify uses a fulfillment order model for managing order fulfillment. When a customer places an order, Shopify creates one or more `fulfillmentOrder` objects representing groups of line items that will be shipped together. The legacy approach of directly creating fulfillments via REST (`POST /fulfillments.json`) is deprecated — the modern flow queries fulfillment orders first, then creates fulfillments linked to them.
The automation starts with the `orders/paid` webhook, which fires when an order's payment is captured. From there, you fetch the `fulfillmentOrders` for that order via GraphQL, then call `fulfillmentCreate` with the carrier and tracking number once the item ships. For dropshippers or 3PL-connected stores, this flow can be fully automated: webhook fires, your system submits the order to the supplier, supplier returns tracking info, and your code calls `fulfillmentCreate`.
The current stable API version is 2026-04. All code should target this version explicitly. See https://shopify.dev/docs/api/admin-graphql/2026-04/mutations/fulfillmentCreate for the complete mutation reference.
https://{shop}.myshopify.com/admin/api/2026-04/graphql.jsonSetting Up Shopify API Authentication for Order Fulfillment
Fulfillment automation requires the `read_orders`, `write_fulfillments`, and `read_fulfillments` scopes. For custom apps (single-store use), generate a token via the Dev Dashboard. For distributed apps, implement OAuth 2.0 with the authorization code grant. As of January 1, 2026, all new apps must be created via dev.shopify.com or the Shopify CLI — not from the Shopify Admin.
- 1Go to dev.shopify.com and log in with your Shopify Partner account.
- 2Click 'Create app' and select 'Custom app' for single-store automation.
- 3Under 'Configuration > Admin API access scopes', enable: read_orders, write_fulfillments, read_fulfillments.
- 4Click 'Save', then navigate to 'API credentials' and click 'Install app'.
- 5Copy the generated Admin API access token (starts with shpat_) — visible only once.
- 6Store the token in an environment variable: export SHOPIFY_ACCESS_TOKEN='shpat_...'
- 7Set your shop domain: export SHOPIFY_SHOP_DOMAIN='yourstore.myshopify.com'
1import os2import requests34SHOP_DOMAIN = os.environ['SHOPIFY_SHOP_DOMAIN']5ACCESS_TOKEN = os.environ['SHOPIFY_ACCESS_TOKEN']67GRAPHQL_URL = f'https://{SHOP_DOMAIN}/admin/api/2026-04/graphql.json'8HEADERS = {9 'X-Shopify-Access-Token': ACCESS_TOKEN,10 'Content-Type': 'application/json',11}1213# Verify auth by querying shop name14test = requests.post(GRAPHQL_URL, json={'query': '{shop{name}}'}, headers=HEADERS)15print(test.json()) # Should show {data: {shop: {name: 'Your Store Name'}}}Security notes
- •Never hardcode the access token — always load from environment variables or a secrets manager.
- •Validate webhook HMAC signatures using base64(HMAC-SHA256), not hex — this is the most common webhook security bug.
- •Verify the raw request body before JSON parsing for HMAC validation.
- •Scope the access token to only the required permissions: read_orders, write_fulfillments, read_fulfillments.
- •Monitor for unexpected 401 errors — they indicate token revocation or expiry (for expiring offline tokens).
- •Use HTTPS for webhook callback URLs — Shopify refuses HTTP endpoints.
Key endpoints
/admin/api/2026-04/graphql.jsonGraphQL mutation `fulfillmentCreate` — creates a fulfillment for one or more fulfillment order line items with optional tracking information.
| Parameter | Type | Required | Description |
|---|---|---|---|
fulfillmentOrderId | string | required | Global ID of the fulfillment order to fulfill (gid://shopify/FulfillmentOrder/{id}) |
trackingInfo.company | string | optional | Carrier name (e.g., USPS, UPS, FedEx, DHL) |
trackingInfo.number | string | optional | Tracking number from the carrier |
notifyCustomer | boolean | optional | Whether to send the shipping confirmation email to the customer |
Request
1{"query": "mutation fulfillmentCreate($fulfillment: FulfillmentInput!) { fulfillmentCreate(fulfillment: $fulfillment) { fulfillment { id status trackingInfo { number url } } userErrors { field message } } }", "variables": {"fulfillment": {"lineItemsByFulfillmentOrder": [{"fulfillmentOrderId": "gid://shopify/FulfillmentOrder/12345"}], "trackingInfo": {"company": "USPS", "number": "9400111899223148739475", "url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223148739475"}, "notifyCustomer": true}}}Response
1{"data": {"fulfillmentCreate": {"fulfillment": {"id": "gid://shopify/Fulfillment/98765", "status": "success", "trackingInfo": [{"number": "9400111899223148739475", "url": "https://tools.usps.com/..."}]}, "userErrors": []}}}/admin/api/2026-04/graphql.jsonGraphQL query for `fulfillmentOrders` — fetches all fulfillment orders for a given order ID, returning the fulfillment order IDs needed for `fulfillmentCreate`.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | required | Order global ID (gid://shopify/Order/{id}) |
Request
1{"query": "{ order(id: \"gid://shopify/Order/123456\") { fulfillmentOrders(first: 5) { edges { node { id status lineItems(first: 10) { edges { node { id sku quantity } } } assignedLocation { name } } } } } }"}Response
1{"data": {"order": {"fulfillmentOrders": {"edges": [{"node": {"id": "gid://shopify/FulfillmentOrder/12345", "status": "OPEN", "lineItems": {"edges": [{"node": {"id": "gid://shopify/FulfillmentOrderLineItem/777", "sku": "WIDGET-RED-M", "quantity": 2}}]}, "assignedLocation": {"name": "Main Warehouse"}}}]}}}}/admin/api/2026-04/graphql.jsonGraphQL mutation `webhookSubscriptionCreate` — subscribes to `orders/paid` events to trigger the fulfillment flow.
| Parameter | Type | Required | Description |
|---|---|---|---|
topic | string | required | ORDERS_PAID — fires when payment is captured for an order |
callbackUrl | string | required | HTTPS URL that will receive the webhook POST |
Request
1{"query": "mutation { webhookSubscriptionCreate(topic: ORDERS_PAID, webhookSubscription: {callbackUrl: \"https://yourapp.com/webhooks/orders-paid\", format: JSON}) { userErrors { field message } webhookSubscription { id } } }"}Response
1{"data": {"webhookSubscriptionCreate": {"userErrors": [], "webhookSubscription": {"id": "gid://shopify/WebhookSubscription/555"}}}}/admin/api/2026-04/graphql.jsonGraphQL mutation `fulfillmentOrderMove` — moves a fulfillment order to a different location, used when reassigning orders between warehouses or fulfillment providers.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | required | FulfillmentOrder global ID to move |
newLocationId | string | required | Target location global ID |
Request
1{"query": "mutation fulfillmentOrderMove($id: ID!, $newLocationId: ID!) { fulfillmentOrderMove(id: $id, newLocationId: $newLocationId) { movedFulfillmentOrder { id } userErrors { field message } } }", "variables": {"id": "gid://shopify/FulfillmentOrder/12345", "newLocationId": "gid://shopify/Location/87654321"}}Response
1{"data": {"fulfillmentOrderMove": {"movedFulfillmentOrder": {"id": "gid://shopify/FulfillmentOrder/12346"}, "userErrors": []}}}Step-by-step automation
Register the orders/paid Webhook
Why: The webhook fires immediately when payment is captured, giving you a real-time signal to start the fulfillment process without polling.
Create an HTTPS endpoint that receives Shopify webhook POST requests. Register it using `webhookSubscriptionCreate`. Your endpoint must respond with 2xx within 5 seconds or Shopify counts the delivery as failed. After 19 consecutive failures (~48 hours), Shopify silently deletes the subscription — your automation stops with no notification.
1curl -X POST 'https://mystore.myshopify.com/admin/api/2026-04/graphql.json' \2 -H 'X-Shopify-Access-Token: shpat_your_token' \3 -H 'Content-Type: application/json' \4 -d '{5 "query": "mutation { webhookSubscriptionCreate(topic: ORDERS_PAID, webhookSubscription: {callbackUrl: \\"https://yourapp.com/webhooks/orders-paid\\", format: JSON}) { userErrors { field message } webhookSubscription { id } } }"6 }'Pro tip: Set up a daily health check that queries `webhookSubscriptions { edges { node { id topic callbackUrl } } }` to verify your subscription still exists — silent deletion is the hardest bug to diagnose.
Expected result: Shopify confirms webhook subscription with an ID. Your endpoint receives POST requests within seconds of payment capture.
Fetch Fulfillment Orders for the Order
Why: The modern Shopify fulfillment model requires a `fulfillmentOrder` ID (not the order ID) to create a fulfillment — you must look up the fulfillment orders first.
When the webhook fires, you receive the order's numeric ID. Query GraphQL with the global order ID (`gid://shopify/Order/{id}`) to get all open fulfillment orders. Each fulfillment order has its own ID and contains the line items to be shipped. Most orders have one fulfillment order; orders split across locations have multiple.
1curl -X POST 'https://mystore.myshopify.com/admin/api/2026-04/graphql.json' \2 -H 'X-Shopify-Access-Token: shpat_your_token' \3 -H 'Content-Type: application/json' \4 -d '{5 "query": "{ order(id: \\"gid://shopify/Order/123456\\") { fulfillmentOrders(first: 5) { edges { node { id status assignedLocation { name } lineItems(first: 50) { edges { node { id sku quantity } } } } } } } }"6 }'Pro tip: Filter for `status === 'OPEN'` — fulfilled or cancelled fulfillment orders cannot be fulfilled again and will return a user error.
Expected result: Array of open fulfillment order objects with their IDs and line item details.
Create Fulfillment with Tracking Number
Why: The `fulfillmentCreate` mutation marks items as shipped and optionally sends the shipping confirmation email to the customer.
Once you have the tracking number from your carrier or 3PL, call `fulfillmentCreate` with the fulfillment order ID and tracking info. Set `notifyCustomer: true` to trigger Shopify's built-in shipping confirmation email. If you're in a dropshipping flow, call this mutation when your supplier confirms shipment.
1curl -X POST 'https://mystore.myshopify.com/admin/api/2026-04/graphql.json' \2 -H 'X-Shopify-Access-Token: shpat_your_token' \3 -H 'Content-Type: application/json' \4 -d '{5 "query": "mutation fulfillmentCreate($fulfillment: FulfillmentInput!) { fulfillmentCreate(fulfillment: $fulfillment) { fulfillment { id status } userErrors { field message } } }",6 "variables": {7 "fulfillment": {8 "lineItemsByFulfillmentOrder": [{"fulfillmentOrderId": "gid://shopify/FulfillmentOrder/12345"}],9 "trackingInfo": {"company": "USPS", "number": "9400111899223148739475"},10 "notifyCustomer": true11 }12 }13 }'Pro tip: Shopify normalizes carrier names — pass 'USPS', 'UPS', 'FedEx', 'DHL', 'Canada Post', etc. For unknown carriers, pass the name as-is and include a tracking URL for clickable tracking links in the notification email.
Expected result: Returns the new Fulfillment object with `status: 'success'` and tracking info. Customer receives shipping notification email if `notifyCustomer: true`.
Monitor Webhook Subscription Health
Why: Shopify deletes webhook subscriptions silently after 19 consecutive failures — without monitoring, your automation can stop working for days undetected.
Build a daily health check that queries all webhook subscriptions and verifies the orders/paid subscription exists. Alert immediately if it's missing. Additionally, monitor the Shopify webhook delivery log in the Partner Dashboard to catch rising failure rates before they hit the 19-failure threshold.
1curl -X POST 'https://mystore.myshopify.com/admin/api/2026-04/graphql.json' \2 -H 'X-Shopify-Access-Token: shpat_your_token' \3 -H 'Content-Type: application/json' \4 -d '{"query": "{ webhookSubscriptions(first: 20) { edges { node { id topic callbackUrl createdAt } } } }"}'Pro tip: Set up a separate alerting channel (Slack/PagerDuty) for the webhook health check — an empty response from the subscriptions query means your whole automation has been silently offline.
Expected result: Daily confirmation that webhook subscription is active, with automatic re-registration if it was deleted.
Complete working code
This script sets up the full order-to-fulfillment pipeline: it registers the webhook, handles incoming order events, fetches fulfillment orders, and creates fulfillments with tracking. In production, use a queue (Redis/SQS) between webhook receipt and fulfillment processing to stay within the 5-second response window.
1import os2import hmac3import hashlib4import base645import logging6import requests7from flask import Flask, request, abort89logging.basicConfig(level=logging.INFO)10app = Flask(__name__)1112SHOP_DOMAIN = os.environ['SHOPIFY_SHOP_DOMAIN']13ACCESS_TOKEN = os.environ['SHOPIFY_ACCESS_TOKEN']14APP_SECRET = os.environ['SHOPIFY_APP_SECRET']1516URL = f'https://{SHOP_DOMAIN}/admin/api/2026-04/graphql.json'17HEADERS = {'X-Shopify-Access-Token': ACCESS_TOKEN, 'Content-Type': 'application/json'}1819def gql(query, variables=None):20 resp = requests.post(URL, json={'query': query, 'variables': variables or {}}, headers=HEADERS)21 resp.raise_for_status()22 return resp.json()2324def get_open_fulfillment_orders(order_id):25 q = f'{{ order(id: "gid://shopify/Order/{order_id}") {{ fulfillmentOrders(first: 5) {{ edges {{ node {{ id status }} }} }} }} }}'26 data = gql(q)['data']['order']27 return [e['node']['id'] for e in data['fulfillmentOrders']['edges'] if e['node']['status'] == 'OPEN']2829def create_fulfillment(fo_id, carrier, tracking):30 mutation = 'mutation fulfillmentCreate($f: FulfillmentInput!) { fulfillmentCreate(fulfillment: $f) { fulfillment { id status } userErrors { field message } } }'31 variables = {'f': {'lineItemsByFulfillmentOrder': [{'fulfillmentOrderId': fo_id}], 'trackingInfo': {'company': carrier, 'number': tracking}, 'notifyCustomer': True}}32 result = gql(mutation, variables)33 errors = result['data']['fulfillmentCreate']['userErrors']34 if errors:35 logging.error(f'Fulfillment errors for {fo_id}: {errors}')36 return None37 return result['data']['fulfillmentCreate']['fulfillment']3839def get_tracking_from_3pl(order_id):40 # Replace with your actual 3PL API call41 return {'carrier': 'USPS', 'tracking': '9400111899223148739475'}4243@app.route('/webhooks/orders-paid', methods=['POST'])44def handle_order_paid():45 # Validate HMAC46 hmac_header = request.headers.get('X-Shopify-Hmac-Sha256', '')47 computed = base64.b64encode(hmac.new(APP_SECRET.encode(), request.get_data(), hashlib.sha256).digest()).decode()48 if not hmac.compare_digest(computed, hmac_header):49 abort(401)5051 order = request.get_json()52 order_id = order['id']53 logging.info(f'Processing order {order_id}')5455 # Respond immediately — process async in production56 fo_ids = get_open_fulfillment_orders(order_id)57 tracking_info = get_tracking_from_3pl(order_id)58 for fo_id in fo_ids:59 fulfillment = create_fulfillment(fo_id, tracking_info['carrier'], tracking_info['tracking'])60 if fulfillment:61 logging.info(f'Created fulfillment {fulfillment["id"]} for FO {fo_id}')6263 return '', 2006465if __name__ == '__main__':66 app.run(port=3000)Error handling
fulfillmentOrder with id gid://shopify/FulfillmentOrder/X is not in OPEN statusThe fulfillment order has already been fulfilled, cancelled, or is in an intermediate state. This often happens when processing duplicate webhook deliveries.
Always filter fulfillment orders by `status === 'OPEN'` before calling `fulfillmentCreate`. Implement idempotency by storing processed order IDs and skipping duplicates.
Do not retry — this is a logical state error. Log the order ID and skip.
ThrottledGraphQL point bucket exhausted. During high-traffic periods (flash sales, large order volumes), parallel webhook processing can exceed the 50 pts/sec restore rate.
Check `extensions.cost.throttleStatus.currentlyAvailable` and back off when low. Use a queue (Redis/SQS) to serialize webhook processing rather than parallel execution.
Exponential backoff starting at 1 second, doubling up to 64 seconds. The GraphQL bucket restores at 50 pts/sec on Standard plans.
Unauthorized: [API] Invalid API key or access tokenAccess token revoked, expired (expiring offline tokens), or app uninstalled from the store.
Re-generate the access token from the Dev Dashboard for custom apps. For OAuth apps, implement the expiring token refresh flow. Add monitoring for 401s to detect revocations early.
No retry until authentication is re-established.
No error message — automation silently stops processing ordersShopify deletes webhook subscriptions after 19 consecutive failed deliveries (~48 hours). This happens when your endpoint is down, returns non-2xx, or takes longer than 5 seconds.
Implement daily webhook health check. Ensure your endpoint returns 2xx within 5 seconds by offloading processing to a background queue. Monitor for failed webhook deliveries in the Shopify Partner Dashboard.
Re-register the webhook immediately when the subscription is found missing.
Variable $fulfillment of type FulfillmentInput! was provided invalid value for lineItemsByFulfillmentOrderThe fulfillmentOrderId is malformed, or partial line item fulfillment was attempted without specifying fulfillmentOrderLineItems.
Ensure the fulfillmentOrderId uses the full `gid://shopify/FulfillmentOrder/` prefix. For partial fulfillments, use `lineItemsByFulfillmentOrder[].fulfillmentOrderLineItems` to specify exactly which line items to fulfill.
Do not retry — fix the payload structure first.
Rate Limits for Shopify Fulfillment API
| Scope | Limit | Window |
|---|---|---|
| GraphQL Admin API — Standard plan | 1,000 points | Restores at 50 points/second |
| Webhook delivery timeout | 5 seconds | Per delivery; failure counts toward 19-failure deletion threshold |
| Webhook retry window | 19 attempts over ~48 hours | Exponential backoff; subscription deleted after exhaustion |
1import time23def gql_with_retry(url, headers, payload, max_retries=5):4 delay = 15 for attempt in range(max_retries):6 resp = requests.post(url, json=payload, headers=headers)7 if resp.status_code == 429:8 print(f'Rate limited (attempt {attempt+1}), waiting {delay}s')9 time.sleep(delay)10 delay = min(delay * 2, 64)11 continue12 resp.raise_for_status()13 result = resp.json()14 available = result.get('extensions', {}).get('cost', {}).get('throttleStatus', {}).get('currentlyAvailable', 1000)15 if available < 100:16 time.sleep(2)17 return result18 raise RuntimeError('Max retries exceeded')- Use a message queue (Redis, SQS, Celery) between webhook receipt and fulfillment processing — always respond to Shopify within 5 seconds.
- Implement idempotency keys using the Shopify order ID to prevent duplicate fulfillments from retry webhook deliveries.
- Monitor `extensions.cost.throttleStatus.currentlyAvailable` and slow down processing when the bucket drops below 150 points.
- Set up a daily health check to verify the webhook subscription still exists — Shopify's silent deletion is the hardest failure mode to detect.
- During high-traffic events, consider upgrading to Shopify Plus for the 10,000-point bucket and 500 pts/sec restore rate.
Security checklist
- Validate every webhook with HMAC-SHA256 using the raw request body and base64 digest — reject any request failing this check with 401.
- Return 2xx within 5 seconds on every webhook delivery — use async queues to avoid timeout failures that lead to subscription deletion.
- Store all credentials (access token, app secret) in environment variables or a secrets manager.
- Implement idempotency to prevent duplicate fulfillments from re-delivered webhook events — track processed order IDs in a database.
- Never log full order payloads in production — they contain customer PII (name, address, email).
- Monitor for 401 errors to detect token revocation — set up an alert for any 401 response from the Shopify API.
- Use HTTPS for webhook callback URLs — Shopify refuses HTTP endpoints.
- Audit webhook subscription list daily to detect silent subscription deletion before it causes customer service issues.
Automation use cases
Dropshipping Fulfillment
intermediateAuto-forward paid orders to your supplier API and create Shopify fulfillments when the supplier returns tracking numbers.
3PL Integration
advancedPush orders to a third-party logistics provider warehouse management system and create fulfillments when the 3PL confirms shipment.
Print-on-Demand Automation
intermediateSend designs and order details to a print-on-demand provider and handle fulfillment creation when items are printed and shipped.
Multi-Carrier Rate Shopping
advancedOn order receipt, query multiple carrier APIs for shipping rates, select the cheapest option, create the shipment, and fulfill in Shopify.
No-code alternatives
Don't want to write code? These platforms can automate the same workflows visually.
Zapier
Free tier available; Starter $19.99/monthZapier's Shopify + carrier integrations (ShipStation, EasyPost) can automate order-to-fulfillment flows without code.
- + Zero code required
- + Native ShipStation and EasyPost integrations
- + Easy multi-step setup
- - Limited to REST API — no GraphQL access
- - Expensive for high order volumes
- - No custom business logic
Make (formerly Integromat)
Free tier (1,000 ops/month); Core $9/monthMake supports complex fulfillment workflows with conditional logic — useful for routing orders to different 3PLs based on location or product.
- + Visual workflow builder
- + More affordable at volume
- + Supports complex conditional routing
- - No native GraphQL
- - Setup complexity for advanced flows
- - Rate limit handling is manual
n8n
Self-hosted free; Cloud Starter €20/monthn8n self-hosted lets you build a complete order-to-fulfillment pipeline with HTTP nodes calling Shopify GraphQL and carrier APIs.
- + Self-hosted (free)
- + Full GraphQL support via HTTP node
- + No per-execution pricing
- - Requires infrastructure management
- - No official Shopify GraphQL node
- - Steeper learning curve
Best practices
- Always respond to Shopify webhook deliveries within 5 seconds — use background queues (Celery, BullMQ, SQS) for the actual fulfillment processing.
- Implement idempotency: store each order ID as 'fulfilled' in a database before calling fulfillmentCreate to prevent duplicate fulfillments from re-delivered webhooks.
- Monitor webhook subscription health daily — Shopify's silent deletion after 19 failures is the #1 cause of unexplained gaps in order processing.
- Filter fulfillment orders by `status === 'OPEN'` before attempting to create fulfillments — already-fulfilled or cancelled orders return user errors.
- Use the modern `fulfillmentCreate` with `lineItemsByFulfillmentOrder` — the legacy REST `POST /fulfillments.json` is deprecated and may stop working.
- For partial fulfillments (shipping line items from the same order separately), specify `fulfillmentOrderLineItems` in each fulfillment request.
- Log all fulfillment mutations with order ID, fulfillment order ID, tracking number, and timestamp for customer service lookup.
- Set `notifyCustomer: false` if you're sending custom shipping notifications via your own ESP — otherwise customers get duplicate emails.
Ask AI to help
Copy one of these prompts to get a personalized, working implementation.
I'm building a Shopify order fulfillment automation using GraphQL API (2026-04). I'm subscribed to `orders/paid` webhooks and calling `fulfillmentCreate` to mark orders as shipped. My current problem is: [describe issue]. Here's my webhook handler and fulfillment mutation code: [paste code]. I'm getting this error: [paste error]. The response includes these userErrors: [paste]. Help me fix this and ensure idempotency so duplicate webhook deliveries don't create duplicate fulfillments.
Build a Shopify order fulfillment dashboard that shows incoming orders (status: unfulfilled), lets an operator enter tracking numbers per order, and triggers the Shopify `fulfillmentCreate` mutation via a Supabase Edge Function. The dashboard should show fulfillment status (pending, fulfilled, error) with timestamps. Use shadcn/ui and Tailwind CSS. Include a webhook health status indicator showing whether the Shopify subscription is active.
Frequently asked questions
Why did my Shopify webhook stop receiving events?
Shopify silently deletes webhook subscriptions after 19 consecutive failed deliveries, which take approximately 48 hours. A failed delivery is any response that takes longer than 5 seconds or returns a non-2xx status code. Check your subscription list with `{ webhookSubscriptions(first: 20) { edges { node { id topic } } } }`. If the subscription is missing, re-register it immediately and investigate why deliveries were failing (endpoint downtime, timeout, errors).
What is the difference between a fulfillmentOrder and a fulfillment?
A `fulfillmentOrder` is Shopify's internal representation of what needs to be shipped — it groups line items by location and tracks fulfillment status. A `fulfillment` is the actual shipment record with tracking information. To fulfill an order, you first query its `fulfillmentOrders` to get the fulfillment order IDs, then call `fulfillmentCreate` with those IDs. You cannot create a fulfillment without first obtaining fulfillment order IDs.
Can I still use the REST POST /fulfillments.json endpoint?
Yes, for custom apps it currently works. However, Shopify has deprecated REST and declared it 'legacy.' New features go to GraphQL only. All new public apps must use GraphQL since April 1, 2025. For future-proof automation, use `fulfillmentCreate` GraphQL mutation from the start — migrating later requires rewriting the integration.
How do I fulfill only part of an order (partial fulfillment)?
Use `lineItemsByFulfillmentOrder[].fulfillmentOrderLineItems` to specify exactly which line items and quantities to fulfill. Each object takes a `fulfillmentOrderLineItemId` and optional `quantity`. If some items ship from a different location, Shopify creates separate fulfillment orders per location — fulfill each separately with its own `fulfillmentCreate` call.
What happens if I call fulfillmentCreate twice for the same order?
The second call returns a userError: 'fulfillmentOrder is not in OPEN status' since the order is already fulfilled after the first call. HTTP status is still 200. This is why idempotency matters — store the Shopify order ID in a database after successful fulfillment and skip re-processing if you receive a duplicate webhook delivery.
Is the Shopify API free?
The Shopify Admin API is free for apps installed on Shopify stores. There are no API call fees. You pay for your Shopify plan (Basic, Standard, Plus) which determines your GraphQL rate limit bucket size. Third-party infrastructure for webhook endpoints and queuing is your own cost.
Can RapidDev help build a custom Shopify fulfillment integration?
Yes. RapidDev has built 600+ apps including Shopify fulfillment pipelines connecting to 3PLs, dropshipping suppliers, print-on-demand services, and custom carrier systems. We handle webhook infrastructure, queue setup, and rate limit management. Book a free consultation at rapidevelopers.com.
Need this automated?
Our team has built 600+ apps with API automations. We can build this for you.
Book a free consultation