Main site

Sample codes > Sendcloud shipping options (live quotes)

Sendcloud shipping options (live quotes)

This example shows how to integrate Sendcloud live shipping quotes into JsRates using Sendcloud’s Shipping options API.

It demonstrates how to:

  • Authenticate with Sendcloud using Basic Auth (API key + secret)
  • Build a fetch-shipping-options request using origin/destination + shipment weight (and optional dimensions)
  • Fetch available shipping options + quotes
  • Convert Sendcloud options into Shopify shipping rates for checkout

Prerequisites

1) Required Sendcloud credentials (JsRates Secrets)

Create the following secrets in Settings → Secrets:

  • SENDCLOUD_API_KEY
  • SENDCLOUD_API_SECRET

Sendcloud’s Shipping options API supports HTTP Basic Authentication.
Set the Authorization header to Basic base64(API_KEY:API_SECRET).

2) Carrier configuration (Sendcloud account)

To receive shipping options/quotes, you must have:

  • a carrier enabled in your Sendcloud account, or
  • a connected direct carrier contract

(Availability depends on your Sendcloud configuration.)

Sendcloud API Reference

Shipping options (quotes) are fetched using:

  • POST https://panel.sendcloud.sc/api/v3/fetch-shipping-options

The request supports fields like:

  • from_country_code, to_country_code (ISO 3166-1 alpha-2)
  • from_postal_code, to_postal_code (recommended for accurate pricing)
  • weight ({ value, unit })
  • Optional filters like carrier_code, contract_id, shipping_product_code, functionalities, dimensions, lead_time, etc.

Sample code

Copy the following code and paste it to a blank calculateShippingRates.js module and save it.

import { enrichItemDetails } from "./modules.js";

// ---------------------------
// Helpers
// ---------------------------
function b64Basic(user, pass) {
  return "Basic " + btoa(`${user}:${pass}`);
}

function num(v, fallback) {
  const n = Number(v);
  return Number.isFinite(n) ? n : fallback;
}

function gramsToKg(grams) {
  const g = Number(grams);
  if (!Number.isFinite(g)) return 0;
  return +(g / 1000).toFixed(3);
}

function sumShipmentWeightKg(DATA) {
  // Prefer actual item grams (Shopify) if present
  const items = Array.isArray(DATA?.items) ? DATA.items : [];
  const totalGrams = items.reduce((acc, it) => acc + (Number(it?.grams) || 0) * (Number(it?.quantity) || 1), 0);

  // Ensure a small non-zero weight so the API can respond predictably
  return Math.max(gramsToKg(totalGrams), 0.001);
}

// Optional: build max dimensions from variant metafields (custom.length/width/height in CM)
// If metafields are missing, we omit "dimensions" completely.
function buildDimensionsCm(DATA) {
  const items = Array.isArray(DATA?.items) ? DATA.items : [];
  let maxL = 0, maxW = 0, maxH = 0;

  for (const it of items) {
    const mf = it?.metafields?.custom || {};
    const l = num(mf.length, 0);
    const w = num(mf.width, 0);
    const h = num(mf.height, 0);
    if (l > 0) maxL = Math.max(maxL, l);
    if (w > 0) maxW = Math.max(maxW, w);
    if (h > 0) maxH = Math.max(maxH, h);
  }

  if (maxL <= 0 || maxW <= 0 || maxH <= 0) return null;

  return {
    length: String(maxL.toFixed(2)),
    width: String(maxW.toFixed(2)),
    height: String(maxH.toFixed(2)),
    unit: "cm",
  };
}

// ---------------------------
// Main rate calculator
// ---------------------------
export async function calculateShippingRates(DATA, env) {
  try {
    // 1) (Optional) enrich dimensions from Shopify metafields
    // If you don't store dimensions in metafields, you can remove this enrichment call.
    DATA = await enrichItemDetails(DATA, [{ namespace: "custom", size: 10 }]);

    const origin = DATA?.origin || {};
    const destination = DATA?.destination || {};

    // 2) Build Sendcloud request body (fetch-shipping-options)
    const weightKg = sumShipmentWeightKg(DATA);

    const body = {
      from_country_code: (origin.country || "").toString(),
      to_country_code: (destination.country || "").toString(),
      from_postal_code: (origin.postal_code || "").toString(),
      to_postal_code: (destination.postal_code || "").toString(),
      weight: {
        value: String(weightKg),
        unit: "kg",
      },

      // Optional filters:
      // carrier_code: "postnl",
      // contract_id: 123,
      // shipping_product_code: "postnl:small",
      // functionalities: { signature: true },

      // Optional insurance:
      // total_insurance: 100.00,
    };

    const dims = buildDimensionsCm(DATA);
    if (dims) body.dimensions = dims;

    // 3) Call Sendcloud
    const url = "https://panel.sendcloud.sc/api/v3/fetch-shipping-options";

    if (!env?.SENDCLOUD_API_KEY || !env?.SENDCLOUD_API_SECRET) {
      console.error("Missing Sendcloud credentials: SENDCLOUD_API_KEY / SENDCLOUD_API_SECRET");
      return { rates: [] };
    }

    const resp = await fetch(url, {
      method: "POST",
      headers: {
        Authorization: b64Basic(env.SENDCLOUD_API_KEY, env.SENDCLOUD_API_SECRET),
        "Content-Type": "application/json",
      },
      body: JSON.stringify(body),
    });

    const json = await resp.json().catch(() => ({}));
    if (!resp.ok) {
      console.error("Sendcloud error:", resp.status, json);
      return { rates: [] };
    }

    // 4) Transform Sendcloud response -> Shopify rates
    const options = Array.isArray(json?.data) ? json.data : [];

    const rates = options
      .map((opt) => {
        // Quotes is typically an array; we take the first quote as the default price.
        const q0 = Array.isArray(opt?.quotes) ? opt.quotes[0] : null;

        // Prefer "total" price
        const total = q0?.price?.total;
        const value = total?.value;
        const currency = total?.currency || DATA.currency || "EUR";

        if (value == null) return null;

        // Sendcloud lead_time is typically in hours; map to a simple description if present.
        const leadTimeHrs = q0?.lead_time;
        const description = Number.isFinite(Number(leadTimeHrs))
          ? `Lead time: ${leadTimeHrs}h`
          : "";

        return {
          service_name: opt?.name || opt?.code || "Sendcloud",
          service_code: opt?.code || "SENDCLOUD",
          total_price: (Number(value) * 100).toFixed(0), // major -> minor units (string)
          currency,
          description,
          // Delivery dates are not guaranteed by this endpoint; leave empty
          min_delivery_date: "",
          max_delivery_date: "",
        };
      })
      .filter(Boolean);

    return { rates };
  } catch (err) {
    console.error("Sendcloud Rate Error:", err?.message || err);
    return { rates: [] };
  }
}

Notes

  • Postal codes: Sendcloud recommends providing from_postal_code and to_postal_code for accurate pricing (including remote surcharges / zonal pricing where applicable).
  • Dimensions: If you provide dimensions, Sendcloud can return billed_weight and more accurate quotes (depends on carrier/product).
  • Multiple parcels: If you need multi-parcel pricing (multi-collo), use the newer Sendcloud endpoint they recommend (see API docs).

Minimal request/response shape (quick view)

Request (example):

{
  "from_country_code": "NL",
  "to_country_code": "NL",
  "weight": { "value": "2", "unit": "kg" },
  "carrier_code": "postnl",
  "functionalities": { "signature": true }
}

Response (example):

{
  "data": [
    {
      "code": "postnl:small/home_address_only,signature",
      "name": "PostNL Klein Pakket ...",
      "carrier": { "code": "postnl", "name": "PostNL" },
      "quotes": [
        {
          "price": { "total": { "value": "0", "currency": "EUR" } },
          "lead_time": 24
        }
      ]
    }
  ]
}

Sendcloud API documentation references

This JsRates example is implemented using the Sendcloud Shipping Options – Fetch shipping options endpoint.
For full details and the authoritative schema, always refer to the official Sendcloud API documentation.

Endpoint used in this implementation

This endpoint is suitable when:

  • You want live quotes based on total shipment weight
  • You are not explicitly modelling multiple parcels
  • You prefer a simpler request payload with fewer required fields

Alternative endpoint: full shipment / multi-parcel pricing

Sendcloud also provides a more expressive endpoint for full shipment modelling:

This endpoint is recommended when:

  • You calculate or control multiple parcels (cartons)
  • You need per-parcel dimensions and weights
  • You want the most accurate pricing and access to advanced features (such as service-point delivery)

Important:
The two endpoints are related but not interchangeable.
/shipping-options supports a full shipment model with parcels, while /fetch-shipping-options is optimized for simpler quote lookups.