SonusLab StorageSonusLab Storage

Webhooks

Signed, retryable HTTP POSTs delivered to your endpoint when something happens in your storage app.

Events

ParameterTypeDescription
upload.completedeventFired after a browser-finalized upload (the /complete call). Server-proxied uploads do NOT emit this event.

Subscribe defensively — ignore unknown event types so new events don't break your handler.

Setup

  1. 1Open the app's Settings tab.
  2. 2Set webhookUrl to your public endpoint.
  3. 3Click Rotate secret to generate the signing secret. Copy it now — it's shown once.

Signature format

Each request carries an X-Sonuslab-Signature header. The format is Stripe-style:

X-Sonuslab-Signature: t=1718632456,v1=8b7c4f...

The signature is an HMAC-SHA256 over `${t}.${rawBody}` using your secret as the key.

Verify against the raw body

JSON parsing changes whitespace, which breaks HMAC verification. Capture the raw request body before parsing.

verifyWebhook

verifyWebhook handles signature check, timestamp freshness, and JSON parse — throws on any failure.

server/api/webhooks/storage.post.ts
import { verifyWebhook } from 'sonuslab-storage/webhook'

export default defineEventHandler(async (event) => {
  const body = await readRawBody(event)
  const signature = getHeader(event, 'x-sonuslab-signature')!
  const secret = process.env.SONUSLAB_WEBHOOK_SECRET!

  try {
    const payload = verifyWebhook({ body: body!, signature, secret })

    switch (payload.event) {
      case 'upload.completed':
        await db.files.markUploaded(payload.data.file.id)
        break
      default:
        // unknown event type — log + continue
    }

    return { ok: true }
  } catch {
    throw createError({ statusCode: 401, statusMessage: 'Invalid signature' })
  }
})
ParameterTypeDescription
bodyrequiredstring | BufferRaw request body, exactly as received.
signaturerequiredstringX-Sonuslab-Signature header value.
secretrequiredstringSigning secret from the app settings.
toleranceSecondsnumberMax age of the signature timestamp. Default 300.

Retry policy

ParameterTypeDescription
Attempt 1immediateInitial delivery.
Attempt 2+1sFirst retry, 1 second later.
Attempt 3+5sSecond retry, 5 seconds after attempt 2.
Attempt 4+30sFinal retry, 30 seconds after attempt 3.

Any 2xx response counts as success. Non-2xx (or timeout > 10s) triggers the next attempt. After the final attempt fails, the delivery is marked failed in the dashboard.

Idempotency

Retries mean your handler may receive the same event twice. Dedupe on the X-Sonuslab-Delivery header — a UUID unique per delivery attempt-set.

const deliveryId = getHeader(event, 'x-sonuslab-delivery')!

const seen = await db.webhookDeliveries.has(deliveryId)
if (seen) return { ok: true, dedup: true }
await db.webhookDeliveries.add(deliveryId)

// ...process event...

Manual retry

The app's Webhooks tab lists every delivery with status, response code, and a Retry button. Use it after fixing a bug in your handler.