Skip to content

Webhook Delivery

Reliable webhook delivery with exponential backoff and selective retry.

ts
class Http4xxError extends Error {
  constructor(public status: number) {
    super(`HTTP ${status}`)
  }
}

const deliverWebhookTask = taskora.task("deliver-webhook", {
  retry: {
    attempts: 8,
    backoff: "exponential",
    delay: 1000,
    maxDelay: 3600_000, // cap at 1 hour
    noRetryOn: [Http4xxError], // don't retry client errors
  },
  timeout: 30_000,
  handler: async (data: { url: string; payload: unknown; secret: string }, ctx) => {
    const body = JSON.stringify(data.payload)
    const signature = createHmac("sha256", data.secret).update(body).digest("hex")

    const res = await fetch(data.url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Webhook-Signature": signature,
        "X-Delivery-Id": ctx.id,
        "X-Delivery-Attempt": String(ctx.attempt),
      },
      body,
      signal: ctx.signal,
    })

    if (res.status >= 400 && res.status < 500) {
      throw new Http4xxError(res.status) // permanent failure
    }

    if (!res.ok) {
      throw new Error(`HTTP ${res.status}`) // retryable
    }

    return { status: res.status, deliveredAt: Date.now() }
  },
})

// Dispatch
deliverWebhookTask.dispatch({
  url: "https://api.partner.com/webhooks",
  payload: { event: "order.created", data: { id: "123" } },
  secret: process.env.WEBHOOK_SECRET,
})

// Monitor DLQ for permanently failed deliveries
const failures = await taskora.deadLetters.list({ task: "deliver-webhook" })

Retry schedule with exponential backoff: 1s → 2s → 4s → 8s → 16s → 32s → 64s → capped at 1h. Total window: ~2 minutes before giving up.

Released under the MIT License.