How to mark digital product as fullfilled from app?

I am developing an app that creates and emails a custom PDF report on checkout based on the user input provided during checkout.

Since the product is digital, I haven’t enabled inventory tracking for the product. I want to mark the lineitem as fulfilled, once the PDF is generated and sent. But Shopify is not creating FulfillmentOrder and FulfillmentLineItem since the product is marked as “Not Physical”.

From what I understand, I need to associate the variant with a fulfillmentService for shopify to create the FulfillmentOrder and FulfillmentLineItem for digital products. But the latest version of Shopify GraphQL API no longer supports setting fulfillmentService to a variant.

What should I do to allow fulfilling a service from my app like the official Digital Downloads app does on checkout?

1 Like

Hi @joycebabu

Digital products on Shopify should still have fulfillment orders created, even when marked as “Not Physical” with inventory tracking disabled. I tested this in my development store and confirmed that fulfillment orders generate automatically for digital products.

If you’re not seeing fulfillment orders for your digital products, could you share a some details of your product settings and what you’re seeing in the admin or API after an order is placed? Some details around the specific API mutations you’re making can also help pinpoint what might be different in your specific setup compared to the expected behavior.

Hope that helps!

Thank you for the response.

How did you check for the fulfillment orders?

I tried creating product using the UI and via API. Then I placed an order with both products, and I can’t retrieve the fulfillment order details.

Here is my query and API response.

query GetOrder($id: ID!) {
    order(id: $id) {
        id
        name
        processedAt
        lineItems(first: 50) {
            edges {
                node {
                    id
                    title
                    variant {
                        id
                    }
                }
            }
        }
        fulfillments {
            id
            status
            trackingInfo {
                number
                url
            }
        }
        fulfillmentOrders(first: 10) {
            edges {
                node {
                    id
                    status
                    requestStatus
                    #supportedActions
                    lineItems(first: 50) {
                      edges {
                          node {
                              id
                              remainingQuantity
                              lineItem {
                                  id
                                  sku
                              }
                          }
                      }
                    }
                }
            }
        }
    }
}
{
  "data": {
    "order": {
      "id": "gid://shopify/Order/6095536423140",
      "name": "#1023",
      "processedAt": "2025-03-28T07:47:36Z",
      "lineItems": {
        "edges": [
          {
            "node": {
              "id": "gid://shopify/LineItem/14786549252324",
              "title": "Test (UI)",
              "variant": {
                "id": "gid://shopify/ProductVariant/46180134387940"
              }
            }
          },
          {
            "node": {
              "id": "gid://shopify/LineItem/14786549285092",
              "title": "Test (API)",
              "variant": {
                "id": "gid://shopify/ProductVariant/46180252713188"
              }
            }
          }
        ]
      },
      "fulfillments": [],
      "fulfillmentOrders": {
        "edges": []
      }
    }
  },
  "extensions": {
    "cost": {
      "requestedQueryCost": 89,
      "actualQueryCost": 8,
      "throttleStatus": {
        "maximumAvailable": 2000,
        "currentlyAvailable": 1993,
        "restoreRate": 100
      }
    }
  }
}

And here is the response from fulfillmentOrders query.

query GetFulfillmentOrders($orderId: ID!) {
  order(id: $orderId) {
    fulfillmentOrders(first: 10) {
      nodes {
        id
        status
        createdAt
        updatedAt
        lineItems(first: 10) {
          nodes {
            id
            totalQuantity
            remainingQuantity
          }
        }
        assignedLocation {
          location {
            id
            name
          }
        }
      }
    }
  }
}
{
  "data": {
    "order": {
      "fulfillmentOrders": {
        "nodes": []
      }
    }
  },
  "extensions": {
    "cost": {
      "requestedQueryCost": 39,
      "actualQueryCost": 3,
      "throttleStatus": {
        "maximumAvailable": 2000,
        "currentlyAvailable": 1997,
        "restoreRate": 100
      }
    }
  }
}

The easiest way to check for the presence of the fulfillment order is through the admin actually. When you’re on the order page, just append /fulfillment_orders.json to the URL and it will return the details (equivalent to a REST call).

The empty response you’re getting returned looks like it could be access scope related. Based on your use case, I would think read_merchant_managed_fulfillment_orders scope would be sufficient. If not, you can see the other scopes listed here:

Thank you. Updating the scope fixed the visibility issue.

2 Likes