Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Guide for test-mode payment scenarios, renewal simulation, local webhook delivery tests, test and live catalog migration, and production launch checks.
.claude/skills/hashgraph-online-testing-and-go-live/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 72% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 59% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 6% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 128% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 116% | 0% |
This skill covers test mode, test payment methods, webhook testing, and the production launch checklist. Use it when building payment flows that need verification before going live, or when preparing to switch from test to production.
Test vs. live mode: Dodo Payments maintains two completely separate environments. Each has its own base URL, API keys, webhooks, products, customers, and transactions. Data does not carry over between them.
https://test.dodopayments.comhttps://live.dodopayments.comEach environment requires separate API keys issued from the dashboard. The SDK defaults to live_mode if you don't specify environment, which is a real footgun in development.
Test payment methods: Dodo documents specific card numbers, VPAs, and payment method scenarios for testing. These only work in test mode.
Webhook testing: The CLI provides commands to listen for webhooks locally and trigger test events without making real payments.
Always explicitly set environment: 'test_mode' during development. The default is live_mode.
typescriptimport DodoPayments from 'dodopayments'; const client = new DodoPayments({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, environment: 'test_mode', // REQUIRED — defaults to 'live_mode' });
pythonfrom dodopayments import DodoPayments client = DodoPayments( bearer_token=os.environ.get("DODO_PAYMENTS_API_KEY"), environment="test_mode", # defaults to "live_mode" )
goclient := dodopayments.NewClient( option.WithBearerToken("dodo_test_..."), option.WithEnvironmentTestMode(), // defaults to WithEnvironmentLiveMode() )
Get your test API key from the Dodo dashboard. Test keys are prefixed dodo_test_.
Use these card numbers in test mode. The expiry can be any future date; the CVC can be any three digits.
| Scenario | Card Number | Result | |---|---|---| | Success | 4242 4242 4242 4242 | Payment succeeds | | Decline | 4000 0000 0000 0002 | Payment declined | | Decline (insufficient funds) | 4000 0000 0000 9995 | Insufficient funds error | | Decline (lost card) | 4000 0000 0000 9987 | Lost card error |
To test subscription renewal failures, use this card:
| Card Number | Behavior | |---|---| | 4000 0000 0000 0069 | Renewal fails on next billing date |
Test UPI success and failure with these VPAs:
| VPA | Result | |---|---| | success@okhdfcbank | Payment succeeds | | failure@okhdfcbank | Payment fails |
BNPL, Apple Pay, Google Pay, and European payment methods (Giropay, iDEAL, Bancontact) are available in test mode. Exact test scenarios vary by method. Refer to the Testing Process documentation for the current test matrix.
To force a subscription to renew on a specific date without waiting, update the next_billing_date:
typescriptawait client.subscriptions.update('sub_123', { next_billing_date: '2026-05-03T00:00:00Z', });
pythonclient.subscriptions.update( 'sub_123', next_billing_date='2026-05-03T00:00:00Z' )
go// NextBillingDate is param.Field[time.Time], not a string. Passing a string // literal compiles to param.Field[string] and will not type-check. client.Subscriptions.Update(ctx, "sub_123", dodopayments.SubscriptionUpdateParams{ NextBillingDate: dodopayments.F(time.Date(2026, time.May, 3, 0, 0, 0, 0, time.UTC)), })
This immediately schedules the renewal. If you used the subscription-renewal-failure card, the failed charge emits payment.failed and the affected subscription enters on_hold, which emits subscription.on_hold.
The CLI ships separately from the SDKs:
bashnpm install -g dodopayments-cli dodo login
dodo login opens the API Keys page, accepts a pasted key, and asks whether it is Test Mode or Live Mode. One key of each can stay authenticated at the same time.
bashdodo wh listen http://localhost:3000/webhook
This forwards real test-mode events to your local URL over an outbound relay. It is not a tunnel: nothing listens publicly and no URL is printed for you to paste into the dashboard. If you need a registerable HTTPS URL, use ngrok instead — that flow is described in the webhook-integration skill.
Two constraints to know before running it:
dodo wh listen only works inside the interactive TUI as /wh listen, which opens a wizard.Events arriving this way carry valid signatures, so verify them normally with webhooks.unwrap().
bashdodo wh trigger payment.success http://localhost:3000/webhook
Sends a realistic mock payload for a chosen event. It runs offline and works even while logged out — which is the tell for the part that matters: these payloads are unsigned. unwrap() rejects them because there is no valid signature to verify.
Use unsafeUnwrap() for triggered events only, and never on an endpoint that also receives real traffic. Event names are <category>.<event>, for example payment.success, subscription.active, or dispute.opened.
As with listen, both arguments are required in direct mode; /wh trigger inside the TUI opens a wizard instead.
Covered in the webhook-integration skill. Verify every webhook that arrives over the network, in test mode as well as live. The single exception is the unsigned payloads produced by dodo wh trigger above.
Products are environment-specific. You cannot reuse a test product ID in production. Instead, recreate the product in live mode or use the dashboard to export and reimport.
Manual approach:
Programmatic approach:
Fetch the product from test mode, extract its configuration, and create it in live mode:
typescript// In test mode const testClient = new DodoPayments({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, environment: 'test_mode', }); const testProduct = await testClient.products.retrieve('pdt_test_123'); // In live mode const liveClient = new DodoPayments({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, environment: 'live_mode', }); const liveProduct = await liveClient.products.create({ name: testProduct.name, price: testProduct.price, tax_category: testProduct.tax_category, // ... other fields });
Before switching to production, verify each item:
dodo_live_).environment: 'live_mode' (or omits it, since live is the default).client.webhooks.unwrap() or the standardwebhooks package.environment unsetThe SDK defaults to live_mode. If you forget to set environment: 'test_mode' during development, your test code will hit production and create real charges.
typescript// WRONG — this hits live mode const client = new DodoPayments({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, }); // CORRECT const client = new DodoPayments({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, environment: 'test_mode', });
Test and live environments are completely separate. A product ID from test mode does not exist in live mode. Always create new products in live mode or use the dashboard to migrate them.
typescript// WRONG — pdt_test_123 does not exist in live mode const session = await liveClient.checkoutSessions.create({ product_cart: [{ product_id: 'pdt_test_123', quantity: 1 }], }); // CORRECT — use a live product ID const session = await liveClient.checkoutSessions.create({ product_cart: [{ product_id: 'pdt_live_456', quantity: 1 }], });
Test and live modes have separate webhook endpoints and secrets. If you copy your test webhook configuration to production without updating the URL and secret, webhooks will fail or go to the wrong place.
DODO_PAYMENTS_WEBHOOK_KEY in production environment variables.Always verify webhook signatures using client.webhooks.unwrap() or the standardwebhooks package. Never trust a webhook without verification, even if it came from Dodo.
typescript// WRONG — no verification app.post('/webhook', (req, res) => { const event = req.body; handleEvent(event); res.json({ received: true }); }); // CORRECT app.post('/webhook', async (req, res) => { try { const unwrapped = client.webhooks.unwrap(req.body.toString(), { headers: { 'webhook-id': req.headers['webhook-id'] as string, 'webhook-signature': req.headers['webhook-signature'] as string, 'webhook-timestamp': req.headers['webhook-timestamp'] as string, }, }); handleEvent(unwrapped); res.json({ received: true }); } catch (error) { res.status(401).json({ error: 'Invalid signature' }); } });
Test keys (prefixed dodo_test_) only work with https://test.dodopayments.com. Live keys are issued per mode from the dashboard. Using a test key with live mode will fail with an authentication error.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 23,816 | 17,616 | -26% | 1 | 1 | 0% | 3,344 | 5,755 | +72% | 0 | 0 | — |
case-02 | fail→pass | 26,438 | 12,697 | -52% | 1 | 1 | 0% | 3,484 | 5,534 | +59% | 0 | 0 | — |
case-03 | fail→pass | 30,953 | 12,751 | -59% | 1 | 1 | 0% | 4,778 | 5,088 | +6% | 0 | 0 | — |
case-04 | fail→pass | 10,844 | 11,381 | +5% | 1 | 1 | 0% | 1,899 | 4,336 | +128% | 0 | 0 | — |
case-05 | pass→pass | 9,118 | 7,924 | -13% | 1 | 1 | 0% | 1,651 | 3,564 | +116% | 0 | 0 | — |
case-06 | fail→pass | 16,089 | 4,571 | -72% | 1 | 1 | 0% | 1,880 | 4,059 | +116% | 0 | 0 | — |
case-07 | pass→pass | 10,056 | 5,647 | -44% | 1 | 1 | 0% | 1,922 | 3,933 | +105% | 0 | 0 | — |
case-08 | fail→pass | 14,930 | 3,554 | -76% | 1 | 1 | 0% | 1,618 | 3,612 | +123% | 0 | 0 | — |
case-09 | fail→fail | 16,811 | 4,065 | -76% | 1 | 1 | 0% | 1,734 | 3,835 | +121% | 0 | 0 | — |
case-10 | fail→pass | 16,987 | 4,939 | -71% | 1 | 1 | 0% | 1,946 | 3,815 | +96% | 0 | 0 | — |
case-11 | fail→pass | 16,789 | 10,182 | -39% | 1 | 1 | 0% | 2,293 | 4,011 | +75% | 0 | 0 | — |
case-12 | pass→pass | 11,886 | 10,761 | -9% | 1 | 1 | 0% | 1,139 | 4,025 | +253% | 0 | 0 | — |
case-13 | pass→pass | 16,706 | 19,453 | +16% | 1 | 1 | 0% | 2,368 | 5,376 | +127% | 0 | 0 | — |
case-14 | pass→pass | 15,856 | 4,703 | -70% | 1 | 1 | 0% | 1,956 | 3,856 | +97% | 0 | 0 | — |
case-15 | pass→pass | 11,463 | 15,120 | +32% | 1 | 1 | 0% | 2,012 | 4,697 | +133% | 0 | 0 | — |
case-16 | fail→fail | 16,280 | 7,830 | -52% | 1 | 1 | 0% | 2,129 | 3,732 | +75% | 0 | 0 | — |
case-17 | pass→pass | 15,267 | 14,335 | -6% | 1 | 1 | 0% | 2,176 | 4,405 | +102% | 0 | 0 | — |
case-18 | pass→pass | 11,468 | 4,541 | -60% | 1 | 1 | 0% | 1,740 | 3,828 | +120% | 0 | 0 | — |
case-19 | fail→pass | 11,787 | 10,246 | -13% | 1 | 1 | 0% | 1,955 | 4,752 | +143% | 0 | 0 | — |
case-20 | pass→pass | 19,196 | 17,683 | -8% | 1 | 1 | 0% | 2,779 | 6,866 | +147% | 0 | 0 | — |
case-21 | fail→fail | 16,600 | 9,101 | -45% | 1 | 1 | 0% | 2,744 | 4,888 | +78% | 0 | 0 | — |
case-22 | fail→fail | 26,627 | 14,489 | -46% | 1 | 1 | 0% | 3,203 | 6,147 | +92% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +41 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.