Building Automating USPS Shipping Label Generation in Medusa

By Kamil Jędras

Featured image

In our latest project, the client needed a solution to simplify the shipping process for their platform, where users could trade products. Each trade required seamless shipping logistics, reducing manual intervention and ensuring accuracy in every step.

Traditional shipping workflows weren’t sufficient, as they couldn’t handle automated label generation for marketplace shipping and the flexibility needed for individual vendor-managed shipping. In our case, the customers that are responsible for shipping the products, as they are “trading” with each other.

To address this, we built a custom software solution for automated USPS shipping label generation, integrated directly with Medusa’s flexible architecture.

In this article, we’ll walk you through how we created this functionality in Medusa custom development for a convenient shipping experience.

Automating shipping logistics

Shipping logistics can be a time-consuming and error-prone part of any e-commerce platform, especially when handled manually. Users expect efficient processes, but without automation, manual effort can slow operations, increase errors, and ultimately impact the user experience.

For this customized solution, we aimed to automate label generation to address these challenges. The client required two shipping options - marketplace-managed and individually managed.

Shipping label generation in Medusa

To implement this functionality, we designed a custom Medusa batch job strategy, that addressed two core requirements: automation and accuracy. Here’s how it works:

1. Marketplace shipping

For users choosing marketplace shipping, we integrated the platform with USPS. Once a trade is confirmed, we begin a batch job that processes each order in a given trade group.

This strategy includes automated shipping label generation, as well as updating trade/order status, sending trade-related emails, and matching customers into trade pairs.

To ensure the process is error-free, we added address validation via USPS API as a mandatory step.

To begin our integration, we create a simple USPS client instance using Axios:

const uspsClient = axios.create({

        baseURL: process.env.USPS_URL,

      });

Next, we need to authorize with USPS and generate an OAuth 2 token. After receiving the token, we set a custom header that should be used in all subsequent requests.

const uspsAuthTokenResponse = await uspsClient.post("/oauth2/v3/token", {

            client_id: process.env.USPS_CLIENT_ID,

            client_secret: process.env.USPS_CLIENT_SECRET,

            grant_type: "client_credentials",

            redirect_uri: process.env.USPS_REDIRECT_URL,

            scope: "payments labels",

          });



          const token = uspsAuthTokenResponse?.data?.access_token;



          if (!token) {

            /* Handle the error here */

          }



          let headers = {

            Authorization: `Bearer ${token}`,

          };

Before we move to generating the labels, we are required to authorize the payment within USPS and generate a payment authorization token. We also need to include that token in the custom header used to communicate with USPS API.

        const uspsPaymentTokenResponse = await uspsClient.post(   

  "/payments/v3/payment-authorization",

            {

              roles: [

                {

                  roleName: "PAYER",

                  CRID: process.env.USPS_CRID,

                  MID: process.env.USPS_MID,

                  manifestMID: process.env.USPS_MANIFEST_MID,

                  accountType: process.env.USPS_ACCOUNT_TYPE,

                  accountNumber: process.env.USPS_ACCOUNT_NUMBER,

                },

                {

                  roleName: "LABEL_OWNER",

                  CRID: process.env.USPS_CRID,

                  MID: process.env.USPS_MID,

                  manifestMID: process.env.USPS_MANIFEST_MID,

                  accountType: process.env.USPS_ACCOUNT_TYPE,

                  accountNumber: process.env.USPS_ACCOUNT_NUMBER,

                },

              ],

            },

            { headers }

          );



          const paymentAuthToken =

            uspsPaymentTokenResponse?.data?.paymentAuthorizationToken;



          if (!paymentAuthToken) {

            /* Handle the error here */

          }

 headers = {

    "X-Payment-Authorization-Token": paymentAuthToken,

    Authorization: `Bearer ${token}`,

  };

With that, we are ready to proceed with creating shipping labels for the orders. First, we should create a request payload by mapping the addresses that customers provided while creating the orders to the schema expected by USPS (more on how should that look here). With the payload ready, we can generate our shipping label:

      const uspsLabelResponse = await uspsClient.post( 

 "/labels/v3/label",

        uspsLabelBody,

        {

          headers,

        }

      );

Now we can extract the label image from the response, temporarily save it on our local machine, and then use Medusa built-in file service to push it to the file storage provider of our choice.

const body = uspsLabelReponse.data.labelImage;          

const tmpFilePath = path.join(os.tmpdir(), fileName);

          fs.writeFileSync(tmpFilePath, Buffer.from(body.trim(), "base64"));



          const fileToUpload = {

            fieldname: name,

            originalname: fileName,

            encoding: "7bit",

            mimetype: "application/pdf",

            path: tmpFilePath,

            buffer: Buffer.from(body.trim(), "base64"),

            size: Buffer.byteLength(body.trim(), "base64"),

          };



const uploadResult: { url: string; key: string } =

        await fileService.upload(fileToUpload);

The last step is to save the URL link to the file in the metadata of the order and properly display it to the customers.

orderServiceTx.update(trade.id, {

  metadata: {

            labelUrl: uploadResult.url,

          },

        })

This automation makes the process faster, saves time, and works well for users who want a simple, hassle-free shipping option.

2. Individual Shipping

For customers who prefer managing shipping independently, we implemented an individual shipping option. While this approach requires more effort from the user, it provides flexibility, especially for those with unique shipping requirements or existing logistics workflows.

Address Validation

One of the most significant risks in shipping logistics is incorrect addresses, leading to failed deliveries and unhappy customers. To mitigate this, we built an address validation mechanism into the checkout flow.

To implement this, we created an endpoint that is called after the customer saves their address, either during the checkout flow or in the customer panel. This way we can instantly tell the users if something is incorrect, and needs to be adjusted.

The endpoint communicates with USPS API that not only verifies the address but also corrects it if it’s possible based on the provided parameters.

To begin, we need to generate an OAuth 2 token and set the authorization header:

const uspsClient = axios.create({

    baseURL: process.env.USPS_URL,

  });



  const uspsAuthTokenResponse = await uspsClient.post("/oauth2/v3/token", {

    client_id: process.env.USPS_CLIENT_ID,

    client_secret: process.env.USPS_CLIENT_SECRET,

    grant_type: "client_credentials",

    redirect_uri: "https://api.tradingeras.com",

    scope: "addresses",

  });



  const token = uspsAuthTokenResponse?.data?.access_token;



  if (!token) {

    /* Handle the error here */

  }



  const headers = {

    Authorization: `Bearer ${token}`,

  };

Next, we generate url query with the address data (USPS requires the data to be sent via url query). Here we need to make sure to remove any special characters and encode the strings to prevent the errors.

import type { Address } from "@medusajs/medusa";

const generateUspsQueryParams = (addr: Address) => {

  return `?streetAddress=${encodeURIComponent(

    addr.address_1?.replace(/[&#?]/g, "")

  )}&secondaryAddress=${encodeURIComponent(

    addr.address_2?.replace(/[&#?]/g, "") || ""

  )}&city=${encodeURIComponent(

    addr.city?.replace(/[&#?]/g, "")

  )}&state=${encodeURIComponent(

    addr.province?.replace(/[&#?]/g, "")

  )}&ZIPCode=${encodeURIComponent(addr.postal_code?.replace(/[&#?]/g, ""))}`;

};

With the query ready, we can validate the address. Based on the received response we can throw an error or return the corrected address:

  const payload = req.body as Address;

  const query = generateUspsQueryParams(payload);

const uspsResponse = await uspsClient.get(`/addresses/v3/address${query}`, {

      headers,

    });



    const resAddress = uspsResponse?.data?.address;



    if (!resAddress) {

      return res.sendStatus(500);

    }



    return res.json({

      ...payload,

      address_1: resAddress.streetAddress,

      address_2: resAddress.secondaryAddress,

      city: resAddress.city,

      province: resAddress.state,

      postal_code: resAddress.ZIPCode,

    });

Results and Benefits

The automated shipping label generation feature transformed the platform’s logistics process. Users now benefit from:

  • Reduced manual effort: Marketplace shipping is fully automated, saving time for customers.

  • Improved accuracy: Address validation ensures that labels are generated with correct details.

  • Flexibility: Customers can still manage shipping independently if needed, making the platform adaptable to different user needs.

To sum up

This functionality highlights how Medusa development enables the creation of custom features that meet unique business needs.

By integrating shipping label generation and address validation, we enhanced the platform’s logistics workflow, delivering a better experience for users.

Custom software development projects like this demonstrate how tailored solutions can improve processes, save time, reduce costs, and provide a better user experience.

Discover how we can help you build a commerce platform tailored to your business

Let's talk about your project

Other blog posts

Building a Custom Order Mechanism for Trading Events in Medusa

In this article, we’ll show you how we created a custom order mechanism for trading events in Medusa...

Next.js + Vercel, the Best Solution for the Frontend of an Online Store

Finding the right frontend solution for your online store is crucial. Enter Next.js and Vercel, a dynamic duo that promises unmatched performance, scalability, and developer experience for frontend developers...

Tell us about your project

Got a project in mind? Let's make it happen!

By clicking “Send Message” you grant us, i.e., Rigby, consent for email marketing of our services as part of the communication regarding your project. You may withdraw your consent, for example via hello@rigbyjs.com.
More information
placeholder

Grzegorz Tomaka

Co-CEO & Co-founder

LinkedIn icon
placeholder

Jakub Zbaski

Co-CEO & Co-founder

LinkedIn icon