Skip to main content
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.