Webhook
Alamat webhook dipasang toko di Pengaturan → Webhook, atau oleh integrasimu sendiri lewat API (alur 6), dengan kejadian yang dipilih. Secret penanda tangan ditampilkan sekali.
document_read.succeeded | Bacaan dokumen selesai. data: runId, documentTypeId, status, provider, model, clientReference. |
document_read.failed | Bacaan dokumen gagal. data: runId, documentTypeId, status, provider, model, error (bahasa Inggris), clientReference. |
blog_generation.succeeded | Artikel selesai ditulis. data: generationId, status, clientReference. |
blog_generation.failed | Artikel gagal ditulis. data: generationId, status, errorCode, clientReference. |
image_generation.succeeded | Gambar selesai dibuat. data: generationId, status, clientReference. |
image_generation.failed | Gambar gagal dibuat. data: generationId, status, errorCode, clientReference. |
top_up.approved | Isi saldo disetujui, saldo sudah bertambah. data: topUpId, reference, amount (yang masuk ke saldo, rupiah bulat; bisa lebih kecil dari yang diminta bila transfernya kurang). |
top_up.rejected | Isi saldo ditolak. data: topUpId, reference, reason. |
balance.low | Saldo turun di bawah peringatan yang dipasang (PUT /v1/billing/low-balance). Dikirim sekali setiap saldo turun melewatinya, dan baru lagi setelah saldo naik di atasnya. Orang-orang toko diberi tahu lewat email paling sering sekali per 12 jam. data: balance, threshold, currency. |
webhook.test | Uji coba dari tombol "Kirim uji coba" di layar toko, atau POST /v1/webhooks/{id}/test. |
Isi setiap kiriman
{
"id": "6f1c…", // sama di setiap pengulangan; abaikan yang sudah diproses
"type": "document_read.succeeded",
"createdAt": "2026-10-14T09:30:00.000Z",
"companyId": "…",
"data": { "runId": "…", "documentTypeId": "…", "status": "succeeded" }
}
Memeriksa tanda tangan
Setiap kiriman membawa header Tekomata-Event, Tekomata-Delivery, dan Tekomata-Signature: t=<detik unix>,v1=<hex>. v1 adalah HMAC-SHA256(secret, "<t>.<body mentah>"). Hitung dari byte mentah sebelum di-parse, bandingkan dengan waktu konstan, dan tolak t yang lebih tua dari 5 menit.
JavaScript (Node)
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = crypto.createHmac("sha256", secret)
.update(`${t}.${rawBody}`).digest("hex");
const given = Buffer.from(v1 ?? "");
// timingSafeEqual throws on unequal lengths; a missing or short v1 is false.
return given.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(expected), given);
}
PHP
function verify(string $raw, string $header, string $secret): bool {
parse_str(str_replace(',', '&', $header), $p);
if (abs(time() - (int)$p['t']) > 300) return false;
return hash_equals(hash_hmac('sha256', $p['t'] . '.' . $raw, $secret), $p['v1']);
}
Python
import hashlib, hmac, time
def verify(raw: bytes, header: str, secret: str) -> bool:
p = dict(part.split("=", 1) for part in header.split(","))
if abs(time.time() - int(p["t"])) > 300:
return False
expected = hmac.new(secret.encode(), p["t"].encode() + b"." + raw, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, p["v1"])
Menjawab dan pengulangan
- Jawab 2xx dalam 10 detik, lalu kerjakan sesudahnya. Jawaban lambat dihitung gagal.
- Yang gagal diulang setelah 1 menit, 5 menit, 30 menit, 2 jam, 6 jam, 12 jam, dan 24 jam, lalu berhenti. Redirect tidak diikuti.
- Kiriman yang sama bisa datang lebih dari sekali, dan urutannya bisa tertukar. Pakai id di isinya untuk mengabaikan yang sudah diproses.
- Setelah 50 kali gagal berturut-turut, alamatnya dimatikan dan layar toko menyebut alasannya.
- Alamat harus https dan bisa dijangkau dari internet.