Warehouses

A warehouse is a physical (or logical) location stock lives at. Every VariantStock row pins to one warehouse + one variant, every Shipment has an origin warehouse, and the routing engine picks a warehouse per order based on availability + proximity. This page covers the fulkruma.warehouses namespace on the Node SDK: the four methods, their typed inputs, and the patterns most teams reach for. For the underlying HTTP surface and the full field tables, see API: Warehouses; for the data model, see Concepts → Warehouse.

Namespace

fulkruma.warehouses — every method on this namespace:

fulkruma.warehouses.create(input)
fulkruma.warehouses.list()
fulkruma.warehouses.update(id, patch)
fulkruma.warehouses.archive(id)

There's no get(id) — you fetch a single warehouse by listing then filtering client-side. The list endpoint returns every warehouse in the workspace including archived ones, so a one-trip lookup is cheap.

Methods

warehouses.create

Signature. fulkruma.warehouses.create(input): Promise<{ warehouse: Warehouse }>

Creates a warehouse in the workspace the API key belongs to. Only name is required — everything else (address, city, postal, coordinates, phone, default flag) is optional and can be filled in later via update. The SDK does not auto-mint an Idempotency-Key for this call because the operation is naturally safe to repeat — warehouse names aren't uniquely constrained.

import { FulkrumaClient } from '@forjio/fulkruma-node';

const fulkruma = new FulkrumaClient({
  keyId: process.env.FULKRUMA_KEY_ID!,
  secret: process.env.FULKRUMA_KEY_SECRET!,
});

const { warehouse } = await fulkruma.warehouses.create({
  name: 'Jakarta DC-1',
  address: 'Jl. Sudirman No. 1',
  city: 'Jakarta',
  postal: '12190',
  lat: -6.2088,
  lng: 106.8456,
  phone: '+62211234567',
  isDefault: true,
});

console.log(warehouse.id); // → 'wh_01HXAB...'

Only one default per workspace. Setting isDefault: true here automatically clears the flag on whichever warehouse currently holds it — the backend does the swap inside a single transaction. The default warehouse is the one that pre-selects in the portal and seeds new variant stock rows.

warehouses.list

Signature. fulkruma.warehouses.list(): Promise<{ warehouses: Warehouse[] }>

Returns every warehouse in the workspace, including archived ones. Filter on archivedAt to find active ones. The response is not paginated — we expect single-digit warehouse counts per merchant, so the list always fits in one response.

const { warehouses } = await fulkruma.warehouses.list();
const active = warehouses.filter((w) => !w.archivedAt);
for (const w of active) {
  console.log(w.id, w.name, w.city);
}

warehouses.update

Signature. fulkruma.warehouses.update(id, patch): Promise<{ warehouse: Warehouse }>

PATCH semantics — only the fields you pass are touched. Pass an updated isDefault: true to promote a warehouse; the API will clear the flag on the previous default in the same transaction.

const { warehouse } = await fulkruma.warehouses.update('wh_01HX...', {
  phone: '+62217654321',
  lat: -6.2090,
  lng: 106.8460,
});

You can't move a warehouse to a different workspace by mutating accountId; it's a server-controlled column.

warehouses.archive

Signature. fulkruma.warehouses.archive(id): Promise<{ archived: boolean }>

Soft-delete. The warehouse stays referenceable from historic shipments and stock movements (so audit trails keep working), but it no longer shows up in the routing pool, and you can't create new stock rows against it. Archiving is not reversible via the API — mint a new warehouse if you need to "un-archive".

await fulkruma.warehouses.archive('wh_01HX...');

If the warehouse still has active stock (onHand > 0 on any variant), the API returns 409 conflict — zero it out first via stock.adjust with a negative delta.

Types

The shape returned by every method:

interface Warehouse {
  id: string;              // 'wh_...'
  accountId: string;       // workspace
  name: string;
  address: string | null;
  city: string | null;
  postal: string | null;
  lat: number | null;
  lng: number | null;
  phone: string | null;
  isDefault: boolean;
  archivedAt: string | null;
  createdAt: string;
  updatedAt: string;
}

For the full field reference (validation rules on each input field, the meaning of each timestamp), see API: Warehouses.

Common patterns

Bootstrap a default warehouse

If you don't know whether a workspace has a warehouse yet (first-run setup, fresh tenants), create-or-promote:

async function ensureDefault(name: string) {
  const { warehouses } = await fulkruma.warehouses.list();
  const active = warehouses.filter((w) => !w.archivedAt);
  if (active.length === 0) {
    return fulkruma.warehouses.create({ name, isDefault: true });
  }
  const def = active.find((w) => w.isDefault);
  return def ?? fulkruma.warehouses.update(active[0].id, { isDefault: true });
}

Resolve a warehouse by city

Routing logic typically asks "which warehouse should fulfill this Jakarta order?":

async function warehouseForCity(city: string) {
  const { warehouses } = await fulkruma.warehouses.list();
  return warehouses
    .filter((w) => !w.archivedAt && w.city?.toLowerCase() === city.toLowerCase())
    .sort((a, b) => Number(b.isDefault) - Number(a.isDefault))[0] ?? null;
}

For real proximity routing (use the lat/lng columns), do the haversine math client-side — we don't expose a server-side "nearest" query.

Promote a new default

await fulkruma.warehouses.update('wh_new...', { isDefault: true });
// API atomically clears isDefault on the prior default — no race window.

Errors

Code Status Cause
validation_error 400 Missing name, bad coordinate range, postal too long.
not_found 404 Warehouse ID doesn't exist or lives in another workspace.
conflict 409 Archive attempted on a warehouse with live stock.
forbidden 403 Key lacks fulkruma:warehouse:write scope.

Every one of these comes through as a FulkrumaError instance — see Errors for the full handling guide.

Next