Desain API arsip data publik tidak cukup hanya menyediakan endpoint unduh dan unggah. Jika data harus disalin ke banyak sistem, dipulihkan saat provider berubah, atau di-ingest ulang setelah gangguan, API harus dirancang agar tahan terhadap retry, event duplikat, urutan pengiriman yang tidak konsisten, dan kegagalan parsial.

Konteks preservasi data publik makin relevan ketika organisasi perlu memastikan data tetap tersedia walau sumber utama berubah, berpindah, atau mengalami gangguan. Dalam praktiknya, tantangan utama bukan sekadar menyimpan file, tetapi menjaga sinkronisasi lintas sistem dengan kontrak yang stabil, identitas data yang konsisten, dan mekanisme distribusi yang aman. Di sinilah webhook, retry, dan idempotency menjadi fondasi.

Tujuan desain: arsip yang bisa direplikasi tanpa merusak data

Untuk arsip data publik, target utamanya biasanya bukan transaksi real-time berlatensi sangat rendah, melainkan konsistensi operasional. Sistem harus tetap benar ketika:

  • provider mengirim event yang sama lebih dari sekali,
  • consumer memproses pesan terlambat,
  • file diunggah ulang dengan metadata yang diperbarui,
  • jaringan putus di tengah proses,
  • ada lebih dari satu salinan data pada sistem berbeda.

Karena itu, desain API yang baik perlu membedakan tiga hal:

  1. Resource identity: identitas permanen dari objek arsip, misalnya dataset, file, versi, atau snapshot.
  2. Event identity: identitas unik tiap notifikasi perubahan.
  3. Request identity: identitas unik sebuah operasi tulis agar aman saat diulang.

Banyak integrasi rusak karena tiga identitas ini dicampur. Misalnya, event ID dipakai sebagai ID resource, atau idempotency key dipakai untuk deduplikasi data jangka panjang. Padahal fungsi masing-masing berbeda.

Kontrak API yang stabil untuk arsip jangka panjang

Pilih identifier yang tidak bergantung pada lokasi fisik

Untuk data publik, URL file bisa berubah karena migrasi bucket, CDN, atau provider. Karena itu, jangan jadikan URL sebagai identitas utama resource. Gunakan identifier stabil seperti:

  • dataset_id untuk koleksi logis,
  • record_id atau object_id untuk item individual,
  • version atau snapshot_at untuk representasi titik waktu tertentu.

Contoh payload metadata yang lebih tahan perubahan:

{
  "dataset_id": "climate-daily-observations",
  "record_id": "station-abc-2026-07-01",
  "version": 3,
  "published_at": "2026-07-01T10:15:00Z",
  "checksum": {
    "algorithm": "sha256",
    "value": "3d7c4d..."
  },
  "content": {
    "media_type": "application/json",
    "byte_size": 18234,
    "download_url": "https://archive.example.org/objects/station-abc-2026-07-01/v3"
  },
  "schema_version": "2026-07",
  "fields": {
    "station_id": "ABC",
    "date": "2026-07-01",
    "avg_temp_c": 29.4,
    "quality_flag": "verified"
  }
}

Mengapa ini bekerja? Karena consumer dapat menyimpan identitas objek dan checksum sebagai sumber kebenaran, sementara lokasi unduh hanya atribut yang bisa berubah.

Bedakan versioning API dan versioning field

Dalam arsip data publik, perubahan paling sering bukan pada endpoint, melainkan pada bentuk data. Karena itu ada dua lapisan versi yang sebaiknya dipisahkan:

  • API version: perubahan kontrak transport atau struktur respons besar, misalnya /v1/events.
  • Schema atau field version: evolusi atribut data di payload, misalnya schema_version.

Gunakan aturan sederhana berikut:

  • Menambah field opsional umumnya tidak perlu menaikkan API version.
  • Mengubah nama field, tipe field, atau makna field lama adalah perubahan yang merusak dan harus diperlakukan sebagai breaking change.
  • Jika field lama perlu diganti, beri masa transisi: kirim field lama dan baru bersamaan, dokumentasikan prioritasnya, lalu hapus sesuai jadwal yang jelas.

Contoh evolusi yang aman:

{
  "record_id": "station-abc-2026-07-01",
  "temperature_c": 29.4,
  "metrics": {
    "avg_temp_c": 29.4
  }
}

Consumer lama masih membaca temperature_c, consumer baru bisa pindah ke metrics.avg_temp_c. Ini lebih aman daripada mengganti field secara mendadak.

Catatan: untuk arsip jangka panjang, dokumentasi perubahan skema sama pentingnya dengan endpoint itu sendiri. Banyak kegagalan integrasi terjadi bukan karena API down, tetapi karena arti data berubah tanpa jejak yang jelas.

Webhook untuk distribusi perubahan: cepat, tapi jangan dijadikan sumber kebenaran tunggal

Webhook cocok untuk memberi tahu consumer bahwa ada data baru, revisi, atau penghapusan logis. Namun webhook sebaiknya diposisikan sebagai trigger sinkronisasi, bukan satu-satunya saluran data final. Pola yang lebih aman:

  1. Provider mengirim event webhook berisi identitas event dan resource.
  2. Consumer memverifikasi signature dan replay protection.
  3. Consumer melakukan fetch ke API sumber untuk mengambil keadaan terbaru resource.
  4. Consumer menyimpan hasil dengan idempotency dan checksum validation.

Pola ini mengurangi risiko karena payload webhook bisa hilang, terpotong, atau datang tidak berurutan. Keadaan final tetap diambil dari endpoint sumber yang stabil.

Contoh payload webhook

{
  "event_id": "evt_01JARCHIVE8Y2K9M1",
  "event_type": "archive.record.published",
  "occurred_at": "2026-07-01T10:16:04Z",
  "delivery_attempt": 2,
  "resource": {
    "dataset_id": "climate-daily-observations",
    "record_id": "station-abc-2026-07-01",
    "version": 3,
    "etag": "W/\"record-v3-9f2a\""
  }
}

Payload di atas sengaja ringkas. Jangan menaruh seluruh data arsip ke webhook jika consumer masih perlu memverifikasi versi terbaru. Webhook yang terlalu besar memperbesar peluang timeout dan retry ganda.

Webhook signature dan replay protection

Webhook wajib ditandatangani. Minimal, provider mengirim:

  • timestamp pengiriman,
  • signature HMAC dari body mentah,
  • identifier key atau versi secret jika rotasi kunci didukung.

Contoh header:

Webhook-Id: wh_01JARCHIVEDELIVERY
Webhook-Timestamp: 1782891364
Webhook-Signature: v1=5a7c0d...

Proses verifikasi di consumer:

  1. Ambil raw body persis seperti diterima. Jangan serialisasi ulang sebelum verifikasi.
  2. Bangun string yang ditandatangani, misalnya timestamp + "." + raw_body.
  3. Hitung HMAC dengan secret bersama.
  4. Bandingkan dengan constant-time comparison.
  5. Tolak request jika timestamp terlalu lama atau terlalu jauh di masa depan.
  6. Simpan Webhook-Id atau kombinasi signature+timestamp untuk mencegah replay.

Contoh pseudocode:

signed_payload = timestamp + "." + raw_body
expected = HMAC_SHA256(secret, signed_payload)
if !constant_time_equals(expected, signature_v1): reject
if abs(now - timestamp) > allowed_skew_seconds: reject
if replay_store.exists(webhook_id): reject_or_ignore
replay_store.put(webhook_id, ttl=24h)

Mengapa replay protection perlu? Signature hanya membuktikan payload valid, bukan bahwa payload itu baru pertama kali dikirim. Tanpa replay protection, request lama yang sah bisa diputar ulang oleh pihak yang memperoleh salinannya.

Idempotency key untuk ingest ulang dan operasi tulis yang aman

Pada arsip data publik, operasi tulis sering diulang dengan sengaja: re-import file lama, sinkronisasi ulang setelah outage, atau backfill ke sistem baru. Tanpa idempotency, retry bisa menghasilkan duplikasi baris, versi palsu, atau status yang meloncat.

Kapan memakai idempotency key

Gunakan idempotency key pada operasi yang efeknya tidak boleh terduplikasi, misalnya:

  • POST /ingest untuk memasukkan objek baru,
  • POST /reindex untuk membangun ulang turunan data,
  • POST /deliveries/{id}/retry untuk pengiriman ulang terkontrol.

Contoh request ingest:

POST /ingest HTTP/1.1
Content-Type: application/json
Idempotency-Key: ingest-climate-daily-observations-station-abc-2026-07-01-v3

{
  "dataset_id": "climate-daily-observations",
  "record_id": "station-abc-2026-07-01",
  "version": 3,
  "checksum": {
    "algorithm": "sha256",
    "value": "3d7c4d..."
  },
  "source_url": "https://mirror.example.org/objects/station-abc-2026-07-01-v3.json"
}

Kunci idempotency harus merepresentasikan operasi logis yang sama. Jika request yang sama dikirim dua kali karena timeout, server harus mengembalikan hasil yang sama atau setidaknya status bahwa operasi sudah diproses.

Penyimpanan idempotency yang benar

Praktik umum:

  • Simpan idempotency_key, hash request, status proses, dan respons akhir.
  • Jika key yang sama datang lagi dengan payload berbeda, kembalikan error konflik.
  • Gunakan TTL sesuai kebutuhan bisnis, tetapi ingat bahwa replay bisa terjadi setelah jeda panjang pada proses arsip.

Contoh tabel sederhana:

CREATE TABLE idempotency_requests (
  idempotency_key TEXT PRIMARY KEY,
  request_hash TEXT NOT NULL,
  status TEXT NOT NULL,
  response_body TEXT,
  created_at TIMESTAMP NOT NULL,
  expires_at TIMESTAMP
);

Kesalahan umum: hanya memeriksa apakah key pernah ada, tanpa memverifikasi bahwa isi request sama. Ini berbahaya karena client bisa tidak sengaja memakai key lama untuk payload baru.

Idempotency key bukan pengganti unique constraint

Walau ada idempotency, database tetap perlu constraint pada identitas bisnis, misalnya kombinasi (dataset_id, record_id, version). Idempotency melindungi dari pengulangan request; constraint melindungi dari kondisi balapan dan bug lain.

Retry aman, deduplikasi event, dan masalah ordering

Aturan dasar retry

Provider maupun consumer biasanya akan melakukan retry. Itu normal. Yang berbahaya adalah ketika keduanya menganggap retry mereka satu-satunya sumber pengulangan. Aturan praktis:

  • Retry hanya untuk kegagalan sementara: timeout, koneksi putus, atau status server 5xx.
  • Jangan retry buta pada 4xx, kecuali ada alasan jelas seperti 429 Too Many Requests dengan kebijakan backoff.
  • Gunakan exponential backoff dengan jitter agar tidak memukul sistem tujuan serentak.
  • Batasi jumlah percobaan dan sediakan dead-letter queue atau status gagal permanen.

Deduplikasi event

Webhook setidaknya harus memiliki event_id unik. Consumer menyimpan event ID yang sudah diproses. Jika event yang sama datang lagi, consumer bisa:

  • mengabaikannya jika efek akhir sudah tercapai, atau
  • mengembalikan 2xx agar provider berhenti retry.

Deduplikasi event berbeda dari idempotency request:

  • Event deduplication mencegah event notifikasi yang sama diproses berulang.
  • Idempotent processing memastikan walau event diproses ulang, hasil akhirnya tetap sama.

Keduanya sebaiknya ada. Hanya dedup saja tidak cukup, karena event berbeda bisa menunjuk resource yang sama.

Ordering issue: jangan mengasumsikan webhook datang berurutan

Dalam sistem terdistribusi, urutan pengiriman dan urutan penerimaan bisa berbeda. Event versi 3 dapat tiba lebih dulu daripada versi 2. Jika consumer menulis data hanya berdasarkan urutan kedatangan, arsip bisa mundur ke versi lama.

Pendekatan yang lebih aman:

  • Simpan version, updated_at, atau sequence yang berasal dari provider.
  • Terapkan compare-and-set: update hanya jika versi masuk lebih baru atau belum pernah ada.
  • Jika ada gap urutan yang penting, lakukan fetch ke endpoint sumber untuk memastikan keadaan final.

Contoh logika:

if incoming.version < stored.version:
  ignore as stale
else if incoming.version == stored.version:
  process idempotently
else:
  fetch latest resource and upsert

Jika provider tidak punya nomor versi monotonik, gunakan timestamp publikasi dengan hati-hati. Timestamp rawan konflik jika dua perubahan dibuat hampir bersamaan atau jam sistem tidak konsisten. Nomor versi eksplisit lebih mudah diandalkan.

Partial failure: webhook sukses, ingest gagal

Kasus umum:

  1. consumer menerima webhook dan membalas 200,
  2. lalu job asynchronous untuk mengambil file gagal,
  3. provider menganggap pengiriman selesai, tetapi consumer belum benar-benar sinkron.

Solusinya adalah memisahkan acknowledgement delivery dari business processing dengan disiplin operasional:

  • balas 2xx hanya jika event sudah tervalidasi dan berhasil dimasukkan ke queue internal yang andal,
  • simpan status pemrosesan per event,
  • sediakan mekanisme rekonsiliasi periodik, misalnya pull endpoint untuk mencari event yang terlewat.

Webhook idealnya bukan satu-satunya jalan pemulihan. Untuk arsip publik, endpoint seperti GET /events?since=... atau GET /records?updated_after=... sangat membantu saat backlog perlu dibangun ulang.

Pola arsitektur yang praktis

Webhook + pull API + object storage

Pola yang sering paling stabil untuk arsip data publik:

  1. Metadata dan event tersedia via API JSON.
  2. File besar disimpan di object storage atau URL unduhan terpisah.
  3. Webhook hanya memberi tahu perubahan.
  4. Consumer menarik metadata terbaru lalu mengunduh objek jika checksum berubah.

Keuntungannya:

  • provider tidak perlu mengirim payload besar via webhook,
  • consumer bisa melakukan retry unduhan secara terpisah,
  • verifikasi integritas lebih mudah dengan checksum/etag,
  • proses backfill lebih sederhana.

Outbox pattern di sisi provider

Jika provider menulis data ke database lalu mengirim webhook langsung dari request handler, ada risiko data berhasil disimpan tetapi event gagal terkirim, atau sebaliknya. Outbox pattern membantu mengatasi ini:

  • tulis perubahan data dan catatan event outbox dalam transaksi yang sama,
  • worker terpisah membaca outbox dan mengirim webhook,
  • tandai status terkirim setelah sukses.

Dengan pola ini, pengiriman event lebih tahan gangguan tanpa mengorbankan konsistensi antara data dan notifikasi.

Observability: tanpa ini, retry hanya menambah kebingungan

Sistem sinkronisasi lintas sistem sulit dioperasikan tanpa observability yang memadai. Minimal, catat dan ukur hal berikut:

Log terstruktur

  • event_id
  • delivery_id atau webhook_id
  • idempotency_key
  • dataset_id, record_id, version
  • status verifikasi signature
  • hasil dedup: baru, duplikat, replay, stale
  • jumlah attempt retry

Metrik utama

  • delivery success rate webhook,
  • retry rate per endpoint atau consumer,
  • latensi dari event dibuat hingga consumer sinkron,
  • jumlah event duplikat,
  • jumlah event stale/out-of-order,
  • jumlah ingest gagal berdasarkan penyebab,
  • ukuran backlog queue.

Trace dan korelasi

Jika memungkinkan, bawa correlation ID dari provider ke consumer. Ini mempercepat debugging saat satu record gagal di tengah banyak sistem.

Tip debugging: saat melihat duplikasi data, cek urutannya: apakah sumbernya dari retry HTTP, retry queue internal, replay webhook, atau backfill manual. Gejalanya mirip, akar masalahnya berbeda.

Daftar edge case integrasi yang sering terlewat

  • Event yang sama dikirim ulang dengan delivery_attempt berbeda.
  • Provider mengirim event published lalu segera corrected untuk record yang sama.
  • Consumer menerima versi baru lebih dulu, lalu versi lama datang belakangan.
  • Unduhan file sukses tetapi checksum tidak cocok.
  • Request timeout setelah server sebenarnya sudah menyimpan data.
  • Idempotency key sama dipakai untuk payload berbeda.
  • Webhook lolos signature, tetapi timestamp sudah terlalu lama.
  • Secret webhook dirotasi, sebagian delivery masih memakai secret lama.
  • Record dihapus logis tetapi file historis harus tetap bisa diakses.
  • Provider dan consumer sama-sama melakukan retry cepat sehingga menimbulkan lonjakan beban.
  • Consumer membalas 200 sebelum pesan benar-benar tersimpan di queue internal.
  • Backfill jutaan record memicu event yang tidak relevan bagi consumer lama.
  • Schema field baru ditambahkan dan parser consumer gagal karena terlalu ketat.
  • Response 429 dari consumer tidak dihormati provider.

Kesalahan umum saat provider dan consumer sama-sama melakukan retry

1. Menganggap 2xx berarti proses bisnis selesai

Padahal bisa jadi baru sebatas diterima oleh HTTP layer. Solusinya: definisikan jelas arti acknowledgement.

2. Tidak membedakan duplicate delivery dan duplicate effect

Walau event sama datang dua kali, hasil bisnis harus tetap satu kali. Ini butuh dedup dan idempotent write path.

3. Retry tanpa backoff dan jitter

Akibatnya, saat incident, kedua sisi saling membanjiri. Gunakan backoff eksponensial dan batas percobaan.

4. Menyimpan event ID tetapi tidak memproteksi write database

Jika dua worker memproses event serupa bersamaan, duplikasi tetap bisa terjadi tanpa unique constraint atau locking yang tepat.

5. Menghapus idempotency record terlalu cepat

Pada alur arsip dan backfill, replay bisa terjadi jauh setelah request awal. TTL yang terlalu pendek membuka celah duplikasi.

6. Mengandalkan timestamp lokal untuk ordering

Jam sistem antar server bisa bergeser. Lebih aman menggunakan versi atau sequence dari provider.

Checklist desain API arsip data publik

  • Resource memiliki identifier stabil yang tidak bergantung pada URL file.
  • Ada pemisahan jelas antara API version, schema version, event ID, dan idempotency key.
  • Webhook ditandatangani dan diverifikasi dari raw body.
  • Ada replay protection berbasis timestamp dan penyimpanan delivery ID.
  • Consumer memperlakukan webhook sebagai trigger, bukan sumber kebenaran final.
  • Provider menyediakan endpoint pull untuk rekonsiliasi dan backfill.
  • Operasi tulis penting mendukung idempotency key.
  • Database memiliki unique constraint pada identitas bisnis.
  • Event dan ingest path tahan terhadap duplicate delivery.
  • Ordering issue ditangani dengan version/sequence, bukan asumsi urutan jaringan.
  • Retry dibatasi, memakai backoff dan jitter, serta memiliki dead-letter handling.
  • Partial failure dapat dideteksi lewat status pemrosesan dan metrik backlog.
  • Checksum atau etag tersedia untuk verifikasi integritas objek.
  • Log, metrik, dan correlation ID cukup untuk menelusuri satu record end-to-end.

Penutup

Desain API arsip data publik yang tahan sinkronisasi lintas sistem bergantung pada disiplin kontrak dan operasi: identitas resource yang stabil, webhook yang aman, retry yang terkendali, pemrosesan idempotent, dan observability yang memadai. Jika satu prinsip diabaikan, masalahnya biasanya tidak langsung terlihat, tetapi muncul saat outage, migrasi, atau ingest ulang skala besar.

Pendekatan paling aman biasanya sederhana: gunakan webhook sebagai sinyal, pull API sebagai sumber kebenaran, checksum untuk integritas, idempotency key untuk operasi tulis, dan deduplikasi event untuk distribusi. Dengan begitu, data publik tetap dapat direplikasi, diverifikasi, dan dipulihkan tanpa menciptakan arsip yang saling bertentangan.