Dokumentasi · Integrasi
Integrasi API dan webhook
Cara menghubungkan HRIS, LMS, atau portal mitra: kunci API, izin, batas pemakaian, dan webhook bertanda tangan.
Diperbarui 2026-11-20
Halaman ini untuk tim teknis mitra: HRIS, LMS, ATS, atau portal perusahaan yang perlu mengirim peserta dan menerima hasil secara otomatis.
1. Yang perlu disiapkan
- Satu klien integrasi dari tim kami. Klien adalah batas data: peserta, hasil, dan room yang dibuat lewat API hanya terlihat oleh klien itu.
- Satu kunci API per lingkungan (produksi dan uji dipisah).
- Satu endpoint HTTPS bila Anda ingin menerima event.
Kunci produksi berawalan sk_live_, kunci uji berawalan sk_test_. Keduanya hanya ditampilkan sekali saat dibuat.
2. Autentikasi
Setiap permintaan membawa header:
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxKunci dipetakan ke klien, lalu diperiksa: status klien, masa berlaku, allowlist IP (bila diatur), izin, batas laju, dan kuota harian. Kegagalan izin dijawab 403; batas laju dan kuota dijawab 429.
3. Izin
Izin efektif adalah irisan antara izin klien dan izin kunci. Artinya mencabut izin di klien langsung mempersempit semua kunci di bawahnya.
| Izin | Kegunaan |
|---|---|
catalog:read | Daftar instrumen yang terbit |
participants:read | Daftar dan detail peserta |
participants:write | Membuat dan mengubah peserta |
results:read | Daftar dan detail hasil beserta skala |
reports:read | Status laporan dan tautan PDF |
orders:read | Pesanan dan status pembayaran |
rooms:read | Daftar dan detail room |
rooms:write | Membuat room dan menugaskan tes |
webhooks:manage | Mengelola endpoint webhook |
4. Bentuk respons
Semua respons sukses memakai amplop data dan meta:
{
"data": { "id": "…" },
"meta": { "requestId": "req_…", "nextCursor": "…" }
}Galat memakai amplop error:
{
"error": {
"code": "validation_error",
"message": "Email peserta tidak sah.",
"requestId": "req_…"
}
}Sertakan requestId saat melapor; kami dapat melacak permintaan itu sampai ke log.
5. Paginasi
Daftar memakai cursor. Ambil meta.nextCursor dan kirim ulang sebagai ?cursor=. limit bawaan 25, maksimum 100. Jangan menyusun cursor sendiri: isinya dapat berubah.
6. Idempotensi
POST /api/v1/participants idempoten berdasarkan email. Mengirim ulang permintaan yang sama mengembalikan peserta yang sama, bukan duplikat. Ini membuat retry aman tanpa kunci idempotensi tambahan.
Bila email itu sudah terdaftar pada klien lain, permintaan ditolak 409. Kami tidak memindahkan data antar klien secara diam-diam.
7. Webhook
Event yang tersedia
| Event | Kapan dikirim |
|---|---|
attempt.started | Peserta mulai mengerjakan tes |
attempt.submitted | Jawaban dikirim dan skor tersedia |
commerce.order.paid | Pembayaran terkonfirmasi |
report.reviewed | Laporan ditinjau atau ditandatangani |
result.invalidated | Hasil dibatalkan karena integritas |
result.reinstated | Hasil dipulihkan |
Berlangganan memakai nama persis, prefiks seperti attempt.*, atau * untuk semua.
Bentuk payload
{
"id": "dlv_…",
"type": "attempt.submitted",
"version": 1,
"occurredAt": "2026-02-03T04:05:06.000Z",
"data": { "resultId": "…", "participantId": "…", "band": "moderate" }
}Payload adalah proyeksi: hanya field yang relevan untuk mitra. Jawaban mentah dan catatan klinis tidak pernah dikirim.
Header
| Header | Isi |
|---|---|
X-Sikotes-Event | Nama event |
X-Sikotes-Delivery | ID pengiriman, stabil antar percobaan |
X-Sikotes-Signature | t=<unix>,v1=<hex hmac-sha256> |
Idempotency-Key | Sama dengan ID pengiriman |
Verifikasi tanda tangan
Tanda tangan dihitung atas ${t}.${rawBody} memakai rahasia endpoint. Verifikasi wajib memakai body mentah, bukan hasil parse ulang.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(
header.split(",").map((part) => part.split("=") as [string, string]),
);
const timestamp = Number(parts.t);
if (!Number.isFinite(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const given = Buffer.from(parts.v1 ?? "", "hex");
const want = Buffer.from(expected, "hex");
return given.length === want.length && timingSafeEqual(given, want);
}Tolak permintaan di luar toleransi 5 menit, dan balas 2xx secepat mungkin. Pekerjaan berat sebaiknya diantrekan setelah balasan dikirim.
Retry
Pengiriman yang gagal diulang dengan backoff eksponensial hingga 8 percobaan. Setelah itu statusnya menjadi gagal permanen dan dapat dijadwalkan ulang dari konsol kami. Endpoint yang gagal 25 kali beruntun dinonaktifkan otomatis.
Pengiriman tidak dijamin berurutan. Gunakan occurredAt untuk mengurutkan dan id untuk mendeteksi duplikat.
8. Batas pemakaian
| Batas | Bawaan | Dapat diubah |
|---|---|---|
| Permintaan per menit | 120 | Ya, sampai 6000 |
| Kuota harian | Tanpa batas | Ya |
| Ukuran body | 16–64 KB per endpoint | Tidak |
| Halaman daftar | 25 (maks 100) | Ya, per permintaan |
Saat menerima 429, hormati header Retry-After dan gunakan backoff dengan jitter.
9. Dokumen OpenAPI
Spesifikasi lengkap tersedia di `/api/v1/openapi.json`. Dokumen itu dihasilkan dari kode yang sama dengan implementasi, sehingga selalu sinkron. Impor ke Postman, Insomnia, atau generator klien favorit Anda.
Bila lebih nyaman membaca di peramban, dua halaman referensi di bawah ini dirender dari sumber yang sama — bukan salinan yang bisa basi:
- Referensi API — setiap endpoint dengan izin, parameter, bentuk body, dan contoh panggilan.
- Referensi webhook — enam event keluar, bentuk payload per event, header tanda tangan, cara verifikasi, dan kebijakan percobaan ulang.
10. Uji cepat
curl -s https://sikotes.id/api/v1/me \
-H "Authorization: Bearer $SIKOTES_API_KEY"Balasan memuat identitas klien, izin efektif, batas pemakaian, dan daftar event webhook yang tersedia. Bila balasan ini sudah benar, sisa integrasi biasanya hanya soal pemetaan data.