—
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.
Domestik Indonesia memakai Biteship; ekspor ASEAN memakai DHL Express.
{{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.co.id) dipakai untuk payment dan shipping.
postal_code dari GET /shipping/areas dan isi otomatis ke
form. Kode pos hasil pencarian inilah yang dikirim ke /shipping/quote dan /shipping/orders,
sehingga ongkir tidak pernah salah karena kode pos yang diketik keliru.
x-api-key + id_merchant di .env server website Anda.GET /shipping/wallet/balance — pastikan cukup sebelum quote & buat resi.GET /shipping/areas untuk pilih wilayah — kode pos terisi otomatis, customer tidak perlu mengetiknya.POST /shipping/quote → tampilkan tarif → customer pilih kurir.POST /shipping/orders dengan rate_id dari langkah 4.GET /shipping/orders/:id/tracking.GET /shipping/orders/:id/label (HTML) atau /label.pdf (PDF).{{ORIGIN_POSTAL}}, berat default {{PACKAGE_WEIGHT}} gram.
Panduan singkat untuk merchant/toko yang baru bergabung FN Shipping — dari akun aktif sampai resi pertama tercetak. Contoh implementasi live: Dirty Klab, KRAZZYDROOGS.50.
origin_postal_code, kota asal, berat default paket..env di bawah).POST /shipping/orders.waybill_id muncul → unduh /label.pdf → tempel ke paket..env server merchant# FN Shipping (backend only)
FN_SHIPPING_API_URL={{BASE_URL}}
FN_MERCHANT_ID={{MERCHANT_ID}}
FN_API_KEY={{API_KEY}}
SHIPPING_VIA_FN_GATEWAY=true
# Opsional — branding label lokal jika fallback tanpa proxy gateway
BRAND_NAME=Nama Toko Anda
| Item | Fungsi |
|---|---|
id_merchant + x-api-key | Auth API dari backend toko |
| Dashboard shipping | Profil asal kirim, riwayat resi, top-up wallet |
| Quote multi-kurir | JNE, J&T, SiCepat, dll. — harga sudah final (termasuk layanan FN) |
| Resi kurir asli | waybill_id dari jaringan kurir via Biteship |
| Label enterprise | PDF/HTML 100×150 mm — desain sama semua merchant, nama toko sebagai pengirim |
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 |
GET | /shipping/wallet/balance | API key atau Bearer | Cek saldo wallet (server integrasi) |
POST | /shipping/quote | API key atau Bearer | Hitung ongkir / COD |
GET | /shipping/areas | API key atau Bearer | Cari wilayah + kode pos otomatis |
GET | /shipping/orders | API key atau Bearer | Daftar riwayat resi merchant |
POST | /shipping/orders | API key atau Bearer | Buat resi pengiriman |
GET | /shipping/orders/:id | API key atau Bearer | Detail pesanan pengiriman |
GET | /shipping/orders/:id/tracking | API key atau Bearer | Lacak paket & riwayat status |
GET | /shipping/orders/:id/label | API key atau Bearer | Label resi HTML (siap cetak) |
GET | /shipping/orders/:id/label.pdf | API key atau Bearer | Label resi PDF (100×150 mm) |
x-api-key + id_merchant bersama-sama (disarankan untuk integrasi backend).
Bearer = token dari POST /auth/login (dashboard, profil, dan bisa juga dipakai untuk quote/order/label).
https://ship.fncreative.co.id · API: https://ship.fncreative.co.id/api.
URL lama https://ship.fncreative.org/api tetap aktif (alias). Kredensial payment gateway: https://pay.fncreative.co.id/api (alias pay.fncreative.org/api).
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.co.id, API pay.fncreative.co.id).| Endpoint | Header yang dipakai |
|---|---|
/shipping/profile, /shipping/wallet (top-up UI) | Authorization: Bearer <session_token> |
/shipping/wallet/balance, /shipping/quote, /shipping/areas, /shipping/orders, /shipping/orders/* | x-api-key + id_merchant atau Authorization: Bearer <session_token> |
/auth/login, /health | Tidak perlu auth |
Cara A — API key (disarankan production backend):
x-api-key: {{API_KEY}}
id_merchant: {{MERCHANT_ID}}
Cara B — Bearer session (cocok untuk dashboard / script internal):
Authorization: Bearer <session_token_dari_POST_/auth/login>
id_merchant — merchant diidentifikasi dari sesi login.
Untuk website publik, selalu gunakan Cara A via backend proxy.
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}},
...
}
}
Setiap cek ongkir dan pembuatan resi memotong saldo wallet FN Shipping merchant. Gunakan endpoint ini dari backend sebelum quote/booking — terutama saat automasi checkout.
/api/shipping/wallet/balance
Cek saldo wallet (server integrasi)
x-api-key: {{API_KEY}}
id_merchant: {{MERCHANT_ID}}
atau
Authorization: Bearer <session_token>
{
"status": true,
"data": {
"balance": 85000,
"min_topup": 10000,
"low_balance": false,
"low_threshold": 20000,
"dashboard_url": "https://ship.fncreative.co.id/dashboard"
}
}
| Field | Arti |
|---|---|
balance | Saldo tersedia (Rp) |
low_balance | true jika saldo di bawah low_threshold |
low_threshold | Batas peringatan saldo rendah (default Rp 20.000) |
min_topup | Minimal nominal top-up via dashboard |
dashboard_url | Link top-up saldo (UI dashboard, bukan API) |
curl "{{BASE_URL}}/shipping/wallet/balance" \
-H "x-api-key: {{API_KEY}}" \
-H "id_merchant: {{MERCHANT_ID}}"
422 insufficient_balance.
GET /shipping/wallet (detail + riwayat) hanya untuk UI dashboard (Bearer).
/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). Ambil otomatis dari GET /shipping/areas — tidak perlu diketik customer. |
site_config | Tidak | Override asal/kurir — jika kosong pakai profil |
item | Tidak | Override berat/nilai/nama barang |
destination_country | Tidak | ISO2 tujuan. Default ID (domestik). Ekspor ASEAN: SG, MY, TH, VN, PH, BN, KH, LA, MM, TL. Asal selalu Indonesia. COD tidak tersedia untuk ekspor. |
Kirim destination_country plus kota & kode pos negara tujuan. Autocomplete /shipping/areas hanya untuk Indonesia — untuk ekspor isi kota/kode pos secara langsung. Kurir ekspor: DHL Express (bukan Biteship). COD tidak tersedia. Sertakan deskripsi & nilai barang (bea cukai); item.hs_code opsional.
{
"method": "expedition",
"destination_country": "SG",
"city": "Singapore",
"postal_code": "018956",
"item": { "name": "Jaket Premium", "value": 450000, "weight": 800 }
}
Response rate ekspor memakai zone: "export", country, dan currency: "IDR". Contoh rate_id: fn:dhl:P (Express Worldwide). Domestik Indonesia tetap Biteship. Tarif DHL Express biasanya jauh lebih mahal dari JNE domestik. Quote ekspor membutuhkan kredensial MyDHL di server FN Shipping.
postal_code untuk setiap wilayah, jadi customer tidak perlu mengetik kode pos manual —
cukup memilih wilayahnya. Backend Anda menyimpan postal_code dari wilayah terpilih, lalu memakainya
untuk POST /shipping/quote dan POST /shipping/orders.
/api/shipping/areas?input=Kediri
Autocomplete wilayah Indonesia sekaligus sumber kode pos tujuan.
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"
}
]
}
}
Sediakan satu kolom pencarian wilayah (atau dropdown provinsi → kota → kecamatan).
Saat customer memilih salah satu hasil, simpan city, province, dan
postal_code dari objek wilayah tersebut. Kolom kode pos di form cukup dibuat
hidden atau read-only.
// 1) Customer mengetik nama kelurahan/kecamatan/kota
const res = await fetch(
`{{BASE_URL}}/shipping/areas?input=${encodeURIComponent(keyword)}`,
{ headers: { "x-api-key": process.env.FN_API_KEY, id_merchant: process.env.FN_MERCHANT_ID } }
);
const { data } = await res.json();
// 2) Customer memilih salah satu wilayah dari data.areas
const area = data.areas[selectedIndex];
// 3) Kode pos terisi otomatis — tidak diketik customer
form.receiver_city.value = area.city;
form.receiver_province.value = area.province;
form.receiver_postal.value = area.postal_code; // hidden / read-only
// 4) Pakai nilai itu untuk hitung ongkir
await fetch("{{BASE_URL}}/shipping/quote", {
method: "POST",
headers: {
"content-type": "application/json",
"x-api-key": process.env.FN_API_KEY,
id_merchant: process.env.FN_MERCHANT_ID,
},
body: JSON.stringify({
method: "expedition",
city: area.city,
province: area.province,
postal_code: area.postal_code,
}),
});
x-api-key tidak terekspos ke publik.
postal_code tetap wajib dikirim pada /shipping/quote
dan /shipping/orders — yang otomatis adalah cara mendapatkannya, bukan boleh dikosongkan.
Kelola pesanan pengiriman: buat resi baru, lihat riwayat, detail, lacak, dan unduh label.
Auth: x-api-key + id_merchant atau Authorization: Bearer.
/api/shipping/orders
Daftar riwayat resi merchant (paginated)
| Param | Contoh | Fungsi |
|---|---|---|
limit | 50 | Maks. baris (default 50) |
offset | 0 | Offset pagination |
month | 2026-06 | Filter bulan (YYYY-MM) |
status | shipped | Filter status |
q | JNE | Cari resi, nama, kota, reference_id |
{
"status": true,
"data": {
"orders": [
{
"reference_id": "INV-2026-00042",
"waybill_id": "1408512600009096",
"courier": "JNE",
"service": "reg",
"recipient_name": "Budi Santoso",
"recipient_phone": "081234567890",
"destination_summary": "Budi · Jakarta Selatan · DKI Jakarta · 12190",
"price": 21290,
"status": "shipped",
"created_at": "2026-06-30T10:00:00.000Z"
}
],
"total": 1,
"offset": 0,
"limit": 50,
"has_more": false,
"summary": { "total": 1, "by_status": { "shipped": 1 } }
}
}
curl "{{BASE_URL}}/shipping/orders?limit=20&month=2026-06" \
-H "x-api-key: {{API_KEY}}" \
-H "id_merchant: {{MERCHANT_ID}}"
reference_id sendiri —
polling detail/tracking cukup via GET /shipping/orders/:id.
/api/shipping/orders
Buat pesanan pengiriman & resi kurir — panggil setelah pembayaran lunas
Wajib gunakan rate_id dari POST /shipping/quote (format fn:jne:reg).
Cek saldo via GET /shipping/wallet/balance sebelum booking.
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). Pakai nilai dari GET /shipping/areas yang dipilih customer. |
destination.city | Tidak | Kota (label & arsip) |
destination.province | Tidak | Provinsi (label & arsip). Untuk ekspor boleh dikosongkan. |
destination.country | Tidak | ISO2 tujuan. Default ID. Untuk ekspor ASEAN isi SG/MY/TH/dll. Bisa juga kirim destination_country di root request. |
quoted_price / shipping_cost | Disarankan | Harga ongkir saat checkout (rates[].price). Dipakai validasi saat booking — jika drift > 8% dari tarif live, sistem menyesuaikan otomatis. |
order_note | Tidak | Catatan internal (disimpan di data order). Tidak dicetak di label fisik resi. |
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.
price_adjusted, checkout_price, dan debit_price
jika tarif live berbeda dari harga checkout. Gunakan debit_price sebagai ongkir final yang didebit wallet.
/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.FN Shipping menyediakan label resi bawaan dengan desain standar AWB 100×150 mm: logo kurir resmi, barcode, alamat penerima/pengirim, dan branding FN CREATIVE di footer. Merchant tidak perlu desain label sendiri — cukup panggil endpoint di bawah dari backend.
/api/shipping/orders/:id/label
Label HTML siap cetak di browser
x-api-key: {{API_KEY}}
id_merchant: {{MERCHANT_ID}}
| Param | Nilai | Fungsi |
|---|---|---|
print | 1 | Buka dialog cetak otomatis saat halaman dimuat |
Content-Type: text/html — halaman label lengkap dengan barcode (JsBarcode).
curl "{{BASE_URL}}/shipping/orders/INV-2026-00042/label" \
-H "x-api-key: {{API_KEY}}" \
-H "id_merchant: {{MERCHANT_ID}}"
/api/shipping/orders/:id/label.pdf
Unduh label sebagai PDF (disarankan untuk admin & thermal printer)
x-api-key: {{API_KEY}}
id_merchant: {{MERCHANT_ID}}
Content-Type: application/pdf — ukuran kertas 100×150 mm, logo kurir embedded, barcode server-side.
Filename: label-resi-{reference_id}.pdf
curl "{{BASE_URL}}/shipping/orders/INV-2026-00042/label.pdf" \
-H "x-api-key: {{API_KEY}}" \
-H "id_merchant: {{MERCHANT_ID}}" \
-o label-resi.pdf
| Bagian | Sumber data |
|---|---|
| Logo kurir + layanan (REG/EZ/dll.) | courier, service dari order |
| Barcode + nomor resi | waybill_id — muncul setelah kurir terbit resi |
| Kode rute kurir | courier_routing_code (contoh 470-JOG04-33) di bawah barcode |
| Alamat penerima | destination saat booking |
| Alamat pengirim | Profil merchant (origin_label, kode pos asal) |
| Grid: Berat · Referensi | Berat paket + reference_id order Anda |
| Grid: Isi paket · Kode rute | Detail item + kode rute kurir |
Footer fncreative.co.id | Teks branding FN (tanpa logo) + badge status bayar |
order_note) — disimpan di API, tidak di label| Query | Fungsi |
|---|---|
?mask=1 | Masking sebagian nama & nomor HP penerima (untuk label customer-facing) |
?print=1 | HTML label auto-trigger cetak |
POST /shipping/orders berhasil.
Jika waybill_id masih kosong, label tetap bisa diunduh — barcode muncul otomatis
setelah resi terbit (panggil ulang endpoint yang sama).
x-api-key, jangan buka URL label langsung di browser customer.
Buat endpoint proxy di backend Anda, contoh: GET /admin/orders/:id/label.pdf
yang meneruskan request ke FN Shipping dengan kredensial server.
// Express — unduh label PDF untuk admin toko
app.get("/admin/shipping/:ref/label.pdf", requireAdmin, async (req, res) => {
const upstream = await fetch(
`${process.env.FN_SHIPPING_API_URL}/shipping/orders/${encodeURIComponent(req.params.ref)}/label.pdf`,
{ headers: {
"x-api-key": process.env.FN_API_KEY,
"id_merchant": process.env.FN_MERCHANT_ID,
}}
);
if (!upstream.ok) return res.status(upstream.status).send(await upstream.text());
res.setHeader("Content-Type", "application/pdf");
res.setHeader("Content-Disposition", upstream.headers.get("content-disposition") || "attachment");
res.send(Buffer.from(await upstream.arrayBuffer()));
});
Setiap opsi kurir di rates[] menyertakan price — ini adalah
ongkir final untuk customer dan wallet merchant. Jangan hitung ulang di sisi toko.
| Field | Arti |
|---|---|
rates[].id | ID kurir/layanan — simpan untuk POST /shipping/orders |
rates[].price | Ongkir final — tampilkan ke customer & simpan sebagai shipping_cost |
rates[].base_price | Biaya kurir sebelum markup (audit/internal) |
rates[].markup_amount | Selisih layanan FN (price − base_price) |
service_fee | Info markup aktif: percent, check_fee per quote |
check_fee_charged | Biaya cek ongkir yang didebit wallet pada request quote ini |
rates[].courier / service | Label kurir untuk UI |
rates[].eta | Estimasi tiba |
| Komponen | Contoh |
|---|---|
| Biaya kurir (Biteship) | Rp 7.000 |
| Markup layanan FN (12%) | Rp 840 |
| Biaya cek ongkir (per quote) | Rp 10 |
Harga ke customer (rates[].price) | Rp 7.850 |
POST /shipping/orders), sistem melakukan re-quote live.
Jika harga checkout menyimpang > 8% dari tarif live, wallet didebit tarif live + markup (response price_adjusted: true).
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 memang ditolak. Periksa x-api-key + id_merchant atau login ulang.
Status ini tidak lagi dipakai untuk gangguan sementara — jika verifikasi akun
tidak bisa dijalankan, yang keluar adalah 503 di bawah.
|
| 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. |
| 503 | merchant_auth_unavailable |
Kredensial Anda kemungkinan besar valid, tetapi layanan verifikasi akun sedang dibatasi
atau tidak dapat dihubungi. Tunggu sesuai Retry-After /
retry_after_seconds lalu ulangi request yang sama — jangan mengganti API key
atau memaksa login ulang.
|
| 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."
}
HTTP/1.1 503 Service Unavailable
Retry-After: 29
{
"status": false,
"code": "merchant_auth_unavailable",
"message": "Verifikasi akun sedang dibatasi (rate limit). Coba lagi sebentar lagi.",
"retry_after_seconds": 29
}
503 sebagai gangguan sementara: ulangi request dengan jeda
(backoff) dan tampilkan pesan netral seperti "cek ongkir sedang sibuk, coba sesaat lagi" ke customer —
bukan pesan minta login.
Alur standar e-commerce / pre-order dengan FN Payment + FN Shipping:
GET /shipping/wallet/balance sebelum quote/bookingGET /shipping/areas → postal_code terisi otomatisPOST /shipping/quote → pilih kurir → simpan rate_id + pricerate.price → QRIS via FN Payment APIPOST /shipping/orderswaybill_id → polling GET /shipping/orders/:id/trackingGET /shipping/orders/:id/label.pdf via backend admin (proxy)// .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 — cek saldo sebelum operasi shipping
async function ensureWalletBalance(min = 20000) {
const data = await fnShipping("/shipping/wallet/balance");
if ((data.balance || 0) < min) {
throw new Error(`Saldo FN Shipping rendah: Rp ${data.balance}. Top-up di dashboard.`);
}
return data;
}
// Langkah B — 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,
quoted_price: order.shipping_cost,
shipping_cost: order.shipping_cost,
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,
},
},
});
}
// 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,
};
}
// Langkah E — unduh label PDF untuk admin/gudang (server-side)
async function downloadLabelPdf(referenceId) {
const res = await fetch(
`${FN_SHIPPING}/shipping/orders/${encodeURIComponent(referenceId)}/label.pdf`,
{ headers: { "x-api-key": process.env.FN_API_KEY, "id_merchant": process.env.FN_MERCHANT_ID } }
);
if (!res.ok) throw new Error("Gagal unduh label PDF");
return Buffer.from(await res.arrayBuffer());
}
| 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.