Skip to main content

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

server/api/webhooks/provider.post.ts
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:

  1. Verify the webhook.
  2. Commit an idempotent event in a Convex mutation.
  3. Schedule a Convex function or create a job record.
  4. Return the provider acknowledgement.
  5. 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.