Tekomata API

Baca foto atau PDF nota jadi data dengan model AI pilihanmu, dalam bentuk baku dan dengan halaman tempat setiap nilai terbaca; tulis artikel blog dengan AI, lalu dengar hasilnya lewat webhook. Lihat saldo dan isi saldo lewat transfer bank. Dibayar dari saldo Tekomata toko — bisa langsung dicoba di bawah dengan access token integrasimu.

6. Memasang webhook sendiri

Integrasimu mengatur alamat webhook-nya lewat API.

Izin: webhooks:manage

  1. 1
    SistemmuPOST /v1/webhooks

    Kirim "url" (https, bisa dijangkau dari internet) dan "events". Jawabannya alamat itu dan "secret" penanda tangan, yang ditampilkan sekali.

  2. 2
    SistemmuPOST /v1/webhooks/{id}/test

    Kirim uji coba webhook.test, lalu lihat hasilnya di GET /v1/webhooks/{id}/deliveries.

  3. 3
    Tekomata

    Setiap kejadian yang dipilih dikirim ke alamat itu, ditandatangani dengan secret-nya.

  4. 4
    SistemmuPATCH /v1/webhooks/{id}

    Ubah alamat atau kejadiannya, atau matikan dengan "isActive": false. POST /v1/webhooks/{id}/secret membuat secret baru.

  • Integrasi hanya melihat dan mengubah alamat yang ia buat sendiri; alamat yang dipasang toko tidak tampak.
  • Kejadian yang boleh dipilih mengikuti izin integrasi: document_read.* dengan documents:read, blog_generation.* dengan blog:generate, image_generation.* dengan images:generate, top_up.* dan balance.low dengan billing:read. Selebihnya ditolak 422 event_not_allowed. Alamat berhenti menerima kejadian itu begitu integrasi kehilangan izinnya atau dimatikan.

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.succeededBacaan dokumen selesai. data: runId, documentTypeId, status, provider, model, clientReference.
document_read.failedBacaan dokumen gagal. data: runId, documentTypeId, status, provider, model, error (bahasa Inggris), clientReference.
blog_generation.succeededArtikel selesai ditulis. data: generationId, status, clientReference.
blog_generation.failedArtikel gagal ditulis. data: generationId, status, errorCode, clientReference.
image_generation.succeededGambar selesai dibuat. data: generationId, status, clientReference.
image_generation.failedGambar gagal dibuat. data: generationId, status, errorCode, clientReference.
top_up.approvedIsi 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.rejectedIsi saldo ditolak. data: topUpId, reference, reason.
balance.lowSaldo 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.testUji 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.