DocsWebhooks

Webhooks

Webhooks are the recommended way to find out when something happens on your account. Instead of polling our API, we POST a JSON event to a URL you control.

Event types

You can subscribe to any combination of these:

EventTriggered when
checkout.completedA customer successfully paid through a checkout session.
checkout.failedA payment attempt failed (declined card, fraud rule, 3DS abandoned).
payout.sentThe crypto payout was broadcast to the network.
payout.failedThe payout couldn't be completed (network issue, invalid address).
merchant.updatedYour merchant settings changed (wallet address, plan, etc.).

Register a webhook

Add an endpoint from the webhooks dashboard, or via the API:

curl https://api.skiro.io/v1/webhooks \
  -H "Authorization: Bearer $SKIRO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yoursite.com/api/skiro-webhook",
    "events": ["checkout.completed", "checkout.failed"]
  }'

The response includes id and a one-time secret. Save the secret: it's how you verify signatures.

Save the secret immediately
The full secret is shown once at creation. After that, it's hashed and unrecoverable. If you lose it, delete the webhook and make a new one.

Event payload

Every webhook delivery has the same envelope shape:

{
  "id": "evt_8f2a91c...",
  "type": "checkout.completed",
  "created_at": "2026-05-29T14:32:00Z",
  "data": {
    // The full transaction or payout object
  }
}

For checkout.completed, the data field contains the full transaction object: including the metadata you attached when creating the session.

Verifying signatures

Every request includes an X-Skiro-Signature header in the form t=<unix_timestamp>,v1=<hex_signature>. Compute HMAC-SHA256 of `${t}.${raw_body}` with your webhook secret and constant-time compare to v1:

import { createHmac, timingSafeEqual } from 'crypto'

export async function POST(req) {
  const raw = await req.text()
  const sig = req.headers.get('x-skiro-signature') || ''
  const m = sig.match(/t=(\d+),v1=([a-f0-9]+)/)
  if (!m) return new Response('bad signature', { status: 401 })
  const [, t, v1] = m

  const expected = createHmac('sha256', process.env.SKIRO_WEBHOOK_SECRET)
    .update(`${t}.${raw}`)
    .digest('hex')

  const a = Buffer.from(expected)
  const b = Buffer.from(v1)
  const ok = a.length === b.length && timingSafeEqual(a, b)
  if (!ok) return new Response('bad signature', { status: 401 })

  const event = JSON.parse(raw)
  // Process the event
  return new Response('ok')
}

Reject signatures whose timestamp is more than five minutes old to prevent replay attacks.

Retries and delivery

If your endpoint returns a non-2xx status code or doesn't respond within 10 seconds, we retry. The retry schedule is:

  • 1st retry: 30 seconds later
  • 2nd retry: 5 minutes later
  • 3rd retry: 30 minutes later
  • 4th retry: 2 hours later
  • 5th retry: 12 hours later

After 5 failed attempts (about 14 hours total), we mark the delivery as permanently failed. You can replay any delivery from the dashboard.

Best practices

  • Respond quickly. Return 200 immediately, then process the event in the background. Long-running webhook handlers cause retry storms.
  • Use idempotency. The same event may be delivered more than once if your endpoint times out and we retry. Use event.id to deduplicate.
  • Verify signatures. Don't skip this. Anyone can POST to a public URL.
  • Log everything. Keep records of webhook deliveries for at least 30 days. The dashboard has a built-in delivery log.
  • Use HTTPS. We won't deliver to plain HTTP endpoints in live mode.
Last updated: May 30, 2026