—
Contoh di bawah memakai API key & ID merchant asli akun Anda. Simpan di server backend — jangan di frontend publik.
Panduan integrasi khusus merchant Anda. Base URL:
{{BASE_URL}} — kredensial sudah terisi di setiap contoh.
{{ORIGIN_POSTAL}}
OpenAPI JSON
FN Shipping adalah gateway pengiriman FN CREATIVE. Website/toko Anda memanggil API ini
dari server backend — bukan dari browser publik. Satu akun FN CREATIVE
(pay.fncreative.org) dipakai untuk payment dan shipping.
x-api-key + id_merchant di .env server website Anda.POST /shipping/quote → tampilkan tarif → customer pilih kurir.POST /shipping/orders dengan rate_id dari langkah 4.GET /shipping/orders/:id/tracking.{{ORIGIN_POSTAL}}, berat default {{PACKAGE_WEIGHT}} gram.
Base URL production: {{BASE_URL}}
| Method | Path | Auth | Fungsi |
|---|---|---|---|
GET | /health | — | Status layanan |
POST | /auth/login | — | Login dashboard (email & password) |
GET | /auth/me | Bearer | Data merchant & API key |
GET | /shipping/profile | Bearer | Baca profil shipping |
PUT | /shipping/profile | Bearer | Simpan profil shipping |
POST | /shipping/quote | API key | Hitung ongkir / COD |
GET | /shipping/areas | API key | Cari wilayah |
POST | /shipping/orders | API key | Buat resi pengiriman |
GET | /shipping/orders/:id | API key | Detail pesanan pengiriman |
GET | /shipping/orders/:id/tracking | API key | Lacak paket & riwayat status |
x-api-key + id_merchant bersama-sama.
Bearer = token dari POST /auth/login (untuk dashboard & kelola profil).
Gunakan header berikut di setiap request API dari server backend website Anda:
x-api-key: {{API_KEY}}
id_merchant: {{MERCHANT_ID}}
Content-Type: application/json
pay.fncreative.org).| Endpoint | Header yang dipakai |
|---|---|
/shipping/quote, /shipping/areas, /shipping/orders/* | x-api-key + id_merchant |
/shipping/profile | Authorization: Bearer <session_token> |
/auth/login, /health | Tidak perlu auth |
x-api-key di JavaScript frontend publik, mobile app tanpa proteksi,
atau repository publik. Panggilan API shipping harus melalui backend Anda.
Profil disimpan per merchant. Server memakai profil ini saat request quote
tidak mengirim site_config atau item.
/api/shipping/profile
Baca profil shipping merchant yang sedang login
Authorization: Bearer <session_token>
{
"status": true,
"data": {
"origin_postal_code": "{{ORIGIN_POSTAL}}",
"origin_city": "{{ORIGIN_CITY}}",
"origin_province": "{{ORIGIN_PROVINCE}}",
"origin_label": "{{ORIGIN_LABEL}}",
"couriers": "{{COURIERS}}",
"package_weight_grams": {{PACKAGE_WEIGHT}},
"default_item_value": {{DEFAULT_ITEM_VALUE}},
"default_item_name": "{{DEFAULT_ITEM_NAME}}",
"cod": {
"enabled": false,
"fee": 0,
"local_city": "",
"area_note": "",
"eta": "Koordinasi via WhatsApp"
},
"profile_ready": {{PROFILE_READY}},
"updated_at": "..."
}
}
/api/shipping/profile
Simpan / update profil shipping
{
"origin_postal_code": "{{ORIGIN_POSTAL}}",
"origin_city": "{{ORIGIN_CITY}}",
"origin_province": "{{ORIGIN_PROVINCE}}",
"origin_label": "{{ORIGIN_LABEL}}",
"package_weight_grams": {{PACKAGE_WEIGHT}},
"default_item_value": {{DEFAULT_ITEM_VALUE}},
"default_item_name": "{{DEFAULT_ITEM_NAME}}",
"couriers": "{{COURIERS}}",
"cod": {
"enabled": true,
"fee": 0,
"local_city": "{{ORIGIN_CITY}}",
"area_note": "Area dalam kota",
"eta": "1-2 hari kerja"
}
}
| Field | Wajib | Keterangan |
|---|---|---|
origin_postal_code | Ya | Kode pos gudang/toko asal pengiriman |
origin_city | Tidak | Kota asal (label & dashboard) |
origin_province | Tidak | Provinsi asal |
origin_label | Tidak | Nama gudang/toko, tampil di response quote |
package_weight_grams | Tidak | Berat default paket (gram). Default: 300 |
default_item_value | Tidak | Nilai barang default (Rp). Default: 150000 |
default_item_name | Tidak | Nama barang default untuk perhitungan ongkir. Default: Paket |
couriers | Tidak | Kurir aktif, pisah koma: jne,jnt,sicepat |
cod | Tidak | Opsi COD lokal per kota merchant |
curl -X PUT {{BASE_URL}}/shipping/profile \
-H "Content-Type: application/json" \
-H "Authorization: Bearer SESSION_TOKEN" \
-d '{
"origin_postal_code": "{{ORIGIN_POSTAL}}",
"origin_city": "{{ORIGIN_CITY}}",
"origin_province": "{{ORIGIN_PROVINCE}}",
"package_weight_grams": {{PACKAGE_WEIGHT}},
"default_item_value": {{DEFAULT_ITEM_VALUE}},
"couriers": "{{COURIERS}}"
}'
{
"status": true,
"message": "Profil shipping disimpan.",
"data": {
"origin_postal_code": "{{ORIGIN_POSTAL}}",
"profile_ready": {{PROFILE_READY}},
...
}
}
/api/shipping/quote
Hitung tarif ekspedisi atau COD lokal
x-api-key: {{API_KEY}}
id_merchant: {{MERCHANT_ID}}
Content-Type: application/json
Profil shipping sudah diisi di dashboard. Server otomatis pakai asal kirim, kurir, berat & nilai barang dari profil.
{
"method": "expedition",
"city": "Jakarta Selatan",
"province": "DKI Jakarta",
"postal_code": "12190"
}
Gunakan jika satu merchant punya beberapa gudang, atau berat/nilai berbeda per produk.
{
"method": "expedition",
"city": "Jakarta Selatan",
"province": "DKI Jakarta",
"postal_code": "12190",
"site_config": {
"originPostalCode": "{{ORIGIN_POSTAL}}",
"couriers": "{{COURIERS}}",
"packageWeightGrams": {{PACKAGE_WEIGHT}}
},
"item": {
"name": "Jaket Premium",
"value": 450000,
"weight": 800,
"quantity": 1
}
}
| Field | Sumber |
|---|---|
originPostalCode | Request site_config → jika kosong, pakai profil merchant |
couriers | Request → profil |
item.weight | Request item → package_weight_grams profil |
item.value | Request item → default_item_value profil |
item.name | Request item → default_item_name profil |
curl -X POST {{BASE_URL}}/shipping/quote \
-H "Content-Type: application/json" \
-H "x-api-key: {{API_KEY}}" \
-H "id_merchant: {{MERCHANT_ID}}" \
-d '{
"method": "expedition",
"city": "Jakarta Selatan",
"province": "DKI Jakarta",
"postal_code": "12190"
}'
{
"status": true,
"data": {
"ok": true,
"method": "expedition",
"provider": "live",
"origin": "{{ORIGIN_LABEL}}",
"destination": "Jakarta Selatan, DKI Jakarta, 12190",
"weight_grams": {{PACKAGE_WEIGHT}},
"rates": [
{
"id": "fn:jne:reg",
"courier": "JNE",
"service": "Reguler",
"courier_code": "jne",
"service_code": "reg",
"price": 21290,
"eta": "2 - 3 days",
"provider": "live"
}
],
"profile": {
"origin_postal_code": "{{ORIGIN_POSTAL}}",
"origin_label": "{{ORIGIN_LABEL}}",
"package_weight_grams": {{PACKAGE_WEIGHT}},
"default_item_value": {{DEFAULT_ITEM_VALUE}}
}
}
}
rates[].price sebagai ongkir final di checkout — jangan hitung ulang di client.
Simpan rates[].id bersama rates[].price untuk referensi saat buat resi.
Aktifkan di profil merchant (cod.enabled: true) lalu panggil:
{
"method": "cod",
"city": "Kota Surabaya",
"province": "Jawa Timur"
}
COD hanya tersedia jika kota tujuan cocok dengan cod.local_city di profil.
| Field | Wajib | Keterangan |
|---|---|---|
method | Ya | expedition atau cod |
city | Ya* | Kota tujuan (*wajib untuk expedition) |
province | Ya* | Provinsi tujuan |
postal_code | Ya* | Kode pos tujuan (wajib expedition) |
site_config | Tidak | Override asal/kurir — jika kosong pakai profil |
item | Tidak | Override berat/nilai/nama barang |
/api/shipping/areas?input=Kediri
Autocomplete wilayah Indonesia. Opsional — untuk form alamat di frontend.
x-api-key: {{API_KEY}}
id_merchant: {{MERCHANT_ID}}
curl "{{BASE_URL}}/shipping/areas?input=Surabaya" \
-H "x-api-key: {{API_KEY}}" \
-H "id_merchant: {{MERCHANT_ID}}"
{
"status": true,
"data": {
"ok": true,
"areas": [
{
"id": "IDNP6IDNC148IDND843IDZ11450",
"name": "Surabaya, Surabaya, Jawa Timur",
"city": "Surabaya",
"province": "Jawa Timur",
"district": "Surabaya",
"postal_code": "60111"
}
]
}
}
Panggil endpoint ini setelah pembayaran customer lunas (bukan saat checkout awal).
Wajib gunakan rate_id dari hasil POST /shipping/quote — format:
fn:<kurir>:<layanan> (contoh: fn:jne:reg).
/api/shipping/orders
Buat pesanan pengiriman & resi kurir
x-api-key: {{API_KEY}}
id_merchant: {{MERCHANT_ID}}
Content-Type: application/json
{
"reference_id": "INV-2026-00042",
"rate_id": "fn:jne:reg",
"destination": {
"contact_name": "Budi Santoso",
"contact_phone": "081234567890",
"contact_email": "budi@example.com",
"address": "Jl. Sudirman No. 1, Kebayoran Baru",
"postal_code": "12190",
"city": "Jakarta Selatan",
"province": "DKI Jakarta",
"note": "Patokan: gerbang hijau"
},
"order_note": "Pre-order — jangan dibanting",
"collection_method": "pickup",
"items": [
{
"name": "{{DEFAULT_ITEM_NAME}}",
"value": {{DEFAULT_ITEM_VALUE}},
"weight": {{PACKAGE_WEIGHT}},
"quantity": 1
}
]
}
| Field | Wajib | Keterangan |
|---|---|---|
reference_id | Disarankan | ID order di sistem Anda (unik). Jika dikirim ulang dengan ID sama, server mengembalikan resi yang sudah ada (idempoten). |
rate_id | Ya | Dari rates[].id hasil quote. Jangan ubah manual. |
destination.contact_name | Ya | Nama penerima |
destination.contact_phone | Ya | Nomor HP penerima (format Indonesia) |
destination.contact_email | Tidak | Email penerima (opsional) |
destination.address | Ya | Alamat lengkap tujuan |
destination.postal_code | Ya | Kode pos tujuan (5 digit) |
destination.city | Tidak | Kota (label & arsip) |
destination.province | Tidak | Provinsi (label & arsip) |
order_note | Tidak | Catatan untuk kurir |
collection_method | Tidak | pickup (default) atau drop_off |
items[] | Tidak | Detail paket. Jika kosong, pakai default dari profil merchant. |
Asal pengiriman (kode pos, alamat gudang) diambil otomatis dari profil merchant Anda — tidak perlu dikirim ulang di setiap request.
curl -X POST {{BASE_URL}}/shipping/orders \
-H "Content-Type: application/json" \
-H "x-api-key: {{API_KEY}}" \
-H "id_merchant: {{MERCHANT_ID}}" \
-d '{
"reference_id": "INV-2026-00042",
"rate_id": "fn:jne:reg",
"destination": {
"contact_name": "Budi Santoso",
"contact_phone": "081234567890",
"address": "Jl. Sudirman No. 1",
"postal_code": "12190"
}
}'
{
"status": true,
"data": {
"id": "INV-2026-00042",
"reference_id": "INV-2026-00042",
"status": "processing",
"courier": "JNE",
"service": "reg",
"rate_id": "fn:jne:reg",
"waybill_id": "",
"tracking_id": "",
"price": 21290,
"origin_postal_code": "{{ORIGIN_POSTAL}}",
"destination": { "contact_name": "Budi Santoso", "postal_code": "12190", "..." : "..." },
"tracking_events": [],
"provider": "live",
"created_at": "2026-06-30T10:00:00.000Z",
"updated_at": "2026-06-30T10:00:00.000Z"
}
}
waybill_id (nomor resi) mungkin masih kosong langsung setelah pembuatan — normal.
Cek lagi via endpoint lacak status beberapa menit kemudian.
/api/shipping/orders/:id
Detail pesanan pengiriman
:id = reference_id yang Anda kirim saat membuat resi (atau id di response).
Hanya order milik merchant Anda yang dapat diakses.
{
"status": true,
"data": {
"id": "INV-2026-00042",
"status": "shipped",
"waybill_id": "JP1234567890",
"tracking_id": "...",
"courier": "JNE",
"..."
}
}
cod). Endpoint /shipping/orders hanya untuk ekspedisi.
/api/shipping/orders/:id/tracking
Status terbaru + riwayat perjalanan paket
x-api-key: {{API_KEY}}
id_merchant: {{MERCHANT_ID}}
{
"status": true,
"data": {
"order": {
"id": "INV-2026-00042",
"status": "shipped",
"waybill_id": "JP1234567890",
"courier": "JNE",
"tracking_events": [ "..." ]
},
"tracking": {
"status": "shipped",
"waybill_id": "JP1234567890",
"events": [
{
"status": "shipped",
"note": "Paket telah dijemput kurir",
"location": "Kediri",
"updated_at": "2026-06-30T14:00:00.000Z"
}
]
}
}
}
status)| Status API | Arti untuk customer |
|---|---|
pending | Resi dibuat, menunggu konfirmasi kurir |
processing | Diproses / menunggu pickup |
shipped | Paket dalam perjalanan |
delivered | Sampai tujuan |
cancelled | Dibatalkan |
reference_id saat membuat resi.GET /shipping/orders/:id/tracking setiap 15–30 menit, atau saat customer membuka halaman lacak pesanan.waybill_id sebagai nomor resi ke customer.tracking.events[] sebagai timeline status.
Setiap opsi kurir di rates[] menyertakan field price — ini adalah
ongkir final yang harus dipakai merchant di checkout dan disimpan ke database order.
| Field | Arti |
|---|---|
rates[].id | ID kurir/layanan — simpan untuk POST /shipping/orders |
rates[].price | Ongkir final — tampilkan ke customer & simpan sebagai shipping_cost |
rates[].courier / service | Label kurir untuk tampilan UI (opsional) |
rates[].eta | Estimasi tiba (opsional, untuk tampilan) |
rates[].price persis seperti yang dikembalikan API.
Semua response memakai envelope: sukses {"status":true,"data":{...}}, gagal {"status":false,"message":"..."}.
| HTTP | Code | Penyebab & solusi |
|---|---|---|
| 401 | — | Kredensial salah / belum login. Periksa x-api-key + id_merchant atau login ulang. |
| 403 | merchant_pending_approval |
Akun belum disetujui admin. Tunggu approval sebelum API aktif. |
| 422 | profile_incomplete |
Profil shipping belum diisi. Login ke dashboard → isi origin_postal_code. |
| 422 | insufficient_balance |
Saldo FN Shipping tidak cukup. Top-up di dashboard merchant. |
| 422 | origin_not_configured |
Kode pos asal kosong di profil dan tidak dikirim di request. |
| 404 | not_found |
Pesanan pengiriman tidak ditemukan — periksa reference_id dan pastikan dibuat dengan merchant yang sama. |
| 400 | — | Validasi input gagal (kota/provinsi/kode pos kosong, rate_id invalid, dll.). |
| 429 | — | Terlalu banyak request. Coba lagi nanti. |
| 500 | — | Kesalahan server atau layanan pengiriman sementara tidak tersedia. |
HTTP/1.1 422 Unprocessable Entity
{
"status": false,
"code": "profile_incomplete",
"message": "Profil shipping belum lengkap. Login ke dashboard FN Shipping dan isi kode pos asal pengiriman terlebih dahulu."
}
Alur standar e-commerce / pre-order dengan FN Payment + FN Shipping:
POST /shipping/quote → pilih kurir → simpan rate_id + pricerate.price → QRIS via FN Payment APIPOST /shipping/orderswaybill_id → polling GET /shipping/orders/:id/tracking// .env server — JANGAN di frontend publik
// FN_SHIPPING_API_URL={{BASE_URL}}
// FN_API_KEY={{API_KEY}}
// FN_MERCHANT_ID={{MERCHANT_ID}}
const FN_SHIPPING = process.env.FN_SHIPPING_API_URL || "{{BASE_URL}}";
const headers = {
"content-type": "application/json",
"x-api-key": process.env.FN_API_KEY,
"id_merchant": process.env.FN_MERCHANT_ID,
};
async function fnShipping(path, { method = "GET", body } = {}) {
const res = await fetch(`${FN_SHIPPING}${path}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
const json = await res.json();
if (!res.ok || !json.status) throw new Error(json.message || "FN Shipping error");
return json.data;
}
// Langkah A — saat customer isi alamat checkout
async function getRates(city, province, postalCode) {
const data = await fnShipping("/shipping/quote", {
method: "POST",
body: {
method: "expedition",
city,
province,
postal_code: postalCode,
},
});
return data.rates; // tampilkan ke customer, simpan rates[].id + rates[].price
}
// Langkah C — setelah pembayaran LUNAS (webhook payment atau cek status)
async function createShipment(order) {
return fnShipping("/shipping/orders", {
method: "POST",
body: {
reference_id: order.id,
rate_id: order.shipping_rate_id, // dari langkah A
destination: {
contact_name: order.customer_name,
contact_phone: order.customer_phone,
contact_email: order.customer_email,
address: order.shipping_address,
postal_code: order.shipping_postal,
city: order.shipping_city,
province: order.shipping_province,
},
order_note: `Order ${order.id}`,
},
});
}
// Langkah D — halaman lacak pesanan customer
async function trackShipment(referenceId) {
const data = await fnShipping(`/shipping/orders/${encodeURIComponent(referenceId)}/tracking`);
return {
resi: data.tracking.waybill_id,
status: data.tracking.status,
timeline: data.tracking.events,
};
}
| Field | Kapan diisi |
|---|---|
shipping_rate_id | Saat customer pilih kurir (dari quote) |
shipping_cost | rates[].price dari quote — jangan hitung ulang |
shipping_courier / shipping_service | Label dari quote (opsional, untuk tampilan) |
fn_shipping_reference | reference_id saat buat resi |
waybill_id | Dari response tracking setelah resi terbit |
shipping_status | Dari tracking.status (polling) |
x-api-key di environment variable — rotasi jika bocor.shipping_rate_id dengan memanggil quote lagi sebelum terima pembayaran (hindari manipulasi harga ongkir).POST /orders) hanya setelah pembayaran benar-benar lunas.reference_id unik per order — aman untuk retry tanpa duplikasi resi.