Update SOLVED and thank you, this helped point me in the right direction.
I managed to build this into a working QR-based local delivery confirmation app for our Shopify store.
The final setup is:
-
A Shopify custom app with Admin API access
-
A small Node / Express backend
-
Hosted externally with HTTPS
-
A QR code printed automatically on our internal order / delivery sheet
-
Driver scans the QR with a normal phone camera
-
A simple mobile page opens
-
Driver taps “Mark as delivered”
-
The backend finds the Shopify order and handles the delivery status through the Admin API
One important thing I discovered during the build was that I could not rely on having a normal Fulfillment ID available at print time.
For our local delivery orders, the order could still be unfulfilled and not have a normal Fulfillment object yet, even though Shopify had a local delivery / fulfillment order connected to it. So the QR code could not simply contain a Fulfillment ID.
The solution I ended up using was:
-
The QR code only identifies the order securely.
-
The backend looks up the Shopify order when the QR is scanned.
-
The backend checks whether a usable Fulfillment already exists.
-
If a Fulfillment exists, it uses that Fulfillment ID.
-
If no Fulfillment exists, it queries the order’s Fulfillment Orders.
-
It finds the open local Fulfillment Order.
-
It creates a Fulfillment from that Fulfillment Order.
-
Then it marks that Fulfillment as delivered using fulfillmentEventCreate with status DELIVERED.
So the working flow became:
QR scan
→ secure token identifies the order
→ backend finds the Shopify order
→ backend finds an existing fulfillment or creates one from the fulfillment order
→ backend calls fulfillmentEventCreate with DELIVERED
→ driver sees a confirmation page
The app uses these scopes:
read_orders
read_fulfillments
write_fulfillments
read_merchant_managed_fulfillment_orders
write_merchant_managed_fulfillment_orders
The final delivery mutation is essentially:
mutation MarkDelivered($fulfillmentEvent: FulfillmentEventInput!) {
fulfillmentEventCreate(fulfillmentEvent: $fulfillmentEvent) {
fulfillmentEvent {
id
status
happenedAt
}
userErrors {
field
message
}
}
}
With variables like:
{
"fulfillmentEvent": {
"fulfillmentId": "gid://shopify/Fulfillment/...",
"status": "DELIVERED",
"happenedAt": "2026-06-10T00:00:00.000Z"
}
}
I also added a safe mode setting.
In safe mode, the QR code, driver page and button all work, but the app does not actually mark the Shopify order as delivered. That made it possible to test the full QR flow without changing a real order too early.
For the printed QR code, I added a backend image endpoint that returns a PNG QR code. That QR code points to a signed secure delivery URL. I first considered storing random tokens, but switched to signed tokens so printed QR codes still work after a restart or redeploy.
So the biggest practical lesson was:
You are right that fulfillmentEventCreate is the final mutation needed to mark the delivery as delivered, but for local delivery orders I also had to handle the case where no Fulfillment exists yet. In that case, the app first creates the Fulfillment from the open local Fulfillment Order, then creates the delivered event.
The app is now working end-to-end in safe mode, including QR generation, QR printing, scanning and the driver confirmation page. The live delivery test is next, where I will enable real delivery mode and test it on an actual delivery after the driver has delivered the order.