Webhook Overview
Webhook memungkinkan IsiKuota mengirim notifikasi real-time ke server Anda setiap kali status transaksi atau deposit berubah — tanpa perlu polling.
Webhook mengirim dua jenis event ke URL yang sama: transaction.updated (order prabayar/game/pascabayar) dan deposit.updated (deposit saldo via API). Cek field event pada payload untuk membedakan keduanya — lihat daftar lengkap Events.
Cara Kerja
Transaksi terjadi
│
▼
IsiKuota memproses
│
▼
Status berubah (Success/Failed)
│
▼
IsiKuota POST ke URL webhook Anda
│
▼
Server Anda membalas HTTP 200
Request Headers
Setiap request webhook dari IsiKuota menyertakan headers berikut:
POST /webhook/isikuota HTTP/1.1
Host: yourdomain.com
Content-Type: application/json
Content-Length: 396
User-Agent: Isikuota-Webhook/1.0
X-Isikuota-Signature: sha256=<HMAC-SHA256 dari JSON body>
| Header | Keterangan |
|---|---|
Content-Type | Selalu application/json |
Content-Length | Ukuran body dalam byte |
User-Agent | Selalu Isikuota-Webhook/1.0 — bisa dipakai untuk whitelist di firewall/WAF |
X-Isikuota-Signature | sha256=<HMAC-SHA256 dari raw JSON body> — lihat Verifikasi Signature |
Payload Webhook
Contoh payload yang dikirim IsiKuota saat status transaksi berubah (transaction.updated) — struktur data untuk deposit.updated berbeda, lihat Events:
{
"event": "transaction.updated",
"event_id": "evt_01J3K8M2N4P5Q6R7S8T9U0V1W2",
"timestamp": "2025-01-15T14:30:00+07:00",
"data": {
"ref_id": "ORDER-2025-001",
"status": "success",
"product_code": "TSEL10",
"product_name": "Telkomsel 10.000",
"customer_no": "081234567890",
"price": 11500,
"sn": "1234567890123456",
"message": "Transaksi berhasil diproses",
"balance": 489000,
"created_at": "2025-01-15T14:29:55+07:00",
"updated_at": "2025-01-15T14:30:00+07:00"
}
}
| Field | Keterangan |
|---|---|
event | Tipe event — transaction.updated atau deposit.updated |
event_id | ID unik per pengiriman (ULID), gunakan untuk deduplikasi |
timestamp | Waktu event dikirim (ISO 8601) |
data.ref_id | ID referensi transaksi Anda |
data.status | success / failed / pending |
data.product_code | Kode produk yang dibeli |
data.product_name | Nama produk yang dibeli |
data.customer_no | Nomor tujuan transaksi |
data.price | Harga produk (Rp) |
data.sn | Serial number/token hasil transaksi (kosong jika belum ada) |
data.message | Pesan status transaksi |
data.balance | Saldo akun Anda setelah transaksi |
data.created_at | Waktu transaksi dibuat |
data.updated_at | Waktu status transaksi terakhir diperbarui |
Ada dua event: transaction.updated dan deposit.updated. Cek event untuk routing, lalu tentukan tindakan berdasarkan data.status. Lihat detail lengkap tiap event di Events.
Verifikasi Signature
Wajib verifikasi setiap request webhook menggunakan header X-Isikuota-Signature untuk memastikan request benar-benar dari IsiKuota.
Nilai header berformat sha256=<hex>. Hitung HMAC dari raw body lalu bandingkan dengan bagian setelah sha256=.
Formula:
X-Isikuota-Signature = "sha256=" + hash_hmac('sha256', raw_body, api_secret)
- PHP
- JavaScript
- Python
- Java
- C#
- cURL / Bash
<?php
function verifyWebhook(string $rawBody, string $signature, string $secret): bool
{
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, $signature);
}
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_ISIKUOTA_SIGNATURE'] ?? '';
if (!verifyWebhook($rawBody, $signature, 'your_secret_key')) {
http_response_code(401);
exit('Invalid signature');
}
$payload = json_decode($rawBody, true);
// Proses webhook...
http_response_code(200);
echo json_encode(['status' => 'ok']);
// Node.js / Express — gunakan raw body parser agar bisa verifikasi signature
const crypto = require('crypto');
function verifyWebhook(rawBody, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-isikuota-signature'];
if (!verifyWebhook(req.body, signature, process.env.API_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const payload = JSON.parse(req.body);
// Proses webhook...
res.status(200).json({ status: 'ok' });
});
# Flask
import hmac, hashlib
from flask import Flask, request, jsonify
app = Flask(__name__)
def verify_webhook(raw_body, signature, secret):
expected = 'sha256=' + hmac.new(
secret.encode(),
raw_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
@app.route('/webhook', methods=['POST'])
def webhook():
raw_body = request.get_data()
signature = request.headers.get('X-Isikuota-Signature', '')
if not verify_webhook(raw_body, signature, 'your_secret_key'):
return jsonify({'error': 'Invalid signature'}), 401
payload = request.get_json()
# Proses webhook...
return jsonify({'status': 'ok'}), 200
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import com.sun.net.httpserver.*;
import java.io.*;
public class WebhookHandler implements HttpHandler {
static final String SECRET = "your_secret_key";
static boolean verifyWebhook(String rawBody, String signature) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(SECRET.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] hash = mac.doFinal(rawBody.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : hash) sb.append(String.format("%02x", b));
String expected = "sha256=" + sb;
return MessageDigest.isEqual(
expected.getBytes(StandardCharsets.UTF_8),
signature.getBytes(StandardCharsets.UTF_8)
);
}
@Override
public void handle(HttpExchange exchange) throws IOException {
String rawBody = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
String signature = exchange.getRequestHeaders().getFirst("X-Isikuota-Signature");
try {
if (signature == null || !verifyWebhook(rawBody, signature)) {
exchange.sendResponseHeaders(401, -1);
return;
}
} catch (Exception e) {
exchange.sendResponseHeaders(500, -1);
return;
}
// Proses webhook...
byte[] response = "{\"status\":\"ok\"}".getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().add("Content-Type", "application/json");
exchange.sendResponseHeaders(200, response.length);
exchange.getResponseBody().write(response);
exchange.getResponseBody().close();
}
}
// ASP.NET Core minimal API
using System.Security.Cryptography;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
const string Secret = "your_secret_key";
static bool VerifyWebhook(string rawBody, string signature)
{
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(Secret));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(rawBody));
var expected = "sha256=" + BitConverter.ToString(hash).Replace("-", "").ToLower();
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected),
Encoding.UTF8.GetBytes(signature)
);
}
app.MapPost("/webhook/isikuota", async (HttpRequest request) =>
{
using var reader = new StreamReader(request.Body);
var rawBody = await reader.ReadToEndAsync();
var signature = request.Headers["X-Isikuota-Signature"].ToString();
if (string.IsNullOrEmpty(signature) || !VerifyWebhook(rawBody, signature))
{
return Results.Json(new { error = "Invalid signature" }, statusCode: 401);
}
// Proses webhook...
return Results.Json(new { status = "ok" });
});
app.Run();
# Hitung ulang signature dari raw body untuk verifikasi manual (mis. saat debugging)
RAW_BODY='{"event":"transaction.updated","event_id":"evt_01J3K8M2N4P5Q6R7S8T9U0V1W2", ...}'
SECRET="your_secret_key"
EXPECTED="sha256=$(echo -n "$RAW_BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')"
echo "$EXPECTED"
# Bandingkan hasilnya dengan header X-Isikuota-Signature yang diterima
# Simulasikan pengiriman webhook ke server lokal Anda untuk testing handler
curl -X POST https://yourdomain.com/webhook/isikuota \
-H "Content-Type: application/json" \
-H "User-Agent: Isikuota-Webhook/1.0" \
-H "X-Isikuota-Signature: $EXPECTED" \
-d "$RAW_BODY"
Persyaratan Server Webhook
| Persyaratan | Detail |
|---|---|
| Protokol | HTTPS (wajib di production) |
| Method | POST |
| Response | HTTP 2xx dalam 15 detik |
| Body response | Bebas (diabaikan IsiKuota) |
Jika server tidak merespons dalam 15 detik atau mengembalikan status bukan 2xx, IsiKuota akan retry hingga 3 kali dengan jeda bertahap (10 detik, 30 detik, lalu 60 detik).
Best Practices
- Selalu verifikasi
X-Isikuota-Signaturesebelum memproses payload - Gunakan timing-safe comparison (
hash_equals/timingSafeEqual) untuk mencegah timing attack - Balas HTTP 200 segera, proses di background/queue
- Simpan log setiap callback untuk audit dan debugging
- Idempoten: gunakan
event_idatauref_iduntuk mendeteksi pengiriman ganda sebelum update DB - Test menggunakan fitur Webhook Tester di Dashboard