Build a Custom Shipping Carrier Integration for Sparkforge

By Sparkforge Support Team ·Last reviewed May 3, 2026

Build a Marketplace app, register a shipping carrier with a callback URL, implement live rates and order webhooks, and post fulfillments back to Sparkforge. The full API flow is below.

This is a developer guide for building a custom shipping carrier integration on the Sparkforge Marketplace. The integration handles OAuth, live shipping rates at checkout, order webhooks, fulfillment, and uninstall cleanup.

What a custom carrier integration unlocks

  • Flexibility: connect any carrier with an API.
  • Accurate rates: show real-time costs from your carrier accounts at checkout.
  • Automated fulfillment: create labels and shipments automatically when an order completes.
  • Tracking sync: push tracking numbers back into Sparkforge orders.

1. Requirements

Technical knowledge

  • Backend development (Node.js, Python, etc.)
  • REST APIs, webhooks, and OAuth 2.0

Backend service

Your service needs to:

  • Handle OAuth redirects from Sparkforge
  • Receive webhooks from both Sparkforge (order events) and your carrier (shipping updates)
  • Call Sparkforge and carrier APIs
  • Serve a frontend UI (or host it separately)

Database

Securely store:

  • Sparkforge OAuth access and refresh tokens, keyed by location ID
  • Carrier credentials (API keys, secrets) per location
  • Mapping between Sparkforge orders and carrier shipments

Frontend (Custom Page)

A simple UI where users initiate the connection, save carrier credentials, and manage settings.

API endpoints your backend exposes

  • OAuth Redirect URI — handles Sparkforge OAuth callback
  • Sparkforge Webhook Listener — receives OrderCreate / OrderStatusUpdate
  • Carrier Credential Management — save/update/delete carrier credentials
  • Carrier Webhook Listener — receives shipment updates from the carrier
  • Live Rates Callback — Sparkforge calls this during checkout

2. Initial setup

Create a Marketplace app

Follow the Sparkforge Marketplace docs and create a new app.

  • App Type: Sub-Account
  • Redirect URI: your OAuth callback (e.g., https://your-backend.com/oauth/callback)
  • Webhook URL: your Sparkforge webhook listener (e.g., https://your-backend.com/webhooks/sparkforge)
  • Webhooks enabled: OrderCreate and OrderStatusUpdate

Required scopes

  • orders.readonly
  • orders.write
  • shipping.readonly
  • shipping.write

Frontend Custom Page

Build a UI for capturing API credentials. Embed it via a Custom Menu Link pointing to your hosted page.

Handling user context

Sparkforge passes an encrypted token (typically a JWT or AES-encrypted payload) when loading your custom page. Decrypt it with your shared secret to get the locationId and userId.

// Backend (Node.js) — decrypt user context
const CryptoJS = require('crypto-js');

function decryptUserData(encryptedData, sharedSecret) {
  const bytes = CryptoJS.AES.decrypt(encryptedData, sharedSecret);
  const json = bytes.toString(CryptoJS.enc.Utf8);
  return JSON.parse(json);
}

app.post('/your-context-endpoint', (req, res) => {
  const { encryptedContextData } = req.body;
  const sharedSecret = process.env.SPARKFORGE_APP_SHARED_SECRET;
  const userData = decryptUserData(encryptedContextData, sharedSecret);
  res.json({ success: true, locationId: userData.locationId });
});
// Frontend — request context from Sparkforge parent window
async function getUserContext() {
  return new Promise((resolve, reject) => {
    const messageHandler = (event) => {
      if (event.data?.message === 'REQUEST_USER_DATA_RESPONSE') {
        window.removeEventListener('message', messageHandler);
        resolve(event.data.payload);
      }
    };
    window.addEventListener('message', messageHandler);
    window.parent.postMessage({ message: 'REQUEST_USER_DATA' }, '*');
  });
}

Carrier credential handling

  1. Capture API key, secret, etc. from your frontend.
  2. Store securely, keyed by locationId.
  3. Validate with a test API call to the carrier.
  4. Use the validated credentials to register your carrier webhook listener URL with the carrier.

3. Live shipping rates

Register your carrier

Your backend posts to POST /shipping-carrier with the location context and carrier details.

POST /shipping-carrier HTTP/1.1
Authorization: Bearer <location_access_token>
Content-Type: application/json

{
  "altId": "LOCATION_ID_HERE",
  "altType": "location",
  "name": "Your Custom Carrier Name",
  "callbackUrl": "https://your-backend.com/rates"
}

The response returns a _id (the shippingCarrierId). Store it alongside the location and credentials.

Implement the live rates callback

Sparkforge POSTs cart and destination details to your callbackUrl during checkout.

// Inbound rates request body
{
  "rate": {
    "altId": "LOCATION_ID_HERE",
    "altType": "location",
    "origin": { "name": "...", "address1": "...", "city": "...", "state": "...", "zip": "...", "country": "US" },
    "destination": { "name": "...", "address1": "...", "city": "...", "state": "...", "zip": "...", "country": "US" },
    "items": [
      { "name": "Product A", "sku": "SKU123", "quantity": 2, "grams": 500, "unitPrice": 25.0, "totalPrice": 50.0 }
    ],
    "currency": "USD"
  }
}

Your backend:

  1. Parse the request body.
  2. Look up carrier credentials by altId.
  3. Translate to your carrier's API format.
  4. Call the carrier for live rates.
  5. Translate the carrier response into Sparkforge's expected format.
// Response back to Sparkforge
{
  "rates": [
    { "serviceName": "Standard Ground", "amount": 15.5, "currency": "USD", "estimatedDays": 5 },
    { "serviceName": "Express Overnight", "amount": 45.0, "currency": "USD", "estimatedDays": 1 }
  ]
}

Configure shipping zones

List zones with GET /shipping-zone?altId=...&altType=location. For each zone, create a carrier-backed rate:

POST /shipping-zones/{zoneId}/rates

{
  "altId": "LOCATION_ID_HERE",
  "altType": "location",
  "name": "My Awesome Shipping - Live Rates",
  "currency": "USD",
  "amount": 0,
  "conditionType": "NONE",
  "isCarrierRate": true,
  "shippingCarrierId": "SHIPPING_CARRIER_ID_HERE"
}

4. Order syncing and fulfillment

Enable order webhooks

Confirm OrderCreate and OrderStatusUpdate are enabled in your app config and pointed at your Sparkforge webhook listener.

Handle the order webhook

app.post('/webhooks/sparkforge', async (req, res) => {
  // 1. Validate webhook signature (recommended)
  const { type, locationId, orderId, status } = req.body;
  if ((type === 'OrderCreate' || type === 'OrderStatusUpdate') && status === 'completed') {
    await processOrderForShipping(locationId, orderId);
  }
  res.status(200).send('ok');
});

async function processOrderForShipping(locationId, orderId) {
  // 1. GET /orders/{orderId} to fetch full details
  // 2. Look up carrier credentials for the location
  // 3. Translate to carrier API format
  // 4. Create the shipment with the carrier
  // 5. Store carrierOrderId <-> sparkforgeOrderId mapping
}

Handle carrier shipping updates

Your carrier webhook listener receives shipment updates (label printed, tracking generated, shipped, delivered). Validate the signature, parse the data, look up the matching Sparkforge order, and trigger fulfillment when a tracking number is available.

app.post('/webhooks/carrier', async (req, res) => {
  const { orderReference, tracking, status } = req.body;
  if (!tracking?.number) return res.status(200).send('no tracking yet');
  // Look up sparkforgeOrderId + locationId from your DB by orderReference
  if (status === 'SHIPPED' || status === 'LABEL_CREATED') {
    await createSparkforgeFulfillment(locationId, sparkforgeOrderId, tracking.number, tracking.url);
  }
  res.status(200).send('ok');
});

async function createSparkforgeFulfillment(locationId, orderId, trackingNumber, trackingUrl) {
  const payload = {
    altId: locationId,
    altType: 'location',
    items: [/* line items being fulfilled */],
    trackings: [{ trackingNumber, shippingCarrier: 'Your Custom Carrier Name', trackingUrl }],
    notifyCustomer: true
  };
  // POST /orders/{orderId}/fulfillments with payload
}

5. Disconnection and uninstallation

Listen for the App Uninstalled webhook from Sparkforge. When it fires:

  1. Look up the shippingCarrierId for that locationId.
  2. Call DELETE /shipping-carrier/{shippingCarrierId}?altId=LOCATION_ID&altType=location.
  3. Delete stored credentials and unregister carrier-side webhooks.

Removing the carrier registration prevents Sparkforge checkouts from calling a callback URL that no longer responds.

Build checklist

  • Auth: store and refresh Sparkforge OAuth tokens; keep carrier credentials encrypted at rest.
  • APIs: implement registration, rates, order fetch, and fulfillment correctly.
  • Webhooks: validate signatures on both sides; idempotent handlers.
  • Logging: log payloads (redacted) for every inbound webhook and outbound API call.
  • Cleanup: handle uninstall cleanly so no orphaned carriers remain.

Frequently Asked Questions

What scopes does a shipping carrier integration need?
orders.readonly, orders.write, shipping.readonly, and shipping.write. Set the app type to Sub-Account so it installs at the location level.
Where do I register my custom shipping carrier?
POST to /shipping-carrier with altId (location ID), altType (location), a display name, and the callbackUrl Sparkforge will hit for live rates. Save the returned _id as your shippingCarrierId.
How does Sparkforge fetch live rates at checkout?
Sparkforge sends a POST request to your callbackUrl with origin, destination, items, and currency. Your backend translates that to your carrier's API, fetches rates, and returns a list of rate objects with serviceName, amount, currency, and optional estimatedDays.
How do I post a fulfillment back to Sparkforge?
After your carrier provides a tracking number via webhook, POST to /orders/{orderId}/fulfillments with altId, altType, items, trackings (with trackingNumber, shippingCarrier, trackingUrl), and notifyCustomer.
What should happen when the user uninstalls the app?
Listen for the App Uninstalled webhook. DELETE /shipping-carrier/{shippingCarrierId} for that location, then delete stored credentials and unregister carrier-side webhooks to prevent dead callback URLs.

Was this article helpful?