Webhooks

Terima notifikasi real-time dari Mutasiku ke endpoint aplikasi Anda.

Webhook dikirim sebagai HTTP POST dari server Mutasiku ke URL endpoint Anda. Daftarkan endpoint di dashboard.


Setup

Buat endpoint di aplikasi Anda

Buat route HTTP yang dapat menerima POST request dari internet.

app.post('/webhook', (req, res) => {
  res.json({ received: true });
});

Daftarkan webhook di dashboard

Buka Settings → Webhooks di dashboard, klik Add Webhook, dan masukkan URL endpoint Anda.

Pilih events yang ingin diterima

Pilih event spesifik atau subscribe ke semua event.

Implementasikan verifikasi signature

Verifikasi header X-Webhook-Signature di setiap request masuk. Lihat bagian Security di bawah.

Test webhook Anda

Gunakan fitur test di dashboard untuk mengirim event percobaan ke endpoint Anda.


Available Events

Account Events

EventKeterangan
account.createdAkun baru dibuat
account.updatedDetail akun diperbarui

Payload account.created

{
  "type": "account.created",
  "data": {
    "id": "acc_123456789",
    "accountId": "acc_123456789",
    "email": "user@example.com",
    "name": "Test User",
    "createdAt": "2023-05-20T12:34:56.789Z"
  }
}

Payload account.updated

{
  "type": "account.updated",
  "data": {
    "id": "acc_123456789",
    "accountId": "acc_123456789",
    "email": "updated@example.com",
    "name": "Updated User",
    "updatedAt": "2023-05-20T12:34:56.789Z"
  }
}

Mutations Events

EventKeterangan
mutations.createdMutasi/transaksi baru terdeteksi
balance.updatedSaldo akun berubah

Payload mutations.created

{
  "type": "mutations.created",
  "data": {
    "accountId": "acc_123456789",
    "amount": 1000000,
    "type": "CREDIT",
    "description": "Transfer masuk",
    "createdAt": "2023-05-20T12:34:56.789Z"
  }
}

Payload balance.updated

{
  "type": "balance.updated",
  "data": {
    "id": "bal_update_123456789",
    "accountId": "acc_123456789",
    "previousBalance": 5000000,
    "newBalance": 6000000,
    "currency": "IDR",
    "updatedAt": "2023-05-20T12:34:56.789Z"
  }
}

Payment Events

EventKeterangan
payment.createdPembayaran baru dibuat
payment.completedPembayaran berhasil
payment.expiredPembayaran kadaluarsa
payment.failedPembayaran gagal

Payload payment.completed

{
  "type": "payment.completed",
  "data": {
    "id": "pay_123456789",
    "orderId": "ord_123456789",
    "status": "COMPLETED",
    "amount": 1000000,
    "uniqueCode": 123,
    "customerName": "Test Customer",
    "customerEmail": "customer@example.com",
    "customerPhone": "62812345678",
    "metadata": { "productId": "prod_123" },
    "transaction": {
      "id": "txn_123456789",
      "amount": 1000000,
      "createdAt": "2023-05-20T12:34:56.789Z"
    },
    "completedAt": "2023-05-20T12:34:56.789Z"
  }
}

Payload payment.expired

{
  "type": "payment.expired",
  "data": {
    "id": "pay_123456789",
    "orderId": "ord_123456789",
    "status": "EXPIRED",
    "amount": 1000000,
    "expiredAt": "2023-05-20T12:34:56.789Z"
  }
}

Security — Verifikasi Signature

Selalu verifikasi X-Webhook-Signature sebelum memproses payload. Abaikan request tanpa signature valid untuk mencegah pemalsuan event.

Signature dibuat menggunakan HMAC-SHA256 dengan secret webhook Anda sebagai key dan field data (JSON string) sebagai message.

signature = HMAC-SHA256(JSON.stringify(payload.data), webhookSecret)

Node.js

const crypto = require('crypto');

function verifySignature(data, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(data))
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

app.post('/webhook', (req, res) => {
  const signature = req.headers['x-webhook-signature'];

  if (!verifySignature(req.body.data, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  switch (req.body.type) {
    case 'payment.completed':
      // handle pembayaran berhasil
      break;
    case 'mutations.created':
      // handle mutasi baru
      break;
  }

  res.json({ received: true });
});

PHP

function verifySignature($data, $signature, $secret) {
  $expected = hash_hmac('sha256', json_encode($data), $secret);
  return hash_equals($expected, $signature);
}

$payload   = json_decode(file_get_contents('php://input'), true);
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';

if (!verifySignature($payload['data'], $signature, 'your_webhook_secret')) {
  http_response_code(401);
  echo json_encode(['error' => 'Invalid signature']);
  exit;
}

echo json_encode(['received' => true]);

Python

import json, hmac, hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)

def verify_signature(data, signature, secret):
    expected = hmac.new(
        key=secret.encode(),
        msg=json.dumps(data).encode(),
        digestmod=hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

@app.route('/webhook', methods=['POST'])
def webhook():
    signature = request.headers.get('X-Webhook-Signature', '')
    payload   = request.json

    if not verify_signature(payload['data'], signature, 'your_webhook_secret'):
        return jsonify({'error': 'Invalid signature'}), 401

    return jsonify({'received': True})

Best Practices

Kembalikan 2xx segera. Jangan tunggu proses selesai — kirim respons 200 lebih dulu, lalu proses event secara asinkron dengan queue/job. Jika endpoint tidak merespons dalam 30 detik, Mutasiku akan retry.

Idempotent handler. Event yang sama bisa dikirim lebih dari sekali. Gunakan field id dari payload sebagai idempotency key untuk mencegah proses duplikat.

Simpan raw payload. Log seluruh payload yang masuk untuk keperluan debugging dan audit.


Troubleshooting

MasalahSolusi
Webhook tidak diterimaPastikan URL endpoint dapat diakses dari internet (bukan localhost)
Signature tidak validGunakan JSON.stringify(payload.data), bukan seluruh payload
TimeoutKembalikan 200 segera, proses logic secara asinkron
Event duplikatImplementasikan idempotency key menggunakan field id di payload

© 2026 PT. Cobra Code Indonesia. All rights reserved.

Last updated: 4/11/2026