1. Fulfillments
  2. Fulfillments

Fulfillments

Fulfillments

Fulfillments are a multi-step process and this guide will go through all steps in detail. As a quick overview, the following are the steps required:

Statuses & Events

Statuses

Status Description
created When a new fulfillment is created.
processing When a fulfillment is being worked on.
picked The items were successfully picked from inventory. (This step is optional)
packed The fulfillment has had all of the required inventory packed into parcels.
shipping A shipping label was purchased. Set automatically when a label is purchased via the shipments API.
completed The fulfillment is fully done. Set manually after the shipment has been handed off to the carrier.
canceled A created fulfillment will no longer be processed.

Events

There is no event for deleting fulfillments since they instead get canceled, but their record will still remain.

Event Description
fulfillment.created When a new fulfillment is created.
fulfillment.updated When a fulfillment is updated in some way.

Overview

The following is a typical lifecycle for a fulfillment.

  1. Create fulfillment

    Add the required inventory that needs to be shipped to the desired recipient from your specified location.

  2. Pick Items (optional)

    If your workflow does a pick and pack operation, you'll increment the `manifested_qty` field for each item.

  3. Create parcel

    Add the box or boxes into which you're going to pack the inventory items.

  4. Pack parcel

    Pack the items into the parcel(s) that you have added.

  5. Generate shipping rates

    Pass the fulfillment ID into the shipments API to get shipping rates for the parcel(s) you are shipping.

  6. Get shipping label

    Purchase a shipping label to complete the fulfillment. You can also set up private rates which do not require you to purchase a label if you are handling the shipping by yourself or sourcing a label off of the platform.


  7. 1. Create fulfillment


    POST
    `/v1/fulfillments`

    To create a fulfillment, you'll need to include the recipient, your location_id from where the fulfillment will be taking place, and the inventory or articles that you want to include. Some optional extra params are in the code example below as well.

    There are two ways to go about working with inventory: either by using the inventory that is stored on Dispatch, or by bypassing it entirely if you are using a third party inventory solution. Both options are documented below.


    Example

    js
            const data = {
      location_id: "loc_afnHMjVUn3gnrvxU5zMvkX",
      type: "domestic", // or "international"
      external_id: "EXT-12345", // Optional
      alternate_external_id: "ALT-67890", // Optional
      recipient: {
        name: "Jamie Jones",
        email: "jamie@packagex.xyz",
        phone: "4844836699",
        address: "500 7th Ave, New York, NY 10018",
      },
      articles: [
        {
          item_id: "item_5og9NZ92SYzHKFTPYSEnVK",
          verified_qty: 25,
        },
        // You can also use item sku, gtin or upc
        {
          sku: "A1231"
          verified_qty: 50,
        },
      ],
      // optional
      sales_order_number: "SO-12345",
      partial_fulfillment: false,
      additional_notes: "Handle with care",
      order_number: "ORD-12345",
      user_id: "user_abc123",
      options: {
        check_inventory: true,
        require_pick_step: false,
        check_parcel_weight: true,
        packing_slip_message: "Thank you for your order!",
        assign_asset: false,
      },
      active_steps: "pick_pack_ship", // or pack_ship
    };
    
    const response = await fetch("https://api.packagex.io/v1/fulfillments", {
      method: "POST",
      headers: {
        "PX-API-KEY": process.env.PX_API_KEY,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(data),
    }).then((res) => res.json());
    
    const fulfillment = response.data;
    
          

    Create Request Body

    Parameter Type Description
    location_id string Required. The ID of the location from which this fulfillment is being shipped
    recipient object The recipient contact details (name, email, phone, address)
    sender object The sender contact details (name, email, phone, address)
    articles array The articles to include in the fulfillment
    type string Fulfillment type. Can be domestic or international
    active_steps string Active steps for the fulfillment. Can be pick, pack, pick_ship, pack_ship, or pick_pack_ship
    external_id string An external identifier for this fulfillment
    alternate_external_id string An alternate external identifier for this fulfillment
    metadata object Key-value pairs of metadata
    complete_at integer Epoch timestamp by which this fulfillment should be completed
    sales_order_number string The sales order number associated with this fulfillment
    partial_fulfillment boolean Whether this is a partial fulfillment. Defaults to false
    additional_notes string Additional notes or comments about this fulfillment (max 512 characters)
    order_number string The order number for this fulfillment
    user_id string The user ID to associate with this fulfillment
    parcel_ids array Array of parcel IDs to include (for internal transfers with previously received parcels)
    options object Fulfillment-specific options (see below)
    shipment_options object Shipment options for the fulfillment (see International Fulfillments)

    Options Object

    Parameter Type Description
    check_inventory boolean Whether to check inventory availability when processing the fulfillment
    require_pick_step boolean Whether the fulfillment requires a pick step before packing
    check_parcel_weight boolean Whether to enforce a maximum parcel weight (50 lbs)
    packing_slip_message string A custom message to include on the packing slip (max 128 characters)
    assign_asset boolean Whether to assign assets to articles during fulfillment

    Article Parameters

    Parameter Type Description
    item_id string The ID of the inventory item
    sku string The SKU of the inventory item
    gtin string The GTIN of the inventory item
    upc string The UPC of the inventory item
    asset_id string The ID of an asset to reference directly
    requested_qty integer The requested quantity
    requested_defective_qty integer The requested defective quantity
    metadata object Per-article metadata key-value pairs
    comment string Per-article comment (max 128 characters)
    value_added_services array Per-article value added services
    reason_code_id string Reason code ID for this article
    order_line_number integer The order line number for this article
    value number Per-article value override

    Internal Fulfillments

    You may want to fulfill an order from one location within your org to another. In this case, the recipient location id must be defined.

    js
            const data = {
      location_id: "loc_afnHMjVUn3gnrvxU5zMvkX",
      recipient: {
        name: "Jamie Jones",
        email: "jamie@packagex.xyz",
        phone: "4844836699",
        address: "500 7th Ave, New York, NY 10018",
        location_id: "loc_hj7gjrk5JaVvyATPDbyURp",
      },
      articles: [
        {
          item_id: "item_5og9NZ92SYzHKFTPYSEnVK",
          verified_qty: 25,
        },
        // You can also use item sku, gtin or upc
        {
          sku: "A1231"
          verified_qty: 50,
        },
      ],
    };
    
    const response = await fetch("https://api.packagex.io/v1/fulfillments", {
      method: "POST",
      headers: {
        "PX-API-KEY": process.env.PX_API_KEY,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(data),
    }).then((res) => res.json());
    
    const fulfillment = response.data;
    
          

    This creates a fulfillment at location loc_afnHMjVUn3gnrvxU5zMvkX and a corresponding manifest at location loc_hj7gjrk5JaVvyATPDbyURp. The resulting manifest will have its linked_fulfillment.id set to this fulfillment's ID, so you can find the destination manifest using the manifest list linked_fulfillment_id filter.

    Using previously received parcels

    If you have previously received parcels in the system that you want to send to a different location without unpacking them, you can specify them within the internal fulfillment

    js
            const data = {
      location_id: "loc_afnHMjVUn3gnrvxU5zMvkX",
      recipient: {
        name: "Jamie Jones",
        email: "jamie@packagex.xyz",
        phone: "4844836699",
        address: "500 7th Ave, New York, NY 10018",
        location_id: "loc_hj7gjrk5JaVvyATPDbyURp",
      },
      parcel_ids: ["prcl_2gqXquSFnCRkPPMPk27ZZZ"],
    };
    
    const response = await fetch("https://api.packagex.io/v1/fulfillments", {
      method: "POST",
      headers: {
        "PX-API-KEY": process.env.PX_API_KEY,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(data),
    }).then((res) => res.json());
    
    const fulfillment = response.data;
    
          

    This will create a corresponding manifest at location loc_hj7gjrk5JaVvyATPDbyURp with linked_fulfillment.id set to this fulfillment's ID. Note that the corresponding manifest cannot be marked as completed until the fulfillment itself has been completed.


    International Fulfillments

    For international fulfillments, you need to set the type to international and provide tax identifiers and customs-related shipment options. The fulfillment type is also auto-determined by comparing sender and recipient address country codes.

    INFO

    Tax ID values (federal_tax_id, state_tax_id, and tax_identifiers[].tax_id) are encrypted at rest and masked in API responses for security. Only the last 4 characters are visible (e.g., "12-3456789" is returned as "******6789"). The full unmasked values are sent to shipping carriers for customs processing.

js
        const data = {
  location_id: "loc_afnHMjVUn3gnrvxU5zMvkX",
  type: "international",
  recipient: {
    name: "Jamie Jones",
    email: "jamie@packagex.xyz",
    phone: "+442012345678",
    address: {
      line1: "221B Baker Street",
      city: "London",
      postal_code: "NW1 6XE",
      country_code: "GB",
    },
    federal_tax_id: "GB123456789",
    state_tax_id: null,
    tax_identifiers: [
      {
        tax_id: "GB123456789",
        tax_id_type: "VAT",
        issuing_country: "GB",
      },
    ],
  },
  sender: {
    name: "John Smith",
    email: "john@example.com",
    phone: "+12125551234",
    address: "500 7th Ave, New York, NY 10018",
    federal_tax_id: "12-3456789",
    tax_identifiers: [
      {
        tax_id: "12-3456789",
        tax_id_type: "EIN",
        issuing_country: "US",
      },
    ],
  },
  shipment_options: {
    incoterm: "DAP",
    content_type: "merchandise",
    non_delivery_option: "return",
    restriction_type: "none",
    customs_signer: "John Smith",
    itn_number: "NOEEI 30.37(a)",
    customs_certify: true,
  },
  articles: [
    {
      item_id: "item_5og9NZ92SYzHKFTPYSEnVK",
      verified_qty: 25,
    },
  ],
  active_steps: "pick_pack_ship",
};

const response = await fetch("https://api.packagex.io/v1/fulfillments", {
  method: "POST",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(data),
}).then((res) => res.json());

const fulfillment = response.data;

      

The shipment_options object for international fulfillments supports the following customs-related fields:

Option Type Description
incoterm string International commercial term. Can be DDP or DAP
content_type string Type of contents for customs. Options: documents, gift, merchandise, returned_goods, sample, dangerous_goods, humanitarian_donation, other
non_delivery_option string What to do if undeliverable. Can be return or abandon
restriction_type string Restrictions on the shipment. Can be none, other, quarantine, or sanitary_phytosanitary_inspection
customs_signer string Name of the customs declaration signer
itn_number string Internal Transaction Number (for U.S. exports)
restriction_comments string Required if restriction_type is not none
contents_explanation string Required if content_type is other
customs_certify boolean Whether the customs declaration has been certified

2. Pick Operation (optional)


POST
`/v1/fulfillments/:fulfillment`

If your workflow is a typical "pick and pack" operation, you may want to first pick all of your items to ensure that you have them in inventory and them pack them into one or more parcels. Pick operation is only enabled if either the active steps in your org settings for fulfillments has the pick step enabled or if you pass the active steps in fulfillment creation.

To do the pick operation, you can increment the verified_qty or defective_qty field. PackageX will automatically make sure that you do not go over the amount that was requested in the fulfillment.

The status of the fulfillment will automatically get updated to picked when all of the items have been accounted for.

You have to specify the layout to pick the quantity from


Example

js
        const data = {
  articles: [
    {
      id: "art_5og9NZ92SYzHKFTPYSEnVK",
      layouts: [
        {
          picked_qty: 2, // increment by 2
          id: "lay_7gQXiuSFnCRkfPMPk27ZZZ",
        },
      ],
    },
    {
      id: "art_9aHfJcifTHoWvNLigrK9EJ",
      layouts: [
        {
          picked_qty: -2, // decrement by 2
          id: "lay_7gQXiuSFnCRkfPMPk27ZZZ",
        },
      ],
    },
    {
      id: "art_1nCNpqtyepe48BMNhzmW54",
      layouts: [
        {
          picked_qty: [2], // reset to 2
          id: "lay_7gQXiuSFnCRkfPMPk27ZZZ",
        },
      ],
    },
  ],
};

const response = await fetch(`https://api.packagex.io/v1/fulfillments/${fulfillment.id}`, {
  method: "POST",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(data),
}).then((res) => res.json());

const fulfillment = response.data;

      


3. Create parcel


POST
`/v1/fulfillments/:fulfillment/parcels`

Once the fulfillment has been created, it needs to be packed. But before that, one or more parcels need to be created. You can create a custom parcel by adding dimensions or by using a predefined package that you set up on the dashboard.

While predefined packages have a maximum weight which is checked by the API, manually created packages do not have a weight check.

Create custom parcel

js
        //All dimensions are in inches. This will create a 10x10x10 box
const data = {
  length: 10,
  width: 10,
  height: 10,
};

const response = await fetch(`https://api.packagex.io/v1/fulfillments/${fulfillment.id}/parcels`, {
  method: "POST",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(data),
}).then((res) => res.json());

//We'll still respond with the fulfillment object here with your new parcel nested inside of it
const fulfillment = response.data;

      

Use predefined parcel

js
        // fetch your org profile
// the `/org` is an alias for `/organization/:org`
// you have most likely already fetched your org profile at some point so you might already have it handy
const response = await fetch(`https://api.packagex.io/v1/org`, {
  method: "GET",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(data),
}).then(res.json());

// breaking down things just to show details
// all app settings are found on the org profile, here we find the fulfillment settings
const fulfillment_settings = response.data.settings.fulfillments;

// get the predefined packages
const predefined_packages = fulfillment_settings.predefined_packages;

// select the predefined package that you want according to your logic.
const selected_package = predefined_packages[0];

// pass the predefined package type as the type field. If this package is custom to you, it will status with pdp_
// if you have used a carrier specific predefined package, this value will be that of the carrier's predefined package.
const data = {
  type: selected_package.type, //pdp_qbpXmaGUYroknGjMaHsFcJ or usps_flat_fate_envelope
};

await fetch(`https://api.packagex.io/v1/fulfillments/${fulfillment.id}/parcels`, {
  method: "POST",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(data),
});

      

4. Pack parcel


POST
`/v1/fulfillments/:fulfillment/parcels/:parcel`

Once the parcel for the fulfillment has been created, it is time to add items into it. You can specify adding items into the parcel the same way as when creating fulfillments or verifying manifests.

Example

js
        const data = {
  articles: [
    {
      id: "item_1nCNpqtyepe48BMNhzmW54",
      verified_qty: 2,
    },
    {
      id: "temp_5og9NZ92SYzHKFTPYSEnVK", //If inventory is externally created
      verified_qty: 2,
      damaged_qty: 5,
    },
  ],
};

const response = await fetch(`https://api.packagex.io/v1/fulfillments/${fulfillment.id}/parcels/${parcel.id}`, {
  method: "POST",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(data),
});

const fulfillment = response.data;

      

Delete parcel


DELETE
`/v1/fulfillments/:fulfillment/parcels/:parcel`

If you decide to delete the parcel from the fulfillment, any inventory attributed to that parcel will be removed and reset.

Example

js
        const response = await fetch(`https://api.packagex.io/v1/fulfillments/${fulfillment.id}/parcels/${parcel.id}`, {
  method: "DELETE",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
});

const fulfillment = response.data;

      

5. Generate shipping rates


POST
`/v1/shipments`

Since we've collected all of the shipping information throughout the process of creating and updating the fulfillment and the parcels within, we are able to generate shipping rates by just passing the fulfillment ID to the shipping API.

This will create a shipment ID on the fulfillment property, which you can use to retrieve the shipping rates and refresh the rates if the rates_generated_at timestamp is more than 15 minutes old.


Get rates first time

js
        const data = {
  fulfillment_id: "ful_hsvAifCQy8F8SJ6jSxxjsv",
};

const response = await fetch(`https://api.packagex.io/v1/shipments`, {
  method: "POST",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(data),
}).then((res) => res.json());

const shipment = response.data;

      

Refresh rates

If you need to refresh the shipping rates, make a call to the refresh endpoint with the shipment ID property on your fulfillment, no body needed.

js
        const response = await fetch(`https://api.packagex.io/v1/shipments/${fulfillment.shipment_id}/refresh`, {
  method: "POST",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
});

const shipment = response.data;

      

6. Generate shipping label


POST
`/v1/shipments/:shipment`

Once you have found the shipping label that suits your needs, you can purchase it by passing the rate ID into the request body. Purchasing a label automatically advances the fulfillment status from packed to shipping. There are additional options availible here, please see the shipments section.

In the response, you'll have the label_url with the PDF of your shipping label(s). If there are multiple shipping labels, the label_url property will combine all shipping labels into one PDF document for easy printing. Each parcel in the shipment will also have its own PDF document attached to it.

Example

js
        const data = {
  rate_id: "rate_rD1hVDRM4qLTezMmq98k1b",
};

const response = await fetch(`https://api.packagex.io/v1/shipments/${fulfillment.shipment_id}`, {
  method: "POST",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(data),
}).then((res) => res.json());

const shipment = response.data;

      


Cancel Fulfillment / Update Status


POST
`/v1/fulfillments/:fulfillment`

To cancel a fulfillment, update its status to "canceled". If you change your mind, reset its status to "processing".

The same options are available if you want to force the system to do a partial fulfillment. You can manually update the status of the fulfillment to packed, then generate a shipping label.


Example

js
        const data = {
  status: "canceled",
};

const response = await fetch(`https://api.packagex.io/v1/fulfillments/${fulfillment.id}`, {
  method: "POST",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(data),
});

const fulfillment = response.data;

      

Update Request Body

Parameter Type Description
status string The fulfillment status to set (e.g., canceled, processing, packed)
external_id string An external identifier for this fulfillment (max 127 characters)
alternate_external_id string An alternate external identifier for this fulfillment
metadata object Key-value pairs of metadata
sender object Updated sender contact details
recipient object Updated recipient contact details
complete_at integer Epoch timestamp by which this fulfillment should be completed
sales_order_number string The sales order number associated with this fulfillment
partial_fulfillment boolean Whether this is a partial fulfillment
additional_notes string Additional notes or comments (max 512 characters)
user_id string The user ID to associate with this fulfillment
reason_code_id string Fulfillment-level reason code ID
shipment_options object Shipment options for the fulfillment
options object Fulfillment-specific options (same as create)
articles array Updated articles (see below)
parcels array Assign layout bins to existing parcels. Each entry requires id (parcel ID) and layout_id. An optional reason_code_id can also be provided.

Article Parameters (Update)

When updating articles, you can include all the same fields as when creating, plus the following:

Parameter Type Description
id string The article ID to update
is_deleted boolean Set to true to soft-delete this article from the fulfillment
picked_qty integer or array The picked quantity. Pass as an integer to increment/decrement, or as a single-element array (e.g., [2]) to reset to that value
layouts array Layouts from which items are picked, each with id and picked_qty

Update an Internal Transfer Fulfillment

An internal transfer fulfillment is updated in the same way as a regular fulfillment. The only difference is, as the articles are packed into the parcels, the articles will be removed from the corresponding manifest and the parcels will be linked to the manifest as well.

Retrieve Fulfillment


GET
`/v1/fulfillments/:fulfillment`

Get a single fulfillment using its id.

js
        const response = await fetch("https://api.packagex.io/v1/fulfillments/ful_czhgjrk5JaVvyATPDbyURp", {
  method: "GET",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
}).then((res) => res.json());

const fulfillment = response.data;

      

Fulfillment Parcels

The following endpoints allow you to list, retrieve, and pick parcels associated with a specific fulfillment.


List Parcels

GET
`/v1/fulfillments/:fulfillment/parcels`

Retrieve all parcels associated with a fulfillment. Results are paginated and returned in the standard list format.

Required Scopes: fulfillments:read

Query Parameters

Parameter Type Description
page integer The page number to retrieve. Defaults to 1
limit integer The number of results per page. Defaults to 25

Example

js
        const response = await fetch("https://api.packagex.io/v1/fulfillments/ful_czhgjrk5JaVvyATPDbyURp/parcels?page=1&limit=25", {
  method: "GET",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
}).then((res) => res.json());

const parcels = response.data;
const pagination = response.pagination;

      

Retrieve Parcel

GET
`/v1/fulfillments/:fulfillment/parcels/:parcel`

Retrieve a specific parcel from a fulfillment by its ID.

Required Scopes: fulfillments:read

Example

js
        const response = await fetch("https://api.packagex.io/v1/fulfillments/ful_czhgjrk5JaVvyATPDbyURp/parcels/prcl_2gqXquSFnCRkPPMPk27ZZZ", {
  method: "GET",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
}).then((res) => res.json());

const parcel = response.data;

      

Pick Parcels

POST
`/v1/fulfillments/:fulfillment/parcels/pick`

Pick one or more parcels for a fulfillment. This is used in pick-and-pack workflows where previously received parcels need to be picked before being packed into a fulfillment.

Each parcel must include a picked boolean. If picked is false, a reason_code_id is required to explain why the parcel was not picked. Picked parcels will have their status updated to reserved_for_fulfillment (or internal_transfer for internal fulfillments), while unpicked parcels will be set back to in_storage.

Required Scopes: fulfillments:write, parcels:write

Request Body

Parameter Type Description
parcels array Required. An array of parcel objects to pick

Parcel Object

Parameter Type Description
id string Required. The parcel ID (prefixed with prcl_)
picked boolean Required. Whether this parcel was successfully picked
reason_code_id string Required if picked is false. The reason code explaining why the parcel was not picked

Example

js
        const data = {
  parcels: [
    {
      id: "prcl_2gqXquSFnCRkPPMPk27ZZZ",
      picked: true,
    },
    {
      id: "prcl_8hkLmnOPqRsTuVwXyZ1234",
      picked: false,
      reason_code_id: "rsnc_aBcDeFgHiJkLmNoPqRsT",
    },
  ],
};

const response = await fetch(`https://api.packagex.io/v1/fulfillments/ful_czhgjrk5JaVvyATPDbyURp/parcels/pick`, {
  method: "POST",
  headers: {
    "PX-API-KEY": process.env.PX_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(data),
}).then((res) => res.json());

const fulfillment = response.data;