Skip to content

Developers

The STOWAGE.ID API

Solve load plans, send freight RFQs and receive webhooks from your own systems. REST + JSON, millimetres & kilograms, OpenAPI 3.1 documentation.

Quickstart

  1. Create a key in the dashboard → API & webhooks (Business or Enterprise plan) and pick the scopes you need.

  2. Send requests to the base URL with the header Authorization: Bearer stw_live_…

  3. Register a webhook endpoint for plan.completed and quote.received, then verify its signature.

Base URL

https://api.stowage.id/v1/ext

Authentication

Every request uses an organisation API key in the Authorization header. Keys are shown once and stored as a SHA-256 hash — keep them on your server, never in front-end or mobile code.

Authorization: Bearer stw_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Scopes

plans:readRead plans
plans:writeSolve & save plans
rfqs:readRead RFQs & quotes
rfqs:writeCreate freight RFQs
ports:readSearch ports (UN/LOCODE)
vessel:readRead plans
vessel:writeRead plans

Rate limits & quotas

Rate limits apply per key over a sliding 1-minute window; call quotas per organisation per calendar month. Exceeding the rate limit → 429 with a Retry-After header; quota used up or a plan without API → 402.

PlanRequests/minute per keyCalls/month
Free · Day Pass · Starter · Professional Not available Not available
Business 60 10,000
Enterprise 600 Unlimited

Synchronous solves are limited to 500 units per request; larger loads use Advanced Optimisation in the Studio (413 response).

Error format

All errors share one shape; HTTP codes: 400 invalid input, 401 bad key, 402 plan/quota, 403 scope, 404 not found, 413 too large, 429 rate limit.

{
  "error": {
    "code": "paywall",
    "message": "Monthly API call quota exhausted",
    "paywall": { "moment": "feature", "quota": "api_calls", "limit": 10000, "used": 10000, "suggestedPlan": "enterprise" }
  }
}

Examples

curl

curl -X POST https://api.stowage.id/v1/ext/plans/solve \
  -H "Authorization: Bearer $STOWAGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "PO 4711",
    "space": { "code": "40HC" },
    "items": [
      { "sku": "CARTON-A", "l": 600, "w": 400, "h": 400, "kg": 12, "qty": 40 },
      { "sku": "CRATE-B", "l": 1200, "w": 800, "h": 1000, "kg": 300, "qty": 6, "stackable": false }
    ],
    "save": true
  }'
curl "https://api.stowage.id/v1/ext/ports?q=Tanjung%20Priok" \
  -H "Authorization: Bearer $STOWAGE_API_KEY"

TypeScript SDK

Dependency-free TypeScript client (fetch + WebCrypto) for Node 18+, Deno, Bun and Cloudflare Workers. stowage-ext.ts

import { createExtClient } from './stowage-ext'; // /developers/stowage-ext.ts

const stowage = createExtClient({ apiKey: process.env.STOWAGE_API_KEY! });

const { result, plan } = await stowage.solve({
  space: { code: '40HC' },
  items: [{ sku: 'CARTON-A', l: 600, w: 400, h: 400, kg: 12, qty: 40 }],
  save: true,
});
console.log(result.utilizationPct, plan?.appUrl);

const { rfq } = await stowage.createRfq({
  title: 'Kopi ke Singapura', originLocode: 'IDTPP', destLocode: 'SGSIN', mode: 'LCL', incoterm: 'FOB',
  readyDate: '2026-12-01', planId: plan?.id, contact: { name: 'Ops', email: 'ops@example.com' },
});

Load planning simulation. Not a substitute for official weighing, VGM, the vessel loading computer, or operator approval.

Endpoint reference

Generated from the OpenAPI 3.1 specification. openapi.json

POST /plans/solve Plans

Solve a load plan

Packs the cargo into one space synchronously with the Stowage heuristic (≤ 500 units in total). With save: true the result is stored as a plan and plan.completed is sent. Scope: plans:write.

Request body · SolveRequest

  • title string
  • space Space · required
  • items CargoItem[] · required
  • options { minSupport, respectUnloadOrder, heavyBottom }
  • save boolean — Store as a load plan (plans quota) and emit plan.completed. Default false
  • includePlacements boolean — Return every placement. Default true

Responses

  • 200 Packing result
  • 400 Invalid input (validation_error)
  • 401 Missing or revoked API key (api_key_required)
  • 402 Plan without the API feature, or the monthly api_calls quota is used up (paywall)
  • 403 The key lacks the scope this endpoint needs (forbidden)
  • 413 More than 500 units (too_many_units): use Advanced Optimisation in the Studio
  • 429 Rate limit for this key exceeded (rate_limited); see Retry-After
GET /plans/{id} Plans

Get a plan

Summary of a saved plan of your organisation; ?document=1 adds the full plan document (items, placements, steps). Scope: plans:read.

Parameters

  • id path · string · required
  • document query · "0" | "1" | "true" | "false"

Responses

  • 200 The plan
  • 400 Invalid input (validation_error)
  • 401 Missing or revoked API key (api_key_required)
  • 402 Plan without the API feature, or the monthly api_calls quota is used up (paywall)
  • 403 The key lacks the scope this endpoint needs (forbidden)
  • 404 No such plan in your organisation
  • 429 Rate limit for this key exceeded (rate_limited); see Retry-After
POST /rfqs Freight RFQ

Create a freight RFQ

Sends a request for quotation to matched, verified vendors. Quotes arrive as quote.received webhooks. Scope: rfqs:write.

Request body · RfqRequest

  • title string · required
  • originLocode string · required
  • destLocode string · required
  • mode "FCL" | "LCL" | "air" | "truck" · required
  • incoterm "EXW" | "FCA" | "FAS" | "FOB" | "CFR" | "CIF" | "CPT" | "CIP" | "DAP" | "DPU" | "DDP" · required
  • readyDate string · required — YYYY-MM-DD, within 12 months
  • planId string — Attach a saved plan: cargo is computed from it
  • cargo { cbm, kg, packages, containers, description }
  • commodity string
  • notes string
  • contact { name, email, phone } · required
  • expiresInDays integer

Responses

  • 201 Created
  • 400 Invalid input (validation_error)
  • 401 Missing or revoked API key (api_key_required)
  • 402 Plan without the API feature, or the monthly api_calls quota is used up (paywall)
  • 403 The key lacks the scope this endpoint needs (forbidden)
  • 404 planId not found in your organisation
  • 429 Rate limit for this key exceeded (rate_limited); see Retry-After
GET /rfqs/{id} Freight RFQ

Get an RFQ with its quotes

Status of an RFQ of your organisation and the quotes received so far (amounts in minor units). Scope: rfqs:read.

Parameters

  • id path · string · required

Responses

  • 200 The RFQ
  • 400 Invalid input (validation_error)
  • 401 Missing or revoked API key (api_key_required)
  • 402 Plan without the API feature, or the monthly api_calls quota is used up (paywall)
  • 403 The key lacks the scope this endpoint needs (forbidden)
  • 404 No such RFQ in your organisation
  • 429 Rate limit for this key exceeded (rate_limited); see Retry-After
GET /ports Reference data

Search ports (UN/LOCODE)

Search by name or UN/LOCODE; without q the curated Indonesian ports are listed first. Scope: ports:read.

Parameters

  • q query · string
  • country query · string
  • seaport query · "0" | "1" | "true" | "false"
  • limit query · string — 1–20, default 20

Responses

  • 200 Matching ports
  • 400 Invalid input (validation_error)
  • 401 Missing or revoked API key (api_key_required)
  • 402 Plan without the API feature, or the monthly api_calls quota is used up (paywall)
  • 403 The key lacks the scope this endpoint needs (forbidden)
  • 429 Rate limit for this key exceeded (rate_limited); see Retry-After
GET /containers Reference data

Equipment catalog

ISO containers, Indonesian trucks, pallets and air ULDs with inner dimensions (mm) and payloads (kg). Codes are accepted by /plans/solve. Any valid key.

Parameters

  • kind query · "containers" | "trucks" | "pallets" | "ulds"

Responses

  • 200 Catalog
  • 400 Invalid input (validation_error)
  • 401 Missing or revoked API key (api_key_required)
  • 402 Plan without the API feature, or the monthly api_calls quota is used up (paywall)
  • 403 The key lacks the scope this endpoint needs (forbidden)
  • 429 Rate limit for this key exceeded (rate_limited); see Retry-After
POST /integrations/{partner}/plans Partner integrations

Partner: create a plan in a connected organisation

For Kontainer.id and Logistikku.id. Authenticated by the integration the customer approved (consent + scopes) and an HMAC signature of the raw body — no API key. Sandbox until production partner credentials are issued. Needs integration scope plans:write.

Parameters

  • partner path · "kontainer_id" | "logistikku_id" · required
  • stowage-integration header · string · required — Integration id (`int_…`) the customer granted you
  • stowage-signature header · string · required — `t=<unix>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>`

Request body · PartnerPlanRequest

  • title string · required
  • externalRef string — Your shipment / booking reference
  • space Space · required
  • items CargoItem[] · required

Responses

  • 201 Plan created
  • 400 Invalid input
  • 401 Unknown integration or bad signature (bad_signature)
  • 402 The organisation has no plan quota left
  • 403 Integration revoked or scope not granted
  • 409 Signature already used (replayed)
GET /integrations/{partner}/me Partner integrations

Partner: shared identity of the consenting user

Name, e-mail and organisation of the user who connected the integration — only with the profile:read scope they approved. Signature over an empty body.

Parameters

  • partner path · "kontainer_id" | "logistikku_id" · required
  • stowage-integration header · string · required — Integration id (`int_…`) the customer granted you
  • stowage-signature header · string · required — `t=<unix>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>`

Responses

  • 200 Identity
  • 401 Unknown integration or bad signature
  • 403 Integration revoked or scope not granted

Webhooks

We POST JSON to your endpoint. Reply 2xx within 10 seconds; otherwise we retry up to 7 times over ≈ 9 hours (30 s, 2 min, 10 min, 30 min, 2 h, 6 h). Every delivery is logged in the dashboard.

Events

plan.completed

A plan was completed

Sent when a plan is saved through the API (save: true), by a partner integration, or when an Advanced Optimisation run finishes. Verify Stowage-Signature before trusting the body.

data: { planId, title, mode, spaceCode, version, utilizationPct, weightUtilizationPct, totalKg, itemCount, unitCount, source, appUrl }

quote.received

A vendor quote arrived

Sent when a vendor submits or revises a quote on one of your RFQs.

data: { rfqId, rfqTitle, quoteId, vendorId, vendorName, amountMinor, currency, transitDays, validUntil, revised }

Verifying signatures

The Stowage-Signature header contains t=<unix seconds>,v1=<hex>. Compute HMAC-SHA256 with the endpoint secret over "<t>.<raw body>", compare it with v1 in constant time, and reject timestamps more than 5 minutes off.

Stowage-Signature: t=1767225600,v1=5f2b…c9e1
import { verifyWebhookSignature } from './stowage-ext';

// Express: app.post('/webhooks/stowage', express.raw({ type: 'application/json' }), handler)
const ok = await verifyWebhookSignature({
  secret: process.env.STOWAGE_WEBHOOK_SECRET!, // whsec_…
  body: req.body.toString('utf8'),             // RAW body, before JSON.parse
  header: req.header('Stowage-Signature'),
});
if (!ok) return res.status(400).end();
const event = JSON.parse(req.body.toString('utf8')); // { id, type, createdAt, orgId, data }
import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(secret: string, rawBody: string, header: string, toleranceSec = 300): boolean {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=') as [string, string]));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  return parts.v1?.length === expected.length && timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}

Kontainer.id & Logistikku.id integrations

Partners send plans to organisations that granted access (consent + scopes) using the Stowage-Integration header and a Stowage-Signature over the raw body — no API key. Shared identity only with the profile:read scope. Currently in sandbox mode.

BODY='{"title":"Booking KTR-991","space":{"code":"20GP"},"items":[{"sku":"BOX","l":1000,"w":1000,"h":1000,"kg":100,"qty":4}]}'
T=$(date +%s)
SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$INTEGRATION_SECRET" -hex | sed 's/^.* //')
curl -X POST https://api.stowage.id/v1/ext/integrations/kontainer_id/plans \
  -H "Content-Type: application/json" \
  -H "Stowage-Integration: $INTEGRATION_ID" \
  -H "Stowage-Signature: t=$T,v1=$SIG" \
  -d "$BODY"

Embeddable widget

Put a CBM calculator, mini planner and "Request a quote" button on your vendor website. Free with a "Powered by Stowage.id" link; white-label on Vendor Premium. Leads land in your vendor inbox; domains are allowlisted.

<script src="https://embed.stowage.id/v1.js" data-vendor="wdg_…" data-locale="en" async></script>

Optional attributes: data-locale="id|en" and data-target="#selector". For SPAs: window.StowageWidget.mount(el, { widget }).

Stowage.id connects users and vendors; the service contract is between the user and the vendor.

Create a widget in the vendor dashboard

For vendors