Untuk developer tenant · kontrak integrasi v2

Integrasi server API dengan marketplace

Cara menghubungkan server API kamu: memverifikasi panggilan buyer yang diteruskan gateway, menerima webhook, dan menarik sinkronisasi entitlement — dengan contoh kode yang diuji terhadap test vector resmi. Tidak perlu akun untuk membacanya.

Diperbarui 23 September 2026 · contoh kode dari contoh-manual-v1.0.0

Cara kerja

Integrasi server API kamu dengan marketplace punya tiga arah, semuanya diautentikasi dengan satu secret milik organisasimu.

  1. BuyerGateway marketplaceServer kamuWajib diverifikasi

    Panggilan API buyer, diteruskan dengan tanda tangan

  2. MarketplaceServer kamuOpsional

    Webhook: entitlement baru, diperpanjang, dicabut

  3. Server kamuMarketplaceOpsional

    Sinkronisasi: tarik entitlement yang berubah

ArahSiapa menandatanganiIsiWajib?
Panggilan buyer — gateway → server kamuMarketplaceSetiap request buyer ke API kamu, diteruskan gateway beserta identitas pemanggilnyaWajib diverifikasi
Webhook — marketplace → server kamuMarketplacePemberitahuan entitlement baru, diperpanjang, atau dicabutOpsional
Sinkronisasi — server kamu → marketplaceKamuMenarik daftar entitlement yang berubahOpsional, 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.

AspekAturan
SecretSatu 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 IDTampil di dashboard tenant → Integrasi; dipakai di header X-Tenant-Id sinkronisasi
AlgoritmaHMAC-SHA256, ditulis sha256=<hex huruf kecil>, di ketiga arah
Toleransi jam300 detik ke dua arah. Pastikan jam server kamu tersinkron (NTP)
Timestamp di headerUnix epoch detik, UTC
Tanggal di bodyISO 8601 UTC, mis. 2026-07-15T08:30:00Z
Nama headerTidak peka huruf besar/kecil — HTTP/2 mengirim semuanya huruf kecil
Versi kontrakv2. Endpoint marketplace berawalan /api/v1/

Mulai cepat

  1. Daftar sebagai tenant dan buat produk. Base URL produk wajib https://.
  2. 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.
  3. Pasang verifikasi panggilan gateway di depan seluruh route API kamu — lewat SDK, atau contoh kode di halaman ini.
  4. Uji implementasimu dengan test vector sebelum menyentuh server sungguhan.
  5. Deploy, lalu tekan Uji integrasi di halaman produk dashboard tenant. Ketiga uji harus lolos — apa yang diuji.
  6. 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:

JalurUntuk siapaMulai dari
SDK resmiBahasa yang sudah punya SDK — satu baris konfigurasi, middleware siap pakaiSDK Python (kayya-api) sedang disiapkan; bahasa lain menyusul
Menulis sendiriBahasa apa pun — ±60 baris kode, hanya pustaka standarContoh kode Python, PHP, Node.js, dan Go di halaman ini, lalu cocokkan dengan test vector
Di lapisan depanKamu sudah punya API gateway atau edge worker di depan server aplikasiRumus 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:

HeaderIsi
X-Marketplace-Buyer-IdUUID buyer di marketplace
X-Marketplace-Entitlement-IdUUID entitlement — sama dengan entitlement_id di webhook & sinkronisasi
X-Marketplace-ProdukSlug produk yang dipanggil — berguna kalau satu base URL melayani beberapa produk
X-Marketplace-Expires-AtISO 8601 UTC, akhir periode berjalan. Informasi sajajangan dipakai untuk menolak
X-Marketplace-Request-IdUUID unik per request. Sertakan saat menghubungi dukungan — kami bisa menelusuri request yang sama
X-Marketplace-TargetPath + query string persis seperti yang dikirim gateway ke server kamu, mis. /v1/cuaca?kota=Denpasar
X-Marketplace-TimestampUnix epoch detik saat gateway menandatangani
X-Marketplace-Signaturesha256=<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 401 palsu pada path berisi spasi atau huruf non-ASCII.
  • prefix_yang_dibuang diisi kalau server kamu di belakang reverse proxy yang membuang awalan path (mis. proxy menerima /v1/cuaca lalu 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:gateway membedakan 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, None
Python 3.9+ · hanya pustaka standar · lolos 35 test vector Berkas lengkap & test-nya (contoh-manual-v1.0.0)

Isi argumen path yang diterima dengan path beserta query string seperti yang sampai ke server kamu:

LingkunganNilainya
PHP$_SERVER['REQUEST_URI']
Expressreq.originalUrl
Node httpreq.url
Go net/httpr.URL.RequestURI()
Djangorequest.get_full_path()
Flaskrequest.full_path.rstrip('?') — Flask selalu menambahkan ?
FastAPI / Starletterequest.url.path, ditambah "?" + request.url.query kalau ada query

Jawaban yang diharapkan

SituasiJawab dengan
Kedua langkah lolosLayani seperti biasa
Header kurang, tanda tangan tidak cocok, timestamp di luar toleransi, atau path tidak cocok401, sebaiknya dengan body {"alasan": "<kode>"}
Request ke akar base URL tanpa header apa punBoleh 401 atau 200jangan 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 alasanArtiUrutan diperiksa
header_kurangSalah satu header wajib tidak ada1
timestampSelisih dengan jam server lebih dari 300 detik2
tanda_tanganTidak ada tanda tangan yang cocok3
pathX-Marketplace-Target ≠ path yang diterima (setelah di-decode) — gateway saja4
  • Jam dari vector, bukan jam mesin. Pakai masukan.sekarang; tanpa itu kasus timestamp lolos 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

EventKapanYang harus kamu lakukan
entitlement.updatedPembelian baru, perpanjangan langganan, pencabutan diniUpsert — aturan urutan
pingTombol Kirim uji di dashboard dan pemeriksaan berkala tiap 15 menitJawab 2xx
Event lain yang tidak kamu kenalEvent baru di versi kontrak berikutnyaJawab 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

JSON
{
  "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"
}
FieldKeterangan
entitlement_idIdentitas entitlement — kunci utama data yang kamu simpan. Sama dengan header X-Marketplace-Entitlement-Id
buyer_idIdentitas buyer di marketplace (bukan identitas di sistem kamu)
product_id, tenant_idProduk kamu yang dibeli, dan organisasimu
durasi_bulanPanjang periode yang dibeli: 1, 3, 6, 9, atau 12
statusactive, expired, atau revoked
expires_atISO 8601 UTC, akhir periode berjalan. Selalu terisi
revoked_atKapan dicabut dini (chargeback, fraud, keputusan moderasi); null kalau tidak dicabut
limit_hitKuota panggilan periode ini; null = tanpa batas. Informatif — kuota ditegakkan gateway, jangan hitung sendiri
updated_atKapan 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

HeaderIsi
X-Marketplace-Signaturesha256=<hex>, atau beberapa dipisah koma selama rotasi secret
X-Marketplace-TimestampUnix epoch detik, ikut ditandatangani
X-Marketplace-EventNama event (untuk routing/log saja)
X-Marketplace-DeliveryUUID, unik per percobaan — bukan kunci idempotensi
User-AgentKayyaAPI-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, None
Python 3.9+ · hanya pustaka standar · lolos 35 test vector Berkas lengkap & test-nya (contoh-manual-v1.0.0)

Fungsi 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:

  1. Cari entitlement_id di data lokal.
  2. Belum ada → insert.
  3. Sudah ada dan updated_at yang datang lebih baru → update seluruh field.
  4. Sudah ada dan updated_at yang datang sama atau lebih lama → abaikan, tetap jawab 200.

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

SituasiStatusAkibatnya di marketplace
Sukses diproses (baru, update, atau diabaikan karena lama/duplikat)2xxSelesai
Tanda tangan/timestamp tidak valid401Tidak di-retry — kesalahan konfigurasi tidak berubah kalau diulang. Tercatat di riwayat pengiriman dashboard
Server kamu error atau tidak menjawab5xx / timeoutDi-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
ParameterKeterangan
sinceHalaman pertama saja. Nilai sampai dari sinkronisasi terakhir yang selesai seluruh halamannya. Kosongkan untuk menarik semuanya. Wajib menyertakan zona waktu
kursorHalaman berikutnya saja. Nilai kursor_berikutnya dari halaman sebelumnya, dikirim apa adanya (di-encode sebagai parameter URL). Jangan dibongkar atau disusun sendiri
HeaderIsi
X-Tenant-IdTenant ID kamu
X-Tenant-TimestampUnix epoch detik
X-Tenant-Signaturesha256=<hex huruf kecil>
User-AgentDisarankan 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())
Python 3.9+ · hanya pustaka standar · lolos 35 test vector Berkas lengkap & test-nya (contoh-manual-v1.0.0)

Response

JSON
{
  "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"
}
FieldKeterangan
hasilMaksimal 200 baris per halaman, urut updated_at. Bentuk baris sama dengan payload webhook tanpa event & tenant_id; terapkan dengan aturan yang sama
sampaiBatas 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_berikutnyaUntuk halaman berikutnya, atau null kalau ini halaman terakhir
StatusKodeKapan
400VALIDATION_ERRORsince tidak valid atau tanpa zona waktu; kursor rusak
401UNAUTHENTICATEDHeader kurang, tanda tangan tidak cocok, timestamp di luar toleransi, atau tenant tidak dikenal — pesannya sengaja tidak membedakan keempatnya
429RATE_LIMITEDTerlalu sering — perlambat interval kamu
500INTERNAL_ERRORKegagalan 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 since berikutnya — pakai sampai. Jam server melewatkan perubahan di jendela 30 detik terakhir.
  • Jangan menyimpan sampai sebelum halaman terakhir. Kalau putaran gagal di tengah, mulai lagi dari since lama — 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.

UjiYang dikirim marketplaceJawaban yang harus kamu berikan
Tanda tangan palsuHeader lengkap dengan tanda tangan salah, ke akar base URL dan ke satu path acak di bawahnya401
Tanda tangan sahRequest bertanda tangan benar ke akar base URLApa saja selain 401 dan 5xx404 pun lolos
Kalau gagal setelah tayangAkibatnya
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 diterimaServer 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
MethodGET
Path acak<path base URL>/uji-integrasi-<12 hex> — berbeda tiap putaran
X-Marketplace-Buyer-Id & X-Marketplace-Entitlement-Id00000000-0000-0000-0000-000000000000 — jangan catat sebagai pemakaian buyer
X-Marketplace-ProdukSlug produk yang diuji
User-AgentKayyaAPI-UjiIntegrasi/1.0
Batas waktu5 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.
  • 3xx pada uji palsu dihitung gagal: verifikasi harus berjalan sebelum redirect.
  • Jawaban 401 dengan 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 → IntegrasiGanti secret. Ada masa tenggang 24 jam:

Selama masa tenggangPerilaku marketplace
Panggilan gateway & webhookDitandatangani dengan kedua secret: X-Marketplace-Signature: sha256=<baru>,sha256=<lama>
SinkronisasiMenerima 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

GejalaPenyebab paling mungkinPerbaikan
401 dengan alasan timestampJam server melenceng lebih dari 5 menitNyalakan sinkronisasi waktu (NTP)
401 dengan alasan tanda_tangan, padahal secret sudah dipasangAwalan whsec_ terpotong; secret lama setelah masa tenggang berakhir; atau secret belum pernah ditampilkan sejak digantiTampilkan atau ganti secret di dashboard → Integrasi, pasang apa adanya
401 dengan alasan pathReverse proxy membuang awalan path, atau query string tidak ikut dibandingkanIsi prefix_yang_dibuang; pakai path beserta query — nilai per framework
Uji "tanda tangan palsu → path acak" gagal dengan 404Verifikasi dipasang per route, bukan di depan routingPindahkan ke middleware
Seluruh panggilan gagal setelah pindah ke HTTP/2Pencarian header peka huruf besar/kecilCari header tanpa memedulikan huruf besar/kecil
Webhook 401 walau secret benarTanda tangan dihitung dari body yang sudah di-parse ulangPakai raw body, sebelum body-parser
Sinkronisasi 401URL disusun ulang library HTTP setelah ditandatangani, atau + tidak di-encodeTandatangani dan kirim string target yang sama persis
Produk turun dari katalog tiba-tibaUji tanda tangan sah ditolak — biasanya secret baru belum terpasang setelah masa tenggang habisLihat kartu Uji integrasi di halaman produk; produk naik lagi sendiri begitu lolos
Produk ditandai gagal health checkAkar base URL tanpa header dijawab 5xxJawab 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.updated menggantikan key.provisioned: juga dikirim saat pencabutan, membawa status & 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 = sampai dari halaman terakhir.
  • User-Agent webhook: KayyaAPI-Webhook/1.0.

Contoh kode di halaman ini dan SDK resmi mengikuti semantic versioning; setiap perubahan kontrak dicatat di sini lebih dulu.