Use this pattern when you need bespoke notifications for a specific job while still keeping
account-wide webhooks for most traffic. Requests should be wrapped with fetchWithPayment so
Coinbase x402 challenges succeed automatically.
1. Attach a webhook to an existing job
const baseUrl = process.env.HORIZON_BASE_URL ?? 'https://api.horizon.new/v1';
const jobWebhook = await fetchWithPayment(`${baseUrl}/jobs/${job.jobId}/webhooks`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: 'https://example.com/webhooks/horizon/critical',
events: ['job.completed'],
secret: process.env.HORIZON_WEBHOOK_SECRET,
}),
}).then((res) => res.json());
console.log('job-level webhook registered', jobWebhook.url);
2. Handle synchronous confirmation
if (jobWebhook.events.includes('job.completed')) {
console.log('Watching for completion events at', jobWebhook.url);
}
3. Listen for callbacks
app.post('/webhooks/horizon/critical', async (req, res) => {
const signature = req.header('X-Horizon-Signature') ?? '';
if (!verifyWebhookSignature(signature, req.body)) {
return res.status(401).end();
}
const { jobId, state, result } = req.body;
if (state === 'succeeded') {
console.log('Critical job succeeded', jobId, result);
}
res.status(204).end();
});
4. Choosing between account and job webhooks
- Use account-level webhooks for most automation.
- Add job-level hooks for SLA-sensitive workflows or downstream reconciliations.
- Remember to remove ad-hoc hooks once the job finishes to avoid stale endpoints.