Skip to content

Fulfillments

Read the shipment groups behind an order, with carrier tracking and a full status timeline

Updated View as Markdown

A fulfillment is one shipment group on an order: a single supplier shipping to a single destination. It carries the status, the carrier tracking numbers, and an append-only timeline of everything that happened while the order was being shipped.

Orders and fulfillments are deliberately not one-to-one. When an order is placed, the platform splits its items into groups and creates a fulfillment for each — one per supplier, one per destination, and always separating ammunition from firearms, since carriers will not accept them in the same box. A three-item order sourced from two distributors, with one item routed to a receiving FFL, produces three fulfillments. Read them as a many-to-one collection hanging off order_id, never as a single “shipping status” for the order.

When to use these endpoints

Poll or fetch fulfillments when you need shipping-side detail that the order endpoints do not carry: tracking numbers, carrier, per-shipment ship dates, and the timeline of distributor events. Filtering the list by order_id is the normal way to answer “where is this customer’s order right now” — you get every leg of it in one call.

If you only need to know whether an order shipped, prefer the outgoing webhooks over polling. tracking.assigned and order.delivered fire as fulfillments progress, and stores with enhanced tracking also receive shipment.in_transit, shipment.out_for_delivery and shipment.delivered. See Webhooks for the full event list and payloads.

Read-only by design

There is no write surface here. You cannot create a fulfillment, attach a tracking number, or move a status through the API. Fulfillments are created automatically when an order is paid, and they advance through the dashboard and through the distributor and carrier integrations. Both endpoints below require the fulfillments:read ability — see Authentication for how abilities are granted to a token.

Identifiers

The fulfillment_id used in the path is a UUID — fulfillments are not numbered per store the way orders are. The order_id field inside a fulfillment payload is a store-scoped order number and matches the ids returned by Orders. Do not feed one to the other. Always take fulfillment_id from a list response or a webhook rather than deriving it.

What is not in the payload

The fulfillment record knows which order items it covers and which FFL it ships to, but neither is exposed on this resource. To reconcile a shipment against line items, or to show the receiving dealer, fetch the order itself from Orders and join on order_id.

List fulfillments

GET/v1/fulfillments
Returns a paginated list of the team's fulfillments, newest first by `created_at`. Each fulfillment represents one shipment group on an order — a single supplier, a single destination — so an order with mixed suppliers or a mix of FFL and direct-ship items produces several rows here.
Required ability
fulfillments:read
Rate limit
60 requests/minute per team

Query parameters

order_idintegeroptional
Restrict to fulfillments belonging to one order, matched on the order's `order_id` (the same id `/v1/orders` returns) and scoped to your team.
Example: 1042
statusstringoptional
Exact-match filter on the fulfillment status. One of: pending, processing, shipped, in_transit, out_for_delivery, on_hold, delivered, picked_up, returned, cancelled, error.
Example: shipped
created_sincestringoptional
Return only fulfillments created at or after this timestamp. Compared against the stored UTC `created_at`; send ISO 8601.
Example: 2026-08-01T00:00:00Z
pageintegeroptional
Page number. Defaults to 1.
Example: 2
per_pageintegeroptional
Results per page. Defaults to 25, capped at 100.
Example: 50
Caveats
  • **Pagination links drop your filters.** The paginator is built without `withQueryString()`, so `links.next` and `meta.links[].url` carry only `?page=N` — following them verbatim silently widens the result set to every fulfillment. Re-send `order_id`, `status`, `created_since` and `per_page` yourself on each page.
  • `per_page` is capped at 100 but never floored or validated. It is cast with `(int)`, so `per_page=0` or a non-numeric value becomes `0`, which is falsy — the paginator then falls back to an internal default of **15**, not the documented default of 25. A negative value is worse: it survives that fallback, and `limit()` ignores values below zero, so `per_page=-1` drops the LIMIT entirely and returns every matching fulfillment in one response with `last_page: 1`. Always send a positive integer.
  • Filters are applied conditionally on truthiness. `order_id=0`, `status=` and `created_since=` are silently ignored rather than returning an empty page.
  • None of the filters are validated. An unrecognised `status` is passed straight to the query and returns an empty page instead of a 422, and an unparseable `created_since` reaches the database as a timestamp literal and produces a 500.
  • Ordering is fixed at `created_at desc`. There are no `sort_by` / `sort_direction` parameters on this endpoint, unlike `/v1/products`.
  • `fulfillment_id` is a UUID — fulfillments are not numbered per store the way orders are. The sibling `order_id` field *is* a store-scoped order number and matches the id used by `/v1/orders/{order_id}`.
  • An order almost always has more than one fulfillment. Fulfillments are split by supplier, by FFL-versus-direct destination, and by ammunition-versus-firearm (those cannot ship together), so treat `order_id` as a many-to-one key. `fulfillment_type` is the supplier slug (`rsr`, `chattanooga`, `lipseys`, `mge`, `shipstation`, `internal`, …). `mge` is always a warehouse-restock leg.
  • `shipments` is always present — both endpoints eager-load the relation — but it is an empty array until a tracking number is recorded. A single fulfillment can have several shipments when the order is split across boxes.
  • `shipments[].tracking_url` is a computed accessor, not a stored column. It is only built for carriers whose name contains `ups`, `fedex`, `usps` or `dhl`; every other carrier returns `null` even when `tracking_number` is populated. Build your own link from `carrier` + `tracking_number` if you support other carriers.
  • `service` is a legacy column. No production code path writes it — only the demo-data generator does — so expect `null` in real data. Carrier service level lives on the shipment record and is not exposed here.
  • `notes` is not merchant free-text. `Fulfillment::markAsError()` overwrites it with the failure message, so it is normally `null` and is populated when `status` is `error`.
  • `timeline` is an append-only array of `{ status, timestamp, note }` objects written by the fulfillment jobs. `timestamp` is ISO 8601; `status` is a `FulfillmentStatus` value. It defaults to `[]`, never `null`.
  • `delivery_failed` / `delivery_reason` are set only by the carrier-tracking webhook, independently of `status`. A fulfillment can be `delivered` with `delivery_failed: true` if the carrier later reported a problem.
  • The resource exposes neither the line items on the fulfillment nor the destination FFL, despite both existing on the underlying record. Fetch [`GET /v1/orders/{order_id}`](/api/resources/orders/) for items and the FFL.
  • Fulfillments are read-only over the API. There is no endpoint to create a fulfillment, push tracking, or change status — those happen in the dashboard and via distributor integrations.
  • `fulfillment_type` is the supplier that ships the fulfillment — the plugin slug (`rsr`, `chattanooga`, `lipseys`, `2aw`, `eprolo`, `printify`, `shipstation`) or `internal` for stock you ship yourself. It is not a destination flag.
Requestbash
curl -sS -G https://api.firearmcart.com/v1/fulfillments \
  -H "Authorization: Bearer $FIREARMCART_TOKEN" \
  -H "Accept: application/json" \
  --data-urlencode "order_id=1042" \
  --data-urlencode "status=shipped" \
  --data-urlencode "per_page=25"
200A page of fulfillments in the standard `data` / `links` / `meta` envelope.
{
  "data": [
    {
      "fulfillment_id": "9c2f1a44-73d1-4c8e-9d3e-2f5b8a61c7aa",
      "order_id": 1042,
      "status": "shipped",
      "fulfillment_type": "rsr",
      "service": null,
      "shipments": [
        {
          "tracking_number": "1Z999AA10123456784",
          "carrier": "UPS",
          "tracking_url": "https://www.ups.com/track?tracknum=1Z999AA10123456784",
          "shipped_at": "2026-08-04T18:22:11+00:00"
        }
      ],
      "timeline": [
        {
          "status": "processing",
          "timestamp": "2026-08-03T15:04:02+00:00",
          "note": "Order submitted to RSR successfully"
        },
        {
          "status": "shipped",
          "timestamp": "2026-08-04T18:22:11+00:00",
          "note": "Additional shipment via UPS - Tracking: 1Z999AA10123456784"
        }
      ],
      "notes": null,
      "processed_at": "2026-08-03T15:04:02+00:00",
      "shipped_at": "2026-08-04T18:22:11+00:00",
      "delivered_at": null,
      "delivery_failed": false,
      "delivery_reason": null,
      "created_at": "2026-08-03T14:58:47+00:00",
      "updated_at": "2026-08-04T18:22:11+00:00"
    },
    {
      "fulfillment_id": "b0e7d2c9-4f11-4a6b-8c02-71d94aa0e3b5",
      "order_id": 1042,
      "status": "pending",
      "fulfillment_type": "internal",
      "service": null,
      "shipments": [],
      "timeline": [],
      "notes": null,
      "processed_at": null,
      "shipped_at": null,
      "delivered_at": null,
      "delivery_failed": false,
      "delivery_reason": null,
      "created_at": "2026-08-03T14:58:47+00:00",
      "updated_at": "2026-08-03T14:58:47+00:00"
    }
  ],
  "links": {
    "first": "https://api.firearmcart.com/v1/fulfillments?page=1",
    "last": "https://api.firearmcart.com/v1/fulfillments?page=1",
    "prev": null,
    "next": null
  },
  "meta": {
    "current_page": 1,
    "from": 1,
    "last_page": 1,
    "links": [
      { "url": null, "label": "« Previous", "page": null, "active": false },
      {
        "url": "https://api.firearmcart.com/v1/fulfillments?page=1",
        "label": "1",
        "page": 1,
        "active": true
      },
      { "url": null, "label": "Next »", "page": null, "active": false }
    ],
    "path": "https://api.firearmcart.com/v1/fulfillments",
    "per_page": 25,
    "to": 2,
    "total": 2
  }
}
403The token is valid but is missing the `fulfillments:read` ability. The same status with a different body is returned when API access is disabled for the team (`{"error": "forbidden", ...}`) or the subscription has lapsed (`{"error": "subscription_expired", ...}`).
{
  "message": "Invalid ability provided."
}
429Rate limit exceeded. Stock throttle response; read `Retry-After` and `X-RateLimit-Remaining` from the headers.
{
  "message": "Too Many Attempts."
}

Retrieve a fulfillment

GET/v1/fulfillments/{fulfillment_id}
Returns a single fulfillment with its shipments and full status timeline. The lookup is scoped to the authenticated team, so another team's id is indistinguishable from one that does not exist.
Required ability
fulfillments:read
Rate limit
60 requests/minute per team

Path parameters

fulfillment_idstring (uuid)required
The fulfillment's UUID — the `fulfillment_id` value returned by `GET /v1/fulfillments`. A value that is not a well-formed UUID is a 404.
Example: 9c2f1a44-73d1-4c8e-9d3e-2f5b8a61c7aa
Caveats
  • A path segment that is not a well-formed UUID — `/v1/fulfillments/abc`, or a numeric id from before the UUID migration — returns the same 404 body as a missing fulfillment. The format is checked before the query runs.
  • A fulfillment belonging to another team returns 404, not 403 — the team scope is part of the `where` clause, so existence is never leaked.
  • `fulfillment_id` is a UUID — fulfillments are not numbered per store the way orders are. The sibling `order_id` field *is* a store-scoped order number and matches the id used by `/v1/orders/{order_id}`.
  • An order almost always has more than one fulfillment. Fulfillments are split by supplier, by FFL-versus-direct destination, and by ammunition-versus-firearm (those cannot ship together), so treat `order_id` as a many-to-one key. `fulfillment_type` is the supplier slug (`rsr`, `chattanooga`, `lipseys`, `mge`, `shipstation`, `internal`, …). `mge` is always a warehouse-restock leg.
  • `shipments` is always present — both endpoints eager-load the relation — but it is an empty array until a tracking number is recorded. A single fulfillment can have several shipments when the order is split across boxes.
  • `shipments[].tracking_url` is a computed accessor, not a stored column. It is only built for carriers whose name contains `ups`, `fedex`, `usps` or `dhl`; every other carrier returns `null` even when `tracking_number` is populated. Build your own link from `carrier` + `tracking_number` if you support other carriers.
  • `service` is a legacy column. No production code path writes it — only the demo-data generator does — so expect `null` in real data. Carrier service level lives on the shipment record and is not exposed here.
  • `notes` is not merchant free-text. `Fulfillment::markAsError()` overwrites it with the failure message, so it is normally `null` and is populated when `status` is `error`.
  • `timeline` is an append-only array of `{ status, timestamp, note }` objects written by the fulfillment jobs. `timestamp` is ISO 8601; `status` is a `FulfillmentStatus` value. It defaults to `[]`, never `null`.
  • `delivery_failed` / `delivery_reason` are set only by the carrier-tracking webhook, independently of `status`. A fulfillment can be `delivered` with `delivery_failed: true` if the carrier later reported a problem.
  • The resource exposes neither the line items on the fulfillment nor the destination FFL, despite both existing on the underlying record. Fetch [`GET /v1/orders/{order_id}`](/api/resources/orders/) for items and the FFL.
  • Fulfillments are read-only over the API. There is no endpoint to create a fulfillment, push tracking, or change status — those happen in the dashboard and via distributor integrations.
  • `fulfillment_type` is the supplier that ships the fulfillment — the plugin slug (`rsr`, `chattanooga`, `lipseys`, `2aw`, `eprolo`, `printify`, `shipstation`) or `internal` for stock you ship yourself. It is not a destination flag.
Requestbash
curl -sS https://api.firearmcart.com/v1/fulfillments/9c2f1a44-73d1-4c8e-9d3e-2f5b8a61c7aa \
  -H "Authorization: Bearer $FIREARMCART_TOKEN" \
  -H "Accept: application/json"
200The fulfillment, wrapped in `data`. The example shows an errored distributor push, where `notes` carries the failure message.
{
  "data": {
    "fulfillment_id": "9c2f1a44-73d1-4c8e-9d3e-2f5b8a61c7aa",
    "order_id": 1042,
    "status": "error",
    "fulfillment_type": "rsr",
    "service": null,
    "shipments": [],
    "timeline": [
      {
        "status": "error",
        "timestamp": "2026-08-03T15:04:02+00:00",
        "note": "Error: Dealer is not on file with RSR."
      }
    ],
    "notes": "Dealer is not on file with RSR.",
    "processed_at": null,
    "shipped_at": null,
    "delivered_at": null,
    "delivery_failed": false,
    "delivery_reason": null,
    "created_at": "2026-08-03T14:58:47+00:00",
    "updated_at": "2026-08-03T15:04:02+00:00"
  }
}
404No fulfillment with that id belongs to your team. This endpoint returns a custom error envelope rather than the framework's bare `{"message": "..."}`.
{
  "error": "not_found",
  "message": "Fulfillment not found."
}
401Missing, revoked or malformed bearer token. A token owned by a user rather than a team instead returns `{"error": "unauthorized", "message": "Invalid API token."}`.
{
  "message": "Unauthenticated."
}
Navigation

Type to search…

↑↓ navigate↵ selectEsc close