Skip to main content
Account-level webhook provisioning is asynchronous. Use fetchWithPayment so the initial provisioning request can replay the 402 challenge automatically.

1. Create the webhook

const baseUrl = process.env.HORIZON_BASE_URL ?? 'https://api.horizon.new/v1';

const createResponse = await fetchWithPayment(`${baseUrl}/webhooks`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    url: 'https://example.com/webhooks/horizon',
    events: ['job.completed', 'job.failed'],
    secret: process.env.HORIZON_WEBHOOK_SECRET,
  }),
});

const provisioning = await createResponse.json();
console.log('provisioning job', provisioning.jobId, provisioning.statusUrl);

2. Handle synchronous completion

if (provisioning.status === 'completed' && provisioning.result) {
  console.log('Webhook ready', provisioning.result.webhookId);
}

3. Poll until active

const status = await fetchWithPayment(provisioning.statusUrl).then((res) => res.json());

if (status.state === 'processing') {
  await new Promise((resolve) => setTimeout(resolve, 2000));
}

if (status.state !== 'succeeded') {
  throw new Error(`Webhook provisioning failed: ${status.error?.code ?? 'unknown'}`);
}

console.log('webhook id', status.result.webhookId);

4. Rotate or delete webhooks

await fetchWithPayment(`${baseUrl}/webhooks/${status.result.webhookId}`, {
  method: 'DELETE',
});
  • Use dedicated secrets per environment to avoid leaking production callbacks.
  • Retry provisioning jobs if you ever rotate the receiving URL or secret.