@xenterprises/fastify-xstripe 1.1.0 → 1.2.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/LICENSE +57 -12
- package/README.md +108 -253
- package/index.d.ts +78 -184
- package/package.json +3 -4
- package/src/webhooks/webhooks.js +10 -10
- package/src/xStripe.js +28 -15
- package/.dockerignore +0 -62
- package/.env.example +0 -116
- package/.gitlab-ci.yml +0 -45
- package/API.md +0 -574
- package/CHANGELOG.md +0 -96
- package/EXAMPLES.md +0 -883
- package/MIGRATION.md +0 -374
- package/QUICK_START.md +0 -179
- package/SECURITY.md +0 -465
- package/TESTING.md +0 -357
- package/server/app.js +0 -557
- package/test/handlers.test.js +0 -959
- package/test/xStripe.integration.test.js +0 -409
package/LICENSE
CHANGED
|
@@ -1,15 +1,60 @@
|
|
|
1
|
-
|
|
1
|
+
PROPRIETARY SOFTWARE LICENSE
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2024
|
|
3
|
+
Copyright (c) 2024-2026 X Enterprises LLC. All Rights Reserved.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
This software and associated documentation files (the "Software") are the
|
|
6
|
+
exclusive property of X Enterprises LLC, a Washington limited liability
|
|
7
|
+
company.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
TERMS AND CONDITIONS
|
|
10
|
+
|
|
11
|
+
1. OWNERSHIP
|
|
12
|
+
All rights, title, and interest in and to the Software, including all
|
|
13
|
+
intellectual property rights, are and shall remain the exclusive property
|
|
14
|
+
of X Enterprises LLC.
|
|
15
|
+
|
|
16
|
+
2. RESTRICTIONS
|
|
17
|
+
Without the prior written consent of X Enterprises LLC, you may not:
|
|
18
|
+
- Copy, modify, or distribute the Software
|
|
19
|
+
- Reverse engineer, decompile, or disassemble the Software
|
|
20
|
+
- Sublicense, sell, lease, or otherwise transfer the Software
|
|
21
|
+
- Remove or alter any proprietary notices or labels
|
|
22
|
+
|
|
23
|
+
3. AUTHORIZED USE
|
|
24
|
+
Use of this Software is limited to authorized employees, contractors, and
|
|
25
|
+
agents of X Enterprises LLC, solely for purposes approved by X Enterprises
|
|
26
|
+
LLC.
|
|
27
|
+
|
|
28
|
+
4. NO WARRANTY
|
|
29
|
+
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
30
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
31
|
+
FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
32
|
+
X ENTERPRISES LLC BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY,
|
|
33
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF,
|
|
34
|
+
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
35
|
+
SOFTWARE.
|
|
36
|
+
|
|
37
|
+
5. LIMITATION OF LIABILITY
|
|
38
|
+
IN NO EVENT SHALL X ENTERPRISES LLC BE LIABLE FOR ANY INDIRECT, INCIDENTAL,
|
|
39
|
+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
40
|
+
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
|
|
41
|
+
OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
|
42
|
+
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
|
|
43
|
+
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
|
44
|
+
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
45
|
+
|
|
46
|
+
6. GOVERNING LAW
|
|
47
|
+
This license shall be governed by and construed in accordance with the laws
|
|
48
|
+
of the State of Washington, United States, without regard to its conflict
|
|
49
|
+
of law provisions.
|
|
50
|
+
|
|
51
|
+
7. TERMINATION
|
|
52
|
+
This license is effective until terminated. X Enterprises LLC may terminate
|
|
53
|
+
this license at any time without notice. Upon termination, you must destroy
|
|
54
|
+
all copies of the Software in your possession.
|
|
55
|
+
|
|
56
|
+
For licensing inquiries, contact: legal@x.enterprises
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
X Enterprises LLC
|
|
60
|
+
Bothell, Washington, United States
|
package/README.md
CHANGED
|
@@ -1,26 +1,11 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @xenterprises/fastify-xstripe
|
|
2
2
|
|
|
3
|
-
Fastify v5 plugin for
|
|
3
|
+
Fastify v5 plugin for Stripe webhook handling with built-in signature verification, 23 default event handlers, and the Stripe client decorated on the Fastify instance.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
- **Fastify v5.0.0+**
|
|
8
|
-
- **Node.js v20+**
|
|
9
|
-
|
|
10
|
-
## Features
|
|
11
|
-
|
|
12
|
-
- **Simple webhook handling** - Clean event-based architecture
|
|
13
|
-
- **Built-in handlers** - 20+ default handlers for common events
|
|
14
|
-
- **Easy to test** - Handlers are pure functions
|
|
15
|
-
- **Type-safe** - Full TypeScript support
|
|
16
|
-
- **Readable** - Clear, self-documenting code
|
|
17
|
-
- **Flexible** - Override any default handler
|
|
18
|
-
- **Production-ready** - Signature verification, error handling, logging
|
|
19
|
-
|
|
20
|
-
## Installation
|
|
5
|
+
## Install
|
|
21
6
|
|
|
22
7
|
```bash
|
|
23
|
-
npm install @xenterprises/fastify-xstripe
|
|
8
|
+
npm install @xenterprises/fastify-xstripe stripe
|
|
24
9
|
```
|
|
25
10
|
|
|
26
11
|
## Quick Start
|
|
@@ -34,298 +19,168 @@ const fastify = Fastify({ logger: true });
|
|
|
34
19
|
await fastify.register(xStripe, {
|
|
35
20
|
apiKey: process.env.STRIPE_API_KEY,
|
|
36
21
|
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
|
|
37
|
-
webhookPath: '/stripe/webhook', // Optional, defaults to /stripe/webhook
|
|
38
22
|
});
|
|
39
23
|
|
|
24
|
+
// Use the Stripe client directly
|
|
25
|
+
const customer = await fastify.stripe.customers.create({ email: 'user@example.com' });
|
|
26
|
+
|
|
40
27
|
await fastify.listen({ port: 3000 });
|
|
41
28
|
```
|
|
42
29
|
|
|
30
|
+
## Options
|
|
31
|
+
|
|
32
|
+
| Name | Type | Default | Required | Description |
|
|
33
|
+
|------|------|---------|----------|-------------|
|
|
34
|
+
| `apiKey` | `string` | — | Yes | Stripe secret API key (`sk_test_...` or `sk_live_...`) |
|
|
35
|
+
| `webhookSecret` | `string` | — | Yes | Stripe webhook signing secret (`whsec_...`) |
|
|
36
|
+
| `webhookPath` | `string` | `"/stripe/webhook"` | No | Path where the webhook POST route is registered |
|
|
37
|
+
| `handlers` | `object` | `{}` | No | Custom event handlers that override the defaults |
|
|
38
|
+
| `apiVersion` | `string` | `"2024-11-20.acacia"` | No | Stripe API version |
|
|
39
|
+
|
|
40
|
+
All options are validated at startup. Invalid or missing required options throw with an `[xStripe]` prefix.
|
|
41
|
+
|
|
42
|
+
## Decorated Properties
|
|
43
|
+
|
|
44
|
+
| Property | Type | Description |
|
|
45
|
+
|----------|------|-------------|
|
|
46
|
+
| `fastify.stripe` | `Stripe` | The initialized Stripe SDK client — use it to call any Stripe API |
|
|
47
|
+
|
|
43
48
|
## Custom Handlers
|
|
44
49
|
|
|
45
|
-
Override default
|
|
50
|
+
Override any default handler with your business logic. Handlers receive `(event, fastify, stripe)`:
|
|
46
51
|
|
|
47
52
|
```javascript
|
|
48
53
|
await fastify.register(xStripe, {
|
|
49
54
|
apiKey: process.env.STRIPE_API_KEY,
|
|
50
55
|
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
|
|
51
56
|
handlers: {
|
|
52
|
-
// Handle new subscription
|
|
53
57
|
'customer.subscription.created': async (event, fastify, stripe) => {
|
|
54
58
|
const subscription = event.data.object;
|
|
55
|
-
|
|
56
|
-
// Update your database
|
|
57
|
-
await fastify.prisma.user.update({
|
|
59
|
+
await db.users.update({
|
|
58
60
|
where: { stripeCustomerId: subscription.customer },
|
|
59
|
-
data: {
|
|
60
|
-
subscriptionId: subscription.id,
|
|
61
|
-
subscriptionStatus: subscription.status,
|
|
62
|
-
planId: subscription.items.data[0]?.price.id,
|
|
63
|
-
},
|
|
61
|
+
data: { subscriptionId: subscription.id, status: subscription.status },
|
|
64
62
|
});
|
|
65
|
-
|
|
66
|
-
// Send welcome email
|
|
67
|
-
await fastify.email.send(
|
|
68
|
-
subscription.customer.email,
|
|
69
|
-
'Welcome to Premium!',
|
|
70
|
-
'<h1>Thanks for subscribing!</h1>'
|
|
71
|
-
);
|
|
72
63
|
},
|
|
73
|
-
|
|
74
|
-
// Handle subscription cancellation
|
|
75
|
-
'customer.subscription.deleted': async (event, fastify, stripe) => {
|
|
76
|
-
const subscription = event.data.object;
|
|
77
|
-
|
|
78
|
-
// Revoke access
|
|
79
|
-
await fastify.prisma.user.update({
|
|
80
|
-
where: { stripeSubscriptionId: subscription.id },
|
|
81
|
-
data: {
|
|
82
|
-
subscriptionStatus: 'canceled',
|
|
83
|
-
hasAccess: false,
|
|
84
|
-
},
|
|
85
|
-
});
|
|
86
|
-
},
|
|
87
|
-
|
|
88
|
-
// Handle failed payment
|
|
89
64
|
'invoice.payment_failed': async (event, fastify, stripe) => {
|
|
90
65
|
const invoice = event.data.object;
|
|
91
|
-
|
|
92
|
-
// Send payment failure email
|
|
93
66
|
const customer = await stripe.customers.retrieve(invoice.customer);
|
|
94
|
-
await
|
|
95
|
-
customer.email,
|
|
96
|
-
'Payment Failed',
|
|
97
|
-
'<p>Please update your payment method.</p>'
|
|
98
|
-
);
|
|
67
|
+
await sendEmail(customer.email, 'Payment Failed', 'Please update your card.');
|
|
99
68
|
},
|
|
100
69
|
},
|
|
101
70
|
});
|
|
102
71
|
```
|
|
103
72
|
|
|
104
|
-
##
|
|
105
|
-
|
|
106
|
-
All handlers receive three parameters:
|
|
107
|
-
|
|
108
|
-
```javascript
|
|
109
|
-
async function handler(event, fastify, stripe) {
|
|
110
|
-
// event - The Stripe webhook event object
|
|
111
|
-
// fastify - The Fastify instance (access to decorators)
|
|
112
|
-
// stripe - The Stripe client instance
|
|
113
|
-
}
|
|
114
|
-
```
|
|
73
|
+
## Default Event Handlers
|
|
115
74
|
|
|
116
|
-
|
|
75
|
+
All 23 built-in handlers log structured data via `fastify.log`. Override any of them via the `handlers` option.
|
|
117
76
|
|
|
118
77
|
### Subscription Events
|
|
119
|
-
- `customer.subscription.created`
|
|
120
|
-
- `customer.subscription.updated`
|
|
121
|
-
- `customer.subscription.deleted`
|
|
122
|
-
- `customer.subscription.trial_will_end`
|
|
123
|
-
- `customer.subscription.paused`
|
|
124
|
-
- `customer.subscription.resumed`
|
|
78
|
+
- `customer.subscription.created` — logs subscriptionId, customerId, status, planId
|
|
79
|
+
- `customer.subscription.updated` — logs subscriptionId, customerId, status, previous changes
|
|
80
|
+
- `customer.subscription.deleted` — logs subscriptionId, customerId, canceledAt
|
|
81
|
+
- `customer.subscription.trial_will_end` — logs subscriptionId, customerId, trialEnd
|
|
82
|
+
- `customer.subscription.paused` — logs subscriptionId, customerId
|
|
83
|
+
- `customer.subscription.resumed` — logs subscriptionId, customerId
|
|
125
84
|
|
|
126
85
|
### Invoice Events
|
|
127
|
-
- `invoice.created`
|
|
128
|
-
- `invoice.finalized`
|
|
129
|
-
- `invoice.paid`
|
|
130
|
-
- `invoice.payment_failed`
|
|
131
|
-
- `invoice.upcoming`
|
|
86
|
+
- `invoice.created` — logs invoiceId, customerId, amount, status
|
|
87
|
+
- `invoice.finalized` — logs invoiceId, customerId, amount
|
|
88
|
+
- `invoice.paid` — logs invoiceId, customerId, subscriptionId, amount
|
|
89
|
+
- `invoice.payment_failed` — logs (warn) invoiceId, customerId, amount, attemptCount
|
|
90
|
+
- `invoice.upcoming` — logs customerId, subscriptionId, amount, periodEnd
|
|
132
91
|
|
|
133
92
|
### Payment Events
|
|
134
|
-
- `payment_intent.succeeded`
|
|
135
|
-
- `payment_intent.payment_failed`
|
|
93
|
+
- `payment_intent.succeeded` — logs paymentIntentId, customerId, amount, currency
|
|
94
|
+
- `payment_intent.payment_failed` — logs (warn) paymentIntentId, customerId, amount, lastPaymentError
|
|
136
95
|
|
|
137
96
|
### Customer Events
|
|
138
|
-
- `customer.created`
|
|
139
|
-
- `customer.updated`
|
|
140
|
-
- `customer.deleted`
|
|
97
|
+
- `customer.created` — logs customerId, email
|
|
98
|
+
- `customer.updated` — logs customerId, previous changes
|
|
99
|
+
- `customer.deleted` — logs customerId
|
|
141
100
|
|
|
142
101
|
### Payment Method Events
|
|
143
|
-
- `payment_method.attached`
|
|
144
|
-
- `payment_method.detached`
|
|
102
|
+
- `payment_method.attached` — logs paymentMethodId, customerId, type
|
|
103
|
+
- `payment_method.detached` — logs paymentMethodId, type
|
|
145
104
|
|
|
146
105
|
### Checkout Events
|
|
147
|
-
- `checkout.session.completed`
|
|
148
|
-
- `checkout.session.expired`
|
|
106
|
+
- `checkout.session.completed` — logs sessionId, customerId, subscriptionId, mode, paymentStatus
|
|
107
|
+
- `checkout.session.expired` — logs sessionId
|
|
149
108
|
|
|
150
|
-
|
|
109
|
+
### Charge Events
|
|
110
|
+
- `charge.succeeded` — logs chargeId, customerId, amount, currency, paymentMethod
|
|
111
|
+
- `charge.failed` — logs (error) chargeId, customerId, amount, failureCode, failureMessage
|
|
112
|
+
- `charge.refunded` — logs chargeId, customerId, amountRefunded, refundCount
|
|
151
113
|
|
|
152
|
-
|
|
114
|
+
## Helper Utilities
|
|
153
115
|
|
|
154
|
-
|
|
155
|
-
# Install Stripe CLI
|
|
156
|
-
brew install stripe/stripe-cli/stripe
|
|
157
|
-
|
|
158
|
-
# Login to Stripe
|
|
159
|
-
stripe login
|
|
160
|
-
|
|
161
|
-
# Forward webhooks to your local server
|
|
162
|
-
stripe listen --forward-to localhost:3000/stripe/webhook
|
|
163
|
-
|
|
164
|
-
# Trigger test events
|
|
165
|
-
stripe trigger customer.subscription.created
|
|
166
|
-
stripe trigger invoice.payment_failed
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
### 2. Test Handlers Directly
|
|
170
|
-
|
|
171
|
-
Since handlers are pure functions, they're easy to test:
|
|
172
|
-
|
|
173
|
-
```javascript
|
|
174
|
-
import { test } from 'node:test';
|
|
175
|
-
import assert from 'node:assert';
|
|
176
|
-
|
|
177
|
-
test('subscription.created handler', async () => {
|
|
178
|
-
const mockEvent = {
|
|
179
|
-
type: 'customer.subscription.created',
|
|
180
|
-
data: {
|
|
181
|
-
object: {
|
|
182
|
-
id: 'sub_123',
|
|
183
|
-
customer: 'cus_123',
|
|
184
|
-
status: 'active',
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
const mockFastify = {
|
|
190
|
-
log: { info: () => {} },
|
|
191
|
-
prisma: {
|
|
192
|
-
user: {
|
|
193
|
-
update: async (data) => {
|
|
194
|
-
assert.equal(data.where.stripeCustomerId, 'cus_123');
|
|
195
|
-
return {};
|
|
196
|
-
},
|
|
197
|
-
},
|
|
198
|
-
},
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
const mockStripe = {};
|
|
202
|
-
|
|
203
|
-
await handlers['customer.subscription.created'](
|
|
204
|
-
mockEvent,
|
|
205
|
-
mockFastify,
|
|
206
|
-
mockStripe
|
|
207
|
-
);
|
|
208
|
-
});
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
## Common Patterns
|
|
212
|
-
|
|
213
|
-
### Update Database on Subscription Change
|
|
214
|
-
|
|
215
|
-
```javascript
|
|
216
|
-
'customer.subscription.updated': async (event, fastify, stripe) => {
|
|
217
|
-
const subscription = event.data.object;
|
|
218
|
-
const previous = event.data.previous_attributes || {};
|
|
219
|
-
|
|
220
|
-
// Check what changed
|
|
221
|
-
if ('status' in previous) {
|
|
222
|
-
await fastify.prisma.user.update({
|
|
223
|
-
where: { stripeSubscriptionId: subscription.id },
|
|
224
|
-
data: { subscriptionStatus: subscription.status },
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
// Handle specific status changes
|
|
228
|
-
if (subscription.status === 'past_due') {
|
|
229
|
-
// Send payment reminder
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
### Send Notification Emails
|
|
116
|
+
Import from `@xenterprises/fastify-xstripe/helpers`:
|
|
236
117
|
|
|
237
118
|
```javascript
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
119
|
+
import { helpers } from '@xenterprises/fastify-xstripe';
|
|
120
|
+
|
|
121
|
+
helpers.formatAmount(2000, 'USD'); // "$20.00"
|
|
122
|
+
helpers.getPlanName(subscription); // "Pro Plan"
|
|
123
|
+
helpers.isActiveSubscription(subscription); // true
|
|
124
|
+
helpers.isInTrial(subscription); // true/false
|
|
125
|
+
helpers.getDaysUntilTrialEnd(subscription); // 3
|
|
126
|
+
helpers.isRenewal(event); // true/false
|
|
127
|
+
helpers.calculateMRR(subscription); // 2000 (cents)
|
|
128
|
+
helpers.getSubscriptionStatusText('active'); // "Active"
|
|
129
|
+
helpers.getEventDescription(event); // "Payment received"
|
|
130
|
+
helpers.getCustomerEmail(event, stripe); // "user@example.com"
|
|
131
|
+
helpers.isTestEvent(event); // true/false
|
|
132
|
+
helpers.getMetadata(event); // { key: "value" }
|
|
133
|
+
helpers.getPaymentMethodType(pm); // "Card"
|
|
134
|
+
helpers.getInvoiceLineItems(invoice); // [{ description, amount, ... }]
|
|
135
|
+
helpers.isSubscriptionInvoice(invoice); // true/false
|
|
136
|
+
helpers.getNextBillingDate(subscription); // Date
|
|
137
|
+
helpers.formatDate(1700000000); // "November 14, 2023"
|
|
248
138
|
```
|
|
249
139
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
```javascript
|
|
253
|
-
'invoice.payment_failed': async (event, fastify, stripe) => {
|
|
254
|
-
const invoice = event.data.object;
|
|
255
|
-
|
|
256
|
-
await fastify.prisma.user.update({
|
|
257
|
-
where: { stripeSubscriptionId: invoice.subscription },
|
|
258
|
-
data: {
|
|
259
|
-
failedPaymentCount: { increment: 1 },
|
|
260
|
-
lastFailedPayment: new Date(),
|
|
261
|
-
},
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
// After 3 failed payments, suspend account
|
|
265
|
-
const user = await fastify.prisma.user.findUnique({
|
|
266
|
-
where: { stripeSubscriptionId: invoice.subscription },
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
if (user.failedPaymentCount >= 3) {
|
|
270
|
-
await fastify.prisma.user.update({
|
|
271
|
-
where: { id: user.id },
|
|
272
|
-
data: { accountSuspended: true },
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
```
|
|
140
|
+
## Environment Variables
|
|
277
141
|
|
|
278
|
-
|
|
142
|
+
| Name | Required | Description |
|
|
143
|
+
|------|----------|-------------|
|
|
144
|
+
| `STRIPE_API_KEY` | Yes | Stripe secret key (`sk_test_...` or `sk_live_...`) |
|
|
145
|
+
| `STRIPE_WEBHOOK_SECRET` | Yes | Webhook signing secret from Stripe Dashboard or CLI (`whsec_...`) |
|
|
279
146
|
|
|
280
|
-
|
|
281
|
-
|--------|------|----------|---------|-------------|
|
|
282
|
-
| `apiKey` | string | Yes | - | Stripe API key |
|
|
283
|
-
| `webhookSecret` | string | No | - | Stripe webhook signing secret |
|
|
284
|
-
| `webhookPath` | string | No | `/stripe/webhook` | Webhook endpoint path |
|
|
285
|
-
| `handlers` | object | No | `{}` | Custom event handlers |
|
|
286
|
-
| `apiVersion` | string | No | `2024-11-20.acacia` | Stripe API version |
|
|
147
|
+
## Error Reference
|
|
287
148
|
|
|
288
|
-
|
|
149
|
+
All errors use the `[xStripe]` prefix for easy identification in logs.
|
|
289
150
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
151
|
+
| Error | When |
|
|
152
|
+
|-------|------|
|
|
153
|
+
| `[xStripe] apiKey is required and must be a string` | Missing or non-string `apiKey` option |
|
|
154
|
+
| `[xStripe] webhookSecret is required and must be a string` | Missing or non-string `webhookSecret` option |
|
|
155
|
+
| `[xStripe] webhookPath must be a string` | Non-string `webhookPath` option |
|
|
156
|
+
| `[xStripe] handlers must be a plain object` | `handlers` is not an object or is an array |
|
|
157
|
+
| `[xStripe] apiVersion must be a string` | Non-string `apiVersion` option |
|
|
158
|
+
| `[xStripe] Missing stripe-signature header` | Webhook request without signature header (HTTP 400) |
|
|
159
|
+
| `[xStripe] Webhook signature verification failed: ...` | Invalid webhook signature (HTTP 400) |
|
|
294
160
|
|
|
295
|
-
##
|
|
161
|
+
## How It Works
|
|
296
162
|
|
|
297
|
-
1. **
|
|
298
|
-
2. **
|
|
299
|
-
3. **
|
|
300
|
-
4. **
|
|
301
|
-
5. **Monitor failed handlers** - Set up alerts for handler errors
|
|
163
|
+
1. **Registration** — Validates all options, initializes the Stripe SDK client, and decorates it as `fastify.stripe`.
|
|
164
|
+
2. **Webhook Route** — Registers a POST route at `webhookPath` that reads the raw body, verifies the Stripe signature using `stripe.webhooks.constructEvent()`, and dispatches to the matching handler.
|
|
165
|
+
3. **Handler Dispatch** — User-provided handlers override defaults via object spread (`{ ...defaultHandlers, ...userHandlers }`). If a handler throws, the error is logged but the webhook still returns HTTP 200 to prevent Stripe retries.
|
|
166
|
+
4. **Stripe Client** — The `fastify.stripe` decorator gives full access to the Stripe SDK for any API call (customers, subscriptions, invoices, etc.).
|
|
302
167
|
|
|
303
|
-
##
|
|
168
|
+
## Testing Webhooks Locally
|
|
304
169
|
|
|
305
170
|
```bash
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
```
|
|
309
|
-
|
|
310
|
-
## Integration with Other xPlugins
|
|
171
|
+
# Install Stripe CLI
|
|
172
|
+
brew install stripe/stripe-cli/stripe
|
|
311
173
|
|
|
312
|
-
|
|
174
|
+
# Login and forward webhooks
|
|
175
|
+
stripe login
|
|
176
|
+
stripe listen --forward-to localhost:3000/stripe/webhook
|
|
313
177
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
// In your handlers:
|
|
320
|
-
'invoice.payment_failed': async (event, fastify, stripe) => {
|
|
321
|
-
// Send email via xConfig/SendGrid
|
|
322
|
-
await fastify.email.send(/* ... */);
|
|
323
|
-
|
|
324
|
-
// Send SMS via xTwilio
|
|
325
|
-
await fastify.sms.send(/* ... */);
|
|
326
|
-
}
|
|
178
|
+
# Trigger test events
|
|
179
|
+
stripe trigger customer.subscription.created
|
|
180
|
+
stripe trigger invoice.payment_failed
|
|
181
|
+
stripe trigger checkout.session.completed
|
|
327
182
|
```
|
|
328
183
|
|
|
329
184
|
## License
|
|
330
185
|
|
|
331
|
-
|
|
186
|
+
UNLICENSED
|