Integrations / Shopify

Shopify integration guide

Shopify stores call our API through a custom app that runs on your own server. The pattern is: Shopify fires an order webhook → your server calls Serian ShipKit → your server writes the fulfillment back to Shopify.

1. Create a Shopify custom app

  1. In your Shopify admin, go to Settings → Apps and sales channels → Develop apps.
  2. Create a new app. Grant it the scopes read_orders, write_fulfillments, and write_shipping.
  3. Install the app and copy its Admin API access token.

2. Register a webhook for new orders

In the app’s configuration, add an orders/paid webhook that points to a URL on your server, for example https://yourserver.com/hooks/shopify.

3. On your server, create a label and fulfill

typescript
import express from "express";
import crypto from "crypto";

const app = express();
app.use(express.raw({ type: "application/json" }));

app.post("/hooks/shopify", async (req, res) => {
  const hmac = req.get("X-Shopify-Hmac-Sha256");
  const expected = crypto
    .createHmac("sha256", process.env.SHOPIFY_WEBHOOK_SECRET!)
    .update(req.body)
    .digest("base64");
  if (hmac !== expected) return res.status(401).end();

  const order = JSON.parse(req.body.toString());

  // 1. Ask Serian ShipKit for rates using the shipping address
  const ratesRes = await fetch("https://api.serianshipkit.com/api/v1/rates", {
    method: "POST",
    headers: {
      Authorization: "Bearer " + process.env.SERIAN_API_KEY!,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      shipper: {
        name: "Your Warehouse",
        street1: process.env.SHIP_FROM_STREET1!,
        city: process.env.SHIP_FROM_CITY!,
        state: process.env.SHIP_FROM_STATE!,
        zip: process.env.SHIP_FROM_ZIP!,
        country: process.env.SHIP_FROM_COUNTRY ?? "GH",
      },
      recipient: {
        name: order.shipping_address.name,
        street1: order.shipping_address.address1,
        city: order.shipping_address.city,
        state: order.shipping_address.province_code,
        zip: order.shipping_address.zip,
        country: order.shipping_address.country_code,
      },
      parcels: [{
        length: 20, width: 15, height: 10, weight: 1,
        dimension_unit: "CM", weight_unit: "KG",
      }],
    }),
  });
  const { rates } = await ratesRes.json();
  const cheapest = rates[0];

  // 2. Create the label
  const shipRes = await fetch("https://api.serianshipkit.com/api/v1/shipments", {
    method: "POST",
    headers: {
      Authorization: "Bearer " + process.env.SERIAN_API_KEY!,
      "Content-Type": "application/json",
      "Idempotency-Key": "shopify-order-" + order.id,
    },
    body: JSON.stringify({
      rate_id: cheapest.rate_id,
      reference: order.name,
    }),
  });
  const shipment = await shipRes.json();

  // 3. Write the fulfillment back into Shopify
  await fetch(
    `https://${process.env.SHOPIFY_STORE}.myshopify.com/admin/api/2024-10/orders/${order.id}/fulfillments.json`,
    {
      method: "POST",
      headers: {
        "X-Shopify-Access-Token": process.env.SHOPIFY_ADMIN_TOKEN!,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        fulfillment: {
          tracking_number: shipment.tracking_number,
          tracking_company: shipment.carrier.toUpperCase(),
          tracking_urls: [shipment.label_url],
          notify_customer: true,
        },
      }),
    }
  );

  res.status(200).end();
});

4. Close the loop with tracking webhooks

Register a webhook in your Serian ShipKit dashboard for shipment.delivered and tracking.updated, pointing to another endpoint on your server. When those fire, update the Shopify fulfillment’s tracking info (the buyer already gets an email from Shopify).

Deploying

Any HTTP-capable host works: Vercel, Render, Fly.io, Railway, your own box. We recommend always sending the Idempotency-Key header so retries are safe, and starting with a sk_test_key until you’ve verified end-to-end.