When do the orders/create|updated webhooks include variant_id?

The orders/create and orders/updated webhooks sometimes include the variant_id in the line items, but sometimes not. I’m curious about the logic and semantics of this. What does it mean when the variant_id is not present vs present? Follow-up/suggestion: why not include it every time?

It doesn’t seem to be as simple as “default variant” vs “explicit variant(s)”. But I would think at least the variant_id should be present if a product has more than one variant? Can I count on that?

Hi @_Ryan! I ran some tests on this to figure out the exact pattern.

variant_id is present when the line item is a product variant from your catalog. It’s null when it’s a custom line item - things like gift wrapping, processing fees, tips, etc. Custom items are created with just a title and price rather than referencing a catalog variant.

Here’s what I found testing different scenarios:

Catalog product variantvariant_id present, product_id present, product_exists: true

Custom line item (created via orderCreate, orderEditAddCustomItem, or draft orders) → variant_id null, product_id null, product_exists: false

Deleted product (variant deleted after order creation) → variant_id null, product_id null, product_exists: false

The last one’s interesting - if a product is deleted after an order is placed, the variant_id changes from present to null in subsequent webhooks. So variant_id reflects the current state, not the original order state.

You can’t distinguish between a custom line item and a deleted product from the webhook payload alone - they look identical. Both have null variant_id, null product_id, and product_exists: false.

The other thing is that a single order can mix both types:

"line_items": [
  {
    "title": "T-Shirt",
    "variant_id": 46734784364693,
    "product_id": 8991142903957,
    "product_exists": true
  },
  {
    "title": "Gift Wrapping",
    "variant_id": null,
    "product_id": null,
    "product_exists": false
  }
]

So to answer your questions:

“Can I count on variant_id being present if a product has more than one variant?” - No, for two reasons: (1) orders can contain custom line items which always have null variant_id, and (2) products can be deleted after order creation, which causes variant_id to become null. The number of variants a product has is irrelevant.

“Why not include it every time?” - Custom line items don’t have a variant. They’re created with {title: "Gift Wrap", price: 5.00} - there’s no variant in the catalog to reference. And for deleted products, the variant_id becomes null to reflect that it no longer exists.

The reliable check is if both variant_id and product_id are null, it’s either a custom item or a deleted product. You can also check product_exists: false as another indicator of non-catalog items.

If you have examples that don’t match what I’ve outlined above, feel free to share the details and I’ll be happy to look into it further!

@Donal-Shopify Thanks for the quick testing! I’ve just realised that I’m not getting the variant_id field in my webhooks at all, in spite of the fact that they’re in my list of filtered fields:

So recently for me the answer to my query has been “never.” Looking in my database, I did get them previously. But at some point I stopped getting variant_id.

When I don’t filter the fields (this is all configured in the app.toml), I do get the variant_id field. But if I’m filtering, I don’t! Can you see if this issue is unique to me?

Oh, I see the issue – I’ve misspelled line_items as line_itmes. Ooops!

@Donal-Shopify The sample webhook payload in the docs has line items with a product_id but with a null variant_id:

{
  "id": 820982911946154508,
  "admin_graphql_api_id": "gid://shopify/Order/820982911946154508",
  "app_id": null,
  "browser_ip": null,
  "buyer_accepts_marketing": true,
  "cancel_reason": "customer",
  "cancelled_at": "2021-12-31T19:00:00-05:00",
  "cart_token": null,
  "checkout_id": null,
  "checkout_token": null,
  "client_details": null,
  "closed_at": null,
  "confirmation_number": null,
  "confirmed": false,
  "contact_email": "jon@example.com",
  "created_at": "2021-12-31T19:00:00-05:00",
  "currency": "USD",
  "current_shipping_price_set": {
    "shop_money": {
      "amount": "0.00",
      "currency_code": "USD"
    },
    "presentment_money": {
      "amount": "0.00",
      "currency_code": "USD"
    }
  },
  "current_subtotal_price": "369.97",
  "current_subtotal_price_set": {
    "shop_money": {
      "amount": "369.97",
      "currency_code": "USD"
    },
    "presentment_money": {
      "amount": "369.97",
      "currency_code": "USD"
    }
  },
  "current_total_additional_fees_set": null,
  "current_total_discounts": "0.00",
  "current_total_discounts_set": {
    "shop_money": {
      "amount": "0.00",
      "currency_code": "USD"
    },
    "presentment_money": {
      "amount": "0.00",
      "currency_code": "USD"
    }
  },
  "current_total_duties_set": null,
  "current_total_price": "369.97",
  "current_total_price_set": {
    "shop_money": {
      "amount": "369.97",
      "currency_code": "USD"
    },
    "presentment_money": {
      "amount": "369.97",
      "currency_code": "USD"
    }
  },
  "current_total_tax": "0.00",
  "current_total_tax_set": {
    "shop_money": {
      "amount": "0.00",
      "currency_code": "USD"
    },
    "presentment_money": {
      "amount": "0.00",
      "currency_code": "USD"
    }
  },
  "customer_locale": "en",
  "device_id": null,
  "discount_codes": [],
  "duties_included": false,
  "email": "jon@example.com",
  "estimated_taxes": false,
  "financial_status": "voided",
  "fulfillment_status": null,
  "landing_site": null,
  "landing_site_ref": null,
  "location_id": null,
  "merchant_business_entity_id": "MTU0ODM4MDAwOQ",
  "merchant_of_record_app_id": null,
  "name": "#9999",
  "note": null,
  "note_attributes": [],
  "number": 234,
  "order_number": 1234,
  "order_status_url": "https://jsmith.myshopify.com/548380009/orders/123456abcd/authenticate?key=abcdefg",
  "original_total_additional_fees_set": null,
  "original_total_duties_set": null,
  "payment_gateway_names": [
    "visa",
    "bogus"
  ],
  "phone": null,
  "po_number": null,
  "presentment_currency": "USD",
  "processed_at": "2021-12-31T19:00:00-05:00",
  "reference": null,
  "referring_site": null,
  "source_identifier": null,
  "source_name": "web",
  "source_url": null,
  "subtotal_price": "359.97",
  "subtotal_price_set": {
    "shop_money": {
      "amount": "359.97",
      "currency_code": "USD"
    },
    "presentment_money": {
      "amount": "359.97",
      "currency_code": "USD"
    }
  },
  "tags": "tag1, tag2",
  "tax_exempt": false,
  "tax_lines": [],
  "taxes_included": false,
  "test": true,
  "token": "123456abcd",
  "total_cash_rounding_payment_adjustment_set": {
    "shop_money": {
      "amount": "0.00",
      "currency_code": "USD"
    },
    "presentment_money": {
      "amount": "0.00",
      "currency_code": "USD"
    }
  },
  "total_cash_rounding_refund_adjustment_set": {
    "shop_money": {
      "amount": "0.00",
      "currency_code": "USD"
    },
    "presentment_money": {
      "amount": "0.00",
      "currency_code": "USD"
    }
  },
  "total_discounts": "20.00",
  "total_discounts_set": {
    "shop_money": {
      "amount": "20.00",
      "currency_code": "USD"
    },
    "presentment_money": {
      "amount": "20.00",
      "currency_code": "USD"
    }
  },
  "total_line_items_price": "369.97",
  "total_line_items_price_set": {
    "shop_money": {
      "amount": "369.97",
      "currency_code": "USD"
    },
    "presentment_money": {
      "amount": "369.97",
      "currency_code": "USD"
    }
  },
  "total_outstanding": "369.97",
  "total_price": "359.97",
  "total_price_set": {
    "shop_money": {
      "amount": "359.97",
      "currency_code": "USD"
    },
    "presentment_money": {
      "amount": "359.97",
      "currency_code": "USD"
    }
  },
  "total_shipping_price_set": {
    "shop_money": {
      "amount": "10.00",
      "currency_code": "USD"
    },
    "presentment_money": {
      "amount": "10.00",
      "currency_code": "USD"
    }
  },
  "total_tax": "0.00",
  "total_tax_set": {
    "shop_money": {
      "amount": "0.00",
      "currency_code": "USD"
    },
    "presentment_money": {
      "amount": "0.00",
      "currency_code": "USD"
    }
  },
  "total_tip_received": "0.00",
  "total_weight": 0,
  "updated_at": "2021-12-31T19:00:00-05:00",
  "user_id": null,
  "billing_address": {
    "first_name": "Steve",
    "address1": "123 Shipping Street",
    "phone": "555-555-SHIP",
    "city": "Shippington",
    "zip": "40003",
    "province": "Kentucky",
    "country": "United States",
    "last_name": "Shipper",
    "address2": null,
    "company": "Shipping Company",
    "latitude": null,
    "longitude": null,
    "name": "Steve Shipper",
    "country_code": "US",
    "province_code": "KY"
  },
  "customer": {
    "id": 115310627314723954,
    "created_at": null,
    "updated_at": null,
    "first_name": "John",
    "last_name": "Smith",
    "state": "disabled",
    "note": null,
    "verified_email": true,
    "multipass_identifier": null,
    "tax_exempt": false,
    "email_marketing_consent": {
      "state": "not_subscribed",
      "opt_in_level": null,
      "consent_updated_at": null
    },
    "sms_marketing_consent": null,
    "tags": "",
    "email": "john@example.com",
    "phone": null,
    "currency": "USD",
    "tax_exemptions": [],
    "admin_graphql_api_id": "gid://shopify/Customer/115310627314723954",
    "default_address": {
      "id": 715243470612851245,
      "customer_id": 115310627314723954,
      "first_name": "John",
      "last_name": "Smith",
      "company": null,
      "address1": "123 Elm St.",
      "address2": null,
      "city": "Ottawa",
      "province": "Ontario",
      "country": "Canada",
      "zip": "K2H7A8",
      "phone": "123-123-1234",
      "name": "John Smith",
      "province_code": "ON",
      "country_code": "CA",
      "country_name": "Canada",
      "default": true
    }
  },
  "discount_applications": [],
  "fulfillments": [],
  "line_items": [
    {
      "id": 487817672276298554,
      "admin_graphql_api_id": "gid://shopify/LineItem/487817672276298554",
      "attributed_staffs": [
        {
          "id": "gid://shopify/StaffMember/902541635",
          "quantity": 1
        }
      ],
      "current_quantity": 1,
      "fulfillable_quantity": 1,
      "fulfillment_service": "manual",
      "fulfillment_status": null,
      "gift_card": false,
      "grams": 100,
      "name": "Aviator sunglasses",
      "price": "89.99",
      "price_set": {
        "shop_money": {
          "amount": "89.99",
          "currency_code": "USD"
        },
        "presentment_money": {
          "amount": "89.99",
          "currency_code": "USD"
        }
      },
      "product_exists": true,
      "product_id": 788032119674292922,
      "properties": [],
      "quantity": 1,
      "requires_shipping": true,
      "sku": "SKU2006-001",
      "taxable": true,
      "title": "Aviator sunglasses",
      "total_discount": "0.00",
      "total_discount_set": {
        "shop_money": {
          "amount": "0.00",
          "currency_code": "USD"
        },
        "presentment_money": {
          "amount": "0.00",
          "currency_code": "USD"
        }
      },
      "variant_id": null,
      "variant_inventory_management": null,
      "variant_title": null,
      "vendor": null,
      "tax_lines": [],
      "duties": [],
      "discount_allocations": []
    },
    {
      "id": 976318377106520349,
      "admin_graphql_api_id": "gid://shopify/LineItem/976318377106520349",
      "attributed_staffs": [],
      "current_quantity": 1,
      "fulfillable_quantity": 1,
      "fulfillment_service": "manual",
      "fulfillment_status": null,
      "gift_card": false,
      "grams": 1000,
      "name": "Mid-century lounger",
      "price": "159.99",
      "price_set": {
        "shop_money": {
          "amount": "159.99",
          "currency_code": "USD"
        },
        "presentment_money": {
          "amount": "159.99",
          "currency_code": "USD"
        }
      },
      "product_exists": true,
      "product_id": 788032119674292922,
      "properties": [],
      "quantity": 1,
      "requires_shipping": true,
      "sku": "SKU2006-020",
      "taxable": true,
      "title": "Mid-century lounger",
      "total_discount": "0.00",
      "total_discount_set": {
        "shop_money": {
          "amount": "0.00",
          "currency_code": "USD"
        },
        "presentment_money": {
          "amount": "0.00",
          "currency_code": "USD"
        }
      },
      "variant_id": null,
      "variant_inventory_management": null,
      "variant_title": null,
      "vendor": null,
      "tax_lines": [],
      "duties": [],
      "discount_allocations": []
    },
    {
      "id": 315789986012684393,
      "admin_graphql_api_id": "gid://shopify/LineItem/315789986012684393",
      "attributed_staffs": [],
      "current_quantity": 1,
      "fulfillable_quantity": 1,
      "fulfillment_service": "manual",
      "fulfillment_status": null,
      "gift_card": false,
      "grams": 500,
      "name": "Coffee table",
      "price": "119.99",
      "price_set": {
        "shop_money": {
          "amount": "119.99",
          "currency_code": "USD"
        },
        "presentment_money": {
          "amount": "119.99",
          "currency_code": "USD"
        }
      },
      "product_exists": true,
      "product_id": 788032119674292922,
      "properties": [],
      "quantity": 1,
      "requires_shipping": true,
      "sku": "SKU2006-035",
      "taxable": true,
      "title": "Coffee table",
      "total_discount": "0.00",
      "total_discount_set": {
        "shop_money": {
          "amount": "0.00",
          "currency_code": "USD"
        },
        "presentment_money": {
          "amount": "0.00",
          "currency_code": "USD"
        }
      },
      "variant_id": null,
      "variant_inventory_management": null,
      "variant_title": null,
      "vendor": null,
      "tax_lines": [],
      "duties": [],
      "discount_allocations": []
    }
  ],
  "payment_terms": null,
  "refunds": [],
  "shipping_address": {
    "first_name": "Steve",
    "address1": "123 Shipping Street",
    "phone": "555-555-SHIP",
    "city": "Shippington",
    "zip": "40003",
    "province": "Kentucky",
    "country": "United States",
    "last_name": "Shipper",
    "address2": null,
    "company": "Shipping Company",
    "latitude": null,
    "longitude": null,
    "name": "Steve Shipper",
    "country_code": "US",
    "province_code": "KY"
  },
  "shipping_lines": [
    {
      "id": 271878346596884015,
      "carrier_identifier": null,
      "code": null,
      "current_discounted_price_set": {
        "shop_money": {
          "amount": "0.00",
          "currency_code": "USD"
        },
        "presentment_money": {
          "amount": "0.00",
          "currency_code": "USD"
        }
      },
      "discounted_price": "0.00",
      "discounted_price_set": {
        "shop_money": {
          "amount": "0.00",
          "currency_code": "USD"
        },
        "presentment_money": {
          "amount": "0.00",
          "currency_code": "USD"
        }
      },
      "is_removed": false,
      "phone": null,
      "price": "10.00",
      "price_set": {
        "shop_money": {
          "amount": "10.00",
          "currency_code": "USD"
        },
        "presentment_money": {
          "amount": "10.00",
          "currency_code": "USD"
        }
      },
      "requested_fulfillment_service_id": null,
      "source": "shopify",
      "title": "Generic Shipping",
      "tax_lines": [],
      "discount_allocations": []
    }
  ],
  "returns": []
}

Is this case possible in reality? If so, what would it mean?

More generally: is there a webhook schema that will tell me which fields are optional (may be missing) vs nullable?

Good catch on that doc example - you’re right to question it. That payload showing product_id present, product_exists: true, but variant_id: null doesn’t match what actually happens in practice.

In reality, if a line item has a product_id and product_exists: true, it will have a variant_id. Every product has at least one variant (even if it’s just the default variant), so they come as a pair. The only time you get null variant_id is when product_id is also null - meaning it’s either a custom line item or a deleted product.

That doc example looks like it might be using placeholder/mock data rather than a real webhook payload. I’d trust the behavior I outlined in my earlier response over what’s shown there. I’ve submitted feedback on this internally so will hopefully be updated shortly.

As for webhook schemas - unfortunately there’s no formal JSON schema documentation that explicitly marks fields as optional vs nullable. The REST API object documentation is your best reference, but even that doesn’t have strict nullability.

Thanks again for flagging this Ryan!

1 Like