Shopify Local Delivery Automation: Mastering 'Mark as Delivered' with the Admin API
Hey everyone! As a Shopify expert who spends a lot of time digging into the community forums, I often come across discussions that are goldmines of practical wisdom. Recently, a thread caught my eye that perfectly illustrates how store owners are pushing the boundaries of what's possible with Shopify's Admin API, especially for tricky scenarios like local delivery.
The original question came from Robert77, who was looking for a way to replicate the native "Mark as delivered" button in the Shopify Admin for local delivery orders. You know the one – it marks the order as delivered and, crucially, sends that all-important delivery confirmation email to the customer. Robert wasn't interested in carrier tracking; this was specifically about optimizing the local delivery workflow.
The Core Challenge: Automating Local Delivery Confirmation
Robert laid out his specific questions, which I think resonate with many of you:
- Is automating "Mark as delivered" possible via the Admin API?
- If so, which endpoint or GraphQL mutation should be used?
- What ID is required (Order ID, Fulfillment Order ID, Fulfillment ID, etc.)?
- What access scopes are necessary for an app to do this?
- Will the customer delivery confirmation email be sent automatically, just like with the manual button?
These are excellent questions, because getting this right means a smoother, more professional delivery experience for your customers and less manual work for your team.
The Initial Answer: Yes, It's Possible!
Thankfully, the community – specifically youssefhe5 – quickly confirmed that, yes, this is absolutely possible through the Admin API. The key, as it turns out, is the fulfillmentEventCreate GraphQL mutation. This mutation allows you to log various events for a fulfillment, including changing its status to DELIVERED.
Here's what youssefhe5 highlighted:
- The Mutation: Use the
fulfillmentEventCreateGraphQL mutation. - The Required ID: You need the Fulfillment ID (e.g.,
gid://shopify/Fulfillment/123456789). You can't just use the Order ID directly for this specific action. If you only have the Order ID, you'll need to query it first to get the associated Fulfillment ID. - Access Scopes: Your app needs the
write_fulfillmentsscope enabled. - The Email Trigger: This is the best part! When you use this mutation to set the status to
DELIVERED, Shopify's system automatically sends the exact same delivery confirmation email to the customer that the manual button triggers. No extra steps needed for email!
The basic mutation structure looks like this:
mutation CreateLocalDeliveryEvent($fulfillmentId: ID!, $fulfillmentEvent: FulfillmentEventInput!) {
fulfillmentEventCreate(fulfillmentId: $fulfillmentId, fulfillmentEvent: $fulfillmentEvent) {
fulfillmentEvent {
id
status
}
userErrors {
field
message
}
}
}
And the variables you'd pass:
{
"fulfillmentId": "gid://shopify/Fulfillment/YOUR_FULFILLMENT_ID",
"fulfillmentEvent": {
"status": "DELIVERED"
}
}
Robert's Real-World Implementation: The Crucial Local Delivery Nuance
Now, here's where Robert77's follow-up post became incredibly insightful. He didn't just stop at the basic answer; he built a full solution and shared the practical hurdles he encountered. This is where the rubber meets the road for local delivery!
Robert discovered that for local delivery orders, you often can't rely on having a normal Fulfillment ID available right when you're preparing the order (e.g., at print time for a delivery sheet). Why? Because the order might still be "unfulfilled" and not have a standard Fulfillment object yet, even if Shopify has a local delivery / fulfillment order connected to it.
This is a critical distinction for local delivery setups where you might be generating delivery manifests or QR codes before the fulfillment object itself is fully created by Shopify.
The Elegant Solution: Handling Missing Fulfillments
Robert's custom app – a Shopify custom app with a Node/Express backend – intelligently handles this. His workflow is brilliant:
- QR Code Identifies Order: Instead of embedding a
Fulfillment IDin the QR code (which might not exist yet), the QR code securely identifies the order. - Backend Lookup: When a driver scans the QR code (using a simple mobile page), the backend looks up the Shopify order.
- Check for Existing Fulfillment: The backend first checks if a usable
Fulfillmentalready exists for that order. - Use Existing Fulfillment: If one exists, great! It uses that
Fulfillment ID. - Create Fulfillment if Missing: If no
Fulfillmentexists, it queries the order'sFulfillment Orders, finds the open localFulfillment Order, and then creates aFulfillmentfrom thatFulfillment Order. This is the game-changer for local delivery. - Mark as Delivered: Then, it marks that newly created (or found)
Fulfillmentas delivered usingfulfillmentEventCreatewith the statusDELIVERED.
Here's Robert's simplified working flow:
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
Expanded API Scopes and Mutation Details
To achieve this, Robert's app uses a more comprehensive set of scopes than just write_fulfillments:
read_orders
read_fulfillments
write_fulfillments
read_merchant_managed_fulfillment_orders
write_merchant_managed_fulfillment_orders
Notice the additional read_merchant_managed_fulfillment_orders and write_merchant_managed_fulfillment_orders. These are crucial for inspecting and creating fulfillments from fulfillment orders, especially in a local delivery context where you're managing the fulfillment process directly.
The final delivery mutation he used, reflecting the full input, looks like this:
mutation MarkDelivered($fulfillmentEvent: FulfillmentEventInput!) {
fulfillmentEventCreate(fulfillmentEvent: $fulfillmentEvent) {
fulfillmentEvent {
id
status
happenedAt
}
userErrors {
field
message
}
}
}
With variables including a happenedAt timestamp:
{
"fulfillmentEvent": {
"fulfillmentId": "gid://shopify/Fulfillment/...",
"status": "DELIVERED",
"happenedAt": "2026-06-10T00:00:00.000Z"
}
}
Smart Testing and QR Code Generation
Robert also implemented a "safe mode" setting in his app, allowing him to test the entire QR flow – generation, printing, scanning, driver page – without actually marking real Shopify orders as delivered. This is a brilliant best practice for any API integration!
For the QR codes themselves, he opted for a backend image endpoint that returns a PNG QR code pointing to a signed secure delivery URL. This is a smart move; using signed tokens means your printed QR codes remain valid even after app restarts or redeployments, which can be an issue with simple random tokens.
Bringing It All Together for Your Store
The biggest takeaway from this community discussion is clear: while the fulfillmentEventCreate mutation is indeed the final piece of the puzzle to mark a local delivery as delivered and trigger that customer email, for local delivery orders, you often need to be prepared to create the Fulfillment first from an open Fulfillment Order if one doesn't already exist. This nuanced step is what makes the difference between a functional API call and a truly robust, real-world solution.
This thread really highlights the power of the Shopify Admin API, not just for basic tasks but for building sophisticated, tailored solutions that perfectly fit your store's unique operational needs. A big shout-out to Robert77 for sharing such detailed insights and to youssefhe5 for the initial guidance. It's fantastic to see how store owners are leveraging these tools to automate and enhance their customer experience!