Dodo Payments
Dodo setup, card payment flow, webhooks, and testing.Dodo Payments Setup
1. Account and dashboard
- Sign up at Dodo Payments and open the Dashboard.
- Create products and set prices (e.g. one product per platform: Website, Web Application, Android, iOS) or use a single product and pass metadata for platform.
2. Environment variables
- DODO_API_KEY — Used for Create Checkout and any other server-side Dodo API calls. Required for
POST /api/dodo/create-checkout. - DODO_WEBHOOK_SECRET — Used to verify
x-dodo-signature(ordodo-signature) onPOST /api/dodo/webhook. If unset, verification is skipped (unsafe in production). - DODO_PRODUCT_ID — Default product when the selected platform is not in the internal
PRODUCT_MAPincreate-checkout. - NEXT_PUBLIC_APP_URL — Base URL for
return_url(e.g.https://your-domainorhttp://localhost:3000). Must match how users reach the app.
3. Product mapping (create-checkout)
/api/dodo/create-checkout maps platform to a Dodo product:
platform (request) | Product id source |
|---|---|
Web Application | PRODUCT_MAP['Web Application'] |
IOS | PRODUCT_MAP['IOS'] |
Android App | PRODUCT_MAP['Android App'] |
Website | PRODUCT_MAP['Website'] |
| Other / missing | DODO_PRODUCT_ID |
PRODUCT_MAP is defined in code; adjust it to match your Dodo product IDs.
Card Payment Flow
1. Create checkout
POST/api/dodo/create-checkout
Body: { sessionId, platform? }
sessionIdrequired;platformdefaults toWebsite.- Builds Dodo payload:
product_cart: [{ product_id, quantity: 1 }]return_url:{NEXT_PUBLIC_APP_URL}/payment/success?sessionId=...&platform=...metadata:{ session_id: sessionId, platform }customization.redirect_immediately: true
- Calls
https://live.dodopayments.com/checkoutswithAuthorization: Bearer DODO_API_KEY. - Response:
{ status: 'success', checkout_url, payment_link }(both same URL). On error:{ status: 'error', error, debug? }.
2. User pays
- Frontend redirects the user to
checkout_url. - User completes card/UPI on Dodo’s page.
- Dodo redirects to
return_url(success or failure).
3. Webhook (payment.succeeded)
- Dodo sends POST to your Webhook URL (e.g.
https://your-domain/api/dodo/webhook) withpayment.succeeded(orpayment_succeeded,payment.completed). - Payload includes
metadata.session_idandmetadata.platform;data.payment_idordata.idas the Dodo payment ID. - Scriptonia updates
Workflow:paymentMethod: 'dodo',dodoPaymentId,currentStage: 'payment_processing',selectedPlatformfrom metadata. - The user can then go to Execute;
POST /api/executedoes not requiretransactionSignaturewhendodoPaymentId/paymentMethod: 'dodo'are set.
4. Confirm payment (optional)
POST/api/dodo/confirm-payment
Body: { sessionId, platform?, paymentId? }
- Used when the client needs to persist payment context (e.g. after redirect from
return_url) before the webhook has run or if the webhook is delayed. - Ensures a
Workflowexists forsessionId(creates if missing). - Updates:
currentStage: 'payment_processing',selectedPlatform(defaultWeb Application),paymentDetails: { amountScript: UNIFIED_PRICE_SCRIPT, paymentMethod: 'dodo', dodoPaymentId }. - Response:
{ status: 'success', message }or{ status: 'error', error }.
5. Execute
- User opens Execution with the same
sessionId(and optionallyplatformin the URL). - POST
/api/executewith{ sessionId }(notransactionSignaturefor Dodo). - Server loads workflow; sees
paymentMethod: 'dodo'and/ordodoPaymentIdand skips SolanaverifyTransaction. Execution runs as for Solana-paid sessions.
Webhook Configuration
URL
In the Dodo dashboard, set the webhook URL to:https://your-ngrok-url/api/dodo/webhook. NEXT_PUBLIC_APP_URL for return_url can stay http://localhost:3000 if users are redirected back to localhost; for Dodo’s server-to-server webhook, the URL must be the public tunnel.
Signature verification
- Header:
x-dodo-signatureordodo-signature(single string; arrays are rejected). - Secret:
DODO_WEBHOOK_SECRET. - Algorithm: HMAC-SHA256 of the raw body (bytes as received), hex-encoded. The handler uses
crypto.timingSafeEqualto compare with the header. - Body parsing:
bodyParser: falsefor/api/dodo/webhookso the raw body can be used for HMAC. The same raw string is thenJSON.parse’d fortype/data/metadata.
{ received: false, error: 'Invalid signature' }. If DODO_WEBHOOK_SECRET is not set, verification is skipped (do not use in production).
Handled events
| Event | Action |
|---|---|
payment.succeeded, payment_succeeded, payment.completed | 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 present: set paymentMethod: null, dodoPaymentId: null on that Workflow. |
| Others | Log as unhandled. |
{ received: true, error } to avoid retries for malformed payloads.
Testing
1. Local webhook
- Run a tunnel:
ngrok http 3000(or your dev port). - In Dodo, set webhook URL to
https://<ngrok-host>/api/dodo/webhook. - Use Dodo test cards (see Dodo docs) and trigger a payment. Check:
- Your server logs for
Dodo webhook received: payment.succeededand any errors. - DB:
Workflowrow forsessionIdhaspaymentMethod: 'dodo',dodoPaymentId,selectedPlatform.
- Your server logs for
2. Create checkout
checkout_url in the response. Open it in a browser and pay with a test card.
3. Confirm payment (without webhook)
If the webhook is not yet configured or is delayed:sessionId. The workflow must have refinedRequirements (from Intent) for execution to succeed; if the session was created only for payment tests, you may need to run Idea → Intent → Platform first.
4. End-to-end
- Idea → Intent → Platform (choose any; Dodo product mapping may vary).
- On Payment, choose “Buy with Card” (or equivalent) →
create-checkout→ redirect to Dodo. - Pay with a test card.
- Redirect to
return_url(/payment/success?sessionId=...&platform=...). Optionally callconfirm-paymentfrom the success page. - When the webhook has run (or after
confirm-payment), go to Execute and run. Execution should complete withouttransactionSignature.
5. Webhook signature
To test verification, send a POST with a wrongx-dodo-signature; the handler should return 401. With DODO_WEBHOOK_SECRET unset, it skips verification and returns 200.