Cara kerja
Integrasi server API kamu dengan marketplace punya tiga arah, semuanya diautentikasi dengan satu secret milik organisasimu.
- BuyerGateway marketplaceServer kamuWajib diverifikasi
Panggilan API buyer, diteruskan dengan tanda tangan
- MarketplaceServer kamuOpsional
Webhook: entitlement baru, diperpanjang, dicabut
- Server kamuMarketplaceOpsional
Sinkronisasi: tarik entitlement yang berubah
| Arah | Siapa menandatangani | Isi | Wajib? |
|---|---|---|---|
| Panggilan buyer — gateway → server kamu | Marketplace | Setiap request buyer ke API kamu, diteruskan gateway beserta identitas pemanggilnya | Wajib diverifikasi |
| Webhook — marketplace → server kamu | Marketplace | Pemberitahuan entitlement baru, diperpanjang, atau dicabut | Opsional |
| Sinkronisasi — server kamu → marketplace | Kamu | Menarik daftar entitlement yang berubah | Opsional, disarankan kalau kamu menyimpan data entitlement |
Akses buyer ditegakkan gateway, bukan server kamu. Key buyer, masa berlaku, kuota, dan pencabutan semuanya diperiksa gateway sebelum request diteruskan. Request yang sampai ke server kamu sudah sah — tugasmu hanya memastikan request itu memang datang dari gateway, bukan dari orang yang menemukan base URL kamu dan memanggilnya langsung.
Kamu tidak pernah menerima API key buyer, lewat jalur mana pun. Buyer dikenali lewat entitlement_id dan buyer_id.
| Aspek | Aturan |
|---|---|
| Secret | Satu per organisasi, dibuat otomatis. Di dashboard tenant → Integrasi, Owner bisa menampilkannya satu kali; setelah itu hanya pratinjau, dan secret yang hilang diganti. Nilainya berawalan whsec_ — pakai apa adanya, termasuk awalannya |
| Tenant ID | Tampil di dashboard tenant → Integrasi; dipakai di header X-Tenant-Id sinkronisasi |
| Algoritma | HMAC-SHA256, ditulis sha256=<hex huruf kecil>, di ketiga arah |
| Toleransi jam | 300 detik ke dua arah. Pastikan jam server kamu tersinkron (NTP) |
| Timestamp di header | Unix epoch detik, UTC |
| Tanggal di body | ISO 8601 UTC, mis. 2026-07-15T08:30:00Z |
| Nama header | Tidak peka huruf besar/kecil — HTTP/2 mengirim semuanya huruf kecil |
| Versi kontrak | v2. Endpoint marketplace berawalan /api/v1/ |
Mulai cepat
- Daftar sebagai tenant dan buat produk. Base URL produk wajib
https://. - Buka dashboard tenant → Integrasi, tampilkan secret (Owner, sekali saja), dan simpan di konfigurasi server kamu — mis. variabel lingkungan
KAYYA_API_SECRET. Jangan menaruhnya di repositori kode. - Pasang verifikasi panggilan gateway di depan seluruh route API kamu — lewat SDK, atau contoh kode di halaman ini.
- Uji implementasimu dengan test vector sebelum menyentuh server sungguhan.
- Deploy, lalu tekan Uji integrasi di halaman produk dashboard tenant. Ketiga uji harus lolos — apa yang diuji.
- Ajukan produk untuk review.
Webhook dan sinkronisasi opsional. Pasang kalau kamu ingin menyimpan data pelanggan di sistemmu sendiri — buyer tetap terlayani tanpa keduanya.
Pilih jalur integrasi
Kamu tidak wajib memakai SDK. Yang dinilai uji integrasi adalah perilaku server kamu — menolak tanda tangan palsu dengan 401 dan menerima tanda tangan yang sah — bukan cara kamu membuatnya. Ketiga jalur ini lolos uji yang sama:
| Jalur | Untuk siapa | Mulai dari |
|---|---|---|
| SDK resmi | Bahasa yang sudah punya SDK — satu baris konfigurasi, middleware siap pakai | SDK Python (kayya-api) sedang disiapkan; bahasa lain menyusul |
| Menulis sendiri | Bahasa apa pun — ±60 baris kode, hanya pustaka standar | Contoh kode Python, PHP, Node.js, dan Go di halaman ini, lalu cocokkan dengan test vector |
| Di lapisan depan | Kamu sudah punya API gateway atau edge worker di depan server aplikasi | Rumus yang sama dengan verifikasi gateway, dijalankan di lapisan itu; server aplikasi tidak disentuh |
Kesalahan paling berbahaya bila menulis sendiri adalah tidak memverifikasi sama sekali — hanya memeriksa ada-tidaknya header X-Marketplace-Buyer-Id. Header itu bisa ditulis siapa saja; tanpa tanda tangan, base URL kamu menjadi API terbuka: gratis, tanpa kuota, dan tanpa tercatat di marketplace.
Verifikasi panggilan gatewayWajib
Buyer memanggil alamat produkmu di marketplace dengan key-nya. Alamat itulah yang tercantum di dokumentasi produkmu untuk buyer — base URL aslimu tidak pernah ditampilkan. Gateway memeriksa key, masa berlaku, dan kuota, membuang kredensial buyer (Authorization, X-Api-Key, Cookie) serta seluruh header berawalan X-Marketplace- yang dikirim buyer, lalu meneruskan request ke {base_url_produk}{path} dengan header berikut:
| Header | Isi |
|---|---|
X-Marketplace-Buyer-Id | UUID buyer di marketplace |
X-Marketplace-Entitlement-Id | UUID entitlement — sama dengan entitlement_id di webhook & sinkronisasi |
X-Marketplace-Produk | Slug produk yang dipanggil — berguna kalau satu base URL melayani beberapa produk |
X-Marketplace-Expires-At | ISO 8601 UTC, akhir periode berjalan. Informasi saja — jangan dipakai untuk menolak |
X-Marketplace-Request-Id | UUID unik per request. Sertakan saat menghubungi dukungan — kami bisa menelusuri request yang sama |
X-Marketplace-Target | Path + query string persis seperti yang dikirim gateway ke server kamu, mis. /v1/cuaca?kota=Denpasar |
X-Marketplace-Timestamp | Unix epoch detik saat gateway menandatangani |
X-Marketplace-Signature | sha256=<hex>, atau beberapa dipisah koma selama rotasi secret |
Langkah 1 — tanda tangan
Yang ditandatangani hanya nilai header, digabung dengan baris baru (\n), tanpa baris baru di akhir:
payload = "v1:gateway"
+ "\n" + X-Marketplace-Timestamp
+ "\n" + X-Marketplace-Request-Id
+ "\n" + METHOD # huruf besar, mis. "GET"
+ "\n" + X-Marketplace-Target
+ "\n" + X-Marketplace-Buyer-Id
+ "\n" + X-Marketplace-Entitlement-Id
+ "\n" + X-Marketplace-Produk
+ "\n" + X-Marketplace-Expires-At
signature = "sha256=" + hex(HMAC_SHA256(key = secret, data = payload))
if header tidak lengkap: tolak 401 (header_kurang)
if abs(sekarang - int(X-Marketplace-Timestamp)) > 300: tolak 401 (timestamp)
if tidak satu pun bagian X-Marketplace-Signature.split(",")
sama (constant-time) dengan signature: tolak 401 (tanda_tangan)Langkah 2 — path
Pastikan request yang kamu terima memang request yang ditandatangani:
diterima = path + query string request yang diterima server kamu
if percent_decode(X-Marketplace-Target)
!= percent_decode(prefix_yang_dibuang + diterima): tolak 401 (path)- Kedua sisi di-decode dulu sebelum dibandingkan — framework berbeda menyajikan path secara berbeda (sebagian sudah di-decode, sebagian mentah). Membandingkan byte mentah menghasilkan
401palsu pada path berisi spasi atau huruf non-ASCII. prefix_yang_dibuangdiisi kalau server kamu di belakang reverse proxy yang membuang awalan path (mis. proxy menerima/v1/cuacalalu meneruskan/cuaca). Kosong untuk kebanyakan server.- Kenapa path tidak langsung ditandatangani: tiap lapisan (gateway, proxy, framework) meng-encode path sedikit berbeda, sehingga tanda tangan atas path mentah gagal di sebagian framework saja. Menandatangani nilai header — yang tidak diubah siapa pun — lalu mencocokkan path setelah di-decode memberi perlindungan yang sama tanpa kegagalan acak itu.
- Body tidak ikut ditandatangani, dan itu disengaja: request buyer bisa berupa unggahan besar atau streaming. Integritas body dijaga TLS — karena itu base URL wajib
https://. - Awalan
v1:gatewaymembedakan tanda tangan ini dari tanda tangan webhook, sehingga tanda tangan dari satu arah tidak bisa dipakai di arah lain.
Contoh kode
Hanya pustaka standar. Pasang di middleware, sebelum routing — bukan di tiap route. Uji integrasi mengetuk satu path acak dan menuntut 401; verifikasi yang hanya dipasang di sebagian route gagal di sana.
from __future__ import annotations
import hashlib
import hmac
import time
from urllib.parse import unquote
TOLERANSI_DETIK = 300
def _tanda_tangan(secret: str, pesan: bytes) -> str:
# Secret dipakai apa adanya, termasuk awalan "whsec_".
return "sha256=" + hmac.new(secret.encode(), pesan, hashlib.sha256).hexdigest()
def _salah_satu_cocok(dihitung: str, header_signature: str) -> bool:
# Selama masa rotasi secret header berisi beberapa tanda tangan dipisah koma.
# compare_digest, bukan ==, supaya waktunya tidak membocorkan isi signature.
return any(hmac.compare_digest(dihitung, s.strip()) for s in header_signature.split(","))
def _timestamp_sah(nilai: str, sekarang: float | None) -> bool:
try:
dikirim = int(nilai)
except ValueError:
return False
jam = time.time() if sekarang is None else sekarang
return abs(jam - dikirim) <= TOLERANSI_DETIK
HEADER_GATEWAY = (
"x-marketplace-timestamp",
"x-marketplace-request-id",
"x-marketplace-target",
"x-marketplace-buyer-id",
"x-marketplace-entitlement-id",
"x-marketplace-produk",
"x-marketplace-expires-at",
"x-marketplace-signature",
)
def verifikasi_gateway(
secret: str,
method: str,
path_diterima: str,
header: dict[str, str],
prefix_yang_dibuang: str = "",
sekarang: float | None = None,
) -> tuple[bool, str | None]:
"""Apakah panggilan ini benar datang dari gateway Kayya API.
``path_diterima``: path + query string seperti diterima server kamu.
Mengembalikan ``(sah, alasan)`` — alasan salah satu ``header_kurang``,
``timestamp``, ``tanda_tangan``, ``path``.
"""
h = {nama.lower(): nilai for nama, nilai in header.items()}
if not all(h.get(nama) for nama in HEADER_GATEWAY):
return False, "header_kurang"
ts = h["x-marketplace-timestamp"]
if not _timestamp_sah(ts, sekarang):
return False, "timestamp" # kemungkinan besar jam server meleset (NTP)
pesan = "\n".join([
"v1:gateway",
ts,
h["x-marketplace-request-id"],
method.upper(),
h["x-marketplace-target"],
h["x-marketplace-buyer-id"],
h["x-marketplace-entitlement-id"],
h["x-marketplace-produk"],
h["x-marketplace-expires-at"],
]).encode()
if not _salah_satu_cocok(_tanda_tangan(secret, pesan), h["x-marketplace-signature"]):
return False, "tanda_tangan"
# Kedua sisi di-decode: framework berbeda menyajikan path sebagian ter-decode.
if unquote(h["x-marketplace-target"]) != unquote(prefix_yang_dibuang + path_diterima):
return False, "path"
return True, NoneIsi argumen path yang diterima dengan path beserta query string seperti yang sampai ke server kamu:
| Lingkungan | Nilainya |
|---|---|
| PHP | $_SERVER['REQUEST_URI'] |
| Express | req.originalUrl |
Node http | req.url |
Go net/http | r.URL.RequestURI() |
| Django | request.get_full_path() |
| Flask | request.full_path.rstrip('?') — Flask selalu menambahkan ? |
| FastAPI / Starlette | request.url.path, ditambah "?" + request.url.query kalau ada query |
Jawaban yang diharapkan
| Situasi | Jawab dengan |
|---|---|
| Kedua langkah lolos | Layani seperti biasa |
| Header kurang, tanda tangan tidak cocok, timestamp di luar toleransi, atau path tidak cocok | 401, sebaiknya dengan body {"alasan": "<kode>"} |
| Request ke akar base URL tanpa header apa pun | Boleh 401 atau 200 — jangan 5xx |
Kode alasan sama dengan kode di test vector: header_kurang, timestamp, tanda_tangan, path. Dashboard tenant membacanya untuk menampilkan penyebab yang spesifik saat uji integrasi gagal. Kodenya tidak membocorkan apa pun — penyerang tetap tidak bisa membuat tanda tangan yang sah.
Baris terakhir penting: marketplace memeriksa kesehatan base URL kamu secara berkala dengan memanggil akarnya tanpa header. Jawaban di bawah 500 dianggap sehat; 5xx berturut-turut membuat produkmu ditandai gagal dan bisa dihentikan sementara.
Jangan menolak berdasarkan X-Marketplace-Expires-At
Keputusan boleh-tidaknya buyer mengakses sepenuhnya milik gateway. Ada keadaan di mana akses tetap sah meski tanggalnya terlihat lewat (mis. masa tenggang pembayaran langganan), dan aturan itu bisa berubah. Server yang ikut menolak berdasarkan tanggal akan menolak buyer yang sedang dilayani gateway. Pakai header ini untuk ditampilkan, bukan untuk memutuskan.
Test vector
Kumpulan kasus uji resmi: masukan persis (secret, header, path, jam) dan jawaban yang wajib dihasilkan implementasimu. Vector dihasilkan dari kode marketplace yang benar-benar menandatangani, dan setiap contoh kode di halaman ini lolos seluruhnya. Implementasi yang benar lolos seluruh vector, tanpa pengecualian.
kayya-test-vectors.json
35 kasus — gateway, webhook, sinkronisasi · dari contoh-manual-v1.0.0
Kode alasan | Arti | Urutan diperiksa |
|---|---|---|
header_kurang | Salah satu header wajib tidak ada | 1 |
timestamp | Selisih dengan jam server lebih dari 300 detik | 2 |
tanda_tangan | Tidak ada tanda tangan yang cocok | 3 |
path | X-Marketplace-Target ≠ path yang diterima (setelah di-decode) — gateway saja | 4 |
- Jam dari vector, bukan jam mesin. Pakai
masukan.sekarang; tanpa itu kasustimestamplolos atau gagal tergantung kapan test dijalankan. - Nama header tidak peka huruf besar/kecil. Beberapa kasus memakai nama huruf kecil, seperti yang dikirim HTTP/2.
- Raw body. Kasus webhook "body di-serialize ulang" hanya gagal kalau tanda tangan dihitung dari byte mentah — seperti seharusnya.
- Secret di dalamnya (
whsec_vektor-uji-…) dibuat khusus untuk uji dan tidak berlaku di mana pun.
WebhookOpsional
Marketplace mengirim POST ke URL webhook yang kamu isi di dashboard tenant → Integrasi. Tanpa webhook, produkmu tetap bisa dijual dan buyer tetap terlayani.
Webhook adalah pemberitahuan, bukan penjaga akses: entitlement yang dicabut sudah ditolak gateway detik itu juga, entah webhook-nya sampai ke kamu atau tidak. Pakai untuk hal-hal di sisimu sendiri — menyiapkan data pelanggan baru, mengirim email sambutan, memperbarui dashboard.
Event
| Event | Kapan | Yang harus kamu lakukan |
|---|---|---|
entitlement.updated | Pembelian baru, perpanjangan langganan, pencabutan dini | Upsert — aturan urutan |
ping | Tombol Kirim uji di dashboard dan pemeriksaan berkala tiap 15 menit | Jawab 2xx |
| Event lain yang tidak kamu kenal | Event baru di versi kontrak berikutnya | Jawab 2xx, abaikan. Jangan 4xx — itu dicatat sebagai kegagalan pengiriman |
Kedaluwarsa tidak dikirim sebagai event — ia terjadi karena waktu berlalu, bukan karena sesuatu terjadi. Hitung sendiri dari expires_at.
Perpanjangan memakai event yang sama, dengan entitlement_id yang sama dan expires_at yang sudah maju. Ini bukan pembelian kedua dan bukan duplikat — server yang mengabaikannya akan menganggap pelanggan kedaluwarsa padahal ia terus membayar.
Payload entitlement.updated
{
"event": "entitlement.updated",
"entitlement_id": "3b7d5c11-8e2f-4a90-b1c2-d3e4f5a6b7c8",
"buyer_id": "0f9c1e2a-7b3d-4c5e-9f10-2a3b4c5d6e7f",
"product_id": "7c1a9e33-4d2b-4f6a-8c0d-9e1f2a3b4c5d",
"tenant_id": "4f1c2b8e-9a7d-4e3f-8b21-6c5d0e9a1f37",
"durasi_bulan": 6,
"status": "active",
"expires_at": "2027-03-21T15:33:20Z",
"revoked_at": null,
"limit_hit": 80000,
"updated_at": "2026-09-21T15:33:18.123456Z"
}| Field | Keterangan |
|---|---|
entitlement_id | Identitas entitlement — kunci utama data yang kamu simpan. Sama dengan header X-Marketplace-Entitlement-Id |
buyer_id | Identitas buyer di marketplace (bukan identitas di sistem kamu) |
product_id, tenant_id | Produk kamu yang dibeli, dan organisasimu |
durasi_bulan | Panjang periode yang dibeli: 1, 3, 6, 9, atau 12 |
status | active, expired, atau revoked |
expires_at | ISO 8601 UTC, akhir periode berjalan. Selalu terisi |
revoked_at | Kapan dicabut dini (chargeback, fraud, keputusan moderasi); null kalau tidak dicabut |
limit_hit | Kuota panggilan periode ini; null = tanpa batas. Informatif — kuota ditegakkan gateway, jangan hitung sendiri |
updated_at | Kapan data ini terakhir berubah, presisi mikrodetik. Dipakai untuk urutan |
Payload ini sama persis dengan satu baris di response sinkronisasi, ditambah event dan tenant_id — satu fungsi upsert melayani keduanya.
Verifikasi tanda tangan
| Header | Isi |
|---|---|
X-Marketplace-Signature | sha256=<hex>, atau beberapa dipisah koma selama rotasi secret |
X-Marketplace-Timestamp | Unix epoch detik, ikut ditandatangani |
X-Marketplace-Event | Nama event (untuk routing/log saja) |
X-Marketplace-Delivery | UUID, unik per percobaan — bukan kunci idempotensi |
User-Agent | KayyaAPI-Webhook/1.0 |
payload = X-Marketplace-Timestamp + "." + raw_request_body
signature = "sha256=" + hex(HMAC_SHA256(key = secret, data = payload))def verifikasi_webhook(
secret: str,
raw_body: bytes,
header: dict[str, str],
sekarang: float | None = None,
) -> tuple[bool, str | None]:
"""Apakah kiriman webhook ini sah. ``raw_body``: byte persis seperti diterima."""
h = {nama.lower(): nilai for nama, nilai in header.items()}
ts = h.get("x-marketplace-timestamp", "")
signature = h.get("x-marketplace-signature", "")
if not ts or not signature:
return False, "header_kurang"
if not _timestamp_sah(ts, sekarang):
return False, "timestamp"
if not _salah_satu_cocok(_tanda_tangan(secret, ts.encode() + b"." + raw_body), signature):
return False, "tanda_tangan"
return True, NoneFungsi pembantunya — penanda tangan, pemeriksa timestamp, dan pembanding constant-time — sama dengan yang di contoh verifikasi gateway; satu berkas memuat ketiganya.
- Secret dipakai apa adanya, termasuk awalan
whsec_— bukan hasil decode base64/hex-nya. - Hitung dari raw body, byte persis seperti diterima. Parse lalu serialize ulang mengubah spasi dan urutan key, dan tanda tangan tidak akan pernah cocok. Ambil raw body sebelum body-parser framework bekerja.
- Bandingkan constant-time, bukan
==—==berhenti di byte pertama yang berbeda, dan selisih waktunya cukup untuk menebak tanda tangan byte demi byte.
Idempotensi & urutan
Marketplace bisa mengirim payload yang sama lebih dari sekali, dan retry percobaan lama bisa tiba setelah perubahan yang lebih baru — jadwal retry membentang sampai ±32 jam. Aturannya:
- Cari
entitlement_iddi data lokal. - Belum ada → insert.
- Sudah ada dan
updated_atyang datang lebih baru → update seluruh field. - Sudah ada dan
updated_atyang datang sama atau lebih lama → abaikan, tetap jawab200.
Langkah 3 adalah jalur normal setiap perpanjangan, bukan kasus pinggiran. Langkah 4 mencegah retry lama menimpa data baru — misalnya pemberitahuan "aktif" yang tertunda menimpa pencabutan yang sudah kamu terima dari sinkronisasi.
Jawaban & retry
| Situasi | Status | Akibatnya di marketplace |
|---|---|---|
| Sukses diproses (baru, update, atau diabaikan karena lama/duplikat) | 2xx | Selesai |
| Tanda tangan/timestamp tidak valid | 401 | Tidak di-retry — kesalahan konfigurasi tidak berubah kalau diulang. Tercatat di riwayat pengiriman dashboard |
| Server kamu error atau tidak menjawab | 5xx / timeout | Di-retry dengan jeda |
Jadwal retry: batas waktu 10 detik per percobaan, lalu 1 menit → 5 menit → 30 menit → 2 jam → 6 jam → 24 jam. Kalau seluruh percobaan habis, webhook kamu dinonaktifkan otomatis dan Owner/Admin diberi tahu di dashboard. Perubahan yang gagal terkirim tidak hilang — ikut tertarik lewat sinkronisasi. Balas secepatnya (di bawah 5 detik); kalau pemrosesanmu berat, simpan payload lalu proses asinkron.
SinkronisasiOpsional
Server kamu menarik daftar entitlement yang berubah. Ini jaring pengaman untuk webhook yang tidak sampai — server kamu mati, retry habis, atau webhook sedang nonaktif — dan cara memulihkan data lokal dari nol. Sinkronisasi tetap berjalan meski webhook tidak dipasang.
Endpoint & tanda tangan
GET https://marketplace-api.kayya.id/api/v1/provisioning/sync/?since={iso8601} ← halaman pertama
GET https://marketplace-api.kayya.id/api/v1/provisioning/sync/?kursor={kursor} ← halaman berikutnya| Parameter | Keterangan |
|---|---|
since | Halaman pertama saja. Nilai sampai dari sinkronisasi terakhir yang selesai seluruh halamannya. Kosongkan untuk menarik semuanya. Wajib menyertakan zona waktu |
kursor | Halaman berikutnya saja. Nilai kursor_berikutnya dari halaman sebelumnya, dikirim apa adanya (di-encode sebagai parameter URL). Jangan dibongkar atau disusun sendiri |
| Header | Isi |
|---|---|
X-Tenant-Id | Tenant ID kamu |
X-Tenant-Timestamp | Unix epoch detik |
X-Tenant-Signature | sha256=<hex huruf kecil> |
User-Agent | Disarankan menyebut SDK & versinya, mis. kayya-api-python/1.0.0 |
payload = X-Tenant-Timestamp + "." + request_target
signature = "sha256=" + hex(HMAC_SHA256(key = secret, data = payload))request_target = path + query string persis seperti yang dikirim, mis. /api/v1/provisioning/sync/?since=2026-08-01T00%3A00%3A00%2B00%3A00. Susun URL-nya sekali, tandatangani string itu, lalu kirim string yang sama — jangan biarkan library HTTP menyusun ulang query setelah kamu menandatanganinya. Tanda + wajib di-encode %2B.
def tanda_tangan_sync(secret: str, timestamp: str, request_target: str) -> str:
"""Nilai ``X-Tenant-Signature``. ``request_target``: path + query persis seperti dikirim."""
return _tanda_tangan(secret, f"{timestamp}.{request_target}".encode())Response
{
"hasil": [
{
"entitlement_id": "3b7d5c11-8e2f-4a90-b1c2-d3e4f5a6b7c8",
"buyer_id": "0f9c1e2a-7b3d-4c5e-9f10-2a3b4c5d6e7f",
"product_id": "7c1a9e33-4d2b-4f6a-8c0d-9e1f2a3b4c5d",
"durasi_bulan": 1,
"status": "revoked",
"expires_at": "2026-10-01T00:00:00Z",
"revoked_at": "2026-09-12T03:14:00Z",
"limit_hit": null,
"updated_at": "2026-09-12T03:14:00.482913Z"
}
],
"sampai": "2026-09-23T04:00:00.000000+00:00",
"kursor_berikutnya": "opaque-string"
}| Field | Keterangan |
|---|---|
hasil | Maksimal 200 baris per halaman, urut updated_at. Bentuk baris sama dengan payload webhook tanpa event & tenant_id; terapkan dengan aturan yang sama |
sampai | Batas atas perubahan yang tercakup — sama di seluruh halaman satu putaran. Sengaja tertinggal ±30 detik supaya perubahan yang transaksinya belum selesai tidak terlewat. Bukan jam — jangan diganti jam server kamu |
kursor_berikutnya | Untuk halaman berikutnya, atau null kalau ini halaman terakhir |
| Status | Kode | Kapan |
|---|---|---|
400 | VALIDATION_ERROR | since tidak valid atau tanpa zona waktu; kursor rusak |
401 | UNAUTHENTICATED | Header kurang, tanda tangan tidak cocok, timestamp di luar toleransi, atau tenant tidak dikenal — pesannya sengaja tidak membedakan keempatnya |
429 | RATE_LIMITED | Terlalu sering — perlambat interval kamu |
500 | INTERNAL_ERROR | Kegagalan tak terduga di marketplace |
Algoritma yang benar
res = GET sync(since = baca_penanda()) # penanda kosong pada sinkronisasi pertama
loop:
terapkan(res.hasil) # aturan idempotensi webhook
if res.kursor_berikutnya is null:
simpan_penanda(res.sampai) # HANYA setelah halaman terakhir
break
res = GET sync(kursor = res.kursor_berikutnya)- Jangan memakai jam server kamu sebagai
sinceberikutnya — pakaisampai. Jam server melewatkan perubahan di jendela 30 detik terakhir. - Jangan menyimpan
sampaisebelum halaman terakhir. Kalau putaran gagal di tengah, mulai lagi darisincelama — baris yang sudah diterapkan akan diabaikan aturan idempotensi. - Interval: tiap 1 jam, plus sekali setiap service kamu start. Interval hanya menentukan seberapa cepat data lokalmu menyusul, bukan seberapa cepat pencabutan berlaku — itu urusan gateway.
- Kegagalan sinkronisasi tidak boleh menghentikan service kamu. Catat, coba lagi dengan jeda, dan tetap layani buyer — tanda tangan gateway cukup untuk melayani request tanpa data lokal sama sekali.
Uji integrasi & syarat tayang
Sebelum produk bisa diajukan review, base URL produk itu harus lolos uji integrasi. Uji berlaku per produk — produk dengan base URL berbeda diuji masing-masing. Jalankan kapan saja lewat tombol Uji integrasi di halaman produk dashboard tenant (paling banyak 20 kali per jam per produk); marketplace juga mengulangnya tiap 15 menit selama produk tayang.
| Uji | Yang dikirim marketplace | Jawaban yang harus kamu berikan |
|---|---|---|
| Tanda tangan palsu | Header lengkap dengan tanda tangan salah, ke akar base URL dan ke satu path acak di bawahnya | 401 |
| Tanda tangan sah | Request bertanda tangan benar ke akar base URL | Apa saja selain 401 dan 5xx — 404 pun lolos |
| Kalau gagal setelah tayang | Akibatnya |
|---|---|
Uji tanda tangan sah ditolak 401 dua kali berturut (±30 menit) | Secret di server kamu tidak cocok, jadi seluruh buyer ikut ditolak → produk diturunkan otomatis, lalu naik lagi sendiri begitu lolos |
| Uji tanda tangan palsu diterima | Server kamu menerima request yang tidak lewat gateway → peringatan di dashboard & email; produk tetap tayang, tapi API kamu terbuka gratis bagi siapa pun yang menemukan base URL-nya |
Pemberitahuan hanya dikirim saat keadaannya berubah. Server yang tidak menjawab atau menjawab 5xx tidak dihitung di sini — itu urusan pemeriksaan kesehatan base URL.
Bentuk panggilan uji
Sama persis dengan panggilan buyer yang diteruskan gateway, dengan beberapa nilai tetap supaya kamu bisa mengenalinya:
| Nilai | |
|---|---|
| Method | GET |
| Path acak | <path base URL>/uji-integrasi-<12 hex> — berbeda tiap putaran |
X-Marketplace-Buyer-Id & X-Marketplace-Entitlement-Id | 00000000-0000-0000-0000-000000000000 — jangan catat sebagai pemakaian buyer |
X-Marketplace-Produk | Slug produk yang diuji |
User-Agent | KayyaAPI-UjiIntegrasi/1.0 |
| Batas waktu | 5 detik per panggilan; redirect tidak diikuti |
- Tanda tangan palsu berbentuk sah (
sha256=+ 64 hex, dari secret acak) — server yang hanya memeriksa bentuk header tetap tertangkap. 3xxpada uji palsu dihitung gagal: verifikasi harus berjalan sebelum redirect.- Jawaban
401dengan body{"alasan": "<kode>"}membuat dashboard menampilkan penyebab yang spesifik untuk uji sah yang gagal. - Uji hanya membuktikan path yang diketuknya. Lindungi seluruh route API kamu, bukan hanya akarnya.
Rotasi secret
Ganti secret di dashboard tenant → Integrasi → Ganti secret. Ada masa tenggang 24 jam:
| Selama masa tenggang | Perilaku marketplace |
|---|---|
| Panggilan gateway & webhook | Ditandatangani dengan kedua secret: X-Marketplace-Signature: sha256=<baru>,sha256=<lama> |
| Sinkronisasi | Menerima tanda tangan dari secret baru maupun lama |
Setelah 24 jam, secret lama berhenti berlaku. Tombol Akhiri masa tenggang sekarang tersedia kalau secret lama bocor dan harus dimatikan seketika.
Urutan yang aman: ganti secret di dashboard → pasang secret baru di konfigurasi server → deploy — semuanya dalam 24 jam. Server yang belum diperbarui tetap berjalan dengan secret lama selama masa tenggang, karena verifikasi menerima salah satu tanda tangan yang cocok.
Pemecahan masalah
| Gejala | Penyebab paling mungkin | Perbaikan |
|---|---|---|
401 dengan alasan timestamp | Jam server melenceng lebih dari 5 menit | Nyalakan sinkronisasi waktu (NTP) |
401 dengan alasan tanda_tangan, padahal secret sudah dipasang | Awalan whsec_ terpotong; secret lama setelah masa tenggang berakhir; atau secret belum pernah ditampilkan sejak diganti | Tampilkan atau ganti secret di dashboard → Integrasi, pasang apa adanya |
401 dengan alasan path | Reverse proxy membuang awalan path, atau query string tidak ikut dibandingkan | Isi prefix_yang_dibuang; pakai path beserta query — nilai per framework |
Uji "tanda tangan palsu → path acak" gagal dengan 404 | Verifikasi dipasang per route, bukan di depan routing | Pindahkan ke middleware |
| Seluruh panggilan gagal setelah pindah ke HTTP/2 | Pencarian header peka huruf besar/kecil | Cari header tanpa memedulikan huruf besar/kecil |
Webhook 401 walau secret benar | Tanda tangan dihitung dari body yang sudah di-parse ulang | Pakai raw body, sebelum body-parser |
Sinkronisasi 401 | URL disusun ulang library HTTP setelah ditandatangani, atau + tidak di-encode | Tandatangani dan kirim string target yang sama persis |
| Produk turun dari katalog tiba-tiba | Uji tanda tangan sah ditolak — biasanya secret baru belum terpasang setelah masa tenggang habis | Lihat kartu Uji integrasi di halaman produk; produk naik lagi sendiri begitu lolos |
| Produk ditandai gagal health check | Akar base URL tanpa header dijawab 5xx | Jawab 401 atau 200, bukan error |
Menghubungi dukungan? Sertakan nilai X-Marketplace-Request-Id dari request yang bermasalah — kami bisa menelusuri request yang sama di sisi kami.
Changelog kontrak
v2 — 23 September 2026. Menggantikan v1 seluruhnya.
- Panggilan buyer dari gateway kini ditandatangani, ditambah header
X-Marketplace-Request-Id&X-Marketplace-Target. Server tenant memverifikasi tanda tangan, bukan key buyer. - Webhook & sinkronisasi tidak lagi membawa API key buyer — diganti
entitlement_id. - Event
entitlement.updatedmenggantikankey.provisioned: juga dikirim saat pencabutan, membawastatus&updated_at. - Syarat berjualan: uji integrasi per produk; webhook opsional.
- Secret dibuat otomatis untuk setiap organisasi; rotasi dengan masa tenggang 24 jam dan multi-signature.
- Sinkronisasi memakai kursor, bukan nomor halaman;
since=sampaidari halaman terakhir. User-Agentwebhook:KayyaAPI-Webhook/1.0.
Contoh kode di halaman ini dan SDK resmi mengikuti semantic versioning; setiap perubahan kontrak dicatat di sini lebih dulu.
