Webhooks

Event yang
sampe — selalu

HMAC-SHA256 signed, multi-secret rotation, 8 retry attempts selama 31 jam dengan jitter. SSRF defense built-in. Plus delivery log searchable + manual replay.

12 Event types
8x Retry attempts
~31h Retry window
HMAC-SHA256 Signing
Stripe-style signing

Signature yang reviewable

Format yang sudah jadi industry standard. Timestamp masuk signed payload — replay attack mati di pintu.

Multi-secret rotation tanpa downtime

Punya dua atau lebih secret yang aktif bareng. Rotate kapan pun tanpa risiko outage — verifier cocokin signature dengan salah satu secret yang valid.

  • HMAC-SHA256 over ${timestamp}.${rawBody}
  • Timestamp di-include dalam signed payload — replay-safe
  • Constant-time compare via SDK helper — tidak ada timing attack
  • Reject timestamp skew lebih dari 5 menit
signature.txt
# Header format (Stripe-style)
X-Kirim-Signature: t=1717012345,v1=<hex>,v1=<hex>
X-Kirim-Event: message.received
X-Kirim-Event-Id: evt_01JFK...
X-Kirim-Delivery-Id: dlv_01JFK...
X-Kirim-Attempt: 1
X-Kirim-Source: kirimdev

# Verification:
# 1. Parse t (unix timestamp) + all v1 signatures
# 2. Compute HMAC-SHA256 of `${t}.${rawBody}` with each secret
# 3. Any match → valid (constant-time compare)
# 4. Reject if t skew > 5 minutes

SDK helper untuk verify

Tidak perlu re-implement HMAC compare sendiri. verifyWebhookSignature dari @kirimdev/sdk/webhooks handle parsing, constant-time compare, dan timestamp validation.

Bekerja sama di Hono, Express, Next.js Route Handlers, atau plain Web fetch handler.

webhook.ts
import { Hono } from 'hono'
import { verifyWebhookSignature }
  from '@kirimdev/sdk/webhooks'

const app = new Hono()

app.post('/webhook', async (c) => {
  await verifyWebhookSignature({
    rawBody: await c.req.text(),
    signatureHeader: c.req.header('x-kirim-signature'),
    secrets: [
      process.env.WEBHOOK_SECRET_CURRENT!,
      process.env.WEBHOOK_SECRET_PREVIOUS!  // rotation
    ]
  })

  const event = c.req.header('x-kirim-event')
  const payload = await c.req.json()

  if (event === 'message.received') {
    await handleIncoming(payload)
  }

  return c.json({ ok: true })
})
Events

12 event types

Subscribe yang Anda butuhkan — masing-masing subscription bisa pilih event set sendiri.

EVENT message.received
EVENT message.status
EVENT conversation.assigned
EVENT conversation.closed
EVENT contact.created
EVENT contact.updated
EVENT customer.created
EVENT customer.updated
EVENT customer.archived
EVENT customer.onboarded
EVENT customer.setup_link.created
EVENT customer.setup_link.consumed
Reliability

Delivery yang tidak menyerah

Network hiccup? Endpoint Anda lagi maintenance? Tidak masalah. 8 attempts selama ~31 jam dengan exponential backoff + jitter.

Exponential backoff

10s → 30s → 2m → 10m → 1h → 6h → 24h, dengan ±20% jitter. Retryable: 408, 429, 5xx, network error.

Delivery log + replay

Setiap delivery attempt di-log. Cari, filter, dan replay manual via GET /v1/webhook_deliveries atau dashboard.

SSRF defense 2-layer

API boundary reject loopback, private IP, DNS rebinding tricks. Worker re-resolve di delivery time.

No redirect follow

3xx tidak di-follow — mencegah signature leak ke endpoint yang tidak terduga.

Multi-secret support

Generate secret baru, deploy, lalu deactivate lama. Tidak ada window outage.

Auto-disable

Subscription yang fail terus-menerus akan di-mark inactive — tidak burn quota di-resource yang sudah dead.

Subscribe via API atau dashboard

Buat subscription via SDK, REST API, atau dashboard UI. Per-org subscriptions di-scope ke events yang Anda pilih. Update events tanpa drop subscription.

subscribe.ts
const sub = await kirim.webhookSubscriptions.create({
  url: 'https://api.acme.com/kirim-webhook',
  events: [
    'message.received',
    'message.status',
    'conversation.assigned'
  ]
})

// Multi-secret untuk rotation
await kirim.webhookSubscriptions.addSecret(sub.id)

Webhook yang tidak perlu di-monitor

Set once, forget. Delivery log selalu ada kalau perlu debug.