Shipping SDKs from Marka. Each carrier ships as its own focused npm package, and @joinmarka/shipping bundles every carrier in one install.
| Package | What it is |
|---|---|
@joinmarka/canada-post |
Canada Post REST APIs: rates, shipments, PDF/ZPL labels, manifests, tracking |
@joinmarka/shipping |
Every Marka carrier SDK in one package (currently Canada Post) |
@joinmarka/canada-post is an unofficial TypeScript SDK for the Canada Post Developer Program REST APIs: rating, non-contract and contract shipping, manifests, labels (PDF and ZPL) and tracking.
Not affiliated with Canada Post. This is an independent, community-maintained SDK. "Canada Post" and related marks belong to Canada Post Corporation. Always check the official documentation for the authoritative behaviour of the API.
- Typed, camelCase inputs and outputs mapped to and from Canada Post's XML. The parsed XML stays available on every response (
raw) for fields the SDK does not map. - Follows Canada Post's
linkelements with the correct versioned media type. - Robust parsing: lists that arrive as one object or an array, numbers sent as strings,
<messages>error bodies (even on HTTP 200). - Safe by default: sandbox environment unless you ask for production, GET-only retries, writes are never retried, and an ambiguous shipment creation is reported as indeterminate instead of being silently retried (and billed twice).
- One small runtime dependency (
fast-xml-parser), built-infetch(injectable), Node 22.12+.
npm install @joinmarka/canada-post| Host | Notes | |
|---|---|---|
"development" (default) |
https://ct.soa-gw.canadapost.ca |
Sandbox. Validates your schema; responses are not real data. |
"production" (explicit only) |
https://soa-gw.canadapost.ca |
Creating a shipment or transmitting a manifest charges postage. |
- Use a development API key with the sandbox and a production key with production. A mismatch returns HTTP 403 (
AA002). - Test the calls that cost nothing first (rating, tracking). In production,
shipments.createfollowed byshipments.transmit,shipments.createwithtransmitShipment: true, andncShipments.createall bill real postage. - If you create test shipments in production, void them (
shipments.void) before transmitting.
import { createCanadaPostClient } from "@joinmarka/canada-post";
const canadaPost = createCanadaPostClient({
username: process.env.CANADAPOST_USERNAME!,
password: process.env.CANADAPOST_PASSWORD!,
customerNumber: process.env.CANADAPOST_CUSTOMER_NUMBER!,
// contractId: "...", // contract customers only
// mobo: "...", // mailing on behalf of another customer
// environment: "production", // default is "development" (sandbox)
});const { quotes } = await canadaPost.rating.getRates({
originPostalCode: "K2B8J6",
destination: { type: "domestic", postalCode: "J0E1X0" },
parcel: { weightKg: 1, dimensionsCm: { length: 20, width: 15, height: 10 } },
});
for (const q of quotes) {
console.log(q.serviceCode, q.priceDetails.due, q.serviceStandard.expectedDeliveryDate);
}Other rating calls: rating.discoverServices({ country }), rating.getService("DOM.EP"), rating.getOption("SO").
Non-contract shipping is for customers without a commercial contract; the credit card saved on your Canada Post profile is charged (in production).
import { findLink } from "@joinmarka/canada-post";
const shipment = await canadaPost.ncShipments.create({
serviceCode: "DOM.EP",
sender: {
company: "Example Co",
contactPhone: "555-555-5555",
address: {
addressLine1: "100 Example Street",
city: "Ottawa",
provState: "ON",
postalZipCode: "K1A0B1",
},
},
destination: {
name: "Jane Example",
address: {
addressLine1: "200 Sample Avenue",
city: "Toronto",
provState: "ON",
postalZipCode: "M5V2T6",
},
},
parcel: { weightKg: 1.2, dimensionsCm: { length: 20, width: 15, height: 10 } },
preferences: { showPackingInstructions: true },
});
console.log(shipment.shipmentId, shipment.trackingPin);
// Follow the label link: PDF (or ZPL for thermal printers).
const labelLink = findLink(shipment.links, "label")!;
const label = await canadaPost.artifacts.get(labelLink); // { contentType, data: Uint8Array }
await canadaPost.artifacts.saveToFile(labelLink, "./label.pdf");artifacts.get returns raw bytes, so ZPL works the same way (new TextDecoder().decode(label.data)). Right after creation the label may still be rendering (HTTP 202); the SDK polls a few times for you.
const summary = await canadaPost.tracking.getSummary({ pin: "1234567890123456" });
const detail = await canadaPost.tracking.getDetails({ pin: "1234567890123456" });
// or by Delivery Notice Card number: { dnc: "315052413796541" }In the sandbox every tracking query answers "No Pin History" (a CanadaPostApiError with code 004); use production PINs in production to test the full range.
const created = await canadaPost.shipments.create({
customerRequestId: "order-1001", // makes a lost create recoverable, see below
groupId: "2030-01-15-warehouse",
requestedShippingPoint: "K2B8J6",
cpcPickupIndicator: true,
serviceCode: "DOM.EP",
sender: {/* ... */},
destination: {/* ... */},
parcel: { weightKg: 2 },
preferences: { showPackingInstructions: true },
});
await canadaPost.shipments.void(created.links.find((l) => l.rel === "self")!); // before transmit only
const { manifestLinks } = await canadaPost.shipments.transmit({
groupIds: ["2030-01-15-warehouse"],
requestedShippingPoint: "K2B8J6",
cpcPickupIndicator: true,
manifestAddress: { company: "Example Co", phoneNumber: "555 555 5555", address: {/* ... */} },
});
const manifest = await canadaPost.manifests.get(manifestLinks[0]!); // may be pending, see below
await canadaPost.artifacts.saveToFile(manifest.artifactLink!, "./manifest.pdf");Every response keeps its links. canadaPost.followLink(link) performs a GET with the link's own media-type as Accept and returns either the parsed XML ({ kind: "xml", raw, text }) or bytes ({ kind: "binary", data, contentType }). Links must point at the configured environment host, so credentials never leave it.
| Namespace | Methods |
|---|---|
rating |
getRates, discoverServices, getService, getOption |
ncShipments |
create, get, getDetails, getReceipt, list, requestRefund |
shipments |
create, get, getDetails, getPrice, getReceipt, void, requestRefund, transmit, getGroups, list |
manifests |
get, list, getDetails |
artifacts |
get, saveToFile |
tracking |
getSummary, getDetails |
client |
followLink, environment, baseUrl |
Versioned media types: rating application/vnd.cpc.ship.rate-v4+xml, non-contract application/vnd.cpc.ncshipment-v4+xml, contract application/vnd.cpc.shipment-v8+xml, manifests application/vnd.cpc.manifest-v8+xml, tracking application/vnd.cpc.track-v2+xml.
Client options: username, password, customerNumber, mobo?, contractId?, environment?, fetch?, timeoutMs? (30 s), retries? (2), retryDelayMs? (250), language? (en-CA/fr-CA), onDebug?.
Not covered yet: reference-number tracking, signatures and delivery certificates, Find a Post Office, Pickup, Returns, merchant registration, SOAP. See docs/research/canadapost-api-contracts.md for the verified wire shapes and every assumption.
Everything the SDK throws extends CanadaPostError.
import {
CanadaPostApiError,
CanadaPostIndeterminateError,
CanadaPostNetworkError,
CanadaPostTimeoutError,
} from "@joinmarka/canada-post";
try {
await canadaPost.rating.getRates(input);
} catch (err) {
if (err instanceof CanadaPostApiError) {
console.error(err.status, err.codes, err.descriptions, err.requestId);
if (err.hasCode("9111")) {
/* no service fits this parcel */
}
if (err.isPending) {
/* HTTP 202: not ready, poll again */
}
} else if (err instanceof CanadaPostTimeoutError || err instanceof CanadaPostNetworkError) {
// a read or a quote: safe to repeat
} else if (err instanceof CanadaPostIndeterminateError) {
// a write may or may not have happened, see below
}
}CanadaPostApiErrorcarries the HTTPstatus, every messagecode/descriptionfrom the<messages>body, and a request id if the gateway sent one. Canada Post sometimes returns a business-rule failure with HTTP 200; the SDK throws for those too.- Retries: only idempotent
GETs are retried, on HTTP 5xx and 429, with exponential backoff (honouringRetry-After).POSTandDELETEare never retried, not even the read-onlygetRatesquote. - Credentials are only sent in the
Authorizationheader. They never appear in errors, and the optionalonDebughook receives headers withAuthorizationredacted.
If a create, transmit, void or refund request times out, loses its connection, or hits a 502/503/504, the server may have processed it, and in production a created label may already be billed. The SDK throws CanadaPostIndeterminateError and does not retry. Before you try again, look the object up:
try {
await canadaPost.shipments.create({ customerRequestId: "order-1001", groupId: "g1" /* ... */ });
} catch (err) {
if (err instanceof CanadaPostIndeterminateError) {
// Contract shipments: search by the id you chose, by group, or by day.
const byId = await canadaPost.shipments.list({ requestId: "order-1001" });
const byGroup = await canadaPost.shipments.list({ groupId: "g1" });
const today = await canadaPost.shipments.list({ noManifest: true, date: new Date() });
// Non-contract shipments: search by creation time (Eastern time).
const recent = await canadaPost.ncShipments.list({ from: new Date(Date.now() - 15 * 60_000) });
// Only create again if none of them contains the shipment.
}
}Always set customerRequestId on contract shipments (Canada Post rejects duplicates with code 7314): it makes the recovery search exact and protects against double creation. A requestId search that answers HTTP 202 means the shipment is still being created; poll again.
import { createMockFetch } from "@joinmarka/canada-post/testing";
const mock = createMockFetch({ status: 200, body: "<price-quotes/>" });
const client = createCanadaPostClient({ /* ... */ fetch: mock.fetch });npm install
npm run build && npm testLive sandbox smoke tests are opt-in and strictly read-only (quotes, discovery, tracking): copy .env.example to .env, then CANADAPOST_LIVE_TESTS=true npm run test:live. The suite refuses to run unless the environment is development. See CONTRIBUTING.md.
- Developer Program home (sign up and get API keys)
- Service directory
- REST fundamentals
- Rating · Non-contract shipping · Contract shipping · Tracking
- Official code samples, API throttle limits, release notes
MIT, copyright Devino Solutions.