Skip to main content

Dodo Payments

Dodo setup, card payment flow, webhooks, and testing.

Dodo Payments Setup

1. Account and dashboard

  1. Sign up at Dodo Payments and open the Dashboard.
  2. 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=your_dodo_api_key
DODO_WEBHOOK_SECRET=your_webhook_secret
DODO_PRODUCT_ID=your_default_product_id
NEXT_PUBLIC_APP_URL=https://your-domain
  • 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 (or dodo-signature) on POST /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_MAP in create-checkout.
  • NEXT_PUBLIC_APP_URL — Base URL for return_url (e.g. https://your-domain or http://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 ApplicationPRODUCT_MAP['Web Application']
IOSPRODUCT_MAP['IOS']
Android AppPRODUCT_MAP['Android App']
WebsitePRODUCT_MAP['Website']
Other / missingDODO_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? }
  • sessionId required; platform defaults to Website.
  • 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/checkouts with Authorization: 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) with payment.succeeded (or payment_succeeded, payment.completed).
  • Payload includes metadata.session_id and metadata.platform; data.payment_id or data.id as the Dodo payment ID.
  • Scriptonia updates Workflow: paymentMethod: 'dodo', dodoPaymentId, currentStage: 'payment_processing', selectedPlatform from metadata.
  • The user can then go to Execute; POST /api/execute does not require transactionSignature when dodoPaymentId / 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 Workflow exists for sessionId (creates if missing).
  • Updates: currentStage: 'payment_processing', selectedPlatform (default Web Application), paymentDetails: { amountScript: UNIFIED_PRICE_SCRIPT, paymentMethod: 'dodo', dodoPaymentId }.
  • Response: { status: 'success', message } or { status: 'error', error }.
The webhook remains the source of truth when it arrives; confirm-payment is a fallback so the user can proceed to Execute without waiting for the webhook.

5. Execute

  • User opens Execution with the same sessionId (and optionally platform in the URL).
  • POST /api/execute with { sessionId } (no transactionSignature for Dodo).
  • Server loads workflow; sees paymentMethod: 'dodo' and/or dodoPaymentId and skips Solana verifyTransaction. Execution runs as for Solana-paid sessions.

Webhook Configuration

URL

In the Dodo dashboard, set the webhook URL to:
https://your-domain/api/dodo/webhook
For local testing, use a tunnel (e.g. ngrok) and set the webhook 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-signature or dodo-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.timingSafeEqual to compare with the header.
  • Body parsing: bodyParser: false for /api/dodo/webhook so the raw body can be used for HMAC. The same raw string is then JSON.parse’d for type/data/metadata.
If the signature is invalid, the handler returns 401 { received: false, error: 'Invalid signature' }. If DODO_WEBHOOK_SECRET is not set, verification is skipped (do not use in production).

Handled events

EventAction
payment.succeeded, payment_succeeded, payment.completedUpdate Workflow: paymentMethod: 'dodo', dodoPaymentId, currentStage: 'payment_processing', selectedPlatform from metadata.
payment.failed, payment_failedLog; optionally update workflow.
refund.succeeded, refund_succeededIf metadata.session_id present: set paymentMethod: null, dodoPaymentId: null on that Workflow.
OthersLog as unhandled.
The handler always returns 200 (or 401 on bad signature) so Dodo does not retry on “success”. On processing errors it still returns 200 and { 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.succeeded and any errors.
    • DB: Workflow row for sessionId has paymentMethod: 'dodo', dodoPaymentId, selectedPlatform.

2. Create checkout

curl -X POST http://localhost:3000/api/dodo/create-checkout \
  -H "Content-Type: application/json" \
  -d '{"sessionId":"test-session-1","platform":"Web Application"}'
Expect 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:
curl -X POST http://localhost:3000/api/dodo/confirm-payment \
  -H "Content-Type: application/json" \
  -d '{"sessionId":"test-session-1","platform":"Web Application","paymentId":"pay_xxx"}'
Then call Execute with 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

  1. Idea → Intent → Platform (choose any; Dodo product mapping may vary).
  2. On Payment, choose “Buy with Card” (or equivalent) → create-checkout → redirect to Dodo.
  3. Pay with a test card.
  4. Redirect to return_url (/payment/success?sessionId=...&platform=...). Optionally call confirm-payment from the success page.
  5. When the webhook has run (or after confirm-payment), go to Execute and run. Execution should complete without transactionSignature.

5. Webhook signature

To test verification, send a POST with a wrong x-dodo-signature; the handler should return 401. With DODO_WEBHOOK_SECRET unset, it skips verification and returns 200.