Webhook Overview
Scriptonia receives Dodo webhooks atPOST /api/dodo/webhook to update workflow state when a payment succeeds, fails, or is refunded. Other webhooks (e.g. GitHub) are outside this doc.
Dodo Webhook
Endpoint
- Body parsing: Disabled (
api.bodyParser: false) so the raw body can be used for signature verification.
Event Types
Handled inpages/api/dodo/webhook.ts:
| Event | Alias | Action |
|---|---|---|
payment.succeeded | payment_succeeded, payment.completed | Read data or root; metadata.session_id, metadata.platform; data.payment_id or data.id. Update Workflow: paymentMethod: 'dodo', dodoPaymentId, currentStage: 'payment_processing', selectedPlatform (from metadata). |
payment.failed | payment_failed | Log; optionally update workflow. |
refund.succeeded | refund_succeeded | If metadata.session_id, set paymentMethod: null, dodoPaymentId: null on Workflow. |
| (other) | — | Log as unhandled; still return 200. |
Security
Signature verification
- Header:
x-dodo-signatureordodo-signature(single string; if array, treated as invalid). - Secret:
DODO_WEBHOOK_SECRET. If unset, verification is skipped (insecure in production). - Algorithm: HMAC-SHA256 of the raw body (UTF-8), hex-encoded; compared with
crypto.timingSafeEqualto the header value. - On invalid signature: 401
{ received: false, error: "Invalid signature" }.
Raw body
- The handler reads the raw body from
req(stream) because Next.js does not parse it whenbodyParser: false. The same bytes must be used for HMAC and forJSON.parse.
Retry Logic
- Server behavior: The handler always returns 200 (or 401 on bad signature) so Dodo does not retry on “success.”
- On processing errors (e.g. DB update fails): the handler still returns 200 and
{ received: true, error: message }to avoid retries for malformed or unprocessable payloads. Log errors for debugging. - Dodo: Configure retries and backoff in the Dodo dashboard. No custom retry logic is implemented in Scriptonia.
Examples
payment.succeeded (conceptual)
prisma.workflow.update({ where: { sessionId }, data: { paymentMethod: 'dodo', dodoPaymentId, currentStage: 'payment_processing', selectedPlatform } }).
Refund
prisma.workflow.update({ where: { sessionId }, data: { paymentMethod: null, dodoPaymentId: null } }).