Webhooks
Signed, retryable HTTP POSTs delivered to your endpoint when something happens in your storage app.
Events
| Parameter | Type | Description |
|---|---|---|
upload.completed | event | Fired 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
- 1Open the app's Settings tab.
- 2Set
webhookUrlto your public endpoint. - 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
verifyWebhook
verifyWebhook handles signature check, timestamp freshness, and JSON parse — throws on any failure.
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' })
}
})| Parameter | Type | Description |
|---|---|---|
bodyrequired | string | Buffer | Raw request body, exactly as received. |
signaturerequired | string | X-Sonuslab-Signature header value. |
secretrequired | string | Signing secret from the app settings. |
toleranceSeconds | number | Max age of the signature timestamp. Default 300. |
Retry policy
| Parameter | Type | Description |
|---|---|---|
Attempt 1 | immediate | Initial delivery. |
Attempt 2 | +1s | First retry, 1 second later. |
Attempt 3 | +5s | Second retry, 5 seconds after attempt 2. |
Attempt 4 | +30s | Final 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.