Webhooks
The Webhooks API lets you register HTTPS endpoints that receive real-time event notifications from Horizon. When an agent completes a skill, encounters an error, or triggers another subscribed event, Horizon sends an HTTP POST request to your configured URL with a JSON payload describing the event.
Supported Events
Section titled “Supported Events”| Event | Description |
|---|---|
skill.completed | A skill execution finished successfully. |
skill.failed | A skill execution failed after all retries. |
agent.error | An agent-level error occurred (e.g., missing credentials, configuration issue). |
agent.status_changed | An agent’s status transitioned (e.g., from active to paused). |
conversation.created | A new conversation was initiated. |
scheduled_job.completed | A scheduled job execution completed. |
scheduled_job.failed | A scheduled job execution failed. |
You can subscribe to one or more events per webhook.
Create a Webhook
Section titled “Create a Webhook”/api/agent-webhooks Register a new webhook endpoint to receive event notifications.
Requires authentication via x-api-key header.
Request Body
| Parameter | Type | Description |
|---|---|---|
| agent_id required | string | The agent whose events this webhook listens to. |
| url required | string | The HTTPS endpoint URL that will receive webhook payloads. |
| events required | string[] | An array of event types to subscribe to (e.g., ['skill.completed', 'agent.error']). |
| secret | string | A shared secret used to generate HMAC-SHA256 signatures for payload verification. If omitted, Horizon generates one automatically. |
Request
Section titled “Request”curl -X POST https://api.horizonplatform.ai/api/agent-webhooks \ -H "x-api-key: hz_live_abc123def456" \ -H "Content-Type: application/json" \ -d '{ "agent_id": "agent_001", "url": "https://example.com/hooks/horizon", "events": ["skill.completed", "skill.failed", "agent.error"], "secret": "whsec_mysecretkey123" }'const response = await fetch( 'https://api.horizonplatform.ai/api/agent-webhooks', { method: 'POST', headers: { 'x-api-key': 'hz_live_abc123def456', 'Content-Type': 'application/json', }, body: JSON.stringify({ agent_id: 'agent_001', url: 'https://example.com/hooks/horizon', events: ['skill.completed', 'skill.failed', 'agent.error'], secret: 'whsec_mysecretkey123', }), });
const webhook = await response.json();console.log(webhook.id); // e.g., "wh_a1b2c3d4"import requests
response = requests.post( 'https://api.horizonplatform.ai/api/agent-webhooks', headers={ 'x-api-key': 'hz_live_abc123def456', 'Content-Type': 'application/json', }, json={ 'agent_id': 'agent_001', 'url': 'https://example.com/hooks/horizon', 'events': ['skill.completed', 'skill.failed', 'agent.error'], 'secret': 'whsec_mysecretkey123', })
webhook = response.json()print(webhook['id'])Response
Section titled “Response”// 201 Created{ "id": "wh_a1b2c3d4", "agent_id": "agent_001", "url": "https://example.com/hooks/horizon", "events": ["skill.completed", "skill.failed", "agent.error"], "secret": "whsec_mysecretkey123", "active": true, "created_at": "2026-03-18T16:00:00Z", "updated_at": "2026-03-18T16:00:00Z"}List Webhooks
Section titled “List Webhooks”/api/agent-webhooks List all webhooks for the authenticated organization.
Requires authentication via x-api-key header.
Request
Section titled “Request”curl -X GET https://api.horizonplatform.ai/api/agent-webhooks \ -H "x-api-key: hz_live_abc123def456"const response = await fetch( 'https://api.horizonplatform.ai/api/agent-webhooks', { headers: { 'x-api-key': 'hz_live_abc123def456' } });
const webhooks = await response.json();import requests
response = requests.get( 'https://api.horizonplatform.ai/api/agent-webhooks', headers={'x-api-key': 'hz_live_abc123def456'})
webhooks = response.json()Get a Webhook
Section titled “Get a Webhook”/api/agent-webhooks/:webhookId Retrieve details of a single webhook by its identifier.
Requires authentication via x-api-key header.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
| webhookId required | string | The webhook identifier. |
Request
Section titled “Request”curl -X GET https://api.horizonplatform.ai/api/agent-webhooks/wh_a1b2c3d4 \ -H "x-api-key: hz_live_abc123def456"const response = await fetch( 'https://api.horizonplatform.ai/api/agent-webhooks/wh_a1b2c3d4', { headers: { 'x-api-key': 'hz_live_abc123def456' } });
const webhook = await response.json();import requests
response = requests.get( 'https://api.horizonplatform.ai/api/agent-webhooks/wh_a1b2c3d4', headers={'x-api-key': 'hz_live_abc123def456'})
webhook = response.json()Update a Webhook
Section titled “Update a Webhook”/api/agent-webhooks/:webhookId Update a webhook's URL, subscribed events, or secret.
Requires authentication via x-api-key header.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
| webhookId required | string | The webhook identifier. |
Request Body
| Parameter | Type | Description |
|---|---|---|
| url | string | Updated HTTPS endpoint URL. |
| events | string[] | Updated list of subscribed event types. |
| secret | string | Updated shared secret for signature verification. |
| active | boolean | Enable or disable the webhook without deleting it. |
Request
Section titled “Request”curl -X PUT https://api.horizonplatform.ai/api/agent-webhooks/wh_a1b2c3d4 \ -H "x-api-key: hz_live_abc123def456" \ -H "Content-Type: application/json" \ -d '{ "events": ["skill.completed", "skill.failed", "agent.error", "scheduled_job.failed"], "active": true }'const response = await fetch( 'https://api.horizonplatform.ai/api/agent-webhooks/wh_a1b2c3d4', { method: 'PUT', headers: { 'x-api-key': 'hz_live_abc123def456', 'Content-Type': 'application/json', }, body: JSON.stringify({ events: ['skill.completed', 'skill.failed', 'agent.error', 'scheduled_job.failed'], active: true, }), });
const updated = await response.json();import requests
response = requests.put( 'https://api.horizonplatform.ai/api/agent-webhooks/wh_a1b2c3d4', headers={ 'x-api-key': 'hz_live_abc123def456', 'Content-Type': 'application/json', }, json={ 'events': ['skill.completed', 'skill.failed', 'agent.error', 'scheduled_job.failed'], 'active': True, })
updated = response.json()Delete a Webhook
Section titled “Delete a Webhook”/api/agent-webhooks/:webhookId Permanently remove a webhook. No further events will be delivered to its URL.
Requires authentication via x-api-key header.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
| webhookId required | string | The webhook identifier. |
Request
Section titled “Request”curl -X DELETE https://api.horizonplatform.ai/api/agent-webhooks/wh_a1b2c3d4 \ -H "x-api-key: hz_live_abc123def456"await fetch( 'https://api.horizonplatform.ai/api/agent-webhooks/wh_a1b2c3d4', { method: 'DELETE', headers: { 'x-api-key': 'hz_live_abc123def456' }, });// 204 No Content on successimport requests
response = requests.delete( 'https://api.horizonplatform.ai/api/agent-webhooks/wh_a1b2c3d4', headers={'x-api-key': 'hz_live_abc123def456'})# 204 No Content on successWebhook Payload Format
Section titled “Webhook Payload Format”When an event fires, Horizon sends an HTTP POST to your configured URL with the following structure:
{ "id": "evt_x9y8z7w6", "event": "skill.completed", "timestamp": "2026-03-18T16:45:00Z", "agent_id": "agent_001", "data": { "job_id": "job_9f8e7d6c", "skill_category": "quickbooks", "skill_name": "profit-and-loss-report", "skill_version": "v1.0", "conversation_id": "conv_8f3a2b1c", "duration_ms": 4230, "result_summary": "P&L report generated for Q1 2026" }}The request includes the following headers:
| Header | Description |
|---|---|
Content-Type | application/json |
x-horizon-signature | HMAC-SHA256 hex digest of the raw request body, signed with your webhook secret. |
x-horizon-event | The event type (e.g., skill.completed). |
x-horizon-delivery | A unique delivery ID for idempotency tracking. |
Verifying Webhook Signatures
Section titled “Verifying Webhook Signatures”To verify that a webhook payload was sent by Horizon and has not been tampered with, compute an HMAC-SHA256 digest of the raw request body using your webhook secret, then compare it with the x-horizon-signature header value.
import crypto from 'node:crypto';
function verifyWebhookSignature(rawBody, signature, secret) { const expected = crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex');
return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) );}
// In your Express handler:app.post('/hooks/horizon', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-horizon-signature']; const isValid = verifyWebhookSignature(req.body, signature, 'whsec_mysecretkey123');
if (!isValid) { return res.status(401).send('Invalid signature'); }
const event = JSON.parse(req.body); console.log('Received event:', event.event); res.status(200).send('OK');});import hmacimport hashlibfrom flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = 'whsec_mysecretkey123'
@app.route('/hooks/horizon', methods=['POST'])def handle_webhook(): signature = request.headers.get('x-horizon-signature', '') raw_body = request.get_data()
expected = hmac.new( WEBHOOK_SECRET.encode(), raw_body, hashlib.sha256 ).hexdigest()
if not hmac.compare_digest(signature, expected): abort(401, 'Invalid signature')
event = request.get_json() print(f"Received event: {event['event']}") return 'OK', 200Delivery and Retries
Section titled “Delivery and Retries”Horizon expects your endpoint to respond with a 2xx status code within 10 seconds. If the delivery fails, Horizon retries up to 5 times with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 30 seconds |
| 2nd retry | 2 minutes |
| 3rd retry | 10 minutes |
| 4th retry | 1 hour |
| 5th retry | 6 hours |
After all retries are exhausted, the webhook is marked as failing. If 10 consecutive deliveries fail, the webhook is automatically deactivated (active: false). You can reactivate it via the Update endpoint after resolving the issue.
Error Responses
Section titled “Error Responses”| Status | Error | Description |
|---|---|---|
400 | validation_error | Invalid request body, non-HTTPS URL, or unsupported event type. |
401 | authentication_required | Missing or invalid API key. |
403 | insufficient_scope | API key lacks the required scope. |
404 | not_found | The specified webhook does not exist. |
429 | rate_limit_exceeded | API key rate limit exceeded. |