Mastering Local Delivery: Automating 'Mark as Delivered' and Customer Emails with Shopify's Admin API
Hey there, fellow store owners and tech-savvy entrepreneurs! Let's chat about something that often comes up in our community discussions: how to make those crucial local delivery processes smoother, especially when it comes to marking orders as delivered and making sure customers get their confirmation emails automatically. It's a common pain point, and I recently stumbled upon a fantastic thread that really dug deep into a robust solution.
The original question came from Robert77, who was looking for a way to replicate the native “Mark as delivered” button in Shopify Admin via the API. You know the one – it not only updates the order status but also triggers that all-important customer delivery confirmation email. Robert77 wasn't asking about carrier tracking; this was specifically for Shopify's Local Delivery orders, which often have their own unique quirks.
The Core Challenge: Mimicking Shopify's Native "Mark as Delivered"
Robert77 laid out his specific needs:
- Is API automation possible?
- 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?
- Will the customer delivery confirmation email be sent automatically?
These are excellent questions that get right to the heart of automating a critical customer touchpoint. Luckily, the community had some insights!
Initial Guidance: The fulfillmentEventCreate Mutation
Youssefhe5 jumped in with some solid initial advice, confirming that yes, this is absolutely possible through the Admin API. The key, he pointed out, is the fulfillmentEventCreate GraphQL mutation. Here’s the rundown he provided:
- 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. 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: Great news here! When you use this mutation to change the status to
DELIVERED, Shopify’s system automatically sends the exact same delivery confirmation email to the customer that the manual button triggers.
Youssefhe5 even shared the basic mutation structure:
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"
}
}
Robert77's Breakthrough: Handling Local Delivery Nuances
This initial guidance was super helpful, but Robert77’s follow-up post revealed an important “gotcha” specific to local delivery orders. He discovered that for these orders, a normal Fulfillment ID might not actually exist at the time you need it – for example, when printing a delivery sheet. The order might still be “unfulfilled” in the traditional sense, even though Shopify had a local delivery / fulfillment order connected to it.
This is a crucial insight! It means your custom app can’t always assume a Fulfillment object is ready and waiting.
Robert77’s Custom App Solution
To overcome this, Robert77 built a clever QR-based local delivery confirmation app. Here’s how his final setup works:
- A Shopify custom app with Admin API access.
- A small Node / Express backend, hosted externally with HTTPS.
- A QR code printed automatically on their internal order / delivery sheet.
- The driver scans the QR with a normal phone camera.
- A simple mobile page opens for the driver.
- Driver taps “Mark as delivered”.
- The backend finds the Shopify order and handles the delivery status through the Admin API.
The Refined Workflow: Conditionally Creating Fulfillments
The biggest practical lesson, as Robert77 put it, was that while fulfillmentEventCreate is indeed the final mutation, for local delivery orders, you often have to handle the case where no Fulfillment exists yet. In that scenario, the app first creates the Fulfillment from the open local Fulfillment Order, and then creates the delivered event.
Here’s his detailed solution flow:
- The QR code only identifies the order securely (not a Fulfillment ID).
- 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
fulfillmentEventCreatewith statusDELIVERED.
This refined flow looks like this:
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
Required Scopes and Final Mutation
To make this all work, Robert77's custom app needed these scopes:
read_orders
read_fulfillments
write_fulfillments
read_merchant_managed_fulfillment_orders
write_merchant_managed_fulfillment_orders
And the final delivery mutation he used, including the happenedAt field for precise timing:
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"
}
}
Smart Additions for Testing and Security
Robert77 also built in a “safe mode” setting, which is brilliant for testing. In safe mode, the entire QR flow – generation, printing, scanning, and driver confirmation – works, but the app doesn’t actually mark the Shopify order as delivered. This allows for thorough testing without messing with live orders.
For the QR codes themselves, he opted for signed secure delivery URLs. This is a smart security move, ensuring that printed QR codes remain valid even after a backend restart or redeploy.
Key Takeaways for Your Store
What can we learn from Robert77’s detailed solution? If you’re looking to automate your local delivery “Mark as delivered” process and trigger those customer emails via the Shopify Admin API, here are the core lessons:
- It’s Possible! Yes, you can absolutely automate this process.
fulfillmentEventCreateis Your Friend: This GraphQL mutation is the key to updating the status and triggering the email.- Local Delivery Needs Special Care: Don’t assume a Fulfillment ID will always exist for local delivery orders. Be prepared to query for Fulfillment Orders and potentially create a Fulfillment dynamically before marking it delivered.
- Plan Your Workflow: Think through the entire journey, from how your drivers interact with the system (like QR codes) to how your backend handles the API calls.
- Test, Test, Test: A “safe mode” or staging environment is invaluable for custom app development, especially when dealing with live order data.
This community discussion really highlights the power of the Shopify Admin API and the ingenuity of store owners like Robert77. By understanding the nuances of how Shopify handles local deliveries, you can build custom solutions that streamline your operations and enhance your customer experience. It’s a fantastic example of how a little bit of custom development can go a long way in tailoring Shopify to your exact business needs!