Webhooks and Jobs
Verify external events before calling Convex and separate request handlers from durable work.
A webhook route authenticates the external sender. A Convex mutation records the idempotent event and schedules durable product work when needed.
Verify before parsing business data
import { api } from '#convex/api'
import { serverConvex } from '#convex/server'
export default defineEventHandler(async (event) => {
const rawBody = await readRawBody(event)
const signature = getHeader(event, 'provider-signature')
if (!rawBody || !signature || !verifyProviderSignature(rawBody, signature)) {
throw createError({ status: 401, statusText: 'Invalid signature' })
}
const payload = parseProviderEvent(rawBody)
await serverConvex(event, { auth: 'none' }).mutation(api.webhooks.receive, {
providerEventId: payload.id,
type: payload.type,
})
return { accepted: true }
})The signature check is application/provider code. auth: 'none' means the Convex call does not inherit the browser session; it does not authenticate the webhook.
Make the mutation idempotent
Store or index the provider event ID. If the provider retries, return the existing result rather than applying the product change twice.
Separate request acknowledgement from durable work
Nitro request handlers are not durable job queues. For longer work:
- Verify the webhook.
- Commit an idempotent event in a Convex mutation.
- Schedule a Convex function or create a job record.
- Return the provider acknowledgement.
- Expose job progress through a query if users need it.
Retry ownership
- The provider retries delivery according to its policy.
- The Convex mutation enforces idempotency.
- Scheduled work owns its retry state.
- The Nuxt route does not retry an ambiguous write blindly.