P
PUGUH

API Webhooks

Referensi API lengkap untuk mengelola webhooks. Terima notifikasi HTTP real-time saat event terjadi di organisasi PUGUH Anda.

Ringkasan Endpoint

MethodEndpointDeskripsi
GET /api/v1/webhooks/endpoints Daftar webhook endpoints
POST /api/v1/webhooks/endpoints Buat webhook endpoint
GET /api/v1/webhooks/endpoints/{id} Detail endpoint
PATCH /api/v1/webhooks/endpoints/{id} Perbarui endpoint
DELETE /api/v1/webhooks/endpoints/{id} Hapus endpoint
GET /api/v1/webhooks/events Daftar tipe event yang tersedia
GET /api/v1/webhooks/endpoints/{id}/deliveries Daftar percobaan pengiriman
POST /api/v1/webhooks/deliveries/{id}/retry Coba ulang pengiriman gagal

Daftar Webhook Endpoints

http
GET /api/v1/webhooks/endpoints

Parameter Query

ParameterTipeDefaultDeskripsi
page int 1 Nomor halaman
page_size int 20 Hasil per halaman (maks 100)
is_active bool - Filter berdasarkan status aktif

Response

json
{
  "items": [
    {
      "id": "wh-3f8a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
      "url": "https://example.com/webhooks/puguh",
      "description": "Production webhook receiver",
      "events": ["user.created", "user.updated", "auth.login"],
      "is_active": true,
      "created_at": "2026-02-15T08:00:00Z",
      "updated_at": "2026-02-18T12:30:00Z"
    }
  ],
  "total": 3,
  "page": 1,
  "page_size": 20,
  "has_next": false,
  "has_prev": false
}

Buat Webhook Endpoint

http
POST /api/v1/webhooks/endpoints
Content-Type: application/json

Request Body

FieldTipeWajibDeskripsi
url string Ya URL HTTPS yang akan menerima payload webhook
events string[] Ya Daftar tipe event yang akan di-subscribe
description string Tidak Deskripsi yang mudah dibaca untuk endpoint ini
secret string Tidak Secret penandatanganan kustom. Jika tidak diisi, akan dibuat otomatis
json
{
  "url": "https://example.com/webhooks/puguh",
  "events": ["user.created", "user.updated", "organization.updated"],
  "description": "Production webhook receiver",
  "secret": "whsec_your_custom_secret_key"
}

Response 201 Created

json
{
  "id": "wh-3f8a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
  "url": "https://example.com/webhooks/puguh",
  "description": "Production webhook receiver",
  "events": ["user.created", "user.updated", "organization.updated"],
  "signing_secret": "whsec_your_custom_secret_key",
  "is_active": true,
  "created_at": "2026-02-20T10:30:00Z",
  "updated_at": "2026-02-20T10:30:00Z"
}

Penting

signing_secret hanya dikembalikan pada response pembuatan. Simpan dengan aman — Anda akan membutuhkannya untuk memverifikasi tanda tangan webhook. Jika hilang, Anda harus merotasi secret dengan memperbarui endpoint.

Detail Webhook Endpoint

http
GET /api/v1/webhooks/endpoints/{id}

Response

json
{
  "id": "wh-3f8a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
  "url": "https://example.com/webhooks/puguh",
  "description": "Production webhook receiver",
  "events": ["user.created", "user.updated", "organization.updated"],
  "is_active": true,
  "created_at": "2026-02-15T08:00:00Z",
  "updated_at": "2026-02-18T12:30:00Z",
  "last_delivery_at": "2026-02-20T09:15:00Z",
  "delivery_stats": {
    "total": 142,
    "successful": 139,
    "failed": 3
  }
}

Perbarui Webhook Endpoint

http
PATCH /api/v1/webhooks/endpoints/{id}
Content-Type: application/json

Request Body

Semua field bersifat opsional. Hanya sertakan field yang ingin diubah.

FieldTipeDeskripsi
url string URL HTTPS baru
events string[] Ganti tipe event yang di-subscribe
description string Perbarui deskripsi
secret string Rotasi signing secret
is_active bool Aktifkan atau nonaktifkan endpoint
json
{
  "events": ["user.created", "user.updated", "user.deleted", "auth.login"],
  "is_active": true
}

Response

json
{
  "id": "wh-3f8a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
  "url": "https://example.com/webhooks/puguh",
  "description": "Production webhook receiver",
  "events": ["user.created", "user.updated", "user.deleted", "auth.login"],
  "is_active": true,
  "created_at": "2026-02-15T08:00:00Z",
  "updated_at": "2026-02-20T11:00:00Z"
}

Hapus Webhook Endpoint

http
DELETE /api/v1/webhooks/endpoints/{id}

Menghapus webhook endpoint secara permanen dan menghentikan semua pengiriman di masa depan.

Response 204 No Content

Mengembalikan response kosong saat berhasil.

Peringatan

Menghapus webhook endpoint bersifat permanen. Pengiriman yang sedang berlangsung akan dibatalkan. Jika Anda ingin menghentikan penerimaan event sementara, gunakan PATCH untuk mengatur is_active ke false.

Daftar Tipe Event

http
GET /api/v1/webhooks/events

Mengembalikan semua tipe event yang tersedia untuk subscription webhook.

Response

json
[
  {
    "type": "user.created",
    "category": "user",
    "description": "A new user has been created"
  },
  {
    "type": "user.updated",
    "category": "user",
    "description": "A user profile has been updated"
  },
  {
    "type": "organization.created",
    "category": "organization",
    "description": "A new organization has been created"
  }
]

Tipe Event yang Tersedia

KategoriTipe EventDeskripsi
User user.created Pengguna baru telah dibuat
User user.updated Profil pengguna telah diperbarui
User user.deleted Pengguna telah dihapus
Organization organization.created Organisasi baru telah dibuat
Organization organization.updated Organisasi telah diperbarui
Member member.invited Pengguna telah diundang ke organisasi
Member member.joined Pengguna telah bergabung ke organisasi
Member member.removed Anggota telah dihapus dari organisasi
Application application.created Aplikasi baru telah dibuat
Application application.updated Aplikasi telah diperbarui
Application application.deleted Aplikasi telah dihapus
Auth auth.login Pengguna telah login
Auth auth.logout Pengguna telah logout
Auth auth.password_changed Pengguna telah mengubah password
Billing billing.subscription.created Subscription baru telah dibuat
Billing billing.invoice.paid Invoice telah dibayar

Daftar Pengiriman

http
GET /api/v1/webhooks/endpoints/{id}/deliveries

Mengembalikan percobaan pengiriman untuk webhook endpoint tertentu.

Parameter Query

ParameterTipeDefaultDeskripsi
page int 1 Nomor halaman
page_size int 20 Hasil per halaman (maks 100)
status string - Filter berdasarkan status: success, failed, pending
event string - Filter berdasarkan tipe event (contoh user.created)

Response

json
{
  "items": [
    {
      "id": "del-a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d",
      "webhook_id": "wh-3f8a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
      "event": "user.created",
      "status": "success",
      "http_status": 200,
      "attempt": 1,
      "max_attempts": 5,
      "request_body": {
        "event": "user.created",
        "timestamp": "2026-02-20T10:30:00Z",
        "data": {
          "id": "usr-uuid",
          "email": "newuser@example.com"
        },
        "webhook_id": "wh-3f8a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
        "delivery_id": "del-a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d"
      },
      "response_body": "OK",
      "duration_ms": 120,
      "delivered_at": "2026-02-20T10:30:01Z",
      "next_retry_at": null
    },
    {
      "id": "del-b2c3d4e5-f6a7-8b9c-0d1e-2f3a4b5c6d7e",
      "webhook_id": "wh-3f8a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
      "event": "auth.login",
      "status": "failed",
      "http_status": 500,
      "attempt": 3,
      "max_attempts": 5,
      "response_body": "Internal Server Error",
      "duration_ms": 5032,
      "delivered_at": "2026-02-20T09:15:05Z",
      "next_retry_at": "2026-02-20T10:15:05Z"
    }
  ],
  "total": 142,
  "page": 1,
  "page_size": 20,
  "has_next": true,
  "has_prev": false
}

Coba Ulang Pengiriman Gagal

http
POST /api/v1/webhooks/deliveries/{id}/retry

Mencoba ulang pengiriman webhook yang gagal secara manual. Pengiriman langsung di-queue ulang tanpa memperhatikan jadwal retry.

Response

json
{
  "id": "del-b2c3d4e5-f6a7-8b9c-0d1e-2f3a4b5c6d7e",
  "status": "pending",
  "attempt": 4,
  "max_attempts": 5,
  "next_retry_at": null,
  "queued_at": "2026-02-20T11:00:00Z"
}

Kebijakan Retry

PUGUH menggunakan exponential backoff untuk retry otomatis: 1 menit, 5 menit, 30 menit, 2 jam, 24 jam. Setelah 5 percobaan gagal, pengiriman ditandai sebagai gagal permanen. Gunakan endpoint ini untuk mencoba ulang secara manual kapan saja.

Format Payload Webhook

Setiap pengiriman webhook mengirim payload JSON dengan struktur yang konsisten:

json
{
  "event": "user.created",
  "timestamp": "2026-02-20T10:30:00Z",
  "data": {
    "id": "usr-a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d",
    "email": "newuser@example.com",
    "full_name": "Jane Doe",
    "created_at": "2026-02-20T10:30:00Z"
  },
  "webhook_id": "wh-3f8a1b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
  "delivery_id": "del-a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d"
}

Field Payload

FieldTipeDeskripsi
event string Tipe event yang memicu pengiriman ini
timestamp string Timestamp ISO 8601 kapan event terjadi
data object Data payload spesifik event (bervariasi per tipe event)
webhook_id string ID webhook endpoint yang menerima pengiriman ini
delivery_id string Identifier unik untuk percobaan pengiriman spesifik ini

Header HTTP yang Dikirim dengan Pengiriman

HeaderDeskripsi
Content-Type application/json
X-Puguh-Signature Tanda tangan HMAC-SHA256 untuk verifikasi payload
X-Puguh-Event Tipe event (contoh user.created)
X-Puguh-Delivery-ID Identifier pengiriman unik
X-Puguh-Timestamp Timestamp Unix pengiriman untuk perlindungan replay
User-Agent PUGUH-Webhooks/1.0

Verifikasi Tanda Tangan

Setiap pengiriman webhook menyertakan header X-Puguh-Signature yang berisi tanda tangan HMAC-SHA256. Anda harus memverifikasi tanda tangan ini untuk memastikan request berasal dari PUGUH dan tidak dimanipulasi.

Cara Kerja Tanda Tangan

  1. PUGUH menggabungkan timestamp pengiriman dan body JSON mentah: timestamp.body
  2. PUGUH menghitung HMAC-SHA256 menggunakan signing secret endpoint Anda sebagai key
  3. Tanda tangan dikirim di header X-Puguh-Signature sebagai sha256=HEX_DIGEST
  4. Server Anda harus menghitung ulang tanda tangan dan membandingkan menggunakan perbandingan constant-time

Contoh Verifikasi Python

python
import hmac
import hashlib
import time

def verify_webhook_signature(
    payload: bytes,
    signature_header: str,
    timestamp_header: str,
    secret: str,
    tolerance_seconds: int = 300
) -> bool:
    """Verify PUGUH webhook signature.

    Args:
        payload: Raw request body bytes.
        signature_header: Value of X-Puguh-Signature header.
        timestamp_header: Value of X-Puguh-Timestamp header.
        secret: Your webhook endpoint signing secret.
        tolerance_seconds: Max age in seconds (default 5 min).

    Returns:
        True if signature is valid and timestamp is within tolerance.
    """
    # 1. Check timestamp to prevent replay attacks
    try:
        timestamp = int(timestamp_header)
    except (ValueError, TypeError):
        return False

    if abs(time.time() - timestamp) > tolerance_seconds:
        return False

    # 2. Compute expected signature
    signed_content = f"{timestamp}.".encode() + payload
    expected = hmac.new(
        secret.encode(),
        signed_content,
        hashlib.sha256
    ).hexdigest()

    # 3. Compare using constant-time comparison
    expected_sig = f"sha256={expected}"
    return hmac.compare_digest(expected_sig, signature_header)


# Usage in a Flask/FastAPI handler
from fastapi import Request, HTTPException

WEBHOOK_SECRET = "whsec_your_signing_secret"

@app.post("/webhooks/puguh")
async def handle_webhook(request: Request):
    payload = await request.body()
    signature = request.headers.get("X-Puguh-Signature", "")
    timestamp = request.headers.get("X-Puguh-Timestamp", "")

    if not verify_webhook_signature(payload, signature, timestamp, WEBHOOK_SECRET):
        raise HTTPException(status_code=401, detail="Invalid signature")

    event = request.headers.get("X-Puguh-Event")
    data = await request.json()

    if event == "user.created":
        handle_user_created(data["data"])
    elif event == "billing.invoice.paid":
        handle_invoice_paid(data["data"])

    return {"received": True}

Contoh Verifikasi TypeScript

typescript
import { createHmac, timingSafeEqual } from "crypto";

function verifyWebhookSignature(
  payload: string,
  signatureHeader: string,
  timestampHeader: string,
  secret: string,
  toleranceSeconds: number = 300
): boolean {
  // 1. Check timestamp to prevent replay attacks
  const timestamp = parseInt(timestampHeader, 10);
  if (isNaN(timestamp)) return false;

  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - timestamp) > toleranceSeconds) return false;

  // 2. Compute expected signature
  const signedContent = `${timestamp}.${payload}`;
  const expectedHex = createHmac("sha256", secret)
    .update(signedContent)
    .digest("hex");

  const expectedSig = `sha256=${expectedHex}`;

  // 3. Compare using constant-time comparison
  if (expectedSig.length !== signatureHeader.length) return false;

  return timingSafeEqual(
    Buffer.from(expectedSig),
    Buffer.from(signatureHeader)
  );
}


// Usage in an Express handler
import express from "express";

const WEBHOOK_SECRET = process.env.PUGUH_WEBHOOK_SECRET!;

app.post(
  "/webhooks/puguh",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const payload = req.body.toString();
    const signature = req.headers["x-puguh-signature"] as string;
    const timestamp = req.headers["x-puguh-timestamp"] as string;

    if (!verifyWebhookSignature(payload, signature, timestamp, WEBHOOK_SECRET)) {
      return res.status(401).json({ detail: "Invalid signature" });
    }

    const event = req.headers["x-puguh-event"] as string;
    const data = JSON.parse(payload);

    switch (event) {
      case "user.created":
        handleUserCreated(data.data);
        break;
      case "billing.invoice.paid":
        handleInvoicePaid(data.data);
        break;
    }

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

Tip Keamanan

Selalu verifikasi header X-Puguh-Timestamp untuk mencegah serangan replay. Tolak pengiriman yang lebih dari 5 menit. Selalu gunakan perbandingan constant-time (contoh hmac.compare_digest di Python, timingSafeEqual di Node.js) untuk mencegah serangan timing.

Praktik Terbaik

Respons Cepat

Webhook endpoint Anda harus mengembalikan kode status 2xx dalam 10 detik. Jika pemrosesan membutuhkan waktu lebih lama, terima webhook segera dan proses event secara asinkron menggunakan antrian background job.

Tangani Pengiriman Duplikat

Gunakan field delivery_id untuk deduplikasi event. Dalam kasus langka (timeout jaringan, retry), event yang sama mungkin dikirim lebih dari sekali. Simpan delivery ID yang sudah diproses dan lewati duplikat.

Gunakan Endpoint HTTPS

URL webhook harus menggunakan HTTPS. PUGUH akan menolak endpoint yang dikonfigurasi dengan URL HTTP.

Pantau Kesehatan Pengiriman

Gunakan endpoint Daftar Pengiriman untuk memantau tingkat kegagalan. Jika endpoint secara konsisten gagal, PUGUH akan otomatis menonaktifkannya setelah 5 kegagalan berturut-turut dan mengirim notifikasi ke owner organisasi.

Response Error

Semua response error menggunakan kode status HTTP dengan pesan detail biasa. Tanpa objek wrapper.

400 Bad Request

json
{
  "detail": "Invalid URL: must be HTTPS"
}

404 Not Found

json
{
  "detail": "Webhook endpoint not found"
}

409 Conflict

json
{
  "detail": "Delivery is already in pending state"
}

422 Unprocessable Entity

json
{
  "detail": "Invalid event type: user.unknown. Use GET /webhooks/events for available types."
}

Terkait