A typed client for the Yalidine shipping API. It covers the parts every integration otherwise rewrites by hand: the auth headers, the rate-limit quota, pagination, batch parcel creation, and the fee math.
It is meant for servers, serverless functions, and edge runtimes. Your API id and token must never reach the browser. The Yalidine docs say so plainly, and this library does nothing to change that. Keep it on the server.
Published as @syscli/yalidine. It needs Node 18 or newer, uses the global
fetch, and ships no runtime dependencies.
npm install @syscli/yalidineimport { Yalidine } from "@syscli/yalidine";
const yal = new Yalidine({
id: process.env.YALIDINE_API_ID!,
token: process.env.YALIDINE_API_TOKEN!,
});
// Reference data. Alger is 16, Setif is 19, Batna is 5.
const wilayas = await yal.wilayas.list();
const desks = await yal.centers.list({ wilaya_id: 16 });
// Create a parcel and read back its tracking.
const created = await yal.parcels.create({
order_id: "ORD-1043",
from_wilaya_name: "Batna",
firstname: "Amine",
familyname: "Khelifi",
contact_phone: "0661234567",
address: "12 Rue Didouche Mourad",
to_commune_name: "Alger Centre",
to_wilaya_name: "Alger",
product_list: "Casque audio",
price: 8500,
do_insurance: false,
declared_value: 8500,
length: 25,
width: 20,
height: 15,
weight: 2,
freeshipping: false,
is_stopdesk: false,
has_exchange: false,
});
console.log(created.tracking); // yal-XXXXXXYalidine returns how many requests you have left per second, minute, hour, and
day on every response. The client reads those headers after each call, success
or error, and keeps them on yal.quota:
await yal.wilayas.list();
console.log(yal.quota.perDay.left); // e.g. 9412Each window starts undefined and fills in once the first response arrives.
Watch these numbers. Accounts that ignore the limit get banned, which is also
why the next section behaves the way it does.
When the API answers 429, the client waits the Retry-After it sends and
tries again, up to three attempts total, before raising a RateLimitError. This
is on out of the box because hammering past the limit is what gets an account
banned, so a short pause is the safer default. Tune or switch it off through the
constructor:
const yal = new Yalidine({
id,
token,
retry: { attempts: 1, respectRetryAfter: true }, // 1 disables retrying
});A Retry-After longer than retry.maxWaitSeconds (60 by default) is not waited
out: the RateLimitError surfaces at once so a request never hangs for an hour.
list() returns a Page. Step through it with nextPage(), or let
paginate() walk every page for you:
const first = await yal.parcels.list({ to_wilaya_id: 16 });
console.log(first.data, first.total, first.hasMore);
for await (const parcel of yal.parcels.paginate({ to_wilaya_id: 16 })) {
console.log(parcel.tracking, parcel.last_status);
}Fetch the fees for a wilaya pair, then total a real shipment for a commune. It applies the math for you: volumetric weight, the overweight surcharge past 5 kg, COD, and optional insurance.
const fees = await yal.fees.get({ from: 5, to: 16 }); // Batna to Alger
const quote = fees.quote(1601, {
weight: 7,
dimensions: { w: 40, h: 30, l: 50 },
declaredValue: 12000,
price: 12000,
stopdesk: true,
insurance: true,
});
// { delivery, overweight, cod, insurance, total }The same functions are exported on their own (volumetricWeight,
billableWeight, overweightFee, percentageFee) when you already hold the rates and want to skip the call.
createMany posts a batch and splits the result so you do not have to read the
raw order-id map yourself:
const result = await yal.parcels.createMany([first, second]);
for (const ok of result.created) console.log(ok.tracking);
for (const bad of result.failed) console.warn(bad.orderId, bad.message);result.byOrderId still holds the raw response for anything the split leaves out.
Parcels are checked before the request leaves. A missing stopdesk_id on a
stop-desk delivery, a missing product_to_collect on an exchange, a price out of
the 0 to 150000 range, or a malformed phone throws a ValidationError, keyed by
order_id, and spends no request or quota:
import { ValidationError } from "@syscli/yalidine";
try {
await yal.parcels.createMany(batch);
} catch (error) {
if (error instanceof ValidationError) {
for (const issue of error.issues) {
console.warn(issue.orderId, issue.field, issue.message);
}
}
}Every failure is a YalidineError, so you can catch one type and branch on it:
import { AuthError, NotFoundError, RateLimitError } from "@syscli/yalidine";AuthError (401/403), NotFoundError (404), RateLimitError (429, with
retryAfter), RequestError (other 4xx/5xx), and ValidationError (client-side,
including a quote() for a commune or option that has no price) all carry
status, a stable code, and the parsed response body.
Transport failures are wrapped too, with the original error on cause:
TimeoutError when timeoutMs runs out and NetworkError when no response
arrives at all.
Worth knowing before you build on it:
- Server only. The credentials grant full account access. Putting them in client-side code exposes them to anyone who opens the network tab. There is no browser build, and that is on purpose.
- Masked PII in reads.
GETandPATCHresponses maskfirstname,familyname,contact_phone,address, and the phone insideqr_text. Those are placeholders, not the real values. Never write them back to your own database. The masked fields carry a doc-comment as a reminder. - Rate limits and the ban. The per-second through per-day limits are real,
and repeatedly blowing past them gets the account banned. The default retry
helps; reading
yal.quotahelps more. - Edits are narrow. A parcel can only be updated or removed while its status
is still en preparation. Once it moves on, the API rejects the change, and so
will your
update/removecall.
npm install
npm run typecheck # src, tests, and examples
npm test # vitest, with msw mocking the API (Node 20+)
npm run build # tsup, dual ESM and CJS plus typesA small set of live tests run against the real API when YALIDINE_API_ID and
YALIDINE_API_TOKEN are set, and are skipped otherwise.
MIT. See LICENSE.