Endpoint ini dipakai sistem lain untuk **membaca satu pesanan** dan **memindahkan statusnya antara `pending` dan `closing`**. Contoh pemakaian yang paling umum: aplikasi closing atau CRM tempat CS menutup deal, lalu status di AdForm ikut berpindah tanpa ada yang mengetik ulang. Semua panggilan memakai **kunci API** yang diterbitkan penjual sendiri dari dashboard. Anda tidak pernah meminta kunci lewat WhatsApp, email, atau chat. ## Rangkuman satu layar | Item | Nilai | |---|---| | Alamat | `https:///api/order_status.php` | | Method | `GET` (baca), `PATCH` (ubah status) | | Auth | Header `X-API-Key: adk_…` | | Scope baca | `orders:read` (+ `customers:read` bila butuh data pelanggan) | | Scope ubah | `orders.status:write` | | Status yang boleh ditulis | `pending`, `closing` — tidak ada yang lain | | Batas laju | 120 permintaan per menit per kunci | | Idempotensi | Header `Idempotency-Key` (opsional, sangat disarankan untuk `PATCH`) | | Content-Type | `application/json; charset=utf-8` | ## Cara penjual menerbitkan kunci Penjual masuk ke dashboard AdForm → **Pengaturan → Developer → API Keys** → **Buat API key**: 1. **Nama konektor** — isi nama sistem yang akan memakainya, misalnya `CRM Closing`. Nama ini ikut terkirim ke webhook sebagai penanda siapa yang mengubah status, jadi pakai nama yang mudah dikenali penjual sendiri. **Harus berbeda dari key lain yang masih aktif di toko itu** — kalau sudah dipakai, penjual menerima penolakan `409` dan diminta memilih nama lain. 2. **Kegunaan** — pilih **Ubah status pesanan**. Kalau sistem Anda juga perlu nama, nomor, atau alamat pembeli, penjual memilih **Ubah status pesanan + baca data pelanggan**. 3. **Kedaluwarsa** — pilih tanggal secara sengaja. Nilai kunci tampil **satu kali** setelah dibuat. Penjual menyalinnya lalu memasukkannya ke sistem Anda sendiri. Penjual bisa mencabut kunci kapan saja dari halaman yang sama. Kolom **Terakhir dipakai** membantunya memastikan sebuah kunci sudah tidak terpakai sebelum dicabut. ## Izin yang diberikan pilihan "Ubah status pesanan" Kunci dengan kegunaan ini mendapat tepat dua izin: `orders:read` dan `orders.status:write`. Yang **bisa** dilakukan: - membaca satu pesanan berdasarkan nomornya; - memindahkan status pesanan antara `pending` dan `closing`. Yang **tidak bisa** dilakukan: - **membaca nama, nomor WhatsApp, surel, atau alamat pembeli** — itu izin terpisah, lihat di bawah; - menandai pesanan lunas, dikirim, selesai, dibatalkan, atau nomor tidak valid; - mengubah harga, jumlah, alamat, atau data pelanggan; - menghapus pesanan; - membuat pesanan baru; - membaca daftar pesanan, produk, atau anggota tim. ### Data pelanggan adalah izin terpisah `orders:read` berarti **boleh membaca pesanan** — produk, jumlah, total, ongkir, kurir, resi, status, waktu dibuat. Blok `customer` (nama, nomor, surel, alamat lengkap) hanya ikut kalau kunci **juga** memegang `customers:read`. Kalau Anda hanya menyelaraskan status, Anda **tidak perlu** izin itu. Rekonsiliasi berjalan penuh tanpa satu pun kolom pribadi, dan penjual tidak perlu menyerahkan data pembelinya untuk sesuatu yang tidak memerlukannya. **Jalur tulis tidak pernah mengirim data pelanggan**, bahkan untuk kunci yang memegang `customers:read`. Yang perlu membaca pesanan memakai `GET`. > `orders.status:write` sengaja terpisah dari `orders:write`. Kunci lama yang sudah terbit dengan `orders:write` **tidak** ikut mendapat kemampuan ubah-status saat endpoint ini dipasang. ## Arti status di AdForm — baca ini sebelum menulis kode AdForm punya **satu** kolom status. Ia mencampur status pembayaran dan tahap penanganan dalam satu nilai. Tidak ada kolom terpisah untuk "sudah dibayar" dan "sudah dikirim". ### `paid` bukan tanda pembayaran berhasil Ini jebakan yang paling mahal, jadi kami tulis dengan angkanya. Ketika pembayaran otomatis lewat payment gateway berhasil, AdForm menulis status **`closing`**, bukan `paid`. Status `paid` hanya pernah ditulis kalau penjual menandainya sendiri dari dashboard. Sebaran nyata seluruh pesanan di AdForm, diukur 27 Agustus 2026 dari 83 basis data toko, 57.323 baris: | Status | Jumlah baris | Porsi | |---|---:|---:| | `pending` | 40.945 | 71,43% | | `closing` | 13.597 | 23,72% | | `invalid_no_hp` | 1.320 | 2,30% | | `completed` | 677 | 1,18% | | **`paid`** | **398** | **0,69%** | | `shipped` | 208 | 0,36% | | `cancelled` | 178 | 0,31% | **`paid` adalah 398 baris dari 57.323.** Sistem yang menunggu `paid` sebagai tanda pembayaran masuk akan menunggu sesuatu yang praktis tidak pernah terjadi. ### Sisi baiknya 95,15% pesanan berada di `pending` atau `closing` — dua status yang endpoint ini izinkan Anda tulis. Batasan yang terlihat sempit itu mencakup hampir seluruh pesanan yang benar-benar ada. ### Tujuh nilai yang mungkin `pending`, `closing`, `invalid_no_hp`, `paid`, `cancelled`, `shipped`, `completed`. Anda **boleh menulis** dua: `pending` dan `closing`. Anda **akan membaca** ketujuhnya lewat `GET`. Perlakukan lima sisanya sebagai keadaan yang hanya bisa diubah penjual dari dashboard-nya. ## `GET` — baca satu pesanan ```http GET /api/order_status.php?id=1287 X-API-Key: adk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` ```json { "success": true, "order": { "id": 1287, "status": "pending", "product": "Kopi Gayo 250g", "variant": "Bubuk", "quantity": 2, "total": 213000, "shipping_fee": 15000, "payment_method": "cod", "courier": "JNE", "resi": null, "_catatan": "blok customer di bawah HANYA ikut kalau kunci memegang customers:read", "customer": { "name": "Rina", "phone": "628111000111", "email": null, "address": "Jl. Merdeka No. 10 RT 02 RW 05", "village": "Dago", "district": "Coblong", "city": "Bandung", "province": "Jawa Barat", "postal_code": "40135" }, "created_at": "2026-08-26T09:14:00+07:00" } } ``` Daftar field di atas **tetap**. Kolom baru yang ditambahkan AdForm di kemudian hari tidak otomatis ikut terkirim. Field `_catatan` di contoh itu bukan bagian jawaban sungguhan — hanya penanda di dokumen ini. Jawaban juga membawa `request_id` di tingkat teratas; sebutkan nilai itu kalau Anda melaporkan masalah. ## `PATCH` — ubah status ```http PATCH /api/order_status.php X-API-Key: adk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Idempotency-Key: spc-1287-closing-01 Content-Type: application/json { "id": 1287, "status": "closing", "expected_previous_status": "pending", "external_reference": "SPC-8871" } ``` | Field | Wajib | Keterangan | |---|---|---| | `id` | ya | Nomor pesanan AdForm | | `status` | ya | `pending` atau `closing` | | `expected_previous_status` | tidak | Status yang Anda kira sedang berlaku. Kalau tidak cocok, permintaan ditolak `409` dan tidak ada yang berubah | | `external_reference` | tidak | Nomor rujukan di sistem Anda, maksimal 120 karakter. Diteruskan ke webhook | Jawaban berhasil: ```json { "order_id": 1287, "previous_status": "pending", "status": "closing", "updated_at": "2026-08-27T14:31:00+07:00", "request_id": "9f2c41ab", "changed": true } ``` Keberhasilan ditandai kode HTTP `200`. Jawaban ini **tidak** membawa data pelanggan — Anda menyatakan tidak memerlukannya pada jalur tulis, dan kami tidak mengirimkannya. | Field | Arti | |---|---| | `order_id` | Nomor pesanan yang diubah | | `previous_status` | Status sebelum permintaan ini | | `status` | Status sesudahnya | | `updated_at` | Waktu perubahan diterapkan (ISO 8601, zona server). **`null` kalau `changed` bernilai `false`** — tidak ada perubahan berarti tidak ada waktu perubahan | | `request_id` | Penanda permintaan, 8 karakter heksadesimal. Sebutkan saat melapor masalah | | `changed` | `false` berarti pesanan **sudah** berada di status yang Anda minta. Itu keberhasilan, bukan kegagalan | **Tidak semua jawaban gagal membawa `request_id`.** Yang berasal dari endpoint ini membawanya: `400`, `404`, `405`, `409`, `422`, `500`. Yang berasal dari lapisan bersama di depannya **tidak**, dan hanya memuat field `error`: | Status | Ditangani oleh | Bentuk badan | |---|---|---| | `401`, `403` | pemeriksa kunci API bersama | `{ "error": "..." }` | | `429` | pembatas laju bersama | `{ "error": "..." }` | Lapisan itu melayani seluruh endpoint ber-kunci-API, jadi bentuk jawabannya sengaja tidak diubah demi satu endpoint. Untuk kedua kelompok status tersebut, cabangkan program Anda pada **status HTTP**, bukan pada keberadaan `code` atau `request_id`. ### `expected_previous_status` — kapan dipakai Kirimkan kalau keputusan Anda didasarkan pada status yang Anda baca sebelumnya. Contoh: sistem Anda membaca pesanan pukul 10.00 (status `pending`), lalu CS menekan tombol closing pukul 10.05. Di antara dua waktu itu penjual mungkin sudah menandai pesanan tersebut lunas dari dashboard. Dengan `expected_previous_status: "pending"`, permintaan Anda ditolak alih-alih menimpa keputusan yang lebih baru. ## Idempotensi Kirim header `Idempotency-Key` pada setiap `PATCH`. Isinya bebas, 8–120 karakter dari `A-Z a-z 0-9 . _ : -`, dan **harus unik per permintaan** — misalnya `spc---`. Kalau permintaan Anda time-out dan Anda tidak tahu apakah ia sampai, kirim ulang **dengan kunci dan isi yang sama persis**. AdForm mengembalikan jawaban aslinya tanpa mengubah pesanan lagi, disertai header: ```http Idempotency-Replayed: true ``` Yang diputar ulang adalah jawaban **aslinya**, jadi `request_id` dan `updated_at` yang kembali adalah milik permintaan pertama. Itu memang yang benar: perubahannya terjadi saat itu, bukan saat kiriman ulang tiba. Aturannya: - **Kunci sama + isi sama** → jawaban asli diputar ulang. Pesanan tidak tersentuh. - **Kunci sama + isi berbeda** → ditolak `422 idempotency_key_reused`. Ini melindungi Anda: kunci yang tidak sengaja dipakai ulang untuk pesanan lain akan ketahuan, bukan diam-diam mengembalikan hasil pesanan pertama. - **Kunci sama, permintaan pertama masih berjalan** → `409 request_in_progress`. Tunggu beberapa detik lalu coba lagi. **Yang diingat hanyalah keberhasilan.** Setiap penolakan melepaskan catatannya, jadi kiriman ulang dengan kunci yang sama dinilai ulang dari awal. Contoh: permintaan Anda ditolak `422 closing_incomplete` karena alamat pesanan belum lengkap; penjual melengkapinya dari dashboard; Anda kirim ulang permintaan yang sama persis dan kali ini berhasil. Kalau penolakan ikut diingat, Anda akan menerima penolakan lama itu selama 30 hari meski datanya sudah benar. Urutan penulisan kunci JSON tidak berpengaruh: `{"id":1,"status":"closing"}` dan `{"status":"closing","id":1}` dianggap permintaan yang sama. **Masa simpan catatan idempotensi: 30 hari** sejak permintaan pertama. Setelah itu catatannya dihapus, dan kiriman ulang dengan kunci yang sama akan dijalankan sebagai permintaan baru. Ini disengaja — tidak ada pemanggil yang mengulang permintaan sebulan kemudian, dan menyimpannya selamanya berarti tabel yang tumbuh tanpa batas. ## Kode error Semua kegagalan menjawab JSON dengan field `error` (kalimat untuk manusia) dan `code` (penanda tetap untuk program Anda). | HTTP | `code` | Arti | Yang harus Anda lakukan | |---|---|---|---| | 400 | `invalid_json` | Body bukan JSON objek | Perbaiki permintaan | | 400 | `invalid_request` | `id` atau `status` kosong / bukan angka positif | Perbaiki permintaan | | 401 | — | Kunci tidak ada, salah bentuk, dicabut, atau kedaluwarsa | Minta penjual menerbitkan kunci baru | | 403 | — | Kunci sah tetapi tidak punya scope yang dibutuhkan | Minta penjual membuat kunci dengan kegunaan **Ubah status pesanan** | | 404 | `order_not_found` | Pesanan tidak ada di akun penjual ini | Jangan ulangi | | 405 | `method_not_allowed` | Method selain `GET`/`PATCH` | Perbaiki permintaan | | 409 | `status_conflict` | `expected_previous_status` tidak cocok dengan status sekarang | Baca ulang pesanannya, putuskan lagi | | 409 | `order_terminal` | Status sekarang sudah final (`paid`, `shipped`, `completed`, `cancelled`, `invalid_no_hp`) | Jangan ulangi. Status final milik penjual | | 409 | `request_in_progress` | Permintaan dengan kunci idempotensi sama sedang berjalan | Tunggu 5 detik, coba lagi | | 409 | `update_conflict` | Status berubah tepat saat diproses | Ulangi permintaan | | 422 | `status_not_allowed` | `status` di luar `pending`/`closing` | Jangan ulangi | | 422 | `expected_status_not_allowed` | `expected_previous_status` di luar `pending`/`closing`. Nilai itu tidak akan pernah cocok, jadi ditolak sekarang alih-alih jadi `409` yang membingungkan | Jangan ulangi | | 422 | `external_reference_too_long` | `external_reference` lebih dari 120 karakter | Perpendek, lalu ulangi | | 422 | `closing_incomplete` | Penjual memakai integrasi AdStack dan data pesanan belum lengkap. Field yang kurang ada di `missing_fields` | Penjual harus melengkapi data dari dashboard | | 422 | `idempotency_key_invalid` | Bentuk `Idempotency-Key` tidak sah | Perbaiki permintaan | | 422 | `idempotency_key_reused` | Kunci idempotensi dipakai untuk isi berbeda | Pakai kunci baru | | 429 | — | Melewati 120 permintaan per menit | Tunggu sesuai header `Retry-After` | | 500 | `internal_error` | Kegagalan di sisi AdForm | Coba lagi dengan kunci idempotensi yang sama | ## Jejak di dashboard penjual Setiap perubahan lewat endpoint ini tercatat di riwayat edit pesanan dengan sumber `api` dan nama kunci sebagai pelakunya. Penjual bisa melihat bahwa statusnya dipindahkan oleh integrasi, dan integrasi yang mana. ## Kaitan dengan webhook Perubahan status lewat endpoint ini memicu event `order.status_changed` seperti perubahan dari dashboard. Payload-nya membawa tiga field tambahan: ```json { "changed_by": "integration", "integration": "CRM Closing", "external_reference": "SPC-8871" } ``` `changed_by` bernilai `integration` di sini. Field ini **tidak muncul** pada perubahan dari dashboard, sehingga penerima webhook yang sudah berjalan tidak perlu menyesuaikan apa pun. Gunanya: membedakan perubahan yang berasal dari sistem Anda sendiri, supaya dua sistem tidak saling memantulkan status. Selengkapnya di [Webhook Keluar](/docs/webhooks). ### Jangan jadikan webhook satu-satunya sumber kebenaran Ketiga jalur yang menulis status di AdForm — penjual dari dashboard, konfirmasi pembayaran otomatis, dan endpoint ini — mengantre `order.status_changed` di dalam transaksi yang sama dengan perubahannya, jadi tidak ada perubahan status yang lolos tanpa event. Tetapi pengantreannya digerbangi kuota kiriman per toko: **3.000 kiriman per jam per toko** (`WEBHOOK_OUT_HOURLY_CAP`). Kalau kuota itu penuh, perubahan statusnya **tetap tersimpan** sementara kirimannya tidak pernah masuk antrean. Tidak ada galat, dan tidak ada percobaan ulang — kiriman itu memang tidak pernah dibuat. Karena itu: pakai webhook sebagai pemberitahuan cepat, dan `GET` sebagai rekonsiliasi. Toko yang ramai adalah justru toko yang paling mungkin menyentuh kuota, dan juga yang paling merugikan kalau statusnya menyimpang tanpa ketahuan. ## Keamanan - Kunci API adalah kredensial. Simpan di penyimpanan rahasia sistem Anda, bukan di kode sumber atau catatan. - **Jangan pernah meminta atau mengirim kunci lewat WhatsApp, email, atau chat.** Penjual menerbitkannya sendiri dari dashboard dan memasukkannya sendiri ke sistem Anda. - Kalau kunci diduga bocor, penjual mencabutnya dari dashboard dan menerbitkan yang baru. Pencabutan berlaku seketika.