Skip to main content
RapidDev - Software Development Agency
API AutomationsShopifyBearer Token

How to Automate Shopify Order Fulfillment using the API

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.

Need help automating? Talk to an expert
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate7 min read30-60 minutesShopifyMay 2026RapidDev Engineering Team
TL;DR

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

Auth

OAuth 2.0 / Access Token

Rate limit

1,000-point bucket, 50 pts/sec restore; 5s webhook timeout

Format

JSON

SDK

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.

Base URLhttps://{shop}.myshopify.com/admin/api/2026-04/graphql.json

Setting 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.

  1. 1Go to dev.shopify.com and log in with your Shopify Partner account.
  2. 2Click 'Create app' and select 'Custom app' for single-store automation.
  3. 3Under 'Configuration > Admin API access scopes', enable: read_orders, write_fulfillments, read_fulfillments.
  4. 4Click 'Save', then navigate to 'API credentials' and click 'Install app'.
  5. 5Copy the generated Admin API access token (starts with shpat_) — visible only once.
  6. 6Store the token in an environment variable: export SHOPIFY_ACCESS_TOKEN='shpat_...'
  7. 7Set your shop domain: export SHOPIFY_SHOP_DOMAIN='yourstore.myshopify.com'
auth.py
1import os
2import requests
3
4SHOP_DOMAIN = os.environ['SHOPIFY_SHOP_DOMAIN']
5ACCESS_TOKEN = os.environ['SHOPIFY_ACCESS_TOKEN']
6
7GRAPHQL_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}
12
13# Verify auth by querying shop name
14test = 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

POST/admin/api/2026-04/graphql.json

GraphQL mutation `fulfillmentCreate` — creates a fulfillment for one or more fulfillment order line items with optional tracking information.

ParameterTypeRequiredDescription
fulfillmentOrderIdstringrequiredGlobal ID of the fulfillment order to fulfill (gid://shopify/FulfillmentOrder/{id})
trackingInfo.companystringoptionalCarrier name (e.g., USPS, UPS, FedEx, DHL)
trackingInfo.numberstringoptionalTracking number from the carrier
notifyCustomerbooleanoptionalWhether to send the shipping confirmation email to the customer

Request

json
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

json
1{"data": {"fulfillmentCreate": {"fulfillment": {"id": "gid://shopify/Fulfillment/98765", "status": "success", "trackingInfo": [{"number": "9400111899223148739475", "url": "https://tools.usps.com/..."}]}, "userErrors": []}}}
POST/admin/api/2026-04/graphql.json

GraphQL query for `fulfillmentOrders` — fetches all fulfillment orders for a given order ID, returning the fulfillment order IDs needed for `fulfillmentCreate`.

ParameterTypeRequiredDescription
idstringrequiredOrder global ID (gid://shopify/Order/{id})

Request

json
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

json
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"}}}]}}}}
POST/admin/api/2026-04/graphql.json

GraphQL mutation `webhookSubscriptionCreate` — subscribes to `orders/paid` events to trigger the fulfillment flow.

ParameterTypeRequiredDescription
topicstringrequiredORDERS_PAID — fires when payment is captured for an order
callbackUrlstringrequiredHTTPS URL that will receive the webhook POST

Request

json
1{"query": "mutation { webhookSubscriptionCreate(topic: ORDERS_PAID, webhookSubscription: {callbackUrl: \"https://yourapp.com/webhooks/orders-paid\", format: JSON}) { userErrors { field message } webhookSubscription { id } } }"}

Response

json
1{"data": {"webhookSubscriptionCreate": {"userErrors": [], "webhookSubscription": {"id": "gid://shopify/WebhookSubscription/555"}}}}
POST/admin/api/2026-04/graphql.json

GraphQL mutation `fulfillmentOrderMove` — moves a fulfillment order to a different location, used when reassigning orders between warehouses or fulfillment providers.

ParameterTypeRequiredDescription
idstringrequiredFulfillmentOrder global ID to move
newLocationIdstringrequiredTarget location global ID

Request

json
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

json
1{"data": {"fulfillmentOrderMove": {"movedFulfillmentOrder": {"id": "gid://shopify/FulfillmentOrder/12346"}, "userErrors": []}}}

Step-by-step automation

1

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.

request.sh
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.

2

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.

request.sh
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.

3

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.

request.sh
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": true
11 }
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`.

4

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.

request.sh
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.

automate_shopify_fulfillment.py
1import os
2import hmac
3import hashlib
4import base64
5import logging
6import requests
7from flask import Flask, request, abort
8
9logging.basicConfig(level=logging.INFO)
10app = Flask(__name__)
11
12SHOP_DOMAIN = os.environ['SHOPIFY_SHOP_DOMAIN']
13ACCESS_TOKEN = os.environ['SHOPIFY_ACCESS_TOKEN']
14APP_SECRET = os.environ['SHOPIFY_APP_SECRET']
15
16URL = f'https://{SHOP_DOMAIN}/admin/api/2026-04/graphql.json'
17HEADERS = {'X-Shopify-Access-Token': ACCESS_TOKEN, 'Content-Type': 'application/json'}
18
19def 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()
23
24def 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']
28
29def 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 None
37 return result['data']['fulfillmentCreate']['fulfillment']
38
39def get_tracking_from_3pl(order_id):
40 # Replace with your actual 3PL API call
41 return {'carrier': 'USPS', 'tracking': '9400111899223148739475'}
42
43@app.route('/webhooks/orders-paid', methods=['POST'])
44def handle_order_paid():
45 # Validate HMAC
46 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)
50
51 order = request.get_json()
52 order_id = order['id']
53 logging.info(f'Processing order {order_id}')
54
55 # Respond immediately process async in production
56 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}')
62
63 return '', 200
64
65if __name__ == '__main__':
66 app.run(port=3000)

Error handling

200 with userErrorsfulfillmentOrder with id gid://shopify/FulfillmentOrder/X is not in OPEN status
Cause

The fulfillment order has already been fulfilled, cancelled, or is in an intermediate state. This often happens when processing duplicate webhook deliveries.

Fix

Always filter fulfillment orders by `status === 'OPEN'` before calling `fulfillmentCreate`. Implement idempotency by storing processed order IDs and skipping duplicates.

Retry strategy

Do not retry — this is a logical state error. Log the order ID and skip.

429Throttled
Cause

GraphQL point bucket exhausted. During high-traffic periods (flash sales, large order volumes), parallel webhook processing can exceed the 50 pts/sec restore rate.

Fix

Check `extensions.cost.throttleStatus.currentlyAvailable` and back off when low. Use a queue (Redis/SQS) to serialize webhook processing rather than parallel execution.

Retry strategy

Exponential backoff starting at 1 second, doubling up to 64 seconds. The GraphQL bucket restores at 50 pts/sec on Standard plans.

401Unauthorized: [API] Invalid API key or access token
Cause

Access token revoked, expired (expiring offline tokens), or app uninstalled from the store.

Fix

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.

Retry strategy

No retry until authentication is re-established.

Webhook silent deletionNo error message — automation silently stops processing orders
Cause

Shopify 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.

Fix

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.

Retry strategy

Re-register the webhook immediately when the subscription is found missing.

400Variable $fulfillment of type FulfillmentInput! was provided invalid value for lineItemsByFulfillmentOrder
Cause

The fulfillmentOrderId is malformed, or partial line item fulfillment was attempted without specifying fulfillmentOrderLineItems.

Fix

Ensure the fulfillmentOrderId uses the full `gid://shopify/FulfillmentOrder/` prefix. For partial fulfillments, use `lineItemsByFulfillmentOrder[].fulfillmentOrderLineItems` to specify exactly which line items to fulfill.

Retry strategy

Do not retry — fix the payload structure first.

Rate Limits for Shopify Fulfillment API

ScopeLimitWindow
GraphQL Admin API — Standard plan1,000 pointsRestores at 50 points/second
Webhook delivery timeout5 secondsPer delivery; failure counts toward 19-failure deletion threshold
Webhook retry window19 attempts over ~48 hoursExponential backoff; subscription deleted after exhaustion
retry-handler.ts
1import time
2
3def gql_with_retry(url, headers, payload, max_retries=5):
4 delay = 1
5 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 continue
12 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 result
18 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

intermediate

Auto-forward paid orders to your supplier API and create Shopify fulfillments when the supplier returns tracking numbers.

3PL Integration

advanced

Push orders to a third-party logistics provider warehouse management system and create fulfillments when the 3PL confirms shipment.

Print-on-Demand Automation

intermediate

Send designs and order details to a print-on-demand provider and handle fulfillment creation when items are printed and shipped.

Multi-Carrier Rate Shopping

advanced

On 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/month

Zapier's Shopify + carrier integrations (ShipStation, EasyPost) can automate order-to-fulfillment flows without code.

Pros
  • + Zero code required
  • + Native ShipStation and EasyPost integrations
  • + Easy multi-step setup
Cons
  • - 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/month

Make supports complex fulfillment workflows with conditional logic — useful for routing orders to different 3PLs based on location or product.

Pros
  • + Visual workflow builder
  • + More affordable at volume
  • + Supports complex conditional routing
Cons
  • - No native GraphQL
  • - Setup complexity for advanced flows
  • - Rate limit handling is manual

n8n

Self-hosted free; Cloud Starter €20/month

n8n self-hosted lets you build a complete order-to-fulfillment pipeline with HTTP nodes calling Shopify GraphQL and carrier APIs.

Pros
  • + Self-hosted (free)
  • + Full GraphQL support via HTTP node
  • + No per-execution pricing
Cons
  • - 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.

ChatGPT / Claude Prompt

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.

Lovable / V0 Prompt

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.

RapidDev

Need this automated?

Our team has built 600+ apps with API automations. We can build this for you.

Book a free consultation

Skip the coding — we'll build it for you

Our experts have built 600+ API automations. From prototype to production in days, not weeks.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.