P
PUGUH

Webhooks API

Complete API reference for managing webhooks. Receive real-time HTTP notifications when events occur in your PUGUH organization.

Endpoints Overview

MethodEndpointDescription
GET /api/v1/webhooks/endpoints List webhook endpoints
POST /api/v1/webhooks/endpoints Create a webhook endpoint
GET /api/v1/webhooks/endpoints/{id} Get endpoint details
PATCH /api/v1/webhooks/endpoints/{id} Update an endpoint
DELETE /api/v1/webhooks/endpoints/{id} Delete an endpoint
GET /api/v1/webhooks/events List available event types
GET /api/v1/webhooks/endpoints/{id}/deliveries List delivery attempts
POST /api/v1/webhooks/deliveries/{id}/retry Retry a failed delivery

List Webhook Endpoints

http
GET /api/v1/webhooks/endpoints

Query Parameters

ParameterTypeDefaultDescription
page int 1 Page number
page_size int 20 Results per page (max 100)
is_active bool - Filter by active status

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
}

Create Webhook Endpoint

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

Request Body

FieldTypeRequiredDescription
url string Yes HTTPS URL that will receive webhook payloads
events string[] Yes List of event types to subscribe to
description string No Human-readable description for this endpoint
secret string No Custom signing secret. If omitted, one is auto-generated
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"
}

Important

The signing_secret is only returned in the create response. Store it securely — you will need it to verify webhook signatures. If you lose it, you must rotate the secret by updating the endpoint.

Get 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
  }
}

Update Webhook Endpoint

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

Request Body

All fields are optional. Only include fields you want to change.

FieldTypeDescription
url string New HTTPS URL
events string[] Replace subscribed event types
description string Update description
secret string Rotate the signing secret
is_active bool Enable or disable the 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"
}

Delete Webhook Endpoint

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

Permanently removes the webhook endpoint and stops all future deliveries.

Response 204 No Content

Returns an empty response on success.

Warning

Deleting a webhook endpoint is permanent. Any in-flight deliveries will be cancelled. If you want to temporarily stop receiving events, use PATCH to set is_active to false instead.

List Event Types

http
GET /api/v1/webhooks/events

Returns all event types available for webhook subscriptions.

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"
  }
]

Available Event Types

CategoryEvent TypeDescription
User user.created A new user has been created
User user.updated A user profile has been updated
User user.deleted A user has been deleted
Organization organization.created A new organization has been created
Organization organization.updated An organization has been updated
Member member.invited A user has been invited to an organization
Member member.joined A user has joined an organization
Member member.removed A member has been removed from an organization
Application application.created A new application has been created
Application application.updated An application has been updated
Application application.deleted An application has been deleted
Auth auth.login A user has logged in
Auth auth.logout A user has logged out
Auth auth.password_changed A user has changed their password
Billing billing.subscription.created A new subscription has been created
Billing billing.invoice.paid An invoice has been paid

List Deliveries

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

Returns delivery attempts for a specific webhook endpoint.

Query Parameters

ParameterTypeDefaultDescription
page int 1 Page number
page_size int 20 Results per page (max 100)
status string - Filter by status: success, failed, pending
event string - Filter by event type (e.g. 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
}

Retry Failed Delivery

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

Manually retry a failed webhook delivery. The delivery is re-queued immediately regardless of the retry schedule.

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"
}

Retry Policy

PUGUH uses exponential backoff for automatic retries: 1 minute, 5 minutes, 30 minutes, 2 hours, 24 hours. After 5 failed attempts, the delivery is marked as permanently failed. Use this endpoint to manually retry at any time.

Webhook Payload Format

Every webhook delivery sends a JSON payload with a consistent structure:

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"
}

Payload Fields

FieldTypeDescription
event string The event type that triggered this delivery
timestamp string ISO 8601 timestamp of when the event occurred
data object Event-specific payload data (varies by event type)
webhook_id string The webhook endpoint ID that received this delivery
delivery_id string Unique identifier for this specific delivery attempt

HTTP Headers Sent with Deliveries

HeaderDescription
Content-Type application/json
X-Puguh-Signature HMAC-SHA256 signature for payload verification
X-Puguh-Event The event type (e.g. user.created)
X-Puguh-Delivery-ID Unique delivery identifier
X-Puguh-Timestamp Unix timestamp of the delivery for replay protection
User-Agent PUGUH-Webhooks/1.0

Signature Verification

Every webhook delivery includes an X-Puguh-Signature header containing an HMAC-SHA256 signature. You must verify this signature to confirm the request came from PUGUH and was not tampered with.

How Signatures Work

  1. PUGUH concatenates the delivery timestamp and the raw JSON body: timestamp.body
  2. PUGUH computes an HMAC-SHA256 using your endpoint's signing secret as the key
  3. The signature is sent in the X-Puguh-Signature header as sha256=HEX_DIGEST
  4. Your server must recompute the signature and compare it using a constant-time comparison

Python Verification Example

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}

TypeScript Verification Example

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 });
  }
);

Security Tip

Always verify the X-Puguh-Timestamp header to prevent replay attacks. Reject any delivery older than 5 minutes. Always use constant-time comparison (e.g. hmac.compare_digest in Python, timingSafeEqual in Node.js) to prevent timing attacks.

Best Practices

Respond Quickly

Your webhook endpoint should return a 2xx status code within 10 seconds. If processing takes longer, accept the webhook immediately and process the event asynchronously using a background job queue.

Handle Duplicate Deliveries

Use the delivery_id field to deduplicate events. In rare cases (network timeouts, retries), the same event may be delivered more than once. Store processed delivery IDs and skip duplicates.

Use HTTPS Endpoints

Webhook URLs must use HTTPS. PUGUH will reject any endpoint configured with an HTTP URL.

Monitor Delivery Health

Use the List Deliveries endpoint to monitor failure rates. If an endpoint consistently fails, PUGUH will automatically disable it after 5 consecutive failures and send a notification to the organization owner.

Error Responses

All error responses use HTTP status codes with a plain detail message. No wrapper object.

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."
}

Related