Skip to main content

Building Your First Webhook System with Hook0

This tutorial demonstrates how to integrate Hook0 into your SaaS platform to provide webhook capabilities to your customers. It uses curl commands to illustrate the API flow, making it easy to understand and implement in any programming language.

What You'll Build

A complete webhook delivery system that:

  • Sends events from your SaaS to Hook0
  • Enables customer subscriptions to specific events
  • Handles automatic delivery, retries, and monitoring
  • Supports multi-tenant filtering with labels

Prerequisites

Set Up Environment Variables

# Set your service token (from dashboard)
export HOOK0_TOKEN="YOUR_TOKEN_HERE"
export HOOK0_API="https://app.hook0.com/api/v1" # Replace by your domain (or http://localhost:8081 locally)

# Set your application ID (shown in dashboard URL or application details)
export APP_ID="YOUR_APPLICATION_ID_HERE"

Save these values:

# Save to .env file for later use
cat > .env <<EOF
HOOK0_TOKEN=$HOOK0_TOKEN
HOOK0_API=$HOOK0_API
APP_ID=$APP_ID
EOF

Architecture Overview

  +----------+  POST event    +--------+  POST webhook   +----------+
| Your |--------------->| |---------------->| Customer |
| SaaS | | Hook0 | | Endpoint |
+----------+ | | +----------+
+--------+

Your SaaS sends events to Hook0, which manages all webhook complexity including delivery, retries, signature verification, and monitoring.

Understanding the API Flow

The integration follows this logical sequence:

  1. Create Event Types - Define what events your SaaS can emit
  2. Create Subscriptions - Enable customers to receive webhooks
  3. Send Events - Push events to Hook0 when actions occur
  4. Monitor Delivery - Track webhook success and failures

Let's walk through each step with practical examples.

Step 1: Define Your Event Types

Hook0 uses a three-part structure for event types: service.resource_type.verb. This provides clear semantics and enables powerful filtering.

Why define event types first?

Event types act as a contract between your SaaS and customer webhooks. They must be created before you can send events or create subscriptions.

Creating a "User Created" Event Type

curl -X POST "$HOOK0_API/event_types" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'",
"service": "user",
"resource_type": "account",
"verb": "created"
}'

Response:

{
"service_name": "user",
"resource_type_name": "account",
"verb_name": "created",
"event_type_name": "user.account.created"
}

Creating an "Order Completed" Event Type

curl -X POST "$HOOK0_API/event_types" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'",
"service": "order",
"resource_type": "purchase",
"verb": "completed"
}'

Response:

{
"service_name":"orders",
"resource_type_name":"purchase",
"verb_name":"completed",
"event_type_name":"order.purchase.completed"
}

Step 2: Send Events to Hook0

Once event types are defined, your SaaS can start sending events. The /api/v1/event endpoint (singular) accepts individual events.

Why this order?

You must create event types before sending events. Hook0 validates that the event type exists before accepting the event.

Sending a User Created Event

When a user signs up in your SaaS:

curl -X POST "$HOOK0_API/event" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'",
"event_id": "'$(uuidgen)'",
"event_type": "user.account.created",
"payload": "{\"user_id\": \"usr_789\", \"email\": \"john@example.com\", \"plan\": \"premium\"}",
"payload_content_type": "application/json",
"occurred_at": "2024-01-15T10:30:00Z",
"labels": {
"tenant_id": "customer_123",
"environment": "production",
"region": "us-east-1"
}
}'

Response:

{
"application_id": "{APP_ID}",
"event_id": "{EVENT_ID}",
"received_at": "2025-12-12T09:25:28.084734Z"
}

Key Points:

  • event_id: Must be a valid UUID (e.g., 69f69ecf-0d9e-4c92-a6e0-3b2676343940)
  • payload: Must be a JSON-encoded string (not an object) - see warning below
  • labels: Critical for multi-tenant filtering - must have at least one key-value pair
Payload Format

The payload field must be a JSON-encoded string, not a JSON object:

Correct: "payload": "{\"user_id\": \"usr_789\"}"

Incorrect: "payload": {"user_id": "usr_789"}

This allows Hook0 to forward the exact payload to webhooks without re-serialization.

The tenant_id label ensures events only go to the right customer.

Sending an Order Completed Event

When an order is fulfilled:

curl -X POST "$HOOK0_API/event" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'",
"event_id": "'$(uuidgen)'",
"event_type": "order.purchase.completed",
"payload": "{\"order_id\": \"ord_456\", \"amount\": 299.99, \"items\": 3}",
"payload_content_type": "application/json",
"occurred_at": "2024-01-15T11:00:00Z",
"labels": {
"tenant_id": "customer_123",
"environment": "production"
}
}'

Response:

{
"application_id": "{APP_ID}",
"event_id": "{EVENT_ID}",
"received_at": "2025-12-12T09:27:27.045191Z"
}

Step 3: Create Customer Subscriptions

Subscriptions define where and how webhooks are delivered. Each subscription filters events based on labels, ensuring customers only receive their own events.

Why labels matter for multi-tenancy

The labels object creates a filter. Only events with matching labels are delivered to that subscription. This is how Hook0 supports multi-tenant SaaS platforms.

Creating a Subscription for a Customer

curl -X POST "$HOOK0_API/subscriptions" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'",
"is_enabled": true,
"event_types": [
"user.account.created",
"order.purchase.completed"
],
"description": "Webhook for Customer ABC Corp",
"labels": {
"tenant_id": "customer_123"
},
"target": {
"type": "http",
"method": "POST",
"url": "https://customer-abc.com/webhooks/hook0",
"headers": {
"X-Customer-Id": "customer_123"
}
},
"metadata": {
"customer_name": "ABC Corp",
"created_by": "api",
"plan": "enterprise"
}
}'

Response:

{
"application_id": "{APP_ID}",
"subscription_id": "{SUBSCRIPTION_ID}",
"is_enabled": true,
"event_types": [
"user.account.created",
"order.purchase.completed"
],
"description": "Webhook for Customer ABC Corp",
"secret": "d48488f1-cddc...",
"metadata": {
"customer_name": "ABC Corp",
"plan": "enterprise",
"created_by": "api"
},
"labels": {
"tenant_id": "customer_123"
},
"target": {
"type": "http",
"method": "POST",
"url": "https://customer-abc.com/webhooks/hook0",
"headers": {
"x-customer-id": "customer_123"
}
},
"created_at": "2025-12-12T09:30:13.822397Z",
"dedicated_workers": []
}

Important: Save the secret - customers need it to verify webhook signatures.

How Multi-Tenant Filtering Works

Event labels                 Subscription filter              Result
------------------------ ------------------------- ------------
tenant_id: "customer_123" -> tenant_id = "customer_123" -> ✅ Delivered
tenant_id: "customer_456" -> tenant_id = "customer_123" -> ❌ Skipped

Step 4: Monitor Webhook Deliveries

Hook0 provides comprehensive monitoring to track webhook success and failures.

List Recent Events

See what events have been sent:

curl -X GET "$HOOK0_API/events/?application_id=$APP_ID" \
-H "Authorization: Bearer $HOOK0_TOKEN"

Response:

[
{
"event_id": "{EVENT_ID}",
"event_type_name": "order.purchase.completed",
"payload_content_type": "application/json",
"ip": "192.168.97.1",
"metadata": {},
"occurred_at": "2024-01-15T11:00:00Z",
"received_at": "2025-12-12T09:27:27.045191Z",
"labels": {
"environment": "production",
"tenant_id": "customer_123"
}
},
{
"event_id": "{EVENT_ID}",
"event_type_name": "user.account.created",
"payload_content_type": "application/json",
"ip": "192.168.97.1",
"metadata": {},
"occurred_at": "2024-01-15T10:30:00Z",
"received_at": "2025-12-12T09:25:28.084734Z",
"labels": {
"environment": "production",
"region": "us-east-1",
"tenant_id": "customer_123"
}
},
{
"event_id": "{EVENT_ID}",
"event_type_name": "user.account.created",
"payload_content_type": "application/json",
"ip": "192.168.97.1",
"metadata": {},
"occurred_at": "2025-12-12T08:39:19Z",
"received_at": "2025-12-12T08:39:19.407621Z",
"labels": {
"environment": "tutorial"
}
}
]

Check Delivery Attempts

Monitor webhook delivery attempts and their status:

curl -X GET "$HOOK0_API/request_attempts/?application_id=$APP_ID" \
-H "Authorization: Bearer $HOOK0_TOKEN"

Response:

[
{
"request_attempt_id": "{REQUEST_ATTEMPT_ID}",
"event_id": "{EVENT_ID}",
"subscription": {
"subscription_id": "{SUBSCRIPTION_ID}",
"description": "Customer webhook"
},
"created_at": "2024-01-15T10:30:01Z",
"picked_at": "2024-01-15T10:30:02Z",
"succeeded_at": "2024-01-15T10:30:03Z",
"failed_at": null,
"retry_count": 0,
"response_id": "{RESPONSE_ID}",
"status": {
"type": "succeeded",
"at": "2024-01-15T10:30:03Z"
}
}
]

Replay Events

You can manually replay an event when needed for business purposes (e.g., re-triggering a workflow). Note that failed deliveries are automatically retried by Hook0:

curl -X POST "$HOOK0_API/events/{EVENT_ID}/replay" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'"
}'

Advanced Patterns

Bulk Event Processing

When you need to send multiple events (e.g., batch import), send them individually but with correlation:

# Event 1 of batch
curl -X POST "$HOOK0_API/event" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'",
"event_id": "'$(uuidgen)'",
"event_type": "user.account.created",
"payload": "{\"user_id\": \"usr_789\", \"email\": \"john@example.com\", \"plan\": \"premium\"}",
"labels": {
"tenant_id": "customer_123",
"batch_id": "batch-001",
"batch_size": "100"
}
}'

# Event 2 of batch
curl -X POST "$HOOK0_API/event" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'",
"event_id": "'$(uuidgen)'",
"event_type": "user.account.created",
"payload": "{\"user_id\": \"usr_7892\", \"email\": \"john2@example.com\", \"plan\": \"premium\"}",
"labels": {
"tenant_id": "customer_123",
"batch_id": "batch-001",
"batch_size": "100"
}
}'

Environment-Based Filtering

Use labels to separate environments:

# Production event
curl -X POST "$HOOK0_API/event" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'",
"event_id": "'$(uuidgen)'",
"event_type": "user.account.created",
"payload": "{\"user_id\": \"usr_123\"}",
"payload_content_type": "application/json",
"occurred_at": "'$(date -u +"%Y-%m-%dT%H:%M:%SZ")'",
"labels": {
"tenant_id": "customer_123",
"environment": "production"
}
}'

Response:

{
"application_id": "{APP_ID}",
"event_id": "{EVENT_ID}",
"received_at": "2025-12-12T09:41:39.757315Z"
}
# Staging subscription (will not receive production events)
curl -X POST "$HOOK0_API/subscriptions" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'",
"is_enabled": true,
"event_types": ["user.account.created"],
"description": "Staging webhook",
"labels": {
"environment": "staging"
},
"target": {
"type": "http",
"method": "POST",
"url": "https://staging.example.com/webhook",
"headers": {
"X-Environment": "staging"
}
}
}'

Response:

{
"application_id": "{APP_ID}",
"subscription_id": "{SUBSCRIPTION_ID}",
"is_enabled": true,
"event_types": ["user.account.created"],
"description": "Staging webhook",
"secret": "{SECRET}",
"labels": {
"environment": "staging"
},
"target": {
"type": "http",
"method": "POST",
"url": "https://staging.example.com/webhook",
"headers": {"x-environment": "staging"}
},
"created_at": "2025-12-12T09:41:41.304925Z"
}

Since this subscription filters on environment: "staging", it will not receive the production event above.

Complete Integration Example

Here's a practical example showing the complete flow from event creation to webhook delivery:

Step 1: Create Event Type (one-time setup)

curl -X POST "$HOOK0_API/event_types" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'",
"service": "billing",
"resource_type": "invoice",
"verb": "paid"
}'

Response:

{
"service_name": "billing",
"resource_type_name": "invoice",
"verb_name": "paid",
"event_type_name": "billing.invoice.paid"
}

Step 2: Create Subscription (customer setup)

curl -X POST "$HOOK0_API/subscriptions" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'",
"is_enabled": true,
"event_types": ["billing.invoice.paid"],
"description": "Customer billing webhook",
"labels": {
"tenant_id": "customer_789"
},
"target": {
"type": "http",
"method": "POST",
"url": "https://customer.example.com/webhooks",
"headers": {
"X-Tenant-Id": "customer_789"
}
}
}'

Response:

{
"application_id": "{APP_ID}",
"subscription_id": "{SUBSCRIPTION_ID}",
"is_enabled": true,
"event_types": ["billing.invoice.paid"],
"description": "Customer billing webhook",
"secret": "{SECRET}",
"labels": {
"tenant_id": "customer_789"
},
"target": {
"type": "http",
"method": "POST",
"url": "https://customer.example.com/webhooks",
"headers": {"x-tenant-id": "customer_789"}
},
"created_at": "2025-12-12T09:43:22.425687Z"
}

⚠️ Save the secret - customers need it to verify webhook signatures.

Step 3: Send Event (when invoice is paid)

curl -X POST "$HOOK0_API/event" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'",
"event_id": "'$(uuidgen)'",
"event_type": "billing.invoice.paid",
"payload": "{\"invoice_id\": \"inv_456\", \"amount\": 1500.00}",
"payload_content_type": "application/json",
"occurred_at": "'$(date -u +"%Y-%m-%dT%H:%M:%SZ")'",
"labels": {
"tenant_id": "customer_789"
}
}'

Response:

{
"application_id": "{APP_ID}",
"event_id": "{EVENT_ID}",
"received_at": "2025-12-12T09:43:24.351417Z"
}

Step 4: Automatic Delivery

Hook0 automatically delivers the webhook to https://customer.example.com/webhooks with:

  • X-Hook0-Signature header for verification
  • The event payload
  • Automatic retries on failure

See Implementing Webhook Authentication for signature verification code in Node.js, Python, and Go.

Implementation Patterns in Different Languages

While this tutorial uses curl for clarity, here's how to implement the same patterns in various languages:

Python

import requests
import uuid
import json
from datetime import datetime, timezone

# Send event
event = {
"application_id": "{APP_ID}",
"event_id": str(uuid.uuid4()),
"event_type": "user.account.created",
"payload": json.dumps({"user_id": "usr_123"}),
"payload_content_type": "application/json",
"occurred_at": datetime.now(timezone.utc).isoformat(),
"labels": {"tenant_id": "customer_123"}
}

response = requests.post(
"https://app.hook0.com/api/v1/event",
headers={"Authorization": "Bearer {YOUR_TOKEN}"},
json=event
)

Go

import (
"bytes"
"encoding/json"
"net/http"
"time"

"github.com/google/uuid"
)

// Send event
event := map[string]interface{}{
"application_id": "{APP_ID}",
"event_id": uuid.New().String(),
"event_type": "user.account.created",
"payload": `{"user_id": "usr_123"}`,
"payload_content_type": "application/json",
"occurred_at": time.Now().UTC().Format(time.RFC3339),
"labels": map[string]string{
"tenant_id": "customer_123",
},
}

jsonData, _ := json.Marshal(event)
req, _ := http.NewRequest("POST",
"https://app.hook0.com/api/v1/event",
bytes.NewBuffer(jsonData))
req.Header.Set("Authorization", "Bearer {YOUR_TOKEN}")
req.Header.Set("Content-Type", "application/json")

client := &http.Client{}
resp, _ := client.Do(req)
defer resp.Body.Close()

Ruby

require 'net/http'
require 'json'
require 'securerandom'
require 'time'

# Send event
event = {
application_id: "{APP_ID}",
event_id: SecureRandom.uuid,
event_type: "user.account.created",
payload: {user_id: "usr_123"}.to_json,
payload_content_type: "application/json",
occurred_at: Time.now.utc.iso8601,
labels: {tenant_id: "customer_123"}
}

uri = URI('https://app.hook0.com/api/v1/event')
http = Net::HTTP.new(uri.host, uri.port)

request = Net::HTTP::Post.new(uri)
request['Authorization'] = 'Bearer {YOUR_TOKEN}'
request['Content-Type'] = 'application/json'
request.body = event.to_json

response = http.request(request)

Best Practices

1. Event Design

  • Use semantic naming: service.resource.verb structure
  • Include context: Add relevant data to help consumers
  • Be consistent: Same structure across all events
  • Version carefully: Plan for schema evolution

2. Label Strategy

  • tenant_id: Always include for multi-tenancy
  • environment: Separate prod/staging/dev
  • region: Support geographic filtering

3. Error Handling

  • Idempotency: Use unique event_ids to prevent duplicates
  • Retry logic: Implement exponential backoff
  • Circuit breaker: Fail fast when Hook0 is down
  • Local queue: Buffer events during outages

4. Security

  • Never log tokens: Keep authentication secure
  • Validate signatures: Customers should verify webhooks
  • Encrypt sensitive data: Do not send PII in plaintext
  • Rate limit: Protect against abuse

Troubleshooting Common Issues

Event Not Delivered

# Check if event was received
curl -X GET "$HOOK0_API/events?application_id=$APP_ID" \
-H "Authorization: Bearer $HOOK0_TOKEN"

# Check subscription filters
curl -X GET "$HOOK0_API/subscriptions?application_id=$APP_ID" \
-H "Authorization: Bearer $HOOK0_TOKEN"
# Verify labels match your events

401 Unauthorized

# Verify token is included
curl -H "Authorization: Bearer $HOOK0_TOKEN" # ✅ Correct
curl -H "Authorization: $HOOK0_TOKEN" # ❌ Wrong - missing Bearer

Event Type Not Found

# List existing event types
curl -X GET "$HOOK0_API/event_types?application_id=$APP_ID" \
-H "Authorization: Bearer $HOOK0_TOKEN"

# Create missing event type before sending events

Summary

You've learned how to:

  1. Create event types using the three-part structure
  2. Send events with proper labeling for multi-tenancy
  3. Create subscriptions with label-based filtering
  4. Monitor deliveries and handle failures
  5. Implement patterns for bulk processing

The key insight is that Hook0's label system enables powerful multi-tenant webhook delivery while keeping the integration simple. By following this flow - define types, send events, create subscriptions, monitor results - you can build a robust webhook system for your SaaS platform.

Next Steps