Creating Webhook Jobs

Webhook jobs represent individual delivery tasks. This guide covers everything you need to know about creating and managing webhook jobs.

What is a Webhook Job?

When you send a webhook event through Hook Mesh, we create a job for each subscribed endpoint. Jobs handle:

  • Delivering your payload to customer endpoints
  • Automatic retries with exponential backoff
  • Signature generation for security
  • Tracking delivery status and history

Basic Job Creation

Creating a webhook job requires three things:

  1. application_id - Which application to send from
  2. event_type - What type of event occurred
  3. payload - The data to send

Node.js / JavaScript

create-job.js
const response = await fetch('https://api.hookmesh.com/v1/webhook-jobs', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.HOOKMESH_API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    application_id: 'app_2Zy3X8qP9rK5mN1vB',
    event_type: 'user.created',
    payload: {
      user_id: 'usr_xyz789',
      email: 'alice@example.com',
      name: 'Alice Johnson',
      created_at: '2026-01-20T15:30:00Z'
    }
  })
});

const job = await response.json();
console.log('Job created:', job.id); // job_5nM8pQ1rK3vL9xB

Python

create_job.py
import requests
import os

response = requests.post(
    'https://api.hookmesh.com/v1/webhook-jobs',
    headers={
        'Authorization': f'Bearer {os.environ.get("HOOKMESH_API_KEY")}',
        'Content-Type': 'application/json'
    },
    json={
        'application_id': 'app_2Zy3X8qP9rK5mN1vB',
        'event_type': 'user.created',
        'payload': {
            'user_id': 'usr_xyz789',
            'email': 'alice@example.com',
            'name': 'Alice Johnson',
            'created_at': '2026-01-20T15:30:00Z'
        }
    }
)

job = response.json()
print(f'Job created: {job["id"]}')

Go

create_job.go
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
    "os"
)

type WebhookJob struct {
    ApplicationID string                 `json:"application_id"`
    EventType     string                 `json:"event_type"`
    Payload       map[string]interface{} `json:"payload"`
}

func main() {
    job := WebhookJob{
        ApplicationID: "app_2Zy3X8qP9rK5mN1vB",
        EventType:     "user.created",
        Payload: map[string]interface{}{
            "user_id":    "usr_xyz789",
            "email":      "alice@example.com",
            "name":       "Alice Johnson",
            "created_at": "2026-01-20T15:30:00Z",
        },
    }

    jsonData, _ := json.Marshal(job)
    req, _ := http.NewRequest(
        "POST",
        "https://api.hookmesh.com/v1/webhook-jobs",
        bytes.NewBuffer(jsonData),
    )

    req.Header.Set("Authorization", "Bearer "+os.Getenv("HOOKMESH_API_KEY"))
    req.Header.Set("Content-Type", "application/json")

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    var result map[string]interface{}
    json.NewDecoder(resp.Body).Decode(&result)
    fmt.Printf("Job created: %s\n", result["id"])
}

PHP / Laravel

create_job.php
<?php

use Illuminate\Support\Facades\Http;

$response = Http::withHeaders([
    'Authorization' => 'Bearer ' . config('services.hookmesh.api_key'),
])->post('https://api.hookmesh.com/v1/webhook-jobs', [
    'application_id' => 'app_2Zy3X8qP9rK5mN1vB',
    'event_type' => 'user.created',
    'payload' => [
        'user_id' => 'usr_xyz789',
        'email' => 'alice@example.com',
        'name' => 'Alice Johnson',
        'created_at' => '2026-01-20T15:30:00Z',
    ]
]);

$job = $response->json();
echo "Job created: {$job['id']}\n";

Response Format

When you create a job, Hook Mesh returns job metadata:

{
  "id": "job_5nM8pQ1rK3vL9xB",
  "application_id": "app_2Zy3X8qP9rK5mN1vB",
  "event_type": "user.created",
  "status": "created",
  "created_at": "2026-01-20T15:30:00Z",
  "expires_at": "2026-01-22T15:30:00Z",
  "attempt_count": 0,
  "max_retries": 6
}
FieldDescription
idUnique job identifier
statusCurrent state: created, executing, succeeded, awaiting_retry, discarded
expires_atJob discarded if not delivered by this time (48 hours)
max_retriesMaximum retry attempts (6 by default)

Adding Custom Headers

You can include custom headers that will be sent with every webhook delivery. This is useful for:

  • Passing additional context (e.g., tenant ID, environment)
  • Adding correlation IDs for tracing
  • Custom authentication tokens
await fetch('https://api.hookmesh.com/v1/webhook-jobs', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.HOOKMESH_API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    application_id: 'app_abc123',
    event_type: 'invoice.paid',
    payload: {
      invoice_id: 'inv_123',
      amount: 4900,
      currency: 'usd'
    },
    headers: {
      'X-Invoice-ID': 'inv_123',
      'X-Tenant-ID': 'tenant_456',
      'X-Environment': 'production'
    }
  })
});
Note: Custom headers are sent to ALL endpoints receiving this webhook. Don't include endpoint-specific information.

Idempotency with Event IDs

To prevent duplicate webhook sends (e.g., from retries or network issues), use the event_id field. If you send the same event_id within 24 hours, Hook Mesh returns the existing job instead of creating a new one.

// First request
const job1 = await fetch('https://api.hookmesh.com/v1/webhook-jobs', {
  method: 'POST',
  headers: { /* ... */ },
  body: JSON.stringify({
    application_id: 'app_abc123',
    event_type: 'order.shipped',
    event_id: 'ord_456_shipped', // Your unique idempotency key
    payload: {
      order_id: 'ord_456',
      tracking_number: '1Z999AA1234567890'
    }
  })
});
// Creates new job: job_xyz123

// Second request (within 24 hours)
const job2 = await fetch('https://api.hookmesh.com/v1/webhook-jobs', {
  method: 'POST',
  headers: { /* ... */ },
  body: JSON.stringify({
    application_id: 'app_abc123',
    event_type: 'order.shipped',
    event_id: 'ord_456_shipped', // Same idempotency key
    payload: {
      order_id: 'ord_456',
      tracking_number: '1Z999AA1234567890'
    }
  })
});
// Returns existing job: job_xyz123 (no duplicate created)
Best practice: Always use event_id for important events. Use a unique identifier from your system (e.g., database ID, transaction ID) combined with the event type.

Payload Size Limits

Webhook payloads have a maximum size of 256 KB. For most use cases this is plenty, but if you need to send larger data:

❌ Don't send large data directly:

{
  "report_id": "rpt_123",
  "report_data": "...50MB of CSV data..." // Too large!
}

✅ Send a reference + download URL:

{
  "report_id": "rpt_123",
  "download_url": "https://yourapp.com/api/reports/rpt_123/download",
  "expires_at": "2026-01-21T15:30:00Z",
  "size_bytes": 52428800,
  "format": "csv"
}

Checking Job Status

After creating a job, you can check its delivery status:

// Get job details
const response = await fetch(
  `https://api.hookmesh.com/v1/webhook-jobs/${jobId}`,
  {
    headers: {
      'Authorization': `Bearer ${process.env.HOOKMESH_API_KEY}`
    }
  }
);

const job = await response.json();

console.log('Status:', job.status);
console.log('Attempts:', job.attempt_count);

// Check delivery details
if (job.deliveries && job.deliveries.length > 0) {
  job.deliveries.forEach(delivery => {
    console.log(`Endpoint: ${delivery.endpoint_id}`);
    console.log(`Status: ${delivery.status_code}`);
    console.log(`Latency: ${delivery.latency_ms}ms`);
  });
}

Job Lifecycle

1

Created

Job is queued for delivery

2

Executing

Currently being delivered to endpoints

3a

Succeeded

Received 2xx response from all endpoints

3b

Awaiting Retry

Failed, will retry with exponential backoff

3c

Discarded

Max retries exceeded or 48-hour window expired

Error Handling

Always handle errors when creating webhook jobs:

try {
  const response = await fetch('https://api.hookmesh.com/v1/webhook-jobs', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.HOOKMESH_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      application_id: 'app_abc123',
      event_type: 'user.created',
      payload: { /* ... */ }
    })
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`Failed to create job: ${error.error.message}`);
  }

  const job = await response.json();
  console.log('Job created successfully:', job.id);

} catch (error) {
  console.error('Error creating webhook job:', error);

  // Log to your monitoring service
  // Sentry.captureException(error);

  // Decide: retry, queue for later, or alert
}

Common Errors

400 Bad Request

Invalid request format or missing required fields. Check your application_id, event_type, and payload structure.

401 Unauthorized

Invalid or missing API key. Verify your HOOKMESH_API_KEY environment variable.

404 Not Found

Application or event type doesn't exist. Verify the application_id and that the event_type has been created.

429 Rate Limited

Too many requests. Wait before retrying. Check X-RateLimit-Reset header for when to retry.

Best Practices

  • Always use event_id for idempotency
  • Keep payloads under 100 KB for best performance
  • Use descriptive event types (user.created, not createUser)
  • Include timestamps in your payload
  • Handle errors gracefully with retry logic
  • Log job IDs for debugging and correlation
  • Don't send sensitive data without encryption
  • Don't send large files - use download URLs

Next Steps