npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

icarry-sdk

v0.1.9

Published

Unofficial iCarry shipping & logistics API client for Node.js — type-safe, secure, zero dependencies

Readme

icarry-sdk

Unofficial, type-safe, secure client for the iCarry shipping & logistics API — zero dependencies.

npm version npm downloads CI TypeScript License: MIT


⚠️ UNOFFICIAL PACKAGE — NOT AFFILIATED WITH iCARRY

This is an unofficial, community-created client for the iCarry Web API. It is not affiliated with, endorsed by, or maintained by iCarry. The iCarry API is documented only as a Postman collection with several known inconsistencies (see Known iCarry inconsistencies). Verify endpoint behavior against your own account before relying on it in production, and treat all response shapes for create/rate/track/payment operations as unverified until confirmed against a live tenant.


A lightweight, framework-agnostic client for the iCarry shipping API. Works with Next.js (server routes / Server Actions), Express, Fastify, Hono, or any modern JavaScript runtime with fetch.

What it is

  • A clean, camelCase, strongly-typed wrapper over iCarry's /api-frontend endpoints.
  • Faithful to the wire contract — it preserves iCarry's exact (and sometimes misspelled) field names internally rather than "fixing" them, so requests actually work.
  • Secure by default — passwords, tokens, and card data are redacted from errors, logs, and hooks; mutating and payment calls are never automatically retried.

Table of contents


Installation

npm install icarry-sdk

Runtime requirements

  • Node.js ≥ 18 is the declared minimum (engines). Actively tested in CI on Node 18, 22, and 24. Node 18 is the declared legacy minimum and is now EOL, retained only for backward-compat testing; Node 20 is EOL and omitted. Prefer a supported LTS — 22 (supported LTS) or 24 (latest LTS) — in production. Uses the global fetch and AbortController, or any runtime with a WHATWG fetch. You may inject a custom fetch via options.
  • Ships both ESM and CommonJS builds with full TypeScript declarations. Zero runtime dependencies.

Quick start

import { ICarryClient } from 'icarry-sdk';

const icarry = new ICarryClient({
  baseUrl: process.env.ICARRY_BASE_URL!, // e.g. https://test.icarry.com
  email: process.env.ICARRY_EMAIL!, // connector email
  password: process.env.ICARRY_PASSWORD!, // connector password
});

const countries = await icarry.countries.list();
const tracking = await icarry.shipments.track('TRACKING_NUMBER');

Get your connector email/password from your iCarry store → Settings → Connectors & Integration (regional portals: lb.icarry.com, uae.icarry.com).

Configuration

new ICarryClient({
  baseUrl: string,                    // required; origin or origin + /api-frontend

  // --- authentication (choose one strategy) ---
  email?: string,
  password?: string,
  token?: string,                     // pre-obtained bearer token
  tokenProvider?: () => Promise<string | undefined>,

  // --- transport ---
  fetch?: typeof fetch,               // inject a custom fetch (defaults to global)
  timeoutMs?: number,                 // default 30000
  retry?: boolean | {                 // default: retries enabled for idempotent GETs
    maxRetries?: number,              //   default 2
    baseDelayMs?: number,             //   default 300
    maxDelayMs?: number,              //   default 5000
    retryableStatuses?: number[],     //   default [408, 429, 500, 502, 503, 504]
  },
  headers?: Record<string, string>,   // extra headers on every request
  userAgent?: string,
  hooks?: ICarryHooks,                // redacted observability hooks
  autoReauth?: boolean,               // default true (re-auth once on 401)
  redactEmail?: boolean,              // default false
});

Authentication

Four modes are supported:

// 1. Connector credentials (authenticates lazily on first protected call)
new ICarryClient({ baseUrl, email, password });

// 2. Explicit bearer token
new ICarryClient({ baseUrl, token });

// 3. Async token provider (e.g. from a secret store)
new ICarryClient({ baseUrl, tokenProvider: async () => loadTokenFromVault() });

// 4. Manual, after construction
const client = new ICarryClient({ baseUrl });
client.auth.setToken(await loadToken());

Behavior:

  • The token is acquired lazily and cached in memory (never persisted to disk or storage).
  • Concurrent first calls trigger one authentication request (deduplicated).
  • On a 401, if the SDK owns the credentials (email/password or tokenProvider) it re-authenticates once and retries. A caller-supplied static token is never silently refreshed — you get an ICarryAuthenticationError. Disable auto-reauth with autoReauth: false.
  • The token is treated as opaque — the SDK makes no JWT/expiry assumptions.
await icarry.auth.getToken();   // force/inspect the token
icarry.auth.setToken('...');    // set manually
icarry.auth.clearToken();       // clear the cache

Base URL

baseUrl is required — the SDK never hardcodes an environment. Pass either the origin (https://test.icarry.com) or a URL already including the API prefix (https://test.icarry.com/api-frontend); the /api-frontend prefix is added idempotently. The known test environment is https://test.icarry.com/api-frontend. Production/regional base URLs are not clearly documented by iCarry — confirm yours before going live.

The base URL is validated strictly at construction (parsed with the WHATWG URL API). It must be an absolute https URL with no embedded credentials, query string, or fragment; plain http is accepted only for local development hosts (localhost, 127.0.0.1, [::1]). Anything else (e.g. https://user:pass@host, https://host?token=…, //host, javascript:/data:/ file:/ftp: schemes) throws an ICarryConfigurationError. Control characters (CR, LF, tab, NUL, DEL, …) are rejected on the original input before trimming, and a baseUrl that already contains the /api-frontend prefix more than once as a path segment (e.g. …/api-frontend/api-frontend) is rejected — a custom base path such as https://proxy.example.com/icarry is preserved and resolves to …/icarry/api-frontend. Client inspection methods (getBaseUrl(), toJSON(), toString()) always return a sanitized value with no credentials, query, or fragment.

Merchant flow

COD rate estimation and order creation using a country code + free-text drop-off location.

const rates = await icarry.merchant.estimateRates({
  dropOffLocation: 'Beirut',
  to: { latitude: 33.8938, longitude: 35.5018 },
  actualWeight: '1.5',
  packageType: 'parcel',
  dimensions: { length: '30', width: '20', height: '10' },
  dropAddress: { countryCode: 'LB', city: 'Beirut' },
  parcels: [{ quantity: 1, weight: '1.5', length: '30', width: '20', height: '10' }],
  cod: { amount: '25.00', currency: 'USD' },
});

const order = await icarry.merchant.createOrder({
  parcels: [{ quantity: 1, weight: '1.5', length: '30', width: '20', height: '10' }],
  dropOff: {
    firstName: 'Test', lastName: 'Recipient', email: '[email protected]', phoneNumber: '0100',
    country: 'lebanon', city: 'beirut', address1: 'Beirut, Lebanon',
  },
  actualWeight: '5', packageType: 'parcel',
  dimensions: { length: '10', width: '50', height: '30' },
  provider: 'Shipping.ICarry.example', methodId: 26, price: '3.60',
  parcel: { quantity: 1, packageValue: '120', packageCurrency: 'USD' },
  cod: { amount: '10', currency: 'USD' },
});

Money and measurements accept number | string; strings are serialized verbatim (no rounding). Units follow your iCarry account configuration — the SDK does not assume cm/inch or a currency.

Marketplace flow

Same as the merchant model plus a pickupLocation (your warehouse/pickup name). Marketplace warehouse creation lives on client.warehouses:

await icarry.warehouses.createMarketplaceWarehouse({
  name: 'Main Warehouse', isActive: true,
  address: {
    firstName: 'Sender', lastName: 'Name', email: '[email protected]',
    country: 'lebanon', city: 'beirut', address1: 'Pickup St', phoneNumber: '0100',
  },
});

const rates = await icarry.marketplace.estimateRates({ pickupLocation: 'Main Warehouse', /* ...merchant rate fields */ });
const order = await icarry.marketplace.createOrder({ pickupLocation: 'Main Warehouse', /* ...merchant order fields */ });

Reading marketplace rates (opt-in)

estimateRates returns AmbiguousApiResult, because iCarry's documented example is empty. A live call returns an array of rate options, so the SDK ships an opt-in reader rather than narrowing the return type on one observation:

import { parseMarketplaceRates, toMarketplaceOrderFields } from 'icarry-sdk';

const result = await icarry.marketplace.estimateRates(rateInput);
const options = parseMarketplaceRates(result); // MarketplaceRateOption[]

const cheapest = [...options].sort((a, b) => a.rate - b.rate)[0];
if (cheapest) {
  await icarry.marketplace.createOrder({ ...orderInput, ...toMarketplaceOrderFields(cheapest) });
}

toMarketplaceOrderFields fills provider, methodId, methodDescription, and price from one option. rate vs formattedPrice: the wire sends both a numeric Rate (2.121) and a formatted Price string ("$2.12"); the option exposes them as rate: number and formattedPrice?: string — display the second, send the first. Every field is on raw too.

parseMarketplaceRates never throws and never calls the API. It returns [] for any shape it does not recognize, so treat an empty array as "no rates or unfamiliar response" and fall back to reading result yourself. Merchant and on-demand rate responses have no equivalent helper — they have not been observed live. See API_COVERAGE.md.

On-demand flow

A different model: country/state ids, From/To coordinates, and a nested dimensions object. Shipment creation returns a shipment id used by the (server-only) payment step.

const rates = await icarry.onDemand.estimateRates({
  pickup: { countryId: 234, stateProvinceId: 1841, geo: { latitude: 25.2, longitude: 55.3 } },
  drop: { countryId: 234, stateProvinceId: 1841, geo: { latitude: 25.1, longitude: 55.2 } },
  actualWeight: 5, packageType: 'documents',
  dimensions: { length: 5, width: 5, height: 5, unit: 'cm' },
  isVendor: false,
});

const shipment = await icarry.onDemand.createShipment({
  pickupAddress: { firstName: 'A', lastName: 'B', email: '[email protected]', phoneNumber: '012',
    countryId: 234, stateProvinceId: 1841, address1: 'Dubai' },
  dropOffAddress: { firstName: 'C', lastName: 'D', email: '[email protected]', phoneNumber: '012',
    countryId: 234, stateProvinceId: 1841, address1: 'Dubai' },
  actualWeight: 5, packageType: 'documents',
  dimensions: { length: 5, width: 5, height: 5 },
  provider: 'Shipping.ICarry.example', methodName: 'Motor Express', price: 1.3,
  parcel: { quantity: 1, currency: 'USD', packageValue: 0 },
});
// Then pay for it — SERVER-ONLY, see the payment warning below.

Tracking

const tracking = await icarry.shipments.track('TRACKING_NUMBER');

The tracking response shape is not documented by iCarry — it is parsed with expect: 'auto' and typed as AmbiguousApiResult (it could be an object, array, string, etc.). Narrow it (e.g. typeof result === 'object' && result !== null) before reading fields.

Cancellation

await icarry.shipments.cancel('TRACKING_NUMBER');

⚠️ iCarry implements cancellation as a mutating GET. The SDK treats it as mutating: it is never cached and never automatically retried.

Packaging slips

The endpoint is named "Pdf" but may return binary PDF or a JSON envelope. The result is a discriminated union decided from the response Content-Type. The SDK never writes a file.

const slip = await icarry.shipments.getPackagingSlip(shipmentId);
if (slip.kind === 'binary') {
  // slip.data: Uint8Array, slip.contentType, slip.filename?
} else {
  // slip.data: unknown (JSON envelope — often a URL or encoded payload)
}

Error handling

All errors extend ICarryError. The SDK sanitizes known sensitive values (passwords, bearer tokens, card numbers, CVVs, and URLs carrying such data) in error messages, details, and the error cause at its boundaries — including plain-text/JSON API error bodies and network-layer messages. The cause is a minimal sanitized Error (name + redacted message + safe code), never the raw thrown object. Narrow with instanceof:

import { ICarryApiError, ICarryAuthenticationError, ICarryTimeoutError } from 'icarry-sdk';

try {
  await icarry.shipments.track('TRACKING_NUMBER');
} catch (error) {
  if (error instanceof ICarryAuthenticationError) {
    // invalid or expired credentials
  } else if (error instanceof ICarryTimeoutError) {
    // request timed out
  } else if (error instanceof ICarryApiError) {
    console.error(error.status, error.details?.code, error.message);
  }
  throw error;
}

Hierarchy: ICarryErrorICarryConfigurationError, ICarryValidationError, ICarryAuthenticationError, ICarryApiError, ICarryNetworkError, ICarryTimeoutError, ICarryAbortError, ICarryResponseParseError. ICarryApiError.details carries a safe { status, method, path, code?, requestId?, details? }path never includes the query string, so payment query parameters cannot leak through errors.

iCarry returns two error body shapes (an RFC 7807 object or a bare JSON string); both are handled.

Retry behavior

Conservative and structural. Only side-effect-free, opted-in calls are ever retried, and only on transient failures (network errors, 408, 429, and selected 5xx), with bounded exponential backoff + full jitter and Retry-After support.

  • Read-only GETs (countries.*, warehouses.getById/list/listWithMeta, shipments.track, getPackagingSlip) are retried by default.
  • Rate estimates (*.estimateRates) are not retried unless you pass { retry: true } per call.
  • Never retried: order creation, shipment creation, all payment operations, MontyPay returns, marketplace warehouse creation, and shipments.cancel (a mutating GET).

Disable retries entirely with retry: false.

Timeouts & cancellation

Every request has a timeout (default 30s, override per call). You may also pass your own AbortSignal; it is combined with the SDK's timeout. A timeout raises ICarryTimeoutError; a caller abort raises ICarryAbortError.

const controller = new AbortController();
setTimeout(() => controller.abort(), 2000);
await icarry.countries.list({ signal: controller.signal, timeoutMs: 5000 });

TypeScript usage

Fully typed. Import input/result types as needed:

import type { MerchantRateInput, Country, PackagingSlip, ICarryClientOptions } from 'icarry-sdk';

Response types honestly reflect incomplete iCarry documentation:

  • High-confidence endpoints (auth, countries, states, warehouses) return concrete typed objects.
  • Endpoints parsed with expect: 'auto' (rate/order/shipment creation, tracking, cancellation, payment, MontyPay) return AmbiguousApiResult — the union of what the parser can actually produce: object | unknown[] | string | number | boolean | null | undefined. Narrow before use, e.g.:
const result = await icarry.shipments.track('TRACKING_NUMBER'); // AmbiguousApiResult
if (typeof result === 'object' && result !== null && !Array.isArray(result)) {
  // safe to read fields here
} else if (typeof result === 'string') {
  // plain-text response
} else if (result === undefined) {
  // empty response
}

TypeScript will reject result.someField without narrowing — this is intentional, not a defect.

Security considerations

See SECURITY.md for the full policy. In short:

  • Keep connector email/password on the server. Never ship them to a browser.
  • Never expose bearer tokens in client-side code; don't commit credentials — use env vars or a secret manager.
  • The SDK sanitizes known sensitive values at its own logging and error boundaries (errors, error causes, and observability hooks). It cannot control URL logging at the HTTP/infrastructure layer, cannot protect original input values that you log yourself, and a custom fetch you inject must not log request URLs.
  • Credentials and tokens are held in runtime-private (#) fields, which reduce accidental exposure via console/JSON.stringify/util.inspect — but they do not protect against malicious code running in the same process.
  • Use TLS (https) endpoints only. Rotate credentials immediately if exposed.
  • The SDK stores no card data and makes no PCI-compliance claim.

⚠️ Server-only payment warning

icarry.payments.createShipmentOrder(...) sends card data as query-string parameters — this is iCarry's contract, not a design choice by this SDK. The SDK preserves it exactly, but:

  • Call it only from a trusted server context. Never from a browser. Query strings are routinely logged by proxies, gateways, and servers.
  • The SDK redacts every card parameter from hooks/errors and never exposes the serialized payment URL, but it cannot control logging at the HTTP/infrastructure layer.
  • Payment calls are never retried, cached, or subject to telemetry. Use obvious placeholders in tests; never commit real card data.
// SERVER-ONLY. Placeholders only.
await icarry.payments.createShipmentOrder(shipmentId, {
  card: {
    cardNumber: 'XXXXXXXXXXXXXXXX', cardCvv: 'XXX', cardType: 'visa', cardName: 'CARDHOLDER',
    cardExpirationMonth: '02', cardExpirationYear: '2039',
  },
  paymentMethodSystemName: 'Payments.MontyPay',
});

MontyPay return operations (processMontyPaySuccess / processMontyPayCancellation) are thin wrappers over iCarry's return URLs. The SDK performs no callback signature verification (iCarry documents none) — never treat a return call as proof of payment; verify status server-side.

Endpoint coverage matrix

See API_COVERAGE.md for the full matrix (method · route · auth · retry class · sensitive-data class · documentation confidence · known ambiguity). All 21 documented routes are implemented. The plugin sections (Shopify/WooCommerce/Magento/OpenCart/MANSATI) are prose in iCarry's docs, not callable REST routes, and are intentionally not implemented.

Known iCarry inconsistencies

The SDK preserves iCarry's exact wire contract while giving you a clean camelCase surface. Notable quirks it handles for you:

| Public (camelCase) | iCarry wire | Note | |---|---|---| | address.country (warehouse) | County | Misspelled wire field that means country. | | cod.currency (merchant/marketplace) | COdCurrency | Odd casing on these endpoints… | | cod.currency (on-demand) | CODCurrency | …but proper casing here. | | methodId (merchant/marketplace) | MethodId | Same concept, different name… | | methodName (on-demand) | MethodName | …than here. | | shipments.cancel | GET /CancelOrder | Mutating GET. | | payments.createShipmentOrder | card in query string | Server-only; redacted. | | getPackagingSlip | "Pdf" endpoint | May return binary or JSON. | | countries.list / getById | PascalCase vs snake_case | Same entity, different casing per route (see below). | | warehouses.list | { Data, recordsTotal, … } | GetAll returns a DataTables envelope, not an array. |

Casing split (live-verified 2026-07-30). iCarry serves the same entity in different casings depending on the route: Country/GetAllCountry and Warehouse/GetAll answer in PascalCase, while Country/GetById, Country/GetStatesByCountryId and Warehouse/GetById answer in snake_case. The SDK's mappers read both spellings per field, so you get one stable camelCase shape either way. Six Country fields are only sent by countries.list() and are absent from countries.getById(): currencyCode, phoneCode, terms/termAliases, requiresNationalShortAddress, hsCodeFormat and hsCodeFormatId.

Country search aliases. Terms arrives as a comma-separated, multilingual string with a trailing comma ("uae,الإمارات العربية المتحدة,…,"). country.terms is the raw string; country.termAliases is it split, trimmed and stripped of empty elements, so the trailing comma costs you nothing. Aliases are not lowercased — they contain Arabic, French, Spanish and emoji, so case folding is left to you.

Warehouse list envelope. Warehouse/GetAll wraps its records in a DataTables envelope. warehouses.list() unwraps it for you; use listWithMeta() when you also want the counters:

const warehouses = await icarry.warehouses.list({ name: 'Baabda' }); // Warehouse[]

const page = await icarry.warehouses.listWithMeta({ name: 'Baabda' });
page.warehouses; // Warehouse[]
page.recordsTotal; // total before the name filter (undefined if a bare array came back)
page.recordsFiltered; // records matching the name filter — the filter is applied server-side

Additionally: response schemas for create/rate/track/confirm/MontyPay are unverified (iCarry's "examples" are auto-generated echoes of the request), so they are returned as open records; error bodies may be a JSON object or a bare string; and no public Swagger/OpenAPI is available.

Low-level request escape hatch

For undocumented or future endpoints. It reuses authentication, timeout/abort, redaction, parsing, retry, and error handling. Prefer the typed resource methods where they exist.

const data = await icarry.request<MyType>({
  method: 'GET',
  path: '/SomeFutureEndpoint', // relative; NO query string or fragment in the path
  query: { foo: 'bar' }, // put parameters here so they can be redacted from metadata
  retryable: true, // opt in only for safe, idempotent calls
});

A path containing a query string, fragment, absolute URL, or control character is rejected with ICarryValidationError before any request runs — always pass parameters via query.

Observability hooks

Optional, best-effort hooks receive redacted, deep-frozen data (nested objects included). A throwing hook never fails a request — errors are swallowed and routed, sanitized (as a SafeHookError), to onHookError if provided. No telemetry runs by default.

new ICarryClient({
  baseUrl,
  token,
  hooks: {
    onRequest: (info) => console.debug(info.method, info.path),  // headers/body/url already redacted
    onResponse: (info) => console.debug(info.status, info.durationMs),
    onRetry: (evt) => console.warn('retrying', evt.path, 'in', evt.delayMs, 'ms'),
  },
});

Testing

npm test            # unit tests (mocked fetch; no live credentials)
npm run test:coverage

An optional live-contract suite is gated behind environment variables and disabled by default. It accepts either connector credentials or a pre-obtained token:

ICARRY_LIVE_TESTS=true ICARRY_BASE_URL=... ICARRY_EMAIL=... ICARRY_PASSWORD=... npm test
# or
ICARRY_LIVE_TESTS=true ICARRY_BASE_URL=... ICARRY_TOKEN=... npm test

Every included live check is read-only (auth, countries, states, warehouses; optional tracking and packaging-slip checks run only when ICARRY_TEST_TRACKING_NUMBER / ICARRY_TEST_SHIPMENT_ID are supplied). It never runs in CI and never uses real card data. Any future mutating or paid check requires a second explicit opt-in (ICARRY_ALLOW_MUTATIONS=true / ICARRY_ALLOW_PAYMENT_TESTS=true) and none ship enabled. The suite can log a summarizeShape of provisional responses to help tighten the still-unverified response types over time. It is a defense-in-depth helper for optional live tests, not a formal anonymizer — it makes no PCI-compliance or guaranteed-PII-detection claim, and does not replace a privacy review. Its guarantees:

  • Only explicitly allowlisted schema keys (id, name, status, createdAt, trackingNumber, warehouseId, …) may appear verbatim. Every other property key is replaced by a structural category — arbitrary identifiers (CustomerABC123, MohammadZaytoun) and identifier-shaped secrets alike become [dynamic-key] / [token-like-key]; emails/phones/URLs/UUIDs/card-like keys become [email-key]/[phone-key]/…
  • Value kinds are aggregated per category (Record<string, kind[]>) so colliding keys can't overwrite each other. Raw property processing is capped (own enumerable keys only) independently of the category cap, so a huge object is bounded and flagged truncated.
  • Sizes are coarse buckets (empty/one/few/many), never exact counts. No response value, nested content, raw non-allowlisted key, or exact property/category count ever appears.

Contributing

See CONTRIBUTING.md. In short: npm ci, then npm run format:check && npm run lint && npm run typecheck && npm test && npm run build before opening a PR.

License

MIT © Zaytoun Solutions. Unofficial — not affiliated with iCarry.