- 1
Buat kunci di dashboard → API & webhook (paket Business atau Enterprise) dan pilih scope yang dibutuhkan.
- 2
Kirim permintaan ke base URL dengan header Authorization: Bearer stw_live_…
- 3
Daftarkan endpoint webhook untuk plan.completed dan quote.received, lalu verifikasi tanda tangannya.
Base URL
https://api.stowage.id/v1/ext
Setiap permintaan memakai kunci API organisasi di header Authorization. Kunci hanya tampil sekali dan kami menyimpannya sebagai hash SHA-256 — simpan di server, jangan di kode front-end atau aplikasi mobile.
Authorization: Bearer stw_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Scope
plans:read | Membaca plan |
plans:write | Menghitung & menyimpan plan |
rfqs:read | Membaca RFQ & penawaran |
rfqs:write | Membuat RFQ freight |
ports:read | Mencari pelabuhan (UN/LOCODE) |
vessel:read | Membaca plan |
vessel:write | Membaca plan |
Batas laju dihitung per kunci dengan jendela geser 1 menit; kuota panggilan per organisasi per bulan kalender. Melebihi batas laju → 429 dengan header Retry-After; kuota habis atau paket tanpa API → 402.
| Paket | Permintaan/menit per kunci | Panggilan/bulan |
| Free · Day Pass · Starter · Professional | Tidak tersedia | Tidak tersedia |
| Business | 60 | 10.000 |
| Enterprise | 600 | Tak terbatas |
Solve sinkron dibatasi 500 unit per permintaan; muatan yang lebih besar memakai Optimasi Lanjutan di Studio (respons 413).
Format error
Semua error memakai bentuk yang sama; kode HTTP: 400 input tidak valid, 401 kunci salah, 402 paket/kuota, 403 scope, 404 tidak ditemukan, 413 terlalu besar, 429 batas laju.
{
"error": {
"code": "paywall",
"message": "Monthly API call quota exhausted",
"paywall": { "moment": "feature", "quota": "api_calls", "limit": 10000, "used": 10000, "suggestedPlan": "enterprise" }
}
}
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
Klien TypeScript tanpa dependensi (fetch + WebCrypto) untuk Node 18+, Deno, Bun, dan 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' },
});
Simulasi perencanaan muatan. Bukan pengganti penimbangan resmi, VGM, loading computer kapal, atau persetujuan operator.
Dibuat otomatis dari spesifikasi OpenAPI 3.1. openapi.json
POST /plans/solve Plans
Hitung rencana muat
Menyusun kargo ke satu ruang muat secara sinkron dengan heuristik Stowage (maks. 500 unit). Dengan save: true hasil disimpan sebagai plan dan webhook plan.completed dikirim. Scope: plans:write.
Body permintaan · SolveRequest
title stringspace Space · wajibitems CargoItem[] · wajiboptions { minSupport, respectUnloadOrder, heavyBottom }save boolean — Store as a load plan (plans quota) and emit plan.completed. Default falseincludePlacements boolean — Return every placement. Default true
Respons
200 Packing result400 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 Studio429 Rate limit for this key exceeded (rate_limited); see Retry-After
GET /plans/{id} Plans
Ambil plan
Ringkasan plan tersimpan milik organisasi Anda; ?document=1 menambahkan dokumen lengkap (barang, penempatan, langkah muat). Scope: plans:read.
Parameter
id path · string · wajibdocument query · "0" | "1" | "true" | "false"
Respons
200 The plan400 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 organisation429 Rate limit for this key exceeded (rate_limited); see Retry-After
POST /rfqs Freight RFQ
Buat RFQ freight
Mengirim permintaan penawaran ke vendor terverifikasi yang cocok. Penawaran datang lewat webhook quote.received. Scope: rfqs:write.
Body permintaan · RfqRequest
title string · wajiboriginLocode string · wajibdestLocode string · wajibmode "FCL" | "LCL" | "air" | "truck" · wajibincoterm "EXW" | "FCA" | "FAS" | "FOB" | "CFR" | "CIF" | "CPT" | "CIP" | "DAP" | "DPU" | "DDP" · wajibreadyDate string · wajib — YYYY-MM-DD, within 12 monthsplanId string — Attach a saved plan: cargo is computed from itcargo { cbm, kg, packages, containers, description }commodity stringnotes stringcontact { name, email, phone } · wajibexpiresInDays integer
Respons
201 Created400 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 organisation429 Rate limit for this key exceeded (rate_limited); see Retry-After
GET /rfqs/{id} Freight RFQ
Ambil RFQ beserta penawaran
Status RFQ organisasi Anda dan penawaran yang sudah masuk (nominal dalam satuan terkecil mata uang). Scope: rfqs:read.
Respons
200 The RFQ400 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 organisation429 Rate limit for this key exceeded (rate_limited); see Retry-After
GET /ports Reference data
Cari pelabuhan (UN/LOCODE)
Cari berdasarkan nama atau UN/LOCODE; tanpa q daftar pelabuhan kurasi Indonesia tampil lebih dulu. Scope: ports:read.
Parameter
q query · stringcountry query · stringseaport query · "0" | "1" | "true" | "false"limit query · string — 1–20, default 20
Respons
200 Matching ports400 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
Katalog kontainer & armada
Kontainer ISO, truk Indonesia, palet, dan ULD udara dengan dimensi dalam (mm) dan muatan (kg). Kodenya dipakai di /plans/solve. Semua kunci valid.
Parameter
kind query · "containers" | "trucks" | "pallets" | "ulds"
Respons
200 Catalog400 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
Mitra: buat plan di organisasi yang terhubung
Untuk Kontainer.id dan Logistikku.id. Diautentikasi dengan integrasi yang disetujui pelanggan (persetujuan + scope) dan tanda tangan HMAC atas body mentah — tanpa API key. Mode sandbox sampai kredensial produksi mitra terbit. Butuh scope integrasi plans:write.
Parameter
partner path · "kontainer_id" | "logistikku_id" · wajibstowage-integration header · string · wajib — Integration id (`int_…`) the customer granted youstowage-signature header · string · wajib — `t=<unix>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>`
Body permintaan · PartnerPlanRequest
title string · wajibexternalRef string — Your shipment / booking referencespace Space · wajibitems CargoItem[] · wajib
Respons
201 Plan created400 Invalid input401 Unknown integration or bad signature (bad_signature)402 The organisation has no plan quota left403 Integration revoked or scope not granted409 Signature already used (replayed)
GET /integrations/{partner}/me Partner integrations
Mitra: identitas bersama pengguna yang memberi izin
Nama, e-mail, dan organisasi pengguna yang menghubungkan integrasi — hanya dengan scope profile:read yang ia setujui. Tanda tangan atas body kosong.
Parameter
partner path · "kontainer_id" | "logistikku_id" · wajibstowage-integration header · string · wajib — Integration id (`int_…`) the customer granted youstowage-signature header · string · wajib — `t=<unix>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>`
Respons
200 Identity401 Unknown integration or bad signature403 Integration revoked or scope not granted
Kami mengirim POST JSON ke endpoint Anda. Balas 2xx dalam 10 detik; selain itu kami mencoba ulang hingga 7 kali dalam ±9 jam (30 dtk, 2 mnt, 10 mnt, 30 mnt, 2 jam, 6 jam). Log setiap pengiriman ada di dashboard.
Event
plan.completed Plan selesai
Dikirim saat plan disimpan lewat API (save: true), oleh integrasi mitra, atau saat Optimasi Lanjutan selesai. Verifikasi Stowage-Signature sebelum memercayai isinya.
data: { planId, title, mode, spaceCode, version, utilizationPct, weightUtilizationPct, totalKg, itemCount, unitCount, source, appUrl }
quote.received Penawaran vendor masuk
Dikirim saat vendor mengirim atau merevisi penawaran pada RFQ Anda.
data: { rfqId, rfqTitle, quoteId, vendorId, vendorName, amountMinor, currency, transitDays, validUntil, revised }
Verifikasi tanda tangan
Header Stowage-Signature berisi t=<unix detik>,v1=<hex>. Hitung HMAC-SHA256 dengan secret endpoint atas string "<t>.<body mentah>", bandingkan dengan v1 secara constant-time, dan tolak bila t berselisih lebih dari 5 menit.
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));
}
Mitra mengirim plan ke organisasi yang sudah memberi izin (persetujuan + scope) memakai header Stowage-Integration dan Stowage-Signature atas body mentah — tanpa kunci API. Identitas bersama hanya dengan scope profile:read. Saat ini mode sandbox.
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"