Audit API contract perlu dilakukan sebelum masalah retry berubah menjadi duplikasi data, status transaksi yang salah, atau integrasi yang terlihat sukses di satu sisi tetapi gagal di sisi lain. Jika client mengirim ulang request karena timeout, menerima respons parsial, atau memproses webhook yang datang terlambat, kontrak API yang lemah akan mudah putus meski endpoint secara teknis masih hidup.

Masalah ini tetap relevan dalam diskusi praktis komunitas developer, termasuk tema-tema yang sering muncul dalam pembahasan integrasi, reliability, dan failure handling. Fokusnya bukan pada teknologi yang sedang populer, melainkan pada satu hal mendasar: apakah kontrak request/response Anda tetap benar saat kondisi jaringan dan urutan event tidak ideal?

Apa yang diaudit dalam API contract

Saat membahas API contract, banyak tim hanya memeriksa bentuk JSON: field apa saja yang wajib, tipe data, dan status code. Itu penting, tetapi belum cukup. Untuk mencegah integrasi putus saat retry gagal, audit harus mencakup perilaku API ketika request diproses lebih dari sekali, diproses sebagian, atau selesai secara asynchronous.

Secara praktis, audit API contract perlu menjawab pertanyaan berikut:

  • Apakah request yang sama aman dikirim ulang?
  • Bagaimana client membedakan antara operasi yang benar-benar gagal dan yang sebenarnya sukses tetapi responsnya hilang?
  • Apakah error code stabil dan cukup spesifik untuk logika retry?
  • Bagaimana status operasi async dilacak?
  • Bagaimana sistem menangani event duplikat dari webhook?
  • Apakah perubahan kontrak bisa dirilis tanpa memutus client lama?

Titik rawan yang paling sering membuat integrasi putus

1. Retry tanpa idempotency

Kasus klasik: client mengirim POST /payments, server berhasil membuat transaksi, tetapi koneksi putus sebelum respons diterima. Client menganggap request gagal lalu melakukan retry. Jika endpoint tidak idempotent, transaksi kedua ikut tercipta.

Bug ini sering muncul karena tim backend berpikir "request kedua memang request baru", sedangkan tim client menganggap "ini percobaan ulang untuk operasi yang sama". Kontrak tidak menyatakan identitas operasi secara eksplisit.

2. Timeout yang dianggap gagal total

Timeout di sisi client tidak selalu berarti server gagal memproses request. Bisa jadi server selesai beberapa detik kemudian, atau berhasil menulis ke database tetapi gagal mengirim balik respons. Jika kontrak tidak menyediakan cara rekonsiliasi, client terpaksa menebak-nebak.

3. Respons parsial atau status ambigu

Contoh umum: API mengembalikan 200 OK dengan body yang tidak lengkap, atau menggunakan satu field status generik seperti failed untuk beberapa jenis kegagalan yang sebenarnya butuh perlakuan berbeda. Akibatnya client tidak tahu mana yang boleh di-retry, mana yang harus dihentikan, dan mana yang perlu pengecekan manual.

4. Operasi async tanpa status yang jelas

Untuk pekerjaan yang diproses di background, contract sering berhenti di "request diterima" tanpa definisi status lanjutan. Client akhirnya menyimpulkan sendiri arti pending, processing, completed, atau failed. Begitu interpretasi berbeda, integrasi mudah pecah.

5. Webhook datang ganda atau tidak berurutan

Webhook hampir selalu bersifat at-least-once delivery: event bisa terkirim lebih dari sekali, dan urutannya tidak selalu sama dengan waktu proses internal. Jika penerima webhook mengasumsikan tepat satu event dan urutan pasti, status lokal akan mudah korup.

Audit API contract: komponen yang wajib diperiksa

Kontrak request harus punya identitas operasi

Untuk endpoint yang menciptakan efek samping seperti pembuatan pembayaran, order, refund, atau provisioning, audit apakah request memiliki idempotency key atau identifier lain yang stabil dari sisi client.

Karakteristik kontrak yang baik:

  • Client mengirim key unik untuk satu niat operasi, bukan untuk tiap percobaan HTTP.
  • Server menyimpan hasil pertama untuk key tersebut.
  • Retry dengan key yang sama mengembalikan hasil yang konsisten, bukan membuat resource baru.
  • Jika payload berbeda tetapi key sama, server mengembalikan error yang jelas karena ada konflik semantic.
POST /payments
Idempotency-Key: 9c4d2b7e-2e8f-4a1f-9f13-6b9f3d8a1201
Content-Type: application/json

{
  "order_id": "ORD-10027",
  "amount": 150000,
  "currency": "IDR"
}

Respons pertama dan retry berikutnya untuk operasi yang sama sebaiknya merujuk ke resource yang sama:

{
  "payment_id": "pay_8f31",
  "status": "accepted",
  "replay": false
}

Saat request yang sama diulang:

{
  "payment_id": "pay_8f31",
  "status": "accepted",
  "replay": true
}

Field replay bukan keharusan, tetapi berguna untuk observability dan debugging. Yang lebih penting adalah hasilnya konsisten.

Response contract harus membedakan hasil sinkron dan async

Jangan memaksa semua hal terlihat sinkron. Jika proses bisnis memang butuh waktu, kontrak harus jujur bahwa request hanya diterima, bukan selesai.

Pola yang lebih aman:

  • 201 Created bila resource benar-benar sudah dibuat dan siap digunakan.
  • 202 Accepted bila request diterima untuk diproses asynchronous.
  • Body respons menyertakan identifier operasi atau resource untuk polling status.
  • Status lifecycle didefinisikan terbatas dan stabil.
{
  "operation_id": "op_72aa",
  "status": "pending",
  "status_url": "/operations/op_72aa"
}

Audit juga perlu memastikan arti setiap status tertulis jelas. Misalnya:

  • pending: request diterima, belum diproses.
  • processing: sedang dijalankan, hasil belum final.
  • succeeded: efek bisnis selesai dan dapat dianggap final.
  • failed: gagal final, aman untuk membuat operasi baru jika sesuai aturan bisnis.
  • unknown: hindari jika tidak benar-benar perlu, karena mudah memicu perilaku client yang tidak konsisten.

Error code harus konsisten dan bisa ditindaklanjuti

Salah satu temuan audit yang sering muncul adalah semua error dibungkus sebagai 400 atau 500. Itu membuat client kesulitan menentukan strategi retry.

Minimal, kontrak error perlu membedakan:

  • Validation error: request salah, jangan retry tanpa perbaikan.
  • Conflict / duplicate: operasi bertabrakan dengan state saat ini atau idempotency key reuse dengan payload berbeda.
  • Rate limit / temporary overload: boleh retry dengan backoff.
  • Upstream/transient failure: kemungkinan aman di-retry jika operasi idempotent.
  • Authentication/authorization: jangan retry buta.
{
  "error": {
    "code": "IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_PAYLOAD",
    "message": "Request sebelumnya dengan idempotency key yang sama memiliki payload berbeda",
    "retryable": false
  }
}

Flag retryable membantu, tetapi jangan dijadikan satu-satunya sumber keputusan. Status code dan error code tetap harus stabil. Audit kontrak perlu memeriksa apakah dokumentasi, implementasi, dan observasi produksi benar-benar selaras.

Webhook contract harus mendukung reconciliation

Webhook tidak cukup hanya mengirim event. Kontrak yang baik harus mengasumsikan event bisa:

  • terkirim lebih dari sekali,
  • datang terlambat,
  • datang tidak berurutan,
  • gagal diproses di sisi penerima.

Karena itu, audit webhook contract sebaiknya memeriksa hal-hal berikut:

  • Setiap event memiliki event_id unik untuk deduplikasi.
  • Event menyertakan resource_id yang bisa di-query ulang ke API sumber.
  • Ada timestamp kejadian dan, bila perlu, versi resource atau sequence yang bisa dipakai untuk ordering.
  • Penerima webhook bisa mengakui penerimaan secara cepat, lalu memproses di background.
  • Tersedia endpoint atau prosedur reconciliation untuk sinkronisasi ulang.

Jika webhook hanya berisi status final tanpa cara mengecek ulang sumber kebenaran, maka satu event yang hilang bisa membuat sistem klien selamanya salah.

Contoh skenario bug nyata yang sering lolos review

Skenario 1: pembayaran ganda karena retry setelah timeout

Urutan kejadian:

  1. Client mengirim request buat pembayaran.
  2. Server membuat record dan mengirim ke payment processor.
  3. Respons ke client timeout.
  4. Client mengirim ulang request yang sama.
  5. Server membuat pembayaran kedua karena tidak ada idempotency key.

Gejalanya biasanya baru terlihat saat ada komplain tagihan ganda. Log HTTP bisa tampak normal karena kedua request memang valid. Akar masalahnya bukan sekadar timeout, melainkan kontrak yang tidak mendefinisikan identitas operasi.

Skenario 2: status lokal salah karena webhook datang sebelum polling selesai

Client memulai operasi async, lalu polling ke endpoint status. Sementara itu webhook sukses dikirim lebih dulu dengan status final, tetapi sistem lokal menimpa status final tersebut ketika hasil polling yang lebih lama masih mengembalikan processing. Ini race condition klasik antara dua sumber update.

Solusi kontraktualnya bukan hanya "urutkan lebih hati-hati", melainkan:

  • tentukan sumber kebenaran utama,
  • sertakan versi atau waktu update,
  • jangan menurunkan state final ke state sementara tanpa aturan eksplisit.

Skenario 3: duplicate submission dari tombol submit

Di sisi frontend, user menekan tombol dua kali karena UI terasa lambat. Browser mengirim dua request hampir bersamaan. Jika backend hanya mengandalkan pemeriksaan "apakah order sudah ada" tanpa proteksi atomik, dua request dapat lolos sebelum salah satunya sempat mengunci state. Ini menghasilkan race condition di level aplikasi dan database.

Audit API contract perlu memeriksa apakah duplicate submission diselesaikan oleh:

  • idempotency key dari client,
  • constraint unik di storage,
  • operasi atomik atau transaksi,
  • respons konflik yang dapat dipahami client.

Checklist audit API contract

Gunakan checklist berikut saat mereview endpoint yang memiliki efek samping atau melibatkan integrasi lintas sistem.

Request/command

  • Apakah endpoint ini aman terhadap retry?
  • Apakah perlu idempotency key atau client-generated request ID?
  • Apakah key didefinisikan untuk satu niat bisnis, bukan satu koneksi HTTP?
  • Apakah ada validasi ketika key sama dipakai dengan payload berbeda?
  • Apakah ada batas retensi untuk penyimpanan hasil idempotency, dan apakah dokumentasinya jelas?

Response

  • Apakah status sinkron dan async dibedakan dengan jelas?
  • Apakah body respons selalu menyertakan identifier yang bisa dipakai untuk rekonsiliasi?
  • Apakah field opsional benar-benar opsional, atau diam-diam dibutuhkan oleh client?
  • Apakah respons parsial dapat dikenali dan ditangani?

Error handling

  • Apakah status code konsisten untuk kelas error yang sama?
  • Apakah ada error code machine-readable yang stabil?
  • Apakah dokumentasi menjelaskan mana yang retryable dan mana yang tidak?
  • Apakah kasus conflict, duplicate, dan throttling dibedakan?

Async status dan state machine

  • Apakah daftar status terbatas dan artinya eksplisit?
  • Apakah transisi status yang valid didefinisikan?
  • Apakah state final tidak bisa tertimpa oleh state lama?
  • Apakah ada endpoint untuk cek status atau ambil ulang resource final?

Webhook

  • Apakah event punya ID unik untuk deduplikasi?
  • Apakah penerima bisa melakukan reconciliation ke API sumber?
  • Apakah event ordering dijamin? Jika tidak, apakah kontrak mengakuinya?
  • Apakah payload webhook cukup untuk identifikasi, tanpa memaksa asumsi urutan?

Storage dan concurrency

  • Apakah ada constraint unik yang mendukung aturan kontrak?
  • Apakah operasi kritis dilakukan atomik?
  • Apakah race condition antara request paralel sudah diuji?

Anti-pattern yang perlu dicari saat audit

  • Menganggap POST pasti tidak bisa di-retry. Dalam praktik integrasi, justru POST paling sering perlu diproteksi dari duplikasi.
  • Mengandalkan timeout sebagai sinyal gagal. Timeout hanya berarti client berhenti menunggu.
  • Semua error jadi 200 dengan field sukses/gagal di body. Ini menyulitkan middleware, observability, dan client generik.
  • Webhook sebagai satu-satunya sumber kebenaran. Jika event hilang, tidak ada cara pulih.
  • Idempotency hanya di cache jangka pendek tanpa aturan jelas. Retry yang datang setelah cache hilang bisa tetap menggandakan operasi.
  • Status terlalu banyak dan ambigu. Semakin banyak status tanpa definisi kuat, semakin besar kemungkinan salah interpretasi.
  • Payload lama dan baru dibedakan diam-diam. Menambah field yang dianggap wajib oleh server tetapi tidak oleh client lama adalah cara cepat memutus integrasi.

Rekomendasi implementasi yang praktis

Simpan hasil idempotency di sisi server

Pola yang umum dan aman:

  1. Terima request dengan idempotency key.
  2. Cek apakah key sudah pernah dipakai.
  3. Jika belum, proses request dalam batas atomik yang masuk akal.
  4. Simpan hasil sukses atau representasi error yang relevan.
  5. Jika request sama datang lagi, kembalikan hasil yang sama.

Yang perlu diaudit bukan hanya keberadaan tabel atau cache idempotency, tetapi juga kapan hasil disimpan. Jika hasil baru disimpan setelah seluruh alur selesai, masih ada celah race condition ketika dua request identik masuk bersamaan.

Gunakan state machine sederhana

Untuk operasi async, dokumentasikan transisi yang valid. Misalnya, pending -> processing -> succeeded|failed. Hindari transisi mundur tanpa alasan kuat. Jika memang ada koreksi status, beri model yang eksplisit, misalnya event kompensasi atau versi baru state, bukan menimpa makna lama diam-diam.

Bangun reconciliation sebagai fitur, bukan tambalan

Reconciliation sering dianggap pekerjaan support atau operasi manual. Padahal dalam integrasi yang sehat, ini bagian dari kontrak. Minimal, sediakan cara bagi client untuk:

  • menanyakan status akhir dari operation_id,
  • mengambil resource berdasarkan identifier bisnis,
  • memverifikasi apakah webhook tertentu sudah tercermin pada state server.

Jika sistem Anda tidak punya jalur rekonsiliasi, maka setiap timeout akan dipaksa menjadi tebakan.

Strategi rollout tanpa memutus client lama

Perbaikan kontrak sering gagal bukan karena desain teknisnya buruk, tetapi karena dirilis terlalu keras. Jika sudah ada client aktif, lakukan perubahan secara bertahap.

1. Tambahkan, jangan langsung mengganti

Jika ingin memperkenalkan idempotency key, mulailah dengan mendukung header baru tanpa langsung mewajibkannya untuk semua client. Pantau siapa yang belum mengirim key, lalu komunikasikan migrasi.

2. Pertahankan field lama selama masa transisi

Jika menambah model status async yang lebih jelas, jangan langsung menghapus field status lama. Anda bisa menambahkan field baru yang lebih eksplisit sambil mendokumentasikan pemetaan dari nilai lama ke yang baru.

3. Versioning hanya jika perubahan memang breaking

Tidak semua perubahan perlu versi endpoint baru. Menambahkan field opsional biasanya aman. Tetapi perubahan arti field, perubahan status code untuk skenario lama, atau perubahan urutan proses yang memengaruhi asumsi client dapat menjadi breaking change dan perlu strategi versioning atau kompatibilitas ganda.

4. Uji dengan traffic nyata dan fault injection

Sebelum mewajibkan kontrak baru, uji skenario berikut di staging atau environment terkontrol:

  • respons server sukses tetapi koneksi diputus sebelum body terkirim,
  • dua request identik masuk bersamaan,
  • webhook terkirim dua kali,
  • webhook datang sebelum polling selesai,
  • retry datang setelah jeda cukup lama.

Audit yang baik tidak berhenti di dokumen OpenAPI atau skema JSON. Ia harus membuktikan bahwa implementasi benar saat kondisi buruk benar-benar terjadi.

Cara debugging saat integrasi mulai menunjukkan gejala putus

  • Telusuri satu operasi end-to-end dengan korelasi ID: request ID, idempotency key, operation ID, resource ID, dan event ID.
  • Bandingkan waktu client timeout dengan waktu commit di server. Sering kali masalahnya bukan gagal proses, melainkan terlambat merespons.
  • Periksa apakah request duplikat punya payload identik atau ada field dinamis yang berubah, misalnya timestamp atau metadata acak.
  • Lihat apakah status final pernah ditimpa status yang lebih lama dari polling atau webhook terlambat.
  • Verifikasi bahwa error code yang terdokumentasi benar-benar muncul di produksi, bukan diganti oleh proxy, gateway, atau handler generik.

Penutup

Audit API contract untuk mencegah integrasi putus saat retry gagal bukan sekadar rapikan dokumentasi. Yang diaudit adalah ketahanan kontrak terhadap kenyataan produksi: timeout, duplicate submission, race condition, respons parsial, proses async, dan webhook yang tidak selalu rapi.

Jika Anda hanya mengambil satu langkah setelah membaca artikel ini, mulailah dari endpoint yang menciptakan efek bisnis paling mahal: pembayaran, order, refund, atau provisioning. Pastikan endpoint itu punya identitas operasi yang stabil, error code yang bisa ditindaklanjuti, status async yang jelas, dan jalur reconciliation yang nyata. Di situlah sebagian besar integrasi berhenti menjadi rapuh.