documentation/tutorials/event-types-subscriptions.md
Rama master · réplica de lectura del repositorio original.
---
title: "Webhook Events & Subscriptions — Design It Right"
description: "Structure events into a type hierarchy, let subscribers filter with wildcards, and route across tenants. The schema design that saves you a migration later."
keywords: [webhook event types, webhook subscriptions, event schema design, webhook routing, webhook filtering, multi-tenant webhooks]
faqItems:
- question: "How should I name webhook event types?"
answer: >-
Use a service.resource_type.verb format in lowercase, like
user.account.created or order.payment.completed. Grouping events under a
shared prefix lets a subscriber filter a whole resource at once. Keep the
names stable once they ship, because renaming an event type breaks every
subscription bound to it.
- question: "How do I subscribe to a group of events, like everything a user does?"
answer: >-
List the event types you want on the subscription, for example
user.account.registered, user.email.verified, and user.profile.updated.
Keeping them under a common prefix such as user.account and user.profile
makes that list easy to read as your catalog grows.
- question: "Should a webhook payload carry the full record or just the IDs?"
answer: >-
Send thin payloads, meaning the IDs plus the fields that changed rather
than the whole database record. Fat payloads get rejected by intermediate
proxies more often, and a thin one pushes the consumer to fetch the
freshest data from your API using the ID you sent.
- question: "How do I stop the same webhook from being processed twice?"
answer: >-
Hook0 delivers at least once, so a network error can make it retry the
same event. Make the consumer idempotent by storing each event_id you
have handled, and when one shows up again, return 200 OK without running
the logic a second time.
---
# Setting up event types and subscriptions
This tutorial covers how to design event schemas, organize [event type](/concepts/event-types) hierarchies, and configure [subscription](/concepts/subscriptions) patterns for your application.
## Prerequisites
- Completed [Getting Started](./getting-started.md) tutorial
- Understanding of JSON schemas
- Familiarity with webhook concepts
- **Node.js 18+** (for the testing script)
### Set up environment variables
```bash
# 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:
```bash
# Save to .env file for later use
cat > .env <<EOF
HOOK0_TOKEN=$HOOK0_TOKEN
HOOK0_API=$HOOK0_API
APP_ID=$APP_ID
EOF
```
## Event type design principles
### Naming conventions
Use the `service.resource_type.verb` format:
```
service.resource_type.verb
user.account.created
user.account.updated
user.account.deleted
order.purchase.created
order.payment.completed
order.shipment.shipped
```
### Hierarchical organization
```
user.* → All user events
user.account.* → User account events
user.profile.* → User profile events
order.* → All order events
order.payment.* → Payment-related events
```
## Step 1: Design your event schema
Before creating event types, plan your event structure.
:::info Schema Documentation
These schemas are for your internal documentation. Hook0 does not validate payload structure - it forwards the payload as-is to webhook endpoints. It's your responsibility to ensure payload consistency.
:::
### User events schema
```json
{
"user.account.created": {
"user_id": "string (required)",
"email": "string (required)",
"name": "string (required)",
"plan": "string (optional)",
"created_at": "ISO 8601 timestamp"
},
"user.account.updated": {
"user_id": "string (required)",
"changes": ["array of changed fields"],
"updated_at": "ISO 8601 timestamp"
}
}
```
### Order events schema
```json
{
"order.purchase.created": {
"order_id": "string (required)",
"customer_id": "string (required)",
"items": "array of order items",
"total_amount": "number",
"currency": "string",
"created_at": "ISO 8601 timestamp"
},
"order.payment.completed": {
"order_id": "string (required)",
"payment_id": "string (required)",
"amount": "number",
"currency": "string",
"payment_method": "string",
"processed_at": "ISO 8601 timestamp"
}
}
```
## Step 2: Create event types
### Basic event type creation
```bash
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:**
<!-- openapi: EventType -->
```json
{
"service_name": "user",
"resource_type_name": "account",
"verb_name": "created",
"event_type_name": "user.account.created"
}
```
## Step 3: Create a full event type hierarchy
Here is a set of event types for a typical SaaS application:
### User lifecycle events
```bash
# User registration
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": "registered"
}'
# Email verification
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": "email",
"verb": "verified"
}'
# Profile updates
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": "profile",
"verb": "updated"
}'
# Account deactivation
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": "deactivated"
}'
```
### Subscription events
```bash
# Subscription created
curl -X POST "$HOOK0_API/event_types" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'",
"service": "subscription",
"resource_type": "plan",
"verb": "created"
}'
# Subscription upgraded
curl -X POST "$HOOK0_API/event_types" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'",
"service": "subscription",
"resource_type": "plan",
"verb": "upgraded"
}'
# Payment failed
curl -X POST "$HOOK0_API/event_types" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'",
"service": "subscription",
"resource_type": "payment",
"verb": "failed"
}'
# Subscription cancelled
curl -X POST "$HOOK0_API/event_types" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'",
"service": "subscription",
"resource_type": "plan",
"verb": "cancelled"
}'
```
## Step 4: Subscription patterns
### Single event type subscription
Simple subscription for one specific event:
```bash
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.registered"],
"description": "Welcome email trigger",
"labels": {
"environment": "production"
},
"target": {
"type": "http",
"method": "POST",
"url": "https://email-service.com/webhooks/welcome",
"headers": {
"Content-Type": "application/json",
"X-Service": "welcome-emails"
}
}
}'
```
**Response:**
<!-- openapi: Subscription -->
```json
{
"application_id": "{APP_ID}",
"subscription_id": "{{SUBSCRIPTION_ID}}",
"is_enabled": true,
"event_types": ["user.account.registered"],
"description": "Welcome email trigger",
"secret": "{SECRET}",
"labels": {
"environment": "production"
},
"target": {
"type": "http",
"method": "POST",
"url": "https://email-service.com/webhooks/welcome",
"headers": {
"content-type": "application/json",
"x-service": "welcome-emails"
}
},
"created_at": "2025-12-12T10:26:24.044337Z",
"updated_at": "2025-12-12T10:26:24.044337Z"
}
```
### Multiple event type subscription
Subscribe to multiple related events:
```bash
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.registered",
"user.email.verified",
"user.profile.updated"
],
"description": "CRM system user sync",
"labels": {
"environment": "production"
},
"target": {
"type": "http",
"method": "POST",
"url": "https://crm.company.com/api/webhooks/users",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer crm-api-key"
}
}
}'
```
### Pattern-based subscription (all user events)
Subscribe to all events matching a pattern:
```bash
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.registered",
"user.email.verified",
"user.profile.updated",
"user.account.deactivated"
],
"description": "Analytics tracking for all user events",
"labels": {
"environment": "production"
},
"target": {
"type": "http",
"method": "POST",
"url": "https://analytics.company.com/api/events",
"headers": {
"Content-Type": "application/json",
"X-Analytics-Source": "user-events"
}
}
}'
```
### Critical events subscription
High-priority events with special handling:
```bash
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": [
"subscription.payment.failed",
"user.account.deactivated"
],
"description": "Alert system for critical events",
"labels": {
"environment": "production"
},
"target": {
"type": "http",
"method": "POST",
"url": "https://alerts.company.com/api/critical",
"headers": {
"Content-Type": "application/json",
"X-Priority": "high",
"X-Alert-Channel": "slack"
}
}
}'
```
## Step 5: Subscription configuration details
### Custom headers and authentication
```bash
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": ["subscription.plan.created"],
"description": "Billing system integration",
"labels": {
"environment": "production"
},
"target": {
"type": "http",
"method": "POST",
"url": "https://billing.company.com/webhooks/subscriptions",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer billing-api-token",
"X-Webhook-Source": "hook0",
"X-Environment": "production"
}
},
"metadata": {
"service": "billing",
"priority": "high",
"team": "finance"
}
}'
```
### Subscription with metadata
Add metadata to organize subscriptions in the dashboard:
:::info Metadata Purpose
The [`metadata`](/concepts/metadata) field is for your internal organization only. It is stored with the subscription and returned in API responses, but is **not** sent in webhook payloads. Use it to tag subscriptions by team, service, priority, etc.
:::
```bash
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.registered", "user.account.deactivated"],
"description": "Marketing automation system",
"labels": {
"department": "marketing"
},
"target": {
"type": "http",
"method": "POST",
"url": "https://marketing.company.com/api/webhooks",
"headers": {
"Content-Type": "application/json"
}
},
"metadata": {
"system": "automation",
"criticality": "medium"
}
}'
```
## Step 6: Test your event types
Create a testing script to validate your event types:
```javascript
// test-events.js (requires Node.js 18+)
const { randomUUID } = require('crypto');
const HOOK0_TOKEN = '{YOUR_TOKEN}';
const HOOK0_API = 'https://app.hook0.com/api/v1/event';
const APPLICATION_ID = '{APP_ID}';
async function sendTestEvent(eventType, payload) {
const response = await fetch(HOOK0_API, {
method: 'POST',
headers: {
'Authorization': `Bearer ${HOOK0_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
application_id: APPLICATION_ID,
event_id: randomUUID(),
event_type: eventType,
payload: JSON.stringify(payload),
payload_content_type: 'application/json',
occurred_at: new Date().toISOString(),
labels: {
environment: 'test',
source: 'event-testing'
}
})
});
if (!response.ok) {
const error = await response.text();
console.error(`❌ ${eventType}: ${response.status} - ${error}`);
return null;
}
const result = await response.json();
console.log(`✅ ${eventType}:`, result.event_id);
return result;
}
async function runTests() {
console.log('🧪 Testing Event Types...\n');
// Test user events (event types follow service.resource_type.verb format)
await sendTestEvent('user.account.registered', {
user_id: 'user_123',
email: 'test@example.com',
name: 'Test User',
plan: 'free',
created_at: new Date().toISOString(),
metadata: {
source: 'web',
campaign: 'social_media'
}
});
await sendTestEvent('user.email.verified', {
user_id: 'user_123',
email: 'test@example.com',
verified_at: new Date().toISOString()
});
await sendTestEvent('user.profile.updated', {
user_id: 'user_123',
changes: ['name', 'avatar'],
previous_values: {
name: 'Old Name',
avatar: null
},
updated_at: new Date().toISOString()
});
// Test subscription events
await sendTestEvent('subscription.plan.created', {
subscription_id: 'sub_123',
user_id: 'user_123',
plan: 'pro',
amount: 29.99,
currency: 'USD',
billing_cycle: 'monthly',
created_at: new Date().toISOString()
});
await sendTestEvent('subscription.payment.failed', {
subscription_id: 'sub_123',
user_id: 'user_123',
payment_id: 'pay_123',
amount: 29.99,
currency: 'USD',
error_code: 'card_declined',
error_message: 'Insufficient funds',
failed_at: new Date().toISOString()
});
console.log('\n✨ Test completed!');
}
runTests().catch(console.error);
```
Run the tests:
```bash
node test-events.js
```
## Step 7: Monitor event types and subscriptions
### List all event types
```bash
curl "$HOOK0_API/event_types/?application_id=$APP_ID" \
-H "Authorization: Bearer $HOOK0_TOKEN"
```
**Response:**
<!-- openapi: EventType -->
```json
[
{
"service_name": "user",
"resource_type_name": "account",
"verb_name": "registered",
"event_type_name": "user.account.registered"
},
{
"service_name": "user",
"resource_type_name": "email",
"verb_name": "verified",
"event_type_name": "user.email.verified"
},
{
"service_name": "subscription",
"resource_type_name": "plan",
"verb_name": "created",
"event_type_name": "subscription.plan.created"
}
]
```
### List all subscriptions
```bash
curl "$HOOK0_API/subscriptions/?application_id=$APP_ID" \
-H "Authorization: Bearer $HOOK0_TOKEN"
```
**Response:**
<!-- openapi: Subscription -->
```json
[
{
"application_id": "{APP_ID}",
"subscription_id": "{SUBSCRIPTION_ID}",
"is_enabled": true,
"event_types": ["user.account.registered"],
"description": "Welcome email trigger",
"secret": "591b414d-8cd4-...",
"metadata": {},
"labels": {
"environment": "production"
},
"target": {
"type": "http",
"method": "POST",
"url": "https://email-service.com/webhooks/welcome",
"headers": {
"content-type": "application/json",
"x-service": "welcome-emails"
}
},
"created_at": "2025-12-12T10:26:24.044337Z",
"updated_at": "2025-12-12T10:26:24.044337Z",
"dedicated_workers": []
}
]
```
### Get subscription details
```bash
curl "$HOOK0_API/subscriptions/{SUBSCRIPTION_ID}?application_id=$APP_ID" \
-H "Authorization: Bearer $HOOK0_TOKEN"
```
**Response:**
<!-- openapi: Subscription -->
```json
{
"application_id": "{APP_ID}",
"subscription_id": "{SUBSCRIPTION_ID}",
"is_enabled": true,
"event_types": ["user.account.registered"],
"description": "Welcome email trigger",
"secret": "591b414d-8cd4-...",
"metadata": {},
"labels": {
"environment": "production"
},
"target": {
"type": "http",
"method": "POST",
"url": "https://email-service.com/webhooks/welcome",
"headers": {
"content-type": "application/json",
"x-service": "welcome-emails"
}
},
"created_at": "2025-12-12T10:26:24.044337Z",
"updated_at": "2025-12-12T10:26:24.044337Z",
"dedicated_workers": []
}
```
## Step 8: Update and manage subscriptions
### Update subscription configuration
:::warning Required Fields
The PUT endpoint requires all subscription fields, not just the ones you want to change. You must include `application_id`, `is_enabled`, `event_types`, `labels`, and `target`.
:::
```bash
curl -X PUT "$HOOK0_API/subscriptions/{SUBSCRIPTION_ID}" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'",
"is_enabled": true,
"event_types": ["user.account.registered", "user.email.verified"],
"description": "Updated CRM integration - removed profile updates",
"labels": {
"environment": "production"
},
"target": {
"type": "http",
"method": "POST",
"url": "https://new-crm.company.com/api/webhooks/users",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer new-crm-api-key"
}
}
}'
```
## Step 9: Delete event types and subscriptions
### Delete event type
To permanently delete an event type:
```bash
curl -X DELETE "$HOOK0_API/event_types/{EVENT_TYPE_NAME}?application_id=$APP_ID" \
-H "Authorization: Bearer $HOOK0_TOKEN"
```
:::warning Permanent Deletion
This permanently deletes the event type. Existing events are preserved, but you won't be able to send new events of this type. This action cannot be undone - you'll need to recreate the event type if needed.
:::
### Disable subscription
To disable a subscription, set `is_enabled` to `false`:
```bash
curl -X PUT "$HOOK0_API/subscriptions/{SUBSCRIPTION_ID}" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'",
"is_enabled": false,
"event_types": ["user.account.created"],
"labels": {
"environment": "production"
},
"target": {
"type": "http",
"method": "POST",
"url": "https://your-endpoint.com/webhook",
"headers": {
"Content-Type": "application/json"
}
}
}'
```
## Production best practices
Designing for webhooks requires a different mindset than REST APIs. These rules matter:
### 1. Consumer idempotency is mandatory
Webhooks implement "At-Least-Once" delivery. In case of network errors, Hook0 may retry sending the same event.
- **Rule:** Consumers must verify the `event_id`.
- **Implementation:** Store processed `event_id`s in a database. If an ID is seen again, return `200 OK` immediately without re-processing logic.
### 2. Async processing (the 200 OK rule)
Your webhook endpoint must be fast. If you perform long operations (sending emails, generating PDFs) synchronously, you risk timeouts.
- **Rule:** Acknowledge receipt first, process later.
- **Implementation:** Receive the webhook → Push to an internal queue (e.g., SQS, RabbitMQ, Redis) → Return `200 OK`. Workers will process the actual job.
### 3. Thin payloads vs. fat payloads
Sending massive objects in webhooks increases latency and failure rates.
- **Recommendation:** Prefer "Thin Payloads" (IDs + changed fields) over "Fat Payloads" (full database record).
- **Why:** Large payloads may be rejected by intermediate proxies. It also ensures the consumer fetches the *freshest* data via your API using the ID provided in the webhook.
### 4. Security verification
Never trust the payload blindly. Any public endpoint can be flooded with fake data.
- **Rule:** Verify the HMAC signature.
- **Implementation:** Use the signing secret provided by Hook0 to hash the incoming body and compare it with the `X-Hook0-Signature` header. Reject any request where the signature does not match. See [Webhook Authentication](./webhook-authentication.md) for implementation examples.
## What you've learned
- Designed hierarchical event type schemas
- Created a full event type set for a SaaS app
- Built subscription patterns (single, multi, pattern-based, critical)
- Configured subscriptions with headers, auth, and metadata
- Tested event types with a script
- Managed the event type and subscription lifecycle
## Next steps
- [Implementing Webhook Authentication](./webhook-authentication.md)
- [Debugging Failed Webhook Deliveries](../how-to-guides/debug-failed-webhooks.md)
## Troubleshooting
### Event not triggering webhooks
1. Check event type exists in application
2. Verify subscription is enabled
3. Confirm event type matches subscription
4. Check webhook endpoint accessibility
### Subscription not receiving events
1. Verify subscription configuration
2. Check event type matching logic
3. Review webhook endpoint logs
4. Monitor Hook0 dashboard for delivery attempts