Saat tim semakin sering memakai AI, code generation, dan otomasi untuk membuat integrasi, kebutuhan akan disiplin kontrak API justru naik. Masalahnya bukan sekadar endpoint bisa dipanggil, tetapi apakah integrasi tetap benar ketika client lama masih hidup, request di-retry berkali-kali, webhook dikirim ulang, atau event datang tidak berurutan.

Kontrak API tahan perubahan berarti perubahan bisa dilakukan tanpa langsung mematahkan integrasi yang sudah berjalan. Praktiknya mencakup versioning eksplisit, kompatibilitas ke belakang, idempotency key untuk operasi yang sensitif, retry dengan backoff yang aman, timeout yang jelas, status code yang konsisten, serta webhook dengan signature verification dan perlindungan replay.

Mengapa kontrak API harus lebih disiplin

AI dapat mempercepat penulisan handler, client SDK, mapping field, bahkan test boilerplate. Namun AI juga cenderung memperbanyak variasi implementasi, asumsi implisit, dan copy-paste pola yang belum tentu aman. Tanpa kontrak yang ketat, hasilnya sering terlihat seperti ini:

  • Client mengandalkan field yang tidak dijanjikan dalam dokumentasi.
  • Server mengubah format error tanpa migrasi.
  • Retry membuat data ganda karena endpoint tidak idempoten.
  • Webhook diproses dua kali karena duplicate delivery.
  • Event lama menimpa status baru karena urutan tidak dijaga.

Karena itu, kontrak API harus dianggap sebagai boundary yang eksplisit: apa yang dijamin, apa yang boleh berubah, dan bagaimana perilaku sistem saat terjadi gangguan jaringan atau delivery ulang.

Prinsip desain kontrak API tahan perubahan

1. Buat versioning eksplisit

Jangan mengandalkan perubahan diam-diam pada payload. Versi API harus terlihat jelas, misalnya di path, header, atau media type. Yang penting bukan formatnya, tetapi konsistensi dan aturan migrasinya.

Pilihan umum:

  • Path versioning: /v1/orders, mudah terlihat dan mudah dipisahkan di routing.
  • Header versioning: cocok jika ingin URL tetap stabil, tetapi perlu dokumentasi dan observabilitas yang baik.
  • Media type versioning: fleksibel, tetapi lebih sulit untuk tim yang belum terbiasa.

Untuk banyak tim backend, path versioning sering paling mudah dikelola karena jelas untuk debugging, gateway, dan log. Contoh:

POST /v1/payments
POST /v2/payments

Kesalahan umum: menambahkan versi hanya saat sudah terlambat, setelah banyak client bergantung pada perilaku yang tidak terdokumentasi.

2. Utamakan backward compatibility

Tidak semua perubahan harus menghasilkan versi baru. Banyak perubahan bisa dilakukan secara kompatibel jika Anda disiplin terhadap kontrak.

Perubahan yang umumnya aman:

  • Menambah field baru yang bersifat opsional.
  • Menambah endpoint baru.
  • Menambah nilai enum jika client sudah didesain untuk mengabaikan nilai yang belum dikenal.
  • Menambah metadata baru di objek error tanpa menghapus field lama.

Perubahan yang biasanya breaking change:

  • Menghapus field yang dipakai client.
  • Mengubah tipe data field, misalnya string menjadi number.
  • Mengubah arti status code.
  • Mengubah field wajib menjadi nama baru tanpa masa transisi.
  • Mengganti format timestamp atau mata uang tanpa dokumentasi dan versi baru.

Aturan praktis: jika client lama yang benar sebelumnya bisa gagal setelah deploy baru, anggap itu breaking change.

3. Tetapkan schema request/response yang konsisten

Tim yang memakai codegen sangat terbantu jika struktur payload konsisten. Hindari respons yang kadang objek, kadang array, atau error yang formatnya berubah-ubah antar endpoint.

Contoh request pembuatan pembayaran:

POST /v1/payments
Content-Type: application/json
Idempotency-Key: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d

{
  "order_id": "ord_12345",
  "amount": 150000,
  "currency": "IDR",
  "customer": {
    "id": "cust_789",
    "email": "[email protected]"
  }
}

Contoh respons sukses:

HTTP/1.1 201 Created
Content-Type: application/json

{
  "data": {
    "payment_id": "pay_abc123",
    "order_id": "ord_12345",
    "amount": 150000,
    "currency": "IDR",
    "status": "pending",
    "created_at": "2026-07-11T10:15:30Z"
  },
  "meta": {
    "request_id": "req_f7a21c"
  }
}

Contoh error schema yang konsisten:

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request tidak valid.",
    "details": [
      {
        "field": "currency",
        "issue": "unsupported_value"
      }
    ],
    "retryable": false
  },
  "meta": {
    "request_id": "req_f7a21c"
  }
}

Format seperti ini membantu karena:

  • Client dapat selalu mencari data atau error.
  • request_id memudahkan pelacakan log.
  • Field retryable membantu keputusan otomatis pada worker atau integrator.

Retry yang aman: idempotency, timeout, dan backoff

1. Kenapa retry tanpa idempotency berbahaya

Gangguan jaringan tidak selalu berarti operasi gagal. Bisa saja server sudah memproses request, tetapi client tidak menerima respons karena timeout atau koneksi putus. Jika client langsung mengirim ulang request yang sama tanpa mekanisme idempotency, hasilnya bisa duplikasi transaksi, order ganda, atau state yang tidak konsisten.

2. Gunakan idempotency key untuk operasi yang menciptakan efek samping

Untuk endpoint seperti membuat pembayaran, membuat order, atau memicu pencairan dana, minta client mengirim Idempotency-Key. Server lalu menyimpan hasil request pertama untuk kombinasi tertentu, dan mengembalikan hasil yang sama ketika request identik dikirim ulang.

Pola implementasi:

  1. Client mengirim Idempotency-Key unik per aksi bisnis.
  2. Server menyimpan key, payload hash, status pemrosesan, dan respons akhir.
  3. Jika request sama datang lagi dengan key yang sama dan payload identik, kembalikan respons sebelumnya.
  4. Jika key sama tetapi payload berbeda, balas error karena ada konflik penggunaan key.

Contoh respons ketika key dipakai ulang dengan payload berbeda:

HTTP/1.1 409 Conflict
Content-Type: application/json

{
  "error": {
    "code": "IDEMPOTENCY_KEY_REUSED",
    "message": "Idempotency key sudah dipakai untuk payload yang berbeda.",
    "retryable": false
  },
  "meta": {
    "request_id": "req_93ab12"
  }
}

Catatan desain: simpan idempotency key dengan masa hidup yang cukup untuk pola retry normal. Durasi pastinya bergantung pada karakteristik bisnis dan kemungkinan retry tertunda, jadi dokumentasikan kebijakan retensinya.

3. Retry dengan exponential backoff dan jitter

Retry yang agresif bisa memperburuk gangguan. Gunakan exponential backoff agar jeda retry makin lama, dan tambahkan jitter agar banyak client tidak memukul server lagi secara bersamaan.

Pseudocode sederhana:

attempt = 0
max_attempts = 5
base_delay_ms = 500

while attempt < max_attempts:
    response = call_api(timeout=3s)

    if response.success:
        return response

    if not response.retryable:
        break

    delay = random_between(0, base_delay_ms * (2 ** attempt))
    sleep(delay)
    attempt += 1

Retry sebaiknya dilakukan pada kondisi seperti:

  • Timeout jaringan.
  • Status 429 Too Many Requests.
  • Status 502, 503, atau 504.
  • Error internal yang secara eksplisit ditandai retryable: true.

Jangan retry membabi buta pada:

  • 400 Bad Request karena payload salah.
  • 401 Unauthorized jika token memang tidak valid.
  • 409 Conflict tertentu yang menandakan masalah logika, bukan gangguan sementara.

4. Tetapkan timeout yang eksplisit

Tanpa timeout, koneksi bisa menggantung dan menghabiskan thread, worker, atau slot koneksi. Pisahkan setidaknya dua jenis timeout:

  • Connect timeout: batas waktu membuka koneksi.
  • Read/response timeout: batas waktu menunggu respons setelah koneksi terbentuk.

Timeout harus disesuaikan dengan karakter endpoint. Endpoint sinkron untuk operasi ringan sebaiknya cepat dan konsisten. Untuk proses berat, lebih aman mengembalikan 202 Accepted dan memproses secara asinkron, lalu sediakan endpoint polling atau webhook hasil.

Status code dan error schema yang bisa diandalkan

Status code sebaiknya mencerminkan kategori hasil, bukan sekadar semua error diberi 200 lalu detailnya diletakkan di body. Ini penting untuk client automation, gateway rule, monitoring, dan retry policy.

Panduan praktis:

  • 200 OK: operasi baca atau aksi berhasil dengan respons sinkron.
  • 201 Created: resource baru berhasil dibuat.
  • 202 Accepted: request diterima tetapi belum selesai diproses.
  • 204 No Content: sukses tanpa body.
  • 400 Bad Request: request tidak sesuai format atau parameter dasar salah.
  • 401 Unauthorized: autentikasi gagal atau belum ada.
  • 403 Forbidden: terautentikasi tetapi tidak punya izin.
  • 404 Not Found: resource tidak ada.
  • 409 Conflict: konflik state atau penggunaan idempotency key yang tidak cocok.
  • 422 Unprocessable Entity: payload bisa dibaca, tetapi secara bisnis/validasi tidak valid.
  • 429 Too Many Requests: rate limit tercapai.
  • 500, 502, 503, 504: gangguan server atau upstream.

Jika memungkinkan, tambahkan header atau metadata yang mendukung debugging dan otomasi, misalnya request_id dan informasi rate limit. Yang penting adalah konsistensi antar endpoint, bukan banyaknya field.

Webhook aman: signature verification, duplicate delivery, dan replay attack

1. Asumsikan webhook bisa terlambat, ganda, dan tidak berurutan

Webhook adalah integrasi antar sistem yang rentan terhadap kegagalan jaringan. Karena itu, penerima webhook harus mengasumsikan beberapa hal berikut akan terjadi:

  • Event yang sama dikirim lebih dari sekali.
  • Event tiba tidak sesuai urutan kejadian.
  • Respons 200 tidak diterima pengirim, sehingga event dikirim ulang.
  • Sebagian proses internal gagal walaupun webhook sudah diterima.

Desain handler webhook yang benar harus idempoten dan tahan terhadap event out-of-order.

2. Verifikasi signature sebelum memproses body

Webhook tidak boleh dipercaya hanya karena datang ke endpoint yang benar. Gunakan signature berbasis secret bersama agar penerima bisa memastikan payload benar-benar dikirim oleh penyedia yang sah dan tidak diubah di tengah jalan.

Contoh header dan payload:

POST /webhooks/payments
Content-Type: application/json
X-Signature: t=1720692930,v1=5f2c0f3c1d...

{
  "id": "evt_001",
  "type": "payment.completed",
  "created_at": "2026-07-11T10:16:00Z",
  "data": {
    "payment_id": "pay_abc123",
    "order_id": "ord_12345",
    "status": "completed",
    "amount": 150000,
    "currency": "IDR"
  }
}

Pseudocode verifikasi:

raw_body = read_raw_request_body()
signature_header = request.headers["X-Signature"]
secret = load_webhook_secret()

parsed = parse_signature_header(signature_header)
timestamp = parsed.timestamp
received_sig = parsed.v1

if abs(now_unix() - timestamp) > allowed_skew_seconds:
    reject(401, "signature expired")

signed_payload = timestamp + "." + raw_body
expected_sig = hmac_sha256_hex(secret, signed_payload)

if not constant_time_equals(expected_sig, received_sig):
    reject(401, "invalid signature")

Poin penting:

  • Gunakan raw body, bukan JSON yang sudah di-parse lalu diserialisasi ulang.
  • Bandingkan signature dengan fungsi constant-time comparison untuk mengurangi risiko timing attack.
  • Validasi timestamp agar payload lama tidak bisa diputar ulang seenaknya.

3. Cegah replay attack

Replay attack terjadi ketika request webhook yang valid direkam lalu dikirim ulang oleh pihak yang tidak berwenang. Signature saja tidak cukup jika tidak ada pembatasan waktu atau deduplikasi event.

Mitigasi yang umum:

  • Periksa timestamp di header signature dan tolak request yang terlalu lama.
  • Simpan event_id yang sudah diproses untuk mencegah pemrosesan ulang.
  • Jika penyedia mendukung, gabungkan deduplikasi berdasarkan event_id dan tipe event.

4. Tangani duplicate delivery dan partial failure

Handler webhook sebaiknya tidak langsung menjalankan semua logika berat secara sinkron. Pola yang aman:

  1. Terima request.
  2. Verifikasi signature.
  3. Simpan event ke tabel inbox atau log event beserta status awal.
  4. Balas cepat dengan status sukses jika penyimpanan berhasil.
  5. Proses event secara asinkron melalui worker.

Dengan pola ini, jika downstream seperti database lain, queue, atau layanan notifikasi sedang gagal, Anda tidak kehilangan event. Worker bisa mengulang proses tanpa meminta pengirim webhook menembak endpoint Anda berulang-ulang.

Contoh respons penerimaan webhook:

HTTP/1.1 202 Accepted
Content-Type: application/json

{
  "data": {
    "received": true
  },
  "meta": {
    "request_id": "req_wh_123"
  }
}

5. Tangani event out-of-order

Event tidak selalu datang sesuai urutan. Misalnya payment.completed bisa tiba sebelum payment.pending karena retry, delay jaringan, atau pemrosesan upstream.

Jangan menimpa state secara buta berdasarkan event terakhir yang diterima. Gunakan salah satu strategi berikut:

  • State machine: definisikan transisi yang valid, misalnya dari pending ke completed, tetapi tidak sebaliknya.
  • Versi/sequence number: terima update hanya jika nomor urut lebih baru.
  • Timestamp domain: gunakan waktu kejadian dari sumber, dengan hati-hati jika ada kemungkinan clock skew.

Jika event tidak punya sequence number, state machine biasanya paling aman karena berfokus pada aturan bisnis, bukan asumsi urutan jaringan.

Pola implementasi yang praktis

1. Gunakan tabel idempotency dan inbox webhook

Dua pola penyimpanan sederhana yang sering berguna:

  • Tabel idempotency: menyimpan key, hash payload, status request, dan respons final.
  • Tabel inbox webhook: menyimpan event masuk, signature status, waktu terima, hasil proses, dan error terakhir.

Keuntungan utamanya adalah auditability dan kemampuan recovery. Saat ada bug atau kegagalan sementara, tim bisa melacak event mana yang diterima, mana yang gagal diproses, dan apakah retry aman dijalankan.

2. Tambahkan request ID dan correlation ID

Untuk debugging integrasi lintas layanan, sertakan request_id pada setiap respons dan propagasikan correlation ID di log internal. Ini mempermudah menjawab pertanyaan seperti:

  • Apakah client sebenarnya menerima respons?
  • Request timeout di sisi mana?
  • Webhook diproses dua kali atau hanya ter-log dua kali?
  • Apakah duplikasi berasal dari retry client atau retry internal?

3. Dokumentasikan perilaku retry dan error

Dokumentasi API yang baik bukan hanya daftar field. Tuliskan juga:

  • Endpoint mana yang mendukung idempotency key.
  • Status code mana yang aman untuk retry.
  • Batas timeout yang direkomendasikan untuk client.
  • Kebijakan webhook redelivery dan deduplikasi.
  • Aturan kompatibilitas antar versi.

Ini penting terutama saat tim memakai AI untuk menghasilkan client code. AI cenderung mengikuti pola dari dokumentasi; jika dokumentasinya kabur, implementasi yang lahir juga kabur.

Langkah migrasi aman tanpa breaking change

Perubahan kontrak tidak harus langsung memutus client lama. Gunakan fase migrasi yang terukur.

  1. Tambahkan field baru sebagai opsional, jangan langsung mengganti field lama.
  2. Dukung dua format sementara jika perlu, misalnya baca customer_id lama dan customer.id baru selama masa transisi.
  3. Berikan sinyal deprecation di dokumentasi, changelog, dan jika perlu di header respons.
  4. Pantau penggunaan endpoint, versi, atau field lama melalui log dan metrics.
  5. Rilis versi baru hanya untuk perubahan yang benar-benar breaking.
  6. Siapkan periode overlap agar client lama dan baru bisa berjalan bersamaan.
  7. Uji dengan contract test sebelum mematikan versi lama.

Contoh migrasi aman:

  • Versi lama mengembalikan status sebagai string sederhana.
  • Versi baru menambah status_detail tanpa menghapus status.
  • Client diberi waktu migrasi untuk memakai field baru.
  • Setelah adopsi cukup, buat versi mayor baru jika memang ingin menghapus field lama.

Anti-pattern yang sering terjadi:

  • Mengubah arti field lama tetapi membiarkan namanya tetap sama.
  • Menghapus field karena dianggap “sudah tidak dipakai”, padahal masih dipakai partner lama.
  • Mengandalkan deploy serentak antara server dan semua client.

Checklist implementasi

  • Versioning API eksplisit dan terdokumentasi.
  • Aturan backward compatibility disepakati tim.
  • Schema request/response konsisten antar endpoint.
  • Error schema memiliki code, message, dan indikator retry jika relevan.
  • Endpoint efek samping mendukung idempotency key.
  • Retry client memakai exponential backoff dan jitter.
  • Timeout koneksi dan respons ditetapkan jelas.
  • Status code dipakai sesuai kategori error/sukses.
  • Webhook diverifikasi dengan signature berbasis secret.
  • Timestamp webhook divalidasi untuk mencegah replay.
  • Duplicate delivery ditangani dengan deduplikasi event.
  • Out-of-order event ditangani dengan state machine atau sequence.
  • Proses webhook berat dijalankan asinkron setelah event disimpan.
  • Request ID dan logging korelasi tersedia untuk debugging.
  • Perubahan breaking dilepas lewat versi baru, bukan diam-diam.

Anti-pattern umum yang perlu dihindari

  • Semua error pakai 200: mempersulit retry policy dan observabilitas.
  • Retry tanpa idempotency: berisiko duplikasi transaksi.
  • Webhook dipercaya tanpa verifikasi: membuka peluang spoofing.
  • Handler webhook sinkron dan berat: mudah timeout lalu menyebabkan redelivery berulang.
  • Field wajib bertambah tanpa versi: client lama langsung gagal.
  • Menganggap urutan event pasti benar: rawan state mundur.
  • Dokumentasi tidak mencantumkan edge case: codegen dan implementasi manual sama-sama berisiko salah.

Penutup

Kontrak API tahan perubahan bukan soal menulis dokumentasi yang rapi saja, tetapi soal merancang sistem agar tetap benar ketika jaringan gagal, request diulang, event terlambat, dan client tidak semuanya diperbarui bersamaan. Saat AI mempercepat pembuatan integrasi, pagar pengamannya harus lebih ketat: versioning eksplisit, kompatibilitas ke belakang, idempotency, retry yang disiplin, dan webhook aman.

Jika Anda hanya mengambil satu pelajaran dari artikel ini, ambil yang ini: anggap kegagalan, duplikasi, dan perubahan sebagai kondisi normal. Dari situ, desain kontrak API Anda akan jauh lebih stabil, mudah di-debug, dan aman untuk diotomasi.