Shipments

A shipment is the record of a parcel handed off to a courier. It carries the courier code + service code, the price the merchant was quoted, an origin + destination, and the items packed into it. Fulkruma wires this to Biteship (its first courier aggregator) and emits fulkruma.shipment.* webhooks as the carrier sends status updates. This page covers the fulkruma.shipments namespace. For HTTP fields, see API: Shipments.

Namespace

fulkruma.shipments — every method:

fulkruma.shipments.create(input)
fulkruma.shipments.get(id)
fulkruma.shipments.list(params?)

There's no update and no cancel — once a label is booked with a courier, mutation goes through the courier's portal (or a support ticket). The closest thing to "cancel" is the carrier's own cancellation event, which Fulkruma surfaces as a shipment.updated webhook.

Methods

shipments.create

Signature. fulkruma.shipments.create(input): Promise<{ shipment: Shipment }>

Books a shipment with the chosen courier. The SDK auto-mints an Idempotency-Key — replay-safe, which matters for shipments because creating one twice means paying the courier twice.

const { shipment } = await fulkruma.shipments.create({
  customerId: 'cus_01HX...',
  courierCode: 'jne',
  courierServiceCode: 'reg',
  courierType: 'standard',
  price: 18_000,
  origin: {
    warehouseId: 'wh_01HX...',  // resolved server-side to address+coords
  },
  destination: {
    contactName: 'Alice Tan',
    contactPhone: '+62812xxxxxxxx',
    address: 'Jl. Diponegoro No. 5',
    postalCode: '10310',
    areaId: 'IDN-JKT-MTH',
  },
  items: [
    { variantId: 'var_01HX...', quantity: 2, weight: 0.3 },
  ],
});

console.log(shipment.id, shipment.trackingNumber);

origin and destination are intentionally loosely typed — the backend validates the shape per courier (Biteship expects a different envelope than a future JNE direct integration). For warehouse-origin shipments, pass { warehouseId } and let the server resolve the address.

The price you pass is what you charge the customer. It must match (or exceed) the quote from shipping.rates for the same courierCode/courierServiceCode/destination. Fulkruma validates this server-side; if you pass below-quote, you'll get 400 validation_error.

shipments.get

Signature. fulkruma.shipments.get(id): Promise<{ shipment: Shipment }>

Fetches one shipment by ship_* ID. Includes the latest tracking events embedded in the response.

const { shipment } = await fulkruma.shipments.get('ship_01HX...');
console.log(shipment.status);                  // 'in_transit'
console.log(shipment.events.length);           // count of tracking events

shipments.list

Signature. fulkruma.shipments.list(params?): Promise<{ shipments: Shipment[] }>

Lists shipments in the workspace. Pass status to filter (e.g. pending, confirmed, picked_up, in_transit, delivered, returned, cancelled).

const { shipments } = await fulkruma.shipments.list({ status: 'in_transit' });
console.log(`${shipments.length} parcels currently moving`);

Status filter is exact-match. There's no date filter — filter the returned rows on createdAt if you need a time-bounded scan.

Types

interface Shipment {
  id: string;              // 'ship_...'
  accountId: string;
  customerId: string | null;
  customerEmail: string | null;
  courierCode: string;
  courierServiceCode: string;
  courierType: string;
  status: ShipmentStatus;
  trackingNumber: string | null;
  price: number;
  insurance: number | null;
  insured: boolean;
  origin: Record<string, unknown>;
  destination: Record<string, unknown>;
  items: Array<{ variantId: string; quantity: number; weight: number; productId?: string }>;
  events: ShipmentEvent[];
  externalSource: string | null;
  externalRef: string | null;
  createdAt: string;
  updatedAt: string;
}

type ShipmentStatus =
  | 'pending' | 'confirmed' | 'picked_up' | 'in_transit'
  | 'delivered' | 'returned' | 'cancelled';

interface ShipmentEvent {
  id: string;
  shipmentId: string;
  status: ShipmentStatus;
  description: string;
  occurredAt: string;
}

For the full courier-code vocabulary and per-courier origin/destination schemas, see API: Shipments.

Common patterns

Quote-then-book

The robust pattern: quote first via shipping.rates, let the customer pick, then book:

const rates = await fulkruma.shipping.rates({
  destination: { areaId: 'IDN-JKT-MTH', postalCode: '10310' },
  items: [{ variantId: 'var_01HX...', quantity: 1, weight: 0.5 }],
});

// User picks a rate in your UI
const picked = (rates as { rates: any[] }).rates[0];

const { shipment } = await fulkruma.shipments.create({
  customerId: 'cus_01HX...',
  courierCode: picked.courierCode,
  courierServiceCode: picked.courierServiceCode,
  courierType: picked.courierType,
  price: picked.price,
  origin: { warehouseId: 'wh_01HX...' },
  destination: { areaId: 'IDN-JKT-MTH', postalCode: '10310', contactName: 'Alice', contactPhone: '+62812xxxxxxxx', address: 'Jl. Diponegoro 5' },
  items: [{ variantId: 'var_01HX...', quantity: 1, weight: 0.5 }],
});

Track via webhook

Don't poll shipments.get in a loop — subscribe to fulkruma.shipment.updated:

fulkruma.webhooks.createEndpoint({
  url: 'https://your-app.example.com/webhooks/fulkruma',
  events: ['fulkruma.shipment.updated', 'fulkruma.shipment.delivered'],
});

Then verify + handle in your endpoint. See webhooks and shipment.updated.

Idempotent re-booking after partial failure

If the courier API timed out and you don't know whether the shipment was created, retry with the same externalRef:

await fulkruma.shipments.create({
  /* ... */
  externalRef: 'order-2026-118',
  externalSource: 'storlaunch',
});

The auto-generated Idempotency-Key covers in-process retries; for cross-process replay use a stable externalRef and check shipments.list({}) first.

Errors

Code Status Cause
validation_error 400 Missing courier fields, price below quote, bad destination shape.
not_found 404 Customer, warehouse, or variant ID missing.
conflict 409 Insufficient stock at the origin warehouse.
courier_error 502 Upstream courier (Biteship) rejected the booking.
forbidden 403 Key lacks fulkruma:shipment:write scope.

courier_error is the one to retry — with backoff — because it usually means a transient upstream blip.

Next

  • Shipping — rate quotes + courier listing + origin config.
  • Stock — the inventory shipments draw from.
  • Webhooks — how to receive status events.
  • API: Shipments — HTTP reference.