Panduan · Terbit
Membuat klien API dari spesifikasi OpenAPI Kasera Pay: yang bisa dibangkitkan mesin, dan empat hal yang tetap harus ditulis tangan
Kontrak API Kasera Pay tersedia sebagai dokumen OpenAPI 3.0.3 di https://pay.kasera.id/v1/openapi.json, dan dokumen itu bisa diunduh tanpa kunci API. Dari satu berkas itu, klien bertipe bisa dibangkitkan untuk TypeScript, Python, dan bahasa lain yang punya generator OpenAPI, sehingga salah ketik nama kolom dan status yang lupa ditangani tertangkap saat kompilasi, bukan di produksi.
Yang jarang dikatakan: klien hasil bangkitan hanya menutup separuh integrasi. Empat hal yang paling menentukan uang tidak ada di spesifikasi dalam bentuk yang bisa dibangkitkan, dan panduan ini memisahkan keduanya dengan tegas. Semua contoh di bawah sudah diuji terhadap spesifikasi yang live saat panduan ini ditulis.
Isi spesifikasinya, dan kapan layak dibangkitkan
Dokumen itu memuat server https://pay.kasera.id, skema autentikasi bearer untuk kunci kp_test_ dan kp_live_, parameter header Idempotency-Key pada pembuatan transaksi, skema ErrorResponse dengan code, fields, dan request_id, serta skema payload webhook WebhookEvent. Status transaksi ditulis sebagai enum lima nilai: pending, succeeded, failed, expired, dan canceled. Rujukan yang ditulis untuk dibaca manusia ada di dokumentasi API.
Integrasi checkout pada umumnya hanya memanggil tiga endpoint: POST /v1/transactions, GET /v1/transactions/:id, dan GET /v1/payment_methods. Untuk itu, klien tulisan tangan sekitar empat puluh baris seperti di panduan integrasi Node.js dan Express sudah cukup. Pembangkitan mulai membayar dirinya saat bahasanya bertipe statis, saat lebih dari satu orang atau satu layanan memanggil API yang sama, atau saat kontraknya ingin dijaga sebagai berkas yang ikut ditinjau di setiap pull request.
Langkah pertama sama untuk semua bahasa:
# Unduh sekali, simpan di repositori, bangkitkan dari salinan itu.
curl -sS https://pay.kasera.id/v1/openapi.json -o openapi/kasera-pay.jsonBangkitkan dari salinan di repositori, bukan langsung dari URL live. Alasannya di bagian terakhir.
TypeScript: openapi-typescript dan openapi-fetch
openapi-typescript mengubah spesifikasi menjadi satu berkas deklarasi tipe tanpa kode runtime, dan openapi-fetch adalah pembungkus fetch yang membaca tipe itu. Diuji dengan openapi-typescript 7.13 dan openapi-fetch 0.17.
npm i openapi-fetch
npm i -D openapi-typescript typescript
npx openapi-typescript openapi/kasera-pay.json -o src/kasera-api.d.tsJangan panggil klien hasil bangkitan dari seluruh penjuru aplikasi. Taruh satu pembungkus tipis yang menambal apa yang tidak dibawa spesifikasi, dan biarkan sisa aplikasi hanya mengenal pembungkus itu:
// src/kasera.ts: satu-satunya berkas yang memanggil klien hasil bangkitan.
import createClient from "openapi-fetch";
import type { paths, components } from "./kasera-api";
export type Transaction = components["schemas"]["Transaction"];
type CreateBody = components["schemas"]["CreateTransactionRequest"];
const api = createClient<paths>({
baseUrl: "https://pay.kasera.id",
headers: { Authorization: `Bearer ${process.env.KASERA_PAY_KEY}` },
});
// Idempotency-Key opsional di spesifikasi, jadi opsional pula di klien
// hasil bangkitan. Di sini dijadikan wajib: satu pesanan, satu kunci,
// dipakai ulang pada setiap percobaan.
export async function createTransaction(body: CreateBody, idempotencyKey: string) {
for (let attempt = 1; ; attempt++) {
const { data, error, response } = await api.POST("/v1/transactions", {
params: { header: { "Idempotency-Key": idempotencyKey } },
body,
});
if (data) return data;
const retryable =
response.status === 429 ||
response.status === 500 ||
(response.status === 400 && error?.error.code === "invalid_body");
if (!retryable || attempt === 3) {
throw new Error(`Kasera Pay ${response.status} ${error?.error.code}`);
}
await new Promise((r) => setTimeout(r, 1000 * 2 ** attempt));
}
}
// Klien hasil bangkitan tidak pernah memaginasi sendiri.
export async function* allSucceeded() {
let cursor: string | undefined;
while (true) {
const { data, error } = await api.GET("/v1/transactions", {
params: { query: { status: "succeeded", limit: 100, starting_after: cursor } },
});
if (error) throw new Error(error.error.code);
yield* data.data;
if (!data.has_more || data.data.length === 0) return;
cursor = data.data[data.data.length - 1].id;
}
}Keuntungannya terlihat pada dua kesalahan yang paling sering lolos ke produksi:
// Salah ketik nama kolom tertangkap sebelum kode dijalankan:
createTransaction({ amont: 150000 }, "order-INV-1042");
// error TS2561: Object literal may only specify known properties,
// but 'amont' does not exist in type ... Did you mean to write 'amount'?
// Status adalah union lima nilai. Switch yang lupa satu nilai gagal dikompilasi.
function label(t: Transaction): string {
switch (t.status) {
case "pending": return "menunggu";
case "succeeded": return "lunas";
case "expired": return "kedaluwarsa";
case "failed": return "gagal";
case "canceled": return "dibatalkan";
default: { const never: never = t.status; return never; }
}
}Switch yang wajib lengkap itu penting karena daftar tagihan yang hanya mengenal lunas dan menunggu akan salah membaca tagihan yang dibatalkan. Kalau suatu saat enum di spesifikasi bertambah, kompilasi berikutnya yang memberi tahu, bukan pembeli.
Python: openapi-python-client
pip install openapi-python-client
openapi-python-client generate --path openapi/kasera-pay.json
pip install -e ./kasera-pay-api-clientGenerator ini menghasilkan paket kasera_pay_api_client dengan satu modul per operasi dan model bertipe untuk setiap skema. Diuji dengan versi 0.29.1:
import os
from kasera_pay_api_client import AuthenticatedClient
from kasera_pay_api_client.api.default import create_transaction
from kasera_pay_api_client.models import CreateTransactionRequest, ErrorResponse
client = AuthenticatedClient(
base_url="https://pay.kasera.id",
token=os.environ["KASERA_PAY_KEY"], # dikirim sebagai "Bearer kp_..."
)
res = create_transaction.sync_detailed(
client=client,
body=CreateTransactionRequest(
amount=150000, description="Pesanan INV-1042", merchant_ref="INV-1042"
),
idempotency_key="order-INV-1042", # parameternya opsional, jangan lupa
)
if isinstance(res.parsed, ErrorResponse):
# Cocokkan pada kode, bukan pada pesan.
raise RuntimeError(f"{res.status_code} {res.parsed.error.code} {res.parsed.error.request_id}")
tx = res.parsed # Transaction; tx.status bertipe TransactionStatusDengan kunci yang salah, panggilan di atas mengembalikan status 401 yang sudah terurai menjadi ErrorResponse dengan kode unauthorized dan request_id yang bisa disebutkan ke dukungan. Perhatikan bahwa idempotency_key adalah argumen opsional. Generator tidak akan mengingatkan kalau argumen itu terlupa, jadi bungkus fungsi ini sekali dan jadikan kuncinya wajib, sama seperti versi TypeScript.
Empat hal yang tetap harus ditulis tangan
1. Verifikasi tanda tangan webhook
OpenAPI 3.0.3 belum punya bagian webhooks, yang baru ada sejak 3.1, jadi generator hanya menghasilkan tipe WebhookEvent, bukan penerimanya. Header Kasera-Signature-V1 berbentuk t=<unix>,v1=<hex>, dengan HMAC-SHA256 atas <unix>.<body mentah>, toleransi lima menit, dan dua entri v1 selama 24 jam setelah rotasi signing secret. Urutannya yang menentukan: verifikasi atas byte mentah dulu, baru mengurai JSON dan memberi tipe.
// src/webhook.ts: ditulis tangan, karena spesifikasi hanya membawa bentuk payload.
import { createHmac, timingSafeEqual } from "node:crypto";
import type { components } from "./kasera-api";
type WebhookEvent = components["schemas"]["WebhookEvent"];
export function verifyAndParse(rawBody: Buffer, header: string | undefined, secret: string): WebhookEvent {
// Kasera-Signature-V1: t=<unix>,v1=<hex>[,v1=<hex>]
const parts = (header ?? "").split(",").map((p) => p.trim().split("="));
const t = parts.find(([k]) => k === "t")?.[1];
const sigs = parts.filter(([k]) => k === "v1").map(([, v]) => v);
if (!t || sigs.length === 0) throw new Error("header tanda tangan tidak lengkap");
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) throw new Error("stempel waktu terlalu lama");
const expected = createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest();
const ok = sigs.some((hex) => {
const got = Buffer.from(hex, "hex");
return got.length === expected.length && timingSafeEqual(got, expected);
});
if (!ok) throw new Error("tanda tangan tidak cocok");
// Baru sekarang body boleh diurai dan diberi tipe hasil bangkitan.
return JSON.parse(rawBody.toString("utf8")) as WebhookEvent;
}Rincian lengkap header, deduplikasi lewat Kasera-Event-Id, dan jadwal pengulangannya ada di dokumentasi webhook.
2. Strategi Idempotency-Key
Spesifikasi hanya bisa mengatakan bahwa header itu ada dan panjangnya paling banyak 255 karakter. Yang tidak bisa dibangkitkan adalah dari mana kuncinya berasal. Turunkan dari sesuatu yang stabil per pesanan, misalnya order-INV-1042, simpan, dan pakai ulang apa adanya pada setiap percobaan. Pengiriman ulang dengan body yang identik mengembalikan objek aslinya dengan status 200, dan body yang berbeda ditolak 409 idempotency_conflict. Kunci yang dibuat baru di setiap percobaan sama saja dengan tidak memakai kunci. Penjelasannya ada di dokumentasi idempotensi.
3. Kebijakan percobaan ulang
Generator tidak mengulang apa pun. Aturannya: 429 dan 500 layak diulang dengan kunci yang sama, 400 invalid_body juga, sedangkan kode 4xx lain berarti permintaannya salah dan akan tetap salah. Batas lajunya 300 permintaan per menit per kunci. Daftar kode beserta artinya ada di dokumentasi galat.
4. Pengulangan halaman daftar
GET /v1/transactions mengembalikan data dan has_more, dengan limit 1 sampai 100 dan kursor starting_after berisi id terakhir yang terbaca. Klien hasil bangkitan hanya mengambil satu halaman. Rekonsiliasi yang lupa mengulang akan diam-diam berhenti di baris ke-20, jumlah bawaan per halaman. Fungsi allSucceeded di pembungkus TypeScript di atas adalah bentuk pengulangannya.
Menjaga salinan spesifikasi tetap jujur
Spesifikasi yang live bisa berubah, misalnya saat kode galat atau kolom baru ditambahkan. Membangkitkan langsung dari URL berarti perubahan itu masuk ke build tanpa pernah dibaca siapa pun. Dengan salinan di repositori, alurnya menjadi:
- Salinan
openapi/kasera-pay.jsonikut di-commit bersama tipe hasil bangkitan. - Satu langkah CI terjadwal mengunduh versi live dan membandingkannya dengan salinan. Kalau berbeda, CI gagal dengan diff-nya, dan tidak ada yang otomatis diganti.
- Orang yang membaca diff memperbarui salinan, membangkitkan ulang, lalu membiarkan kompilator menunjuk setiap tempat yang perlu disesuaikan.
Sebelum kunci live dipakai, uji seluruh jalur dengan kunci kp_test_, yang hanya melihat dan membuat objek tes. Caranya ada di dokumentasi mode tes.
Pertanyaan yang sering muncul
Apakah ada SDK resmi Kasera Pay yang bisa dipasang langsung?
Tidak ada SDK resmi per bahasa. Yang diterbitkan adalah kontraknya sendiri di pay.kasera.id/v1/openapi.json, dan dari situ klien bertipe bisa dibangkitkan untuk bahasa apa pun yang punya generator OpenAPI 3.0. Alternatifnya adalah klien kecil yang ditulis tangan, seperti di panduan integrasi per bahasa.
Perlukah kunci API untuk mengunduh spesifikasinya?
Tidak. Dokumen /v1/openapi.json adalah satu-satunya endpoint di spesifikasi yang tidak meminta Authorization, jadi bisa diunduh dari CI atau mesin build yang tidak memegang kunci apa pun. Semua endpoint lain meminta Authorization: Bearer dengan kunci kp_test_ atau kp_live_.
Kenapa kolom tanggal di klien hasil bangkitan bertipe string, bukan Date atau datetime?
Karena spesifikasinya menulis stempel waktu sebagai string ISO-8601 dengan offset +07:00 tanpa format date-time, dan generator menghormati itu apa adanya. Hasilnya aman, karena tidak ada konversi zona waktu diam-diam, tetapi penguraian ke tipe tanggal harus dilakukan sendiri di satu tempat, bukan tersebar di setiap pemakaian.
Spesifikasinya memuat endpoint yang tidak dipakai. Perlukah dibuang sebelum dibangkitkan?
Tidak perlu. Membangkitkan semuanya tidak memanggil apa pun, dan pembungkus tipis di satu berkas sudah membatasi endpoint yang benar-benar dipakai aplikasi. Sebagian endpoint di spesifikasi, misalnya langganan yang masih beta tertutup, menjawab 404 untuk akun yang belum ikut beta, jadi keberadaannya di klien tidak berarti bisa dipakai.