Skip to main content

Event Labels

Labels are key-value pairs attached to events and used for filtering and routing webhooks to subscriptions. They enable powerful use cases like multi-tenancy, environment-based routing, and priority filtering.

What Are Labels?

Labels are metadata attached to events that subscriptions can filter on. Each label consists of:

  • Key: A string identifier (e.g., tenant_id, environment, priority)
  • Value: A string value (e.g., tenant_123, production, high)
Minimum Label Requirement

Every event must include at least one label. The API will reject events with an empty labels: {} object. This ensures proper routing and filtering of events to subscriptions.

{
"event_id": "evt_123",
"event_type": "user.account.created",
"labels": {
"tenant_id": "tenant_abc",
"environment": "production",
"region": "us-east-1",
"priority": "high"
},
"payload": { ... }
}

How Labels Work

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

Event Sending with Labels

When sending an event, include labels in the request:

curl -X POST "$HOOK0_API/event" \
-H "Authorization: Bearer $HOOK0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"application_id": "'"$APP_ID"'",
"event_id": "evt_456",
"event_type": "user.account.created",
"payload": "{\"user_id\": \"user_789\"}",
"payload_content_type": "application/json",
"labels": {
"tenant_id": "acme_corp",
"environment": "production",
"source": "web_app"
}
}'

Subscription Filtering

Subscriptions use labels to filter 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", "user.account.updated"],
"labels": {
"tenant_id": "acme_corp"
},
"description": "Webhooks for ACME Corp tenant",
"target": {
"type": "http",
"method": "POST",
"url": "https://acme.example.com/webhooks"
}
}'

Filtering logic:

  • Subscription receives event if all subscription labels match the event labels
  • If event does not have a required label, subscription will not receive it
  • Multiple label filters per subscription are supported

Common Use Cases

1. Multi-Tenancy

Route events to tenant-specific webhooks:

// Send event with tenant label
await hook0.sendEvent({
eventType: 'invoice.created',
payload: { invoiceId: 'inv_123', amount: 99.99 },
labels: {
tenant_id: 'acme_corp',
organization: 'org_456'
}
});

// Tenant A subscription (only receives tenant_a events)
{
"labels": { "tenant_id": "acme_corp" },
"target": { "url": "https://acme.example.com/webhooks" }
}

// Tenant B subscription (only receives tenant_b events)
{
"labels": { "tenant_id": "globex_inc" },
"target": { "url": "https://globex.example.com/webhooks" }
}

Benefits:

  • Complete data isolation between tenants
  • Tenants only receive their own events
  • Scalable to thousands of tenants

2. Environment Filtering

Separate production, staging, and development events:

// Production event
await hook0.sendEvent({
eventType: 'deployment.completed',
labels: { environment: 'production' }
});

// Staging event
await hook0.sendEvent({
eventType: 'deployment.completed',
labels: { environment: 'staging' }
});

Subscriptions filter by environment:

{
"labels": { "environment": "production" },
"description": "Production monitoring",
"target": { "url": "https://monitoring.prod.example.com/webhooks" }
}

3. Priority Routing

Route high-priority events to dedicated endpoints:

// High-priority event
await hook0.sendEvent({
eventType: 'payment.transaction.failed',
labels: {
priority: 'critical',
alert_team: 'payments'
}
});

// Normal-priority event
await hook0.sendEvent({
eventType: 'payment.transaction.succeeded',
labels: { priority: 'normal' }
});

Subscriptions handle priorities differently:

{
"labels": { "priority": "critical" },
"description": "Critical alerts to Slack",
"target": {
"url": "https://hooks.slack.com/services/critical-alerts",
"headers": { "X-Priority": "urgent" }
}
}

4. Geographic Routing

Route events based on region or data center:

await hook0.sendEvent({
eventType: 'order.order.created',
payload: { orderId: 'ord_123' },
labels: {
region: 'us-east-1',
country: 'US'
}
});

Region-specific subscriptions:

{
"labels": { "region": "us-east-1" },
"description": "US East fulfillment center",
"target": { "url": "https://fulfillment.us-east.example.com/webhooks" }
}

5. Feature Flags

Route events based on feature enablement:

await hook0.sendEvent({
eventType: 'user.account.created',
labels: {
feature_new_onboarding: 'enabled',
experiment_group: 'variant_a'
}
});

6. Source Tracking

Identify event origin:

await hook0.sendEvent({
eventType: 'purchase.completed',
labels: {
source: 'mobile_app',
platform: 'ios',
version: '2.1.0'
}
});

Label Structure

JSON Format

Labels are a flat JSON object with string keys and string values:

{
"labels": {
"key1": "value1",
"key2": "value2",
"key3": "value3"
}
}

Validation Rules

ConstraintValueError Code
Max labels per event10labels-size
Key length1-50 characterslabels-property-length
Value length1-50 characterslabels-property-length
Value typeString onlylabels-property-type
// ✅ Valid labels
{
"tenant": "acme",
"env": "prod",
"region": "us"
}

// ❌ Too many labels (max 10)
{
"label1": "value1",
"label2": "value2",
// ... 11 labels total
}

// ❌ Value too long (max 50 chars)
{
"key": "this_value_is_way_too_long_and_exceeds_the_fifty_character_limit"
}

// ❌ Non-string value
{
"priority": 1, // Must be "1" (string)
"active": true // Must be "true" (string)
}

Reserved Labels

Hook0 does not strictly reserve label keys, but common conventions exist:

{
"tenant_id": "unique_tenant_identifier",
"environment": "production|staging|development",
"region": "aws_region_or_datacenter",
"priority": "critical|high|normal|low",
"source": "api|web|mobile|batch",
"version": "api_or_schema_version"
}

Label Naming Conventions

✅ Good practices:

  • Use snake_case: tenant_id, event_source
  • Be descriptive: payment_provider not pp
  • Use consistent naming across events
  • Keep keys short but meaningful

❌ Avoid:

  • Camel case: tenantId (use tenant_id)
  • Special characters: tenant@id, tenant.id
  • Generic names: data, info, value
  • Sensitive information: credit_card, ssn, password

Advanced Patterns

Multi-Label Filtering (Multiple Subscriptions)

Since subscriptions support only one label filter, use multiple subscriptions for AND logic:

// Event with multiple labels
await hook0.sendEvent({
eventType: 'order.order.created',
labels: {
tenant_id: 'acme_corp',
environment: 'production',
priority: 'high'
}
});

Create separate subscriptions for different filtering needs:

// Subscription 1: All ACME Corp events
{
"labels": { "tenant_id": "acme_corp" },
"target": { "url": "https://acme.example.com/all-events" }
}

// Subscription 2: All production events (all tenants)
{
"labels": { "environment": "production" },
"target": { "url": "https://monitoring.example.com/production" }
}

// Subscription 3: All high-priority events
{
"labels": { "priority": "high" },
"target": { "url": "https://alerts.example.com/high-priority" }
}
info

Each subscription filters independently. An event with labels {tenant_id: "acme_corp", environment: "production"} will trigger both subscriptions above.

Wildcard Simulation

Hook0 does not support wildcard labels, but you can achieve similar results:

// Strategy 1: Use hierarchical values
labels: {
scope: "acme_corp.us_east.production"
}

// Strategy 2: Use multiple labels
labels: {
tenant: "acme_corp",
region: "us_east",
env: "production"
}

// Strategy 3: Subscribe to multiple specific values
// Create subscriptions for tenant_a, tenant_b, tenant_c separately

Dynamic Label Values

Generate labels programmatically:

function sendOrderEvent(order) {
return hook0.sendEvent({
eventType: 'order.order.created',
payload: order,
labels: {
tenant_id: order.tenantId,
environment: process.env.NODE_ENV,
region: process.env.AWS_REGION,
order_total: categorizeOrderTotal(order.total), // "small"|"medium"|"large"
customer_type: order.customer.isPremium ? "premium" : "standard"
}
});
}

function categorizeOrderTotal(total) {
if (total < 50) return 'small';
if (total < 500) return 'medium';
return 'large';
}

Label-Based Subscription Management

Organize subscriptions using labels:

// Create subscription with metadata describing label usage
await hook0.createSubscription({
eventTypes: ['payment.transaction.succeeded'],
labelKey: 'tenant_id',
labelValue: 'acme_corp',
description: 'ACME Corp payment webhooks',
metadata: {
tenant_name: 'ACME Corporation',
tenant_tier: 'enterprise',
contact_email: 'webhooks@acme.com',
label_strategy: 'tenant_isolation'
}
});

Best Practices

Design Principles

  1. Keep labels simple: Use for filtering, not business logic
  2. Use consistent naming: Establish conventions and stick to them
  3. Document label schema: Maintain documentation of label keys and allowed values
  4. Limit label count: Use only necessary labels (remember 10 max)
  5. Avoid PII: Do not include sensitive data in labels

Example Label Schema Documentation

# labels-schema.yaml
labels:
tenant_id:
type: string
pattern: "^[a-z0-9_]{3,50}$"
description: "Unique tenant identifier"
required: true

environment:
type: string
enum: [production, staging, development]
description: "Deployment environment"
required: true

priority:
type: string
enum: [critical, high, normal, low]
description: "Event priority level"
required: false

region:
type: string
pattern: "^[a-z]{2}-[a-z]+-[0-9]$"
description: "AWS region identifier"
required: false

Implementation Checklist

Event sending:

  • ✅ Validate label values before sending
  • ✅ Use constants for label keys
  • ✅ Include required labels (tenant_id, environment)
  • ✅ Keep label count under 10
  • ✅ Use string values only

Subscription configuration:

  • ✅ Choose appropriate labels for filtering
  • ✅ Document subscription filtering logic
  • ✅ Test subscription receives expected events
  • ✅ Monitor subscription delivery metrics

Code Example: Label Helper

// labels.js
const REQUIRED_LABELS = ['tenant_id', 'environment'];
const MAX_LABELS = 10;
const MAX_LENGTH = 50;

class LabelValidator {
static validate(labels) {
// Check required labels
for (const key of REQUIRED_LABELS) {
if (!labels[key]) {
throw new Error(`Missing required label: ${key}`);
}
}

// Check label count
if (Object.keys(labels).length > MAX_LABELS) {
throw new Error(`Too many labels (max ${MAX_LABELS})`);
}

// Check lengths and types
for (const [key, value] of Object.entries(labels)) {
if (typeof value !== 'string') {
throw new Error(`Label ${key} must be a string`);
}
if (key.length > MAX_LENGTH || value.length > MAX_LENGTH) {
throw new Error(`Label ${key} exceeds max length ${MAX_LENGTH}`);
}
}

return true;
}

static create(tenant, environment, extra = {}) {
const labels = {
tenant_id: tenant,
environment: environment,
...extra
};

this.validate(labels);
return labels;
}
}

// Usage
const labels = LabelValidator.create('acme_corp', 'production', {
region: 'us-east-1',
priority: 'high'
});

await hook0.sendEvent({
eventType: 'order.order.created',
labels
});

Troubleshooting

Events Not Reaching Subscription

Symptom: Events sent but subscription not triggered

Causes:

  1. Label mismatch: Event label value does not match subscription filter
  2. Missing label: Event does not include the filtered label key
  3. Case sensitivity: Label values are case-sensitive

Solutions:

// Check event labels
const event = await hook0.getEvent('evt_123');
console.log('Event labels:', event.labels);

// Check subscription filter
const subscription = await hook0.getSubscription('sub_456');
console.log('Subscription labels:', subscription.labels);

// Verify match (all subscription labels must match event labels)
const matches = Object.entries(subscription.labels).every(
([key, value]) => event.labels[key] === value
);
console.log('Labels match:', matches);

Validation Errors

Symptom: 400 Bad Request with label validation error

Error codes:

  • labels-size: Too many labels (max 10)
  • labels-property-length: Key or value exceeds 50 characters
  • labels-property-type: Non-string value detected

Solutions:

// Fix: Reduce label count
// ❌ Bad (11 labels)
labels: { l1: "v1", l2: "v2", ..., l11: "v11" }

// ✅ Good (10 labels)
labels: { l1: "v1", l2: "v2", ..., l10: "v10" }

// Fix: Shorten values
// ❌ Bad (too long)
labels: { key: "this_is_a_very_long_value_that_exceeds_fifty_characters_limit" }

// ✅ Good
labels: { key: "short_value" }

// Fix: Convert to string
// ❌ Bad (wrong type)
labels: { priority: 1, active: true }

// ✅ Good
labels: { priority: "1", active: "true" }

Performance Considerations

Issue: Too many subscriptions with different label filters

Impact:

  • Increased database queries
  • Slower event routing
  • Higher resource usage

Optimization:

// ❌ Inefficient: 100 subscriptions for 100 tenants
for (const tenant of tenants) {
await createSubscription({
labelKey: 'tenant_id',
labelValue: tenant.id,
target: { url: `https://${tenant.domain}/webhooks` }
});
}

// ✅ Better: Single webhook endpoint handles routing
await createSubscription({
labelKey: 'environment',
labelValue: 'production',
target: { url: 'https://webhook-router.example.com/receive' }
});

// Webhook router extracts tenant from event.labels.tenant_id
// and forwards to appropriate tenant endpoint

API Reference

Event Creation with Labels

POST /api/v1/event
Content-Type: application/json
Authorization: Bearer TOKEN

{
"application_id": "app_123",
"event_id": "evt_456",
"event_type": "user.account.created",
"payload": "{}",
"labels": {
"tenant_id": "acme_corp",
"environment": "production"
}
}

Subscription Creation with Label Filter

POST /api/v1/subscriptions
Content-Type: application/json
Authorization: Bearer TOKEN

{
"application_id": "app_123",
"event_types": ["user.account.created"],
"labels": {
"tenant_id": "acme_corp"
},
"target": {
"type": "http",
"method": "POST",
"url": "https://example.com/webhooks"
}
}

Querying Events by Labels

GET /api/v1/events?application_id=app_123
Authorization: Bearer TOKEN

Response includes labels for filtering client-side:

{
"events": [
{
"event_id": "evt_123",
"labels": {
"tenant_id": "acme_corp",
"environment": "production"
}
}
]
}

Next Steps