Shopify API fulfillment - The impossible task

Ok… where the begin… Why is it IMPOSSIBLE to test fulfillment API code??

I have spent over 5 hours trying to fulfill test orders in my software using API.

I have managed to get everything to work except fulfillments.

I have:

  • Set the right API permissions to do fulfillment
  • Enabled test mode and bogus gateway to test order
  • Tried both legacy mode and new mode
  • Tried specific versions of API
    ** I have also tried many combinations outside of the above to get a result i can fulfill an order with.

I continuously get 404 not found (using newer API version) or 406 status code using the older legacy API.

This led me down the path of creating inventory and assigning products to it.

Raising several internal orders (none of which fulfill)

Changing the order creation settings to not auto fulfill so that it doesnt have a fulfillment cancelled flag.

Basically everything I possibly can to fulfill a single test order and it just keeps failing. both myself and ChatGPT are all out of ideas here.

See the FULL code here, it is in vb . NET, I have omitted personal information from any further text:

Public Async Function CreateFulfillment(orderId As String,
trackingNumber As String,
trackingCompany As String,
trackingUrl As String) As Task(Of Boolean)

Using client As New HttpClient()
client.DefaultRequestHeaders.Accept.Clear()
client.DefaultRequestHeaders.Accept.Add(New MediaTypeWithQualityHeaderValue(“application/json”))
client.DefaultRequestHeaders.Add(“X-Shopify-Access-Token”, API)
client.DefaultRequestHeaders.UserAgent.ParseAdd(“Mozilla/5.0 (WixMate)”)

Try
'---------------------------------------------------------
’ STEP 1: Try modern Fulfillment Orders API first
'---------------------------------------------------------
Dim fulfillOrderUrl As String = $“{BaseUrl}fulfillment_orders.json?order_id={orderId}”
Dim fulfillOrderResp As HttpResponseMessage = Await client.GetAsync(fulfillOrderUrl)
Dim fulfillOrderBody As String = Await fulfillOrderResp.Content.ReadAsStringAsync()

StrCatch = "Normal way" & vbCrLf & "=========" & vbCrLf & vbCrLf & "URL: " & fulfillOrderUrl & vbCrLf & vbCrLf
StrCatch &= $"fulfillorderresp: {fulfillOrderResp.ToString}" & vbCrLf & vbCrLf
StrCatch &= $"fulfillorderresp status code: {fulfillOrderResp.StatusCode}" & vbCrLf & vbCrLf
StrCatch &= $"fulfillorderBody: {fulfillOrderBody.ToString}" & vbCrLf & vbCrLf


Clipboard.SetText(StrCatch)


If fulfillOrderResp.IsSuccessStatusCode Then
 Dim fulfillOrderJson As JObject = JObject.Parse(fulfillOrderBody)
 Dim fulfillmentOrders = fulfillOrderJson("fulfillment_orders")

 If fulfillmentOrders Is Nothing OrElse Not fulfillmentOrders.Any() Then
  Debug.WriteLine("No fulfillment orders found for this order (empty array). Falling back to legacy API.")
  Return Await CreateLegacyFulfillment(client, orderId, trackingNumber, trackingCompany, trackingUrl)
 End If

 ' Use the first fulfillment order
 Dim fulfillmentOrderId As String = fulfillmentOrders(0)("id").ToString()
 Dim url As String = $"{BaseUrl}admin/api/2024-10/fulfillment_orders/{fulfillmentOrderId}/fulfillments.json"

 Dim payload As New JObject(
                New JProperty("fulfillment",
                    New JObject(
                        New JProperty("message", "Order shipped"),
                        New JProperty("notify_customer", True),
                        New JProperty("tracking_info",
                            New JObject(
                                New JProperty("number", trackingNumber),
                                New JProperty("company", trackingCompany),
                                New JProperty("url", trackingUrl)
                            )
                        )
                    )
                )
            )

 Dim content As New StringContent(payload.ToString(), Encoding.UTF8, "application/json")
 Dim response As HttpResponseMessage = Await client.PostAsync(url, content)
 Dim body As String = Await response.Content.ReadAsStringAsync()

 Debug.WriteLine($"[FulfillmentOrders POST] Status {CInt(response.StatusCode)} - {body}")

 If Not response.IsSuccessStatusCode Then
  Throw New ApplicationException($"Shopify fulfillment failed (HTTP {CInt(response.StatusCode)}): {body}")
 End If

 Return True

ElseIf fulfillOrderResp.StatusCode = HttpStatusCode.NotFound Then
 Debug.WriteLine("No fulfillment orders exist for this order (404). Falling back to legacy API.")
 Return Await CreateLegacyFulfillment(client, orderId, trackingNumber, trackingCompany, trackingUrl)

Else
 Throw New ApplicationException($"Failed to fetch fulfillment orders: {fulfillOrderResp.StatusCode} {fulfillOrderBody}")
End If

Catch ex As Exception
Throw New ApplicationException($“Error fulfilling Shopify order {orderId}:{vbCrLf}{ex.Message}”, ex)
End Try
End Using
End Function

Dim StrCatch As String = “”

Private Async Function CreateLegacyFulfillment(client As HttpClient,
orderId As String,
trackingNumber As String,
trackingCompany As String,
trackingUrl As String) As Task(Of Boolean)

Try
Dim url As String = $“{BaseUrl}orders/{orderId}/fulfillments.json”
Dim payload As New JObject(
New JProperty(“fulfillment”,
New JObject(
New JProperty(“notify_customer”, True),
New JProperty(“tracking_number”, trackingNumber),
New JProperty(“tracking_company”, trackingCompany),
New JProperty(“tracking_url”, trackingUrl)
)
)
)

Dim content As New StringContent(payload.ToString(), Encoding.UTF8, “application/json”)
Dim response As HttpResponseMessage = Await client.PostAsync(url, content)
Dim body As String = Await response.Content.ReadAsStringAsync()

StrCatch &= “Legacy way” & vbCrLf & “=========” & vbCrLf & vbCrLf & "URL: " & url & vbCrLf & vbCrLf
StrCatch &= "Status code | Response: " & response.ToString & “|” & CInt(response.StatusCode) & vbCrLf & vbCrLf
StrCatch &= "Body: " & body

Clipboard.SetText(StrCatch)

Debug.WriteLine($“[LegacyFulfillment] Status {CInt(response.StatusCode)} - {body}”)

If Not response.IsSuccessStatusCode Then
Throw New ApplicationException($“Legacy fulfillment failed (HTTP {CInt(response.StatusCode)}): {body}”)
End If

Return True

Catch ex As Exception
Throw New ApplicationException($“Error fulfilling order (legacy path): {vbCrLf}{ex.Message}”, ex)
End Try
End Function

Here are the results of trying to fulfill an order that was NOT manual but made via the website in test mode with the bogus payment provider setup. I will share below the concatenated string collecting data about the fulfillment process and how it went:

Normal way

URL: https://xxx.myshopify.com/admin/fulfillment_orders.json?order_id=5808026484771

fulfillorderresp: StatusCode: 404, ReasonPhrase: ‘Not Found’, Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
{
Transfer-Encoding: chunked
Connection: keep-alive
x-sorting-hat-podid: 34
x-sorting-hat-shopid: 19773111
shopify-web-runtime: web
cross-origin-opener-policy: noopener-allow-popups
vary: Accept-Encoding
referrer-policy: origin-when-cross-origin
x-frame-options: DENY
x-shopid: 19773111
x-shardid: 34
x-stats-apiclientid: 1900478
x-stats-apipermissionid: 10524426275
x-shopify-api-version: 2024-10
x-shopify-api-version-warning: About Shopify API versioning
x-shopify-shop-api-call-limit: 1/40
strict-transport-security: max-age=63072000; includeSubDomains; preload
x-request-id: 11d031d2-b739-4c6f-8908-72773e66221f-1761081768
server-timing: upstream_processing;dur=35, upstream_verdict_flag_enabled;dur=0.334;desc=“count”, upstream__y;desc=“73bca28b-830f-4984-8132-3a0d318c96d3”, upstream__s;desc=“4bac29d2-1e65-4263-ba42-67436a76f16a”
server-timing: socket;dur=0;desc=“Socket ready”, headers;dur=48;desc=“Headers”, reused;desc=“Socket reused”
server-timing: cfRequestDuration;dur=342.999935
content-security-policy: default-src ‘self’ data: blob: ‘unsafe-inline’ ‘unsafe-eval’ https://* shopify-pos://; block-all-mixed-content; child-src ‘self’ https:// shopify-pos://; connect-src ‘self’ wss:// https://*; frame-ancestors ‘none’; img-src ‘self’ data: blob: https:; script-src https://cdn.shopify.com https://cdn.shopifycdn.net https://checkout.pci.shopifyinc.com https://checkout.pci.shopifyinc.com/build/75a428d/card_fields.js https://api.stripe.com https://mpsnare.iesnare.com https://appcenter.intuit.com https://www.paypal.com https://js.braintreegateway.com https://c.paypal.com https://maps.googleapis.com https://www.google-analytics.com https://v.shopify.com ‘self’ ‘unsafe-inline’ ‘unsafe-eval’; upgrade-insecure-requests; report-uri /csp-report?source%5Baction%5D=error_404&source%5Bapp%5D=Shopify&source%5Bcontroller%5D=admin%2Ferrors&source%5Bsection%5D=admin_api&source%5Buuid%5D=11d031d2-b739-4c6f-8908-72773e66221f-1761081768; report-to shopify-csp
x-content-type-options: nosniff
x-download-options: noopen
x-permitted-cross-domain-policies: none
x-xss-protection: 1; mode=block
reporting-endpoints: shopify-csp=“/csp-report?source%5Baction%5D=error_404&source%5Bapp%5D=Shopify&source%5Bcontroller%5D=admin%2Ferrors&source%5Bsection%5D=admin_api&source%5Buuid%5D=11d031d2-b739-4c6f-8908-72773e66221f-1761081768”
x-dc: gcp-us-central1,gcp-us-central1,gcp-us-central1,gcp-us-central1,gcp-us-central1
Date: Tue, 21 Oct 2025 21:22:49 GMT
Set-Cookie: koa.sid=QhrSmSPLW66EatLuuRa4widmaxPk_RxU; path=/admin; expires=Wed, 22 Oct 2025 21:22:49 GMT; samesite=lax; secure; httponly
Set-Cookie: koa.sid.sig=Co1gStwez2xSwjM73zN_Nl2g04Q; path=/admin; expires=Wed, 22 Oct 2025 21:22:49 GMT; samesite=lax; secure; httponly
Alt-Svc: h3=“:443”; ma=86400
cf-cache-status: DYNAMIC
Report-To: {“endpoints”:[{“url”:“https://a.nel.cloudflare.com/report/v4?s=m0aAhwiNCc4EqWD9tgSgqe91E5B9FXxf4zm8NiOtqmVVt0ulUvzMhVq4zSxUnJJ%2FepCJi9txFZ1pn5K715Rl08AonXszod7W%2FB%2FAfZI%2BXFMFYkl%2FdeQ1bItjy%2FuArfazS0ia9UUKGQP1vFTvcns%3D”}],“group”:“cf-nel”,“max_age”:604800}
NEL: {“success_fraction”:0.01,“report_to”:“cf-nel”,“max_age”:604800}
Server: cloudflare
CF-RAY: 9923cfff2fb0d713-BNE
Content-Type: application/json; charset=utf-8
}

fulfillorderresp status code: NotFound

fulfillorderBody: {“errors”:“Not Found”}

Legacy way

URL: https://xxx.myshopify.com/admin/orders/5808026484771/fulfillments.json

Status code | Response: StatusCode: 406, ReasonPhrase: ‘Not Acceptable’, Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
{
Connection: keep-alive
x-sorting-hat-podid: 34
x-sorting-hat-shopid: 19773111
shopify-web-runtime: web
cross-origin-opener-policy: noopener-allow-popups
referrer-policy: origin-when-cross-origin
x-frame-options: DENY
x-shopid: 19773111
x-shardid: 34
x-stats-apiclientid: 1900478
x-stats-apipermissionid: 10524426275
x-shopify-api-version: 2024-10
x-shopify-api-version-warning: About Shopify API versioning
x-shopify-shop-api-call-limit: 1/40
strict-transport-security: max-age=63072000; includeSubDomains; preload
x-request-id: 1655f260-24f2-472c-b185-c9160ab75432-1761081769
server-timing: upstream_processing;dur=26, upstream_verdict_flag_enabled;dur=0.223;desc=“count”, upstream__y;desc=“482e1c9d-b204-41cb-9ba4-1f0c4afa898d”, upstream__s;desc=“cea19428-24d7-4a66-b09b-f7eb586fbc94”
server-timing: socket;dur=0;desc=“Socket ready”, headers;dur=37;desc=“Headers”, reused;desc=“Socket reused”
server-timing: cfRequestDuration;dur=328.000069
content-security-policy: default-src ‘self’ data: blob: ‘unsafe-inline’ ‘unsafe-eval’ https://* shopify-pos://; block-all-mixed-content; child-src ‘self’ https:// shopify-pos://; connect-src ‘self’ wss:// https://*; frame-ancestors ‘none’; img-src ‘self’ data: blob: https:; script-src https://cdn.shopify.com https://cdn.shopifycdn.net https://checkout.pci.shopifyinc.com https://checkout.pci.shopifyinc.com/build/75a428d/card_fields.js https://api.stripe.com https://mpsnare.iesnare.com https://appcenter.intuit.com https://www.paypal.com https://js.braintreegateway.com https://c.paypal.com https://maps.googleapis.com https://www.google-analytics.com https://v.shopify.com ‘self’ ‘unsafe-inline’ ‘unsafe-eval’; upgrade-insecure-requests; report-uri /csp-report?source%5Baction%5D=error_404&source%5Bapp%5D=Shopify&source%5Bcontroller%5D=admin%2Ferrors&source%5Bsection%5D=admin_api&source%5Buuid%5D=1655f260-24f2-472c-b185-c9160ab75432-1761081769; report-to shopify-csp
x-content-type-options: nosniff
x-download-options: noopen
x-permitted-cross-domain-policies: none
x-xss-protection: 1; mode=block
reporting-endpoints: shopify-csp=“/csp-report?source%5Baction%5D=error_404&source%5Bapp%5D=Shopify&source%5Bcontroller%5D=admin%2Ferrors&source%5Bsection%5D=admin_api&source%5Buuid%5D=1655f260-24f2-472c-b185-c9160ab75432-1761081769”
x-dc: gcp-us-central1,gcp-us-central1,gcp-us-central1,gcp-us-central1,gcp-us-central1
Alt-Svc: h3=“:443”; ma=86400
cf-cache-status: DYNAMIC
Report-To: {“endpoints”:[{“url”:“https://a.nel.cloudflare.com/report/v4?s=ZjE4QYJPhwEjzM1WFQnoYz7khwWMcfic2Olfts2ZTdU%2FR35p5Li4LVCDuMijZYHRocG8szV6OkRFoDrysGcmK5iCdCwGuj3%2FR45wuab9kB%2FGBOdfzWLUxylR9JnuUU9NIp3TFEkYkzTkyegnycM%3D”}],“group”:“cf-nel”,“max_age”:604800}
NEL: {“success_fraction”:0.01,“report_to”:“cf-nel”,“max_age”:604800}
CF-RAY: 9923d0016d5fd713-BNE
Date: Tue, 21 Oct 2025 21:22:49 GMT
Server: cloudflare
Content-Length: 0
Content-Type: application/json
}|406

Body:

Can someone please, please, please, explain how anyone, ever, in the history of Shopify has managed to fulfill a single order using API that wasn’t an actual 100% legitimate purchase made via the store. Test mode and other orders do not work.

ALSO, WARNING TO OTHERS READING THIS!!!
After enabling test mode, I am now struck dealing with support to remove test mode as it it has wiped all other payments methods somehow and now the entire store is stuck in test mode. Hopefully this rep i’m on hold to can fix it soon.

Hi @CNC3D :waving_hand: I get being in a voodoo spiral but please respect your time and others.
Use the formatting buttons for LOOOOOOOONG pieces of code; espeically for something as klunky as vb.
Take a break , make something nice then go back to review what’s being worked on.

Uh huh, could you link to the shopify docs for the exact api endpoints you think your using.

Really step back and go to first principles, try to get to the api goal with as few manual api steps as possible.
A minimal reproducible example https://stackoverflow.com/help/minimal-reproducible-example

Such as using the graphiql tool to make some mutations
GraphiQL for the Admin API
or using curl, etc before combing it into the complexity of scripting logic.

Also see the actual developer forums when this deep in the weeds: https://community.shopify.dev

I remember running into a similar issue and getting 404s for what should be valid API calls. I can’t remember exactly but I think it may have had something to do with scopes. Here are some of the scopes I use on my apps for working with fulfillments. Long shot, but maybe it will help!

SCOPES=read_assigned_fulfillment_orders,read_fulfillments,read_locations,read_merchant_managed_fulfillment_orders,read_orders,read_third_party_fulfillment_orders

And for testing orders, you’ll want to set up inventory assigned by location to each fulfillment service. And utilize the built-in Shopify routing to then assign the fulfillment orders

To successfully test fulfillments via the API you need to use the newer fulfillment orders endpoints rather than the deprecated Fulfillment API. Make sure your app has the read_assigned_fulfillment_orders and write_assigned_fulfillment_orders scopes, and that the user account you’re authenticating with has permission to fulfill and ship orders. When you create a test order, ensure it has an open fulfillment order; you can fetch it with the GET /admin/api/2023-10/orders/{order_id}/fulfillment_orders.json endpoint. Then call POST /admin/api/2023-10/fulfillments.json (or the corresponding GraphQL mutation) with the fulfillment order ID and a valid tracking number/carrier to create the fulfillment. A 404 or 406 error usually means the order doesn’t have an open fulfillment order or your app lacks the correct scopes. Using bogus gateway/test mode is fine as long as the order hasn’t been auto‑fulfilled already.

Also note that the legacy Fulfillment API is being phased out, so switching to the new fulfillment orders workflow is required for test and production stores. If you continue to see errors, double‑check that your API version matches the endpoints you’re calling and that your request body includes required fields (e.g. tracking numbers and notify_customer if needed).

Yep. I have done this as well. Didn’t help.

Yes, all of this has been done, permissions added as per original post.

Yes this is why the 2 methods in my original post.

CreateFulfillment()

If that fails (currently always) it falls back to this and tries again.

CreateLegacyFulfillment()