@dodopayments/opencode-plugin 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.mcp.json +4 -2
- package/README.md +92 -12
- package/opencode-plugin/README.md +22 -3
- package/opencode-plugin/index.js +19 -12
- package/package.json +1 -1
- package/skills/best-practices/SKILL.md +335 -179
- package/skills/better-auth-integration/SKILL.md +214 -0
- package/skills/billing-sdk/SKILL.md +357 -370
- package/skills/checkout-integration/SKILL.md +580 -329
- package/skills/credit-based-billing/SKILL.md +448 -775
- package/skills/customer-management/SKILL.md +281 -0
- package/skills/discounts-and-promotions/SKILL.md +464 -0
- package/skills/framework-adapters/SKILL.md +644 -0
- package/skills/license-keys/SKILL.md +324 -199
- package/skills/localized-pricing/SKILL.md +230 -0
- package/skills/mobile-checkout/SKILL.md +521 -0
- package/skills/product-catalog-management/SKILL.md +311 -0
- package/skills/refunds-and-disputes/SKILL.md +333 -0
- package/skills/subscription-integration/SKILL.md +323 -404
- package/skills/testing-and-go-live/SKILL.md +335 -0
- package/skills/usage-based-billing/SKILL.md +339 -479
- package/skills/webhook-integration/SKILL.md +600 -402
|
@@ -1,43 +1,65 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dodo-best-practices
|
|
3
|
-
description:
|
|
3
|
+
description: Guide for initial Dodo Payments setup, including SDK installation, test and live environments, API keys, and the canonical checkout-to-webhook architecture.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Dodo Payments Integration Guide
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
This skill covers the foundational concepts and setup for Dodo Payments. Use it when starting a new integration, setting up the SDK, or understanding the core payment flow.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
## When to use this skill
|
|
11
|
+
|
|
12
|
+
- You're building a new payment integration and need to understand Dodo's architecture
|
|
13
|
+
- You need to install and initialize the SDK in your language
|
|
14
|
+
- You want to understand the canonical payment flow and webhook verification
|
|
15
|
+
- You're choosing between framework adapters or payment methods
|
|
16
|
+
- You need to know what Dodo handles versus what you must build
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## What is Dodo Payments
|
|
21
|
+
|
|
22
|
+
Dodo Payments is a Merchant of Record (MoR). That means Dodo is the legal seller on every transaction, handles sales tax registration and calculation across all jurisdictions, remits taxes to authorities, and manages card-network disputes and chargebacks. As a developer, you don't build a sales-tax engine or dispute-handling system. You create checkout sessions, listen to webhooks, and grant access when payment succeeds.
|
|
11
23
|
|
|
12
24
|
---
|
|
13
25
|
|
|
14
|
-
##
|
|
26
|
+
## Environment Setup
|
|
27
|
+
|
|
28
|
+
### Base URLs
|
|
29
|
+
|
|
30
|
+
Only two real base URLs exist:
|
|
31
|
+
|
|
32
|
+
- **Live**: `https://live.dodopayments.com`
|
|
33
|
+
- **Test**: `https://test.dodopayments.com`
|
|
34
|
+
|
|
35
|
+
Never use `api.dodopayments.com` — it has no DNS record and cannot be reached.
|
|
36
|
+
|
|
37
|
+
### API Keys
|
|
38
|
+
|
|
39
|
+
Keys have two formats:
|
|
40
|
+
|
|
41
|
+
- **Test**: `dodo_test_...`
|
|
42
|
+
- **Live**: `dodo_live_...`
|
|
15
43
|
|
|
16
44
|
### Environment Variables
|
|
17
|
-
- `DODO_PAYMENTS_API_KEY` - Your API key from the dashboard
|
|
18
|
-
- `DODO_PAYMENTS_WEBHOOK_SECRET` - Webhook signing secret for verification
|
|
19
45
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
46
|
+
```bash
|
|
47
|
+
DODO_PAYMENTS_API_KEY # Bearer token for API requests
|
|
48
|
+
DODO_PAYMENTS_WEBHOOK_KEY # Secret for webhook signature verification
|
|
49
|
+
```
|
|
23
50
|
|
|
24
|
-
###
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
- **Webhooks**: Dashboard → Developer → Webhooks
|
|
28
|
-
- **Products**: Dashboard → Products
|
|
51
|
+
### Default Environment
|
|
52
|
+
|
|
53
|
+
The `environment` parameter defaults to `live_mode` if omitted. Always pass `test_mode` explicitly during development.
|
|
29
54
|
|
|
30
55
|
---
|
|
31
56
|
|
|
32
|
-
## SDK Installation
|
|
57
|
+
## SDK Installation & Initialization
|
|
58
|
+
|
|
59
|
+
### TypeScript/Node.js
|
|
33
60
|
|
|
34
|
-
### TypeScript/JavaScript
|
|
35
61
|
```bash
|
|
36
62
|
npm install dodopayments
|
|
37
|
-
# or
|
|
38
|
-
yarn add dodopayments
|
|
39
|
-
# or
|
|
40
|
-
pnpm add dodopayments
|
|
41
63
|
```
|
|
42
64
|
|
|
43
65
|
```typescript
|
|
@@ -45,271 +67,405 @@ import DodoPayments from 'dodopayments';
|
|
|
45
67
|
|
|
46
68
|
const client = new DodoPayments({
|
|
47
69
|
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
|
|
48
|
-
environment: '
|
|
70
|
+
environment: 'test_mode', // defaults to 'live_mode'
|
|
49
71
|
});
|
|
50
72
|
```
|
|
51
73
|
|
|
74
|
+
Constructor options:
|
|
75
|
+
|
|
76
|
+
| Option | Type | Default | Notes |
|
|
77
|
+
|---|---|---|---|
|
|
78
|
+
| `bearerToken` | string | env `DODO_PAYMENTS_API_KEY` | Required for API calls |
|
|
79
|
+
| `environment` | `'test_mode' \| 'live_mode'` | `'live_mode'` | Explicit in dev |
|
|
80
|
+
| `webhookKey` | string | env `DODO_PAYMENTS_WEBHOOK_KEY` | For `webhooks.unwrap()` |
|
|
81
|
+
| `baseURL` | string | — | Override; mutually exclusive with `environment` |
|
|
82
|
+
|
|
52
83
|
### Python
|
|
84
|
+
|
|
53
85
|
```bash
|
|
54
86
|
pip install dodopayments
|
|
55
87
|
```
|
|
56
88
|
|
|
57
89
|
```python
|
|
90
|
+
import os
|
|
58
91
|
from dodopayments import DodoPayments
|
|
59
92
|
|
|
60
|
-
client = DodoPayments(
|
|
93
|
+
client = DodoPayments(
|
|
94
|
+
bearer_token=os.environ.get("DODO_PAYMENTS_API_KEY"),
|
|
95
|
+
environment="test_mode", # defaults to "live_mode"
|
|
96
|
+
)
|
|
61
97
|
```
|
|
62
98
|
|
|
63
99
|
### Go
|
|
100
|
+
|
|
64
101
|
```bash
|
|
65
|
-
go get github.com/dodopayments/dodopayments-go
|
|
102
|
+
go get -u github.com/dodopayments/dodopayments-go@v1.110.0
|
|
66
103
|
```
|
|
67
104
|
|
|
68
105
|
```go
|
|
69
|
-
import
|
|
106
|
+
import (
|
|
107
|
+
"os"
|
|
108
|
+
|
|
109
|
+
"github.com/dodopayments/dodopayments-go"
|
|
110
|
+
"github.com/dodopayments/dodopayments-go/option"
|
|
111
|
+
)
|
|
70
112
|
|
|
71
113
|
client := dodopayments.NewClient(
|
|
72
114
|
option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")),
|
|
115
|
+
option.WithEnvironmentTestMode(), // defaults to live
|
|
73
116
|
)
|
|
74
117
|
```
|
|
75
118
|
|
|
76
119
|
### PHP
|
|
120
|
+
|
|
77
121
|
```bash
|
|
78
|
-
composer require dodopayments/client
|
|
122
|
+
composer require "dodopayments/client:6.19.0"
|
|
79
123
|
```
|
|
80
124
|
|
|
81
125
|
```php
|
|
82
126
|
use Dodopayments\Client;
|
|
83
127
|
|
|
84
|
-
$client = new Client(
|
|
128
|
+
$client = new Client(
|
|
129
|
+
bearerToken: getenv('DODO_PAYMENTS_API_KEY') ?: 'My Bearer Token',
|
|
130
|
+
environment: 'test_mode',
|
|
131
|
+
);
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Ruby
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
gem "dodopayments", "~> 2.22.0"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
dodo_payments = Dodopayments::Client.new(
|
|
142
|
+
bearer_token: ENV["DODO_PAYMENTS_API_KEY"],
|
|
143
|
+
environment: "test_mode"
|
|
144
|
+
)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Java
|
|
148
|
+
|
|
149
|
+
```xml
|
|
150
|
+
<dependency>
|
|
151
|
+
<groupId>com.dodopayments.api</groupId>
|
|
152
|
+
<artifactId>dodo-payments-java</artifactId>
|
|
153
|
+
<version>1.110.0</version>
|
|
154
|
+
</dependency>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
```java
|
|
158
|
+
DodoPaymentsClient client = DodoPaymentsOkHttpClient.fromEnv();
|
|
159
|
+
// Reads DODO_PAYMENTS_API_KEY, DODO_PAYMENTS_WEBHOOK_KEY, DODO_PAYMENTS_BASE_URL
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Kotlin
|
|
163
|
+
|
|
164
|
+
```xml
|
|
165
|
+
<dependency>
|
|
166
|
+
<groupId>com.dodopayments.api</groupId>
|
|
167
|
+
<artifactId>dodo-payments-kotlin</artifactId>
|
|
168
|
+
<version>1.110.0</version>
|
|
169
|
+
</dependency>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
```kotlin
|
|
173
|
+
val client: DodoPaymentsClient = DodoPaymentsOkHttpClient.fromEnv()
|
|
85
174
|
```
|
|
86
175
|
|
|
87
176
|
---
|
|
88
177
|
|
|
178
|
+
## Framework Adapters
|
|
179
|
+
|
|
180
|
+
Dodo publishes framework-specific packages under `@dodopayments/*`. Use the one matching your stack:
|
|
181
|
+
|
|
182
|
+
| Framework | Package | Use if |
|
|
183
|
+
|---|---|---|
|
|
184
|
+
| Next.js | `@dodopayments/nextjs` | Building with Next.js App Router |
|
|
185
|
+
| Nuxt | `@dodopayments/nuxt` | Building with Nuxt 3+ |
|
|
186
|
+
| Express | `@dodopayments/express` | Using Express.js |
|
|
187
|
+
| Fastify | `@dodopayments/fastify` | Using Fastify |
|
|
188
|
+
| Hono | `@dodopayments/hono` | Using Hono (Edge/Node) |
|
|
189
|
+
| Astro | `@dodopayments/astro` | Using Astro endpoints |
|
|
190
|
+
| SvelteKit | `@dodopayments/sveltekit` | Using SvelteKit server routes |
|
|
191
|
+
| Remix | `@dodopayments/remix` | Using Remix loaders/actions |
|
|
192
|
+
| TanStack Start | `@dodopayments/tanstack` | Using TanStack Start |
|
|
193
|
+
| Better Auth | `@dodopayments/better-auth` | Integrating with Better Auth |
|
|
194
|
+
| Convex | `@dodopayments/convex` | Using Convex backend |
|
|
195
|
+
| Bun | `@dodopayments/bun` | Using Bun.serve() |
|
|
196
|
+
|
|
197
|
+
Each adapter provides checkout, customer portal, and webhook handlers tailored to the framework's conventions.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
89
201
|
## Core Concepts
|
|
90
202
|
|
|
91
203
|
### Products
|
|
92
|
-
Products are the items you sell. Create them in the dashboard or via API:
|
|
93
|
-
- **One-time**: Single purchase products
|
|
94
|
-
- **Subscription**: Recurring billing products
|
|
95
|
-
- **Usage-based**: Metered billing per consumption
|
|
96
204
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
- **
|
|
100
|
-
- **
|
|
101
|
-
-
|
|
102
|
-
|
|
205
|
+
Items you sell. Create in the dashboard or via API. Types:
|
|
206
|
+
|
|
207
|
+
- **One-time**: Single purchase
|
|
208
|
+
- **Subscription**: Recurring billing
|
|
209
|
+
- **Usage-based**: Metered consumption
|
|
210
|
+
|
|
211
|
+
### Customers
|
|
212
|
+
|
|
213
|
+
Represent buyers. Can have multiple payment methods, subscriptions, and credit balances. Create explicitly or implicitly during checkout.
|
|
214
|
+
|
|
103
215
|
### Checkout Sessions
|
|
104
|
-
The primary way to collect payments. Create a checkout session and redirect customers:
|
|
105
216
|
|
|
106
|
-
|
|
107
|
-
const session = await client.checkoutSessions.create({
|
|
108
|
-
product_cart: [
|
|
109
|
-
{ product_id: 'prod_xxxxx', quantity: 1 }
|
|
110
|
-
],
|
|
111
|
-
customer: {
|
|
112
|
-
email: 'customer@example.com',
|
|
113
|
-
name: 'John Doe',
|
|
114
|
-
},
|
|
115
|
-
return_url: 'https://yoursite.com/success',
|
|
116
|
-
});
|
|
217
|
+
The primary payment collection method. Create a session server-side, redirect the customer to the hosted checkout URL, and listen for webhooks to confirm payment.
|
|
117
218
|
|
|
118
|
-
|
|
119
|
-
|
|
219
|
+
See the `checkout-integration` skill for detailed checkout configuration and the `subscription-integration` skill for recurring lifecycle management.
|
|
220
|
+
|
|
221
|
+
### Subscriptions
|
|
222
|
+
|
|
223
|
+
Recurring charges on a schedule. Managed through checkout sessions or the subscriptions API. See the `subscription-integration` skill for lifecycle, trials, plan changes, and on-demand charging.
|
|
120
224
|
|
|
121
225
|
### Webhooks
|
|
122
|
-
Listen to events for real-time updates:
|
|
123
|
-
- `payment.succeeded` - Payment completed
|
|
124
|
-
- `payment.failed` - Payment failed
|
|
125
|
-
- `subscription.active` - Subscription activated
|
|
126
|
-
- `subscription.cancelled` - Subscription cancelled
|
|
127
|
-
- `refund.succeeded` - Refund processed
|
|
128
|
-
- `dispute.opened` - Dispute received
|
|
129
|
-
- `license_key.created` - License key generated
|
|
130
|
-
- `credit.added` - Credits granted to customer
|
|
131
|
-
- `credit.deducted` - Credits consumed
|
|
132
|
-
- `credit.balance_low` - Credit balance below threshold
|
|
133
|
-
---
|
|
134
226
|
|
|
135
|
-
|
|
227
|
+
Real-time event notifications. Dodo sends events like `payment.succeeded`, `subscription.active`, `refund.succeeded`, and `credit.deducted`. Webhook signature verification is covered in the `webhook-integration` skill.
|
|
136
228
|
|
|
137
|
-
###
|
|
229
|
+
### Credit Entitlements
|
|
138
230
|
|
|
139
|
-
|
|
140
|
-
2. Create checkout session with product ID
|
|
141
|
-
3. Redirect customer to checkout URL
|
|
142
|
-
4. Handle `payment.succeeded` webhook
|
|
143
|
-
5. Fulfill order / grant access
|
|
231
|
+
Virtual balances (API calls, tokens, compute hours) attached to products. Configured per product with rollover, overage, and expiration rules. See the `credit-based-billing` skill.
|
|
144
232
|
|
|
145
|
-
|
|
146
|
-
// Create checkout for one-time payment
|
|
147
|
-
const session = await client.checkoutSessions.create({
|
|
148
|
-
product_cart: [{ product_id: 'prod_one_time_product', quantity: 1 }],
|
|
149
|
-
customer: { email: 'customer@example.com' },
|
|
150
|
-
return_url: 'https://yoursite.com/success',
|
|
151
|
-
});
|
|
152
|
-
```
|
|
233
|
+
---
|
|
153
234
|
|
|
154
|
-
|
|
235
|
+
## The Canonical Payment Flow
|
|
155
236
|
|
|
156
|
-
1. Create
|
|
157
|
-
2.
|
|
158
|
-
3.
|
|
159
|
-
4.
|
|
237
|
+
1. **Create checkout session** on your server with product ID and customer email.
|
|
238
|
+
2. **Redirect customer** to the `checkout_url` returned.
|
|
239
|
+
3. **Customer pays** on the hosted checkout.
|
|
240
|
+
4. **Dodo sends webhook** (e.g., `payment.succeeded`) to your endpoint.
|
|
241
|
+
5. **Verify the webhook signature** using `client.webhooks.unwrap()`.
|
|
242
|
+
6. **Grant access** only after webhook verification succeeds.
|
|
243
|
+
|
|
244
|
+
Never grant access based on the browser `return_url` redirect alone. The webhook is the authoritative confirmation.
|
|
160
245
|
|
|
161
246
|
```typescript
|
|
162
|
-
|
|
247
|
+
import express from 'express';
|
|
248
|
+
|
|
249
|
+
const app = express();
|
|
250
|
+
|
|
251
|
+
// 1. Create session
|
|
163
252
|
const session = await client.checkoutSessions.create({
|
|
164
|
-
product_cart: [{ product_id: '
|
|
165
|
-
subscription_data: { trial_period_days: 14 }, // Optional trial
|
|
253
|
+
product_cart: [{ product_id: 'pdt_example', quantity: 1 }],
|
|
166
254
|
customer: { email: 'customer@example.com' },
|
|
167
255
|
return_url: 'https://yoursite.com/success',
|
|
168
256
|
});
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
### Webhook Verification
|
|
172
257
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
258
|
+
// 2. Redirect to session.checkout_url
|
|
259
|
+
|
|
260
|
+
// 3. Listen for webhook with the exact raw request bytes
|
|
261
|
+
app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
|
|
262
|
+
try {
|
|
263
|
+
// 4. Verify signature
|
|
264
|
+
const webhookId = req.headers['webhook-id'] as string;
|
|
265
|
+
const event = client.webhooks.unwrap(req.body.toString(), {
|
|
266
|
+
headers: {
|
|
267
|
+
'webhook-id': webhookId,
|
|
268
|
+
'webhook-signature': req.headers['webhook-signature'] as string,
|
|
269
|
+
'webhook-timestamp': req.headers['webhook-timestamp'] as string,
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// 5. Suppress duplicates with an atomic unique insert and run side effects
|
|
274
|
+
// in the same database transaction.
|
|
275
|
+
const handled = await processWebhookOnce(webhookId, async () => {
|
|
276
|
+
if (event.type === 'payment.succeeded') {
|
|
277
|
+
// ONE-TIME purchases only. Subscription access starts on subscription.active.
|
|
278
|
+
const payment = event.data;
|
|
279
|
+
await grantOneTimeAccess(payment.customer.customer_id);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (event.type === 'subscription.active') {
|
|
283
|
+
await grantSubscriptionAccess(event.data);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
res.json({ received: true, duplicate: !handled });
|
|
288
|
+
} catch (error) {
|
|
289
|
+
res.status(401).json({ error: 'Invalid signature' });
|
|
290
|
+
}
|
|
291
|
+
});
|
|
189
292
|
```
|
|
190
293
|
|
|
191
294
|
---
|
|
192
295
|
|
|
193
|
-
## API
|
|
296
|
+
## API Foundations
|
|
194
297
|
|
|
195
|
-
###
|
|
196
|
-
1. Navigate to Dashboard → Developer → API
|
|
197
|
-
2. Click "Create API Key"
|
|
198
|
-
3. Copy and securely store the key
|
|
298
|
+
### Authentication
|
|
199
299
|
|
|
200
|
-
|
|
201
|
-
- Never expose API keys in client-side code
|
|
202
|
-
- Use environment variables
|
|
203
|
-
- Rotate keys periodically
|
|
204
|
-
- Use test mode keys for development
|
|
300
|
+
All requests use Bearer token authentication:
|
|
205
301
|
|
|
206
|
-
|
|
302
|
+
```http
|
|
303
|
+
Authorization: Bearer dodo_live_...
|
|
304
|
+
```
|
|
207
305
|
|
|
208
|
-
|
|
306
|
+
### Pagination
|
|
209
307
|
|
|
210
|
-
|
|
308
|
+
List endpoints are page-numbered. They accept `page_size` and `page_number`, and the response
|
|
309
|
+
exposes the rows on `items`:
|
|
211
310
|
|
|
212
311
|
```typescript
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
312
|
+
const payments = await client.payments.list({
|
|
313
|
+
page_size: 50,
|
|
314
|
+
page_number: 0,
|
|
216
315
|
});
|
|
217
316
|
|
|
218
|
-
|
|
317
|
+
for (const payment of payments.items) {
|
|
318
|
+
console.log(payment.payment_id);
|
|
319
|
+
}
|
|
219
320
|
```
|
|
220
321
|
|
|
221
|
-
|
|
322
|
+
The SDK can also walk every page for you:
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
for await (const payment of client.payments.list()) {
|
|
326
|
+
console.log(payment.payment_id);
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Rate Limits
|
|
331
|
+
|
|
332
|
+
Dodo enforces rate limits. The SDK automatically retries `429` responses as described below.
|
|
222
333
|
|
|
223
|
-
|
|
334
|
+
### SDK Error Classes
|
|
224
335
|
|
|
225
|
-
|
|
336
|
+
The SDK throws typed errors. Catch and inspect:
|
|
226
337
|
|
|
227
338
|
```typescript
|
|
228
339
|
try {
|
|
229
|
-
|
|
340
|
+
await client.checkoutSessions.create({...});
|
|
230
341
|
} catch (error) {
|
|
231
|
-
if (error
|
|
232
|
-
|
|
233
|
-
} else if (error.status === 401) {
|
|
234
|
-
// Invalid API key
|
|
235
|
-
} else if (error.status === 429) {
|
|
236
|
-
// Rate limited - implement backoff
|
|
342
|
+
if (error instanceof DodoPayments.APIError) {
|
|
343
|
+
console.error(error.status, error.message);
|
|
237
344
|
}
|
|
238
345
|
}
|
|
239
346
|
```
|
|
240
347
|
|
|
348
|
+
### Retries
|
|
349
|
+
|
|
350
|
+
The SDK retries twice by default with a short exponential backoff on connection errors and `408`, `409`, `429`, and `5xx` responses. Do not add an unconditional custom retry loop.
|
|
351
|
+
|
|
352
|
+
Override the default for all requests when constructing the client:
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
const clientWithoutRetries = new DodoPayments({
|
|
356
|
+
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
|
|
357
|
+
environment: 'test_mode',
|
|
358
|
+
maxRetries: 0,
|
|
359
|
+
});
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Or override it for one request:
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
await client.checkoutSessions.create(
|
|
366
|
+
{
|
|
367
|
+
product_cart: [{ product_id: 'pdt_example', quantity: 1 }],
|
|
368
|
+
customer: { email: 'customer@example.com' },
|
|
369
|
+
},
|
|
370
|
+
{ maxRetries: 0 },
|
|
371
|
+
);
|
|
372
|
+
```
|
|
373
|
+
|
|
241
374
|
---
|
|
242
375
|
|
|
243
|
-
##
|
|
376
|
+
## Webhook Verification
|
|
244
377
|
|
|
245
|
-
|
|
246
|
-
- Use test API keys (start with `sk_test_`)
|
|
247
|
-
- Test webhooks with dashboard tools
|
|
248
|
-
- Use test card numbers:
|
|
249
|
-
- `4242 4242 4242 4242` - Success
|
|
250
|
-
- `4000 0000 0000 0002` - Decline
|
|
378
|
+
Webhook signature verification is mandatory. Never trust the payload without verification.
|
|
251
379
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
380
|
+
Dodo implements the Standard Webhooks spec. The signed message is `webhook-id.webhook-timestamp.raw_body` (period-joined), HMAC-SHA256, base64-encoded.
|
|
381
|
+
|
|
382
|
+
**Use the SDK helper:**
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
const event = client.webhooks.unwrap(req.body.toString(), {
|
|
386
|
+
headers: {
|
|
387
|
+
'webhook-id': req.headers['webhook-id'] as string,
|
|
388
|
+
'webhook-signature': req.headers['webhook-signature'] as string,
|
|
389
|
+
'webhook-timestamp': req.headers['webhook-timestamp'] as string,
|
|
390
|
+
},
|
|
391
|
+
});
|
|
256
392
|
```
|
|
257
|
-
|
|
393
|
+
|
|
394
|
+
The `unwrap()` method verifies the signature and parses the payload. If verification fails, it throws an error.
|
|
395
|
+
|
|
396
|
+
For detailed webhook setup, event types, and testing, see the `webhook-integration` skill.
|
|
258
397
|
|
|
259
398
|
---
|
|
260
399
|
|
|
261
|
-
##
|
|
400
|
+
## Common Mistakes
|
|
262
401
|
|
|
263
|
-
###
|
|
264
|
-
|
|
402
|
+
### 1. Granting access on `return_url` redirect
|
|
403
|
+
|
|
404
|
+
The browser redirect is not proof of payment. Always wait for the webhook.
|
|
265
405
|
|
|
266
406
|
```typescript
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
407
|
+
// WRONG
|
|
408
|
+
app.get('/success', (req, res) => {
|
|
409
|
+
grantAccess(req.query.customer_id); // No verification!
|
|
410
|
+
});
|
|
270
411
|
|
|
271
|
-
|
|
272
|
-
|
|
412
|
+
// CORRECT
|
|
413
|
+
app.post('/webhook', async (req, res) => {
|
|
414
|
+
const event = client.webhooks.unwrap(...);
|
|
415
|
+
if (event.type === 'payment.succeeded') {
|
|
416
|
+
grantAccess(event.data.customer.customer_id);
|
|
417
|
+
}
|
|
273
418
|
});
|
|
419
|
+
```
|
|
274
420
|
|
|
275
|
-
|
|
276
|
-
const { productId, email } = await req.json();
|
|
277
|
-
|
|
278
|
-
const session = await client.checkoutSessions.create({
|
|
279
|
-
product_cart: [{ product_id: productId, quantity: 1 }],
|
|
280
|
-
customer: { email },
|
|
281
|
-
return_url: `${process.env.NEXT_PUBLIC_URL}/success`,
|
|
282
|
-
});
|
|
421
|
+
### 2. Hand-rolling webhook verification
|
|
283
422
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
423
|
+
Don't implement HMAC verification yourself. Use `client.webhooks.unwrap()`.
|
|
424
|
+
|
|
425
|
+
The old approach of signing just the payload is wrong because Standard Webhooks signs `webhook-id.webhook-timestamp.raw_body`. Hand-rolled HMAC will never match.
|
|
426
|
+
|
|
427
|
+
### 3. Re-serializing the request body
|
|
428
|
+
|
|
429
|
+
Webhook verification requires the exact raw body. If you parse JSON and re-stringify it, the signature breaks.
|
|
287
430
|
|
|
288
|
-
### Express.js
|
|
289
431
|
```typescript
|
|
290
|
-
|
|
291
|
-
|
|
432
|
+
// WRONG
|
|
433
|
+
const body = JSON.parse(req.body);
|
|
434
|
+
const event = client.webhooks.unwrap(JSON.stringify(body), {...});
|
|
292
435
|
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
app.post('/create-checkout', async (req, res) => {
|
|
297
|
-
const session = await client.checkoutSessions.create({
|
|
298
|
-
product_cart: [{ product_id: req.body.productId, quantity: 1 }],
|
|
299
|
-
customer: { email: req.body.email },
|
|
300
|
-
return_url: 'https://yoursite.com/success',
|
|
301
|
-
});
|
|
302
|
-
res.json({ url: session.checkout_url });
|
|
303
|
-
});
|
|
436
|
+
// CORRECT
|
|
437
|
+
const event = client.webhooks.unwrap(req.body.toString(), {...});
|
|
304
438
|
```
|
|
305
439
|
|
|
440
|
+
### 4. Using deprecated APIs
|
|
441
|
+
|
|
442
|
+
Don't use `client.payments.create()` or `client.subscriptions.create()` for new integrations. Both are deprecated. Use `client.checkoutSessions.create()`.
|
|
443
|
+
|
|
444
|
+
### 5. Forgetting to set `environment: 'test_mode'`
|
|
445
|
+
|
|
446
|
+
The default is `live_mode`. Always pass `test_mode` explicitly during development to avoid charging real cards.
|
|
447
|
+
|
|
448
|
+
### 6. Storing API keys in code
|
|
449
|
+
|
|
450
|
+
Never hardcode keys. Always use environment variables.
|
|
451
|
+
|
|
452
|
+
### 7. Ignoring the `webhook-timestamp` header
|
|
453
|
+
|
|
454
|
+
The timestamp prevents replay attacks. `client.webhooks.unwrap()` validates it automatically, but if you hand-roll verification, check that the timestamp is recent (within a few minutes).
|
|
455
|
+
|
|
456
|
+
### 8. Using `unsafeUnwrap()` in production
|
|
457
|
+
|
|
458
|
+
`unsafeUnwrap()` skips signature verification. Use it only for unsigned test payloads from `dodo wh trigger`. Never use it for production webhooks.
|
|
459
|
+
|
|
306
460
|
---
|
|
307
461
|
|
|
308
462
|
## Resources
|
|
309
463
|
|
|
310
|
-
- [
|
|
464
|
+
- [Dodo Payments Docs](https://docs.dodopayments.com)
|
|
311
465
|
- [API Reference](https://docs.dodopayments.com/api-reference/introduction)
|
|
312
466
|
- [SDK Repositories](https://github.com/dodopayments)
|
|
313
|
-
- [
|
|
314
|
-
- [
|
|
467
|
+
- [Checkout Sessions Guide](https://docs.dodopayments.com/developer-resources/checkout-session)
|
|
468
|
+
- [Webhook Guide](https://docs.dodopayments.com/developer-resources/webhooks)
|
|
469
|
+
- [Customer Portal](https://docs.dodopayments.com/features/customer-portal)
|
|
470
|
+
- [Subscription Integration](https://docs.dodopayments.com/developer-resources/subscription-integration-guide)
|
|
315
471
|
- [Credit-Based Billing](https://docs.dodopayments.com/features/credit-based-billing)
|