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.
completed The fulfillment is done and has a shipping label. Event fulfillments that are just for pickup will still have a shipping label.
canceled A created fulfillment will not 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
      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;
    
          

    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.

    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. Note that the corresponding manifest can not be marked as received until the fulfillment 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.data.data.data.data.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. 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 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/fulfillment/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;