@velopeplabs/ship
v0.4.0
Published
Typed TypeScript client for the VeloShip shipping API: FedEx, USPS and UPS rates, labels and tracking, with idempotent purchases, retries and webhook verification.
Maintainers
Readme
@velopeplabs/ship
Typed client for the shipping API. It has no dependencies and runs anywhere fetch does: Node, Bun,
Cloudflare Workers, Convex and browsers.
import { VeloShipClient } from "@velopeplabs/ship";
const shipping = new VeloShipClient({
apiKey: process.env.VELOSHIP_API_KEY!, // sk_test_… in development, sk_live_… in production
});Common calls
// Prices at checkout. Nothing is stored.
const { rates } = await shipping.rates.quote({
to_address: { street1: "1 Market St", city: "San Francisco", state: "CA", zip: "94105" },
parcel: { preset: "small_box", weight: 24 },
sort: "fastest", // or "cheapest", or "best_value" (the default)
descriptive: true, // adds a plain-English description of each service
});
// Create and buy in one call. from_address comes from settings.
const shipment = await shipping.shipments.create({
to_address: { name: "Jane Doe", phone: "4155551234", street1: "1 Market St", city: "San Francisco", state: "CA", zip: "94105" },
parcel: { preset: "small_box", weight: 24 },
service: "FEDEX_GROUND",
reference: order.id,
});
const { contentType, data } = await shipping.shipments.label(shipment.id);
const tracker = await shipping.trackers.retrieve(shipment.tracker_id!);
await shipping.shipments.refund(shipment.id); // void before it ships
for await (const s of shipping.shipments.listAll({ status: "purchased" })) console.log(s.tracking_code);Test-mode scenarios
With a sk_test_ key, make the mock carriers misbehave to exercise failure handling. Live mode ignores it.
const shipping = new VeloShipClient({ apiKey: "sk_test_…", scenario: "carrier_down" }); // every call
await shipping.shipments.buy(id, { service: "FEDEX_GROUND" }, { scenario: "purchase_timeout" }); // one callErrors
import { VeloShipError } from "@velopeplabs/ship";
try {
await shipping.shipments.create({ ...order, service: "FEDEX_2_DAY" });
} catch (err) {
if (err instanceof VeloShipError) {
err.status; // 422
err.code; // "CARRIER.ERROR", "SHIPMENT.RATES.EXPIRED", …
err.details; // FedEx's error list, validation issues, …
err.shipmentId; // set when the shipment was created but the purchase failed
err.requestId; // quote it when reporting a problem
}
}What the client handles for you
- Idempotency: every
POSTcarries anIdempotency-Key, and the same key is reused on retries. Pass{ idempotencyKey }to tie it to your own order ID. - Retries: network errors,
429,502,503and504are retried twice by default (maxRetries), honoringRetry-After. An unconfirmed purchase (SHIPMENT.PURCHASE_UNKNOWN) is never retried, because reconciliation handles it. - Read-your-writes: the client echoes
x-d1-bookmark, so aretrieveright after acreatesees the new shipment even when served by a database replica.
Webhooks
import { constructWebhookEvent } from "@velopeplabs/ship";
const event = await constructWebhookEvent(process.env.VELOSHIP_WEBHOOK_SECRET!, req.headers.get("x-webhook-signature"), await req.text());
if (event.type === "tracker.updated") event.data.status; // typed as TrackerVerify against the raw body, before any JSON parsing. Signatures older than 5 minutes are rejected.
Test mode
Use an sk_test_… key, created with POST /v1/api_keys and {"mode":"test"}, for development and
CI. Test mode has its own data and never calls FedEx. The root key can opt in with
new VeloShipClient({ ..., mode: "test" }).
The full API reference is in docs/api.md.
