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.