@flopay/node 1.9.15 → 1.9.17

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.
Files changed (2) hide show
  1. package/README.md +151 -140
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @flopay/node
2
2
 
3
- Server-side FloPay SDK for Node.js. Creates checkout sessions via the billing API, manages Stripe customers, and verifies webhooks.
3
+ Server-side FloPay SDK for Node.js. Creates checkout sessions through the FloPay billing API, retrieves and expires Stripe checkout sessions, manages Stripe customers, and verifies Stripe webhooks.
4
4
 
5
5
  ## Installation
6
6
 
@@ -8,186 +8,197 @@ Server-side FloPay SDK for Node.js. Creates checkout sessions via the billing AP
8
8
  pnpm add @flopay/node
9
9
  ```
10
10
 
11
- Depends on `stripe` (^22.2.0) and `@flopay/shared`.
11
+ `stripe` and `@flopay/shared` install automatically as dependencies. Use it only from trusted server code: the constructor takes a Stripe secret key, which must never reach a browser.
12
12
 
13
- ## Quick Start
13
+ ## Usage
14
14
 
15
15
  ### Initialize
16
16
 
17
+ Create one client with your Stripe secret key. Direct Stripe operations use the API version in `STRIPE_API_VERSION` unless you pass `apiVersion`; pass `stripeSecretKey` to use a different key for them.
18
+
17
19
  ```ts
18
20
  import { FloPay } from '@flopay/node';
19
21
 
20
- const flopay = new FloPay('sk_test_...');
21
- // Direct Stripe operations default to API version 2026-04-22.dahlia.
22
+ const flopay = new FloPay('sk_test_...'); // load the real key from your secret store
22
23
  ```
23
24
 
24
- ### Create a Checkout Session (Billing API)
25
+ ### Create a checkout session
26
+
27
+ `flopay.checkout.sessions.create()` posts to the billing API's `POST /v1/checkouts/sessions` and resolves with a `CheckoutSessionResult`:
28
+
29
+ - `{ status: 201, redirectUrl, nonce }`: send the buyer to `redirectUrl`, the hosted checkout page (`checkoutBaseUrl` + `/secure`, with the session id and your `redirectParams`). `nonce` is the session-bound checkout token; forward it as the `x-checkout-session-token` header on every continuation call for the same session.
30
+ - `{ status: 204 }`: the buyer already has a payment method on file and nothing needs to be shown.
31
+
32
+ The session currency is required. Pass `currency` on the session, or on the first item, subscription, or product. `checkoutMode` defaults to `'confirm'` on this method, and `timeoutMs` to 12 seconds.
33
+
34
+ `checkoutMetadata` accepts `Record<string, string> | null` and is sent unchanged: omitting it leaves the request body unchanged, an explicit `null` is preserved, and `{}` stays an empty map. The billing API is the only authority on metadata limits.
35
+
36
+ ### Retry safely with an idempotency key
37
+
38
+ Pass the same `idempotencyKey` when you retry one logical session creation after an unknown outcome. A non-blank key of at most 255 characters is sent unchanged as the `Idempotency-Key` header. Empty, whitespace-only, overlong, and header-unsafe values are ignored without throwing, and no key is generated when you omit the option. `@flopay/node` has no internal retry loop, so your server owns retry timing and key reuse.
39
+
40
+ ### Authorise now, capture later
41
+
42
+ To authorise an eligible one-time card cart without capturing it, pass `captureMethod: 'manual'`. Manual capture cannot be combined with subscriptions, and omitting the option keeps immediate capture. The later capture is deliberately not an `@flopay/node` method: call the merchant-authenticated REST endpoint from trusted server code with a stable `Idempotency-Key`, as described in the [payments and authorisations reference](https://flopay.com/docs/technical-reference/rest/payments).
43
+
44
+ ### Saved cards
45
+
46
+ Saved-card management is also not an `@flopay/node` credential surface. From trusted server code, use the authenticated REST API to list display-only payment methods, create a setup session, and delete an eligible method, then pass only the setup session's `sessionId` and `nonce` to `FloPayCardSetup` in `@flopay/react`. Replace a card setup-first, then delete, so a failed or cancelled setup leaves the existing card intact. See the [payment methods reference](https://flopay.com/docs/technical-reference/rest/payment-methods).
47
+
48
+ ### Stripe checkout sessions and customers
49
+
50
+ `checkout.sessions.retrieve()`, `expire()`, and `listLineItems()` call Stripe directly and normalize the result. `customers.create()`, `retrieve()`, and `update()` manage Stripe customers and return a normalized `Customer`.
51
+
52
+ ### Verify webhooks
53
+
54
+ `webhooks.constructEvent()` verifies the `Stripe-Signature` header against your webhook signing secret and returns a normalized `WebhookEvent` (`id`, `type`, `data`, `created`). Pass the raw, unparsed request body.
55
+
56
+ ## Examples
57
+
58
+ Create a checkout session and redirect the buyer:
25
59
 
26
60
  ```ts
61
+ import { FloPay } from '@flopay/node';
62
+
63
+ function requiredEnv(name: string): string {
64
+ const value = process.env[name];
65
+ if (!value) throw new Error(`${name} is not set`);
66
+ return value;
67
+ }
68
+
69
+ const flopay = new FloPay(requiredEnv('STRIPE_SECRET_KEY'));
70
+
27
71
  const result = await flopay.checkout.sessions.create({
28
72
  billingApiUrl: 'https://billing.example.com',
29
73
  checkoutBaseUrl: 'https://checkout.example.com',
30
74
  clientId: 'client_123',
31
- items: [{
32
- providerItemId: 'prod_abc',
33
- providerItemName: 'Pro Plan',
34
- totalAmount: 49.99,
35
- overrideAmount: 24.99,
36
- currency: 'USD',
37
- }],
38
- subscriptions: [{
39
- providerPlanId: 'plan_xyz',
40
- providerPlanName: 'Monthly Plan',
41
- totalAmount: 9.99,
42
- currency: 'USD',
43
- }],
44
- account: {
45
- userId: 'user_1',
46
- email: 'user@example.com',
47
- firstName: 'John',
48
- lastName: 'Doe',
49
- },
50
- successUrl: 'https://example.com/success',
51
- cancelUrl: 'https://example.com/cancel',
52
- checkoutMode: 'confirm', // 'confirm' | 'auto' | 'full'
75
+ currency: 'USD',
76
+ items: [
77
+ {
78
+ providerItemId: 'prod_abc',
79
+ providerItemName: 'Pro Plan',
80
+ totalAmount: 49.99,
81
+ overrideAmount: 24.99,
82
+ },
83
+ ],
84
+ subscriptions: [{ providerPlanId: 'plan_xyz', providerPlanName: 'Monthly Plan', totalAmount: 9.99 }],
85
+ account: { userId: 'user_1', email: 'user@example.com', firstName: 'Jane', lastName: 'Doe' },
86
+ successUrl: 'https://shop.example.com/success',
87
+ cancelUrl: 'https://shop.example.com/cancel',
88
+ checkoutMode: 'full',
53
89
  checkoutMetadata: { merchantOrderId: 'order-123', campaign: 'summer' },
54
90
  couponCodes: ['SAVE10'],
55
91
  tagsData: { googleContainerId: 'GTM-XXXX' },
56
- redirectParams: { email: 'user@example.com', mode: 'confirm' },
57
- timeoutMs: 12000,
58
- idempotencyKey: 'order-123-attempt-group',
92
+ redirectParams: { email: 'user@example.com' },
93
+ timeoutMs: 12_000,
94
+ idempotencyKey: 'checkout:order-123',
59
95
  });
60
96
 
61
- // result = { status: 201, redirectUrl: 'https://checkout.example.com/secure?id=uuid&...' }
62
- // result = { status: 204 } (payment method already on file)
97
+ if (result.status === 201 && 'redirectUrl' in result) {
98
+ console.log('Redirect the buyer to', result.redirectUrl);
99
+ } else if (result.status === 204) {
100
+ console.log('A payment method is already on file.');
101
+ }
63
102
  ```
64
103
 
65
- `checkoutMetadata` accepts `Record<string, string> | null` and is sent
66
- unchanged. Omission leaves the request body unchanged, explicit `null` is
67
- preserved, and `{}` remains an empty map. The backend is the sole runtime
68
- authority for metadata limits; a validation rejection is a `FloPayError` with
69
- the backend code, message, and HTTP `statusCode` intact.
70
-
71
- Pass the same `idempotencyKey` when retrying one logical session creation after
72
- an unknown outcome. `@flopay/node` sends a supplied non-whitespace key of at most
73
- 255 characters unchanged as `Idempotency-Key`; it does not generate a key or add
74
- the header when the option is omitted. Empty, whitespace-only, overlong, and
75
- Fetch-header-unsafe values are ignored without throwing or emitting a malformed
76
- header. The Node SDK has no internal retry loop, so the server integration owns
77
- retry timing and key reuse.
78
-
79
- To authorise an eligible one-time card cart without capturing it, pass
80
- `captureMethod: 'manual'` to `checkout.sessions.create`. Do not combine manual
81
- capture with subscriptions; the SDK rejects that cart before making a request.
82
- Omitting the option preserves immediate capture and leaves the wire key absent.
83
-
84
- The later full capture is intentionally not an `@flopay/node` method. Use the
85
- merchant-authenticated REST endpoint from trusted server code with a stable
86
- `Idempotency-Key`; never expose merchant credentials to a browser. See the
87
- repository's [pre-authorisation guide](../../docs/PREAUTHORIZATION.md).
88
-
89
- Saved-card management is also intentionally not an `@flopay/node` credential
90
- surface. From trusted merchant server code, use the authenticated REST API to
91
- list display-only methods, create a setup session, and delete an eligible
92
- method; pass only the resulting setup `sessionId` and `nonce` to
93
- `<FloPayCardSetup>` in `@flopay/react`. Replacement is setup-first, then delete,
94
- so a failed or cancelled setup leaves the existing card intact. The coordinated
95
- integration guide is tracked in
96
- [TeamFloPay/docs#38](https://github.com/TeamFloPay/docs/issues/38).
97
-
98
- ### Retrieve / Expire a Checkout Session (Stripe Direct)
104
+ Verify a Stripe webhook in a plain Node.js HTTP server:
99
105
 
100
106
  ```ts
101
- const session = await flopay.checkout.sessions.retrieve('cs_test_...');
102
- // { id, clientSecret, mode, status, amount, currency, metadata }
103
-
104
- const expired = await flopay.checkout.sessions.expire('cs_test_...');
107
+ import { createServer } from 'node:http';
108
+ import { FloPay } from '@flopay/node';
105
109
 
106
- const lineItems = await flopay.checkout.sessions.listLineItems('cs_test_...');
107
- // [{ price: 'price_xxx', quantity: 1 }]
110
+ const flopay = new FloPay(process.env.STRIPE_SECRET_KEY ?? 'sk_test_...');
111
+ const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET ?? 'whsec_...';
112
+ const MAX_BODY_BYTES = 64 * 1024; // Pick a limit that fits your largest expected event.
113
+
114
+ createServer((request, response) => {
115
+ const chunks: Buffer[] = [];
116
+ let received = 0;
117
+ request.on('data', (chunk: Buffer) => {
118
+ received += chunk.length;
119
+ if (received > MAX_BODY_BYTES) {
120
+ response.writeHead(413, { Connection: 'close' }).end('Payload too large');
121
+ request.destroy();
122
+ return;
123
+ }
124
+ chunks.push(chunk);
125
+ });
126
+ request.on('end', () => {
127
+ try {
128
+ const event = flopay.webhooks.constructEvent(
129
+ Buffer.concat(chunks),
130
+ String(request.headers['stripe-signature']),
131
+ webhookSecret,
132
+ );
133
+ console.log(event.type, event.created);
134
+ response.writeHead(200).end();
135
+ } catch {
136
+ response.writeHead(400).end('Invalid signature');
137
+ }
138
+ });
139
+ }).listen(3000);
108
140
  ```
109
141
 
110
- ### Manage Customers
142
+ Manage Stripe customers:
111
143
 
112
144
  ```ts
113
- const customer = await flopay.customers.create({
114
- email: 'user@example.com',
115
- name: 'John Doe',
116
- metadata: { plan: 'pro' },
117
- });
118
- // { id: 'cus_xxx', email: '...', firstName: 'John', lastName: 'Doe' }
145
+ import { FloPay } from '@flopay/node';
119
146
 
120
- const retrieved = await flopay.customers.retrieve('cus_xxx');
147
+ const flopay = new FloPay(process.env.STRIPE_SECRET_KEY ?? 'sk_test_...');
121
148
 
122
- const updated = await flopay.customers.update('cus_xxx', {
149
+ const customer = await flopay.customers.create({
150
+ email: 'user@example.com',
123
151
  name: 'Jane Doe',
124
- metadata: { plan: 'enterprise' },
152
+ metadata: { plan: 'pro' },
125
153
  });
126
- ```
127
-
128
- ### Verify Webhooks
129
-
130
- ```ts
131
- import { FloPay } from '@flopay/node';
132
-
133
- const flopay = new FloPay('sk_test_...');
134
-
135
- // In your webhook handler (e.g. Express, Next.js API route):
136
- const event = flopay.webhooks.constructEvent(
137
- requestBody, // raw body string or Buffer
138
- signatureHeader, // Stripe-Signature header
139
- 'whsec_...', // webhook signing secret
140
- );
141
-
142
- console.log(event.type); // e.g. 'checkout.session.completed'
143
- console.log(event.data); // event payload
144
- console.log(event.created); // unix timestamp
145
- ```
146
-
147
- ## API Reference
148
154
 
149
- ### Constructor
150
-
151
- ```ts
152
- new FloPay(secretKey: string, options?: FloPayNodeOptions)
155
+ const updated = await flopay.customers.update(customer.id, { metadata: { plan: 'enterprise' } });
156
+ const session = await flopay.checkout.sessions.retrieve('cs_test_...');
157
+ console.log(updated.email, session.status, session.amount);
153
158
  ```
154
159
 
155
- | Parameter | Type | Description |
156
- |-----------|------|-------------|
157
- | `secretKey` | `string` | Stripe secret key (`sk_test_...` or `sk_live_...`). Required. |
158
- | `options.apiVersion` | `string?` | Direct Stripe API version override. Defaults to `2026-04-22.dahlia`. |
159
- | `options.stripeSecretKey` | `string?` | Explicit Stripe key override for direct Stripe operations. |
160
+ ## Public API
160
161
 
161
- ### `flopay.checkout.sessions`
162
+ ### Exports
162
163
 
163
- | Method | Returns | Description |
164
- |--------|---------|-------------|
165
- | `create(params)` | `Promise<CheckoutSessionResult>` | Creates a session via billing API (`POST /v1/checkouts/sessions`). Returns `{ status: 201, redirectUrl, nonce }` or `{ status: 204 }`. The `nonce` is the session-bound checkout token returned by the backend; forward it as `x-checkout-session-token` on every continuation call against the same session. |
166
- | `retrieve(id)` | `Promise<CheckoutSession>` | Retrieves a Stripe checkout session and normalizes it |
167
- | `expire(id)` | `Promise<CheckoutSession>` | Expires a Stripe checkout session |
168
- | `listLineItems(id)` | `Promise<LineItem[]>` | Lists line items for a Stripe checkout session |
164
+ | Export | Description |
165
+ |--------|-------------|
166
+ | `FloPay` | Server-side client: `new FloPay(secretKey, options?)` with a Stripe secret key (`sk_test_...` or `sk_live_...`) |
167
+ | `FloPayNodeOptions` | Constructor options |
168
+ | `STRIPE_API_VERSION` | `'2026-04-22.dahlia'`, the default Stripe API version for direct Stripe operations |
169
169
 
170
- ### `flopay.customers`
170
+ ### FloPayNodeOptions
171
171
 
172
- | Method | Returns | Description |
173
- |--------|---------|-------------|
174
- | `create(params)` | `Promise<Customer>` | Creates a Stripe customer |
175
- | `retrieve(id)` | `Promise<Customer>` | Retrieves a Stripe customer. Throws if deleted. |
176
- | `update(id, params)` | `Promise<Customer>` | Updates a Stripe customer |
172
+ | Option | Type | Description |
173
+ |--------|------|-------------|
174
+ | `apiVersion` | `string?` | Stripe API version override for direct Stripe operations |
175
+ | `stripeSecretKey` | `string?` | Stripe secret key for direct Stripe operations, instead of the constructor key |
177
176
 
178
- ### `flopay.webhooks`
177
+ ### FloPay methods
179
178
 
180
179
  | Method | Returns | Description |
181
180
  |--------|---------|-------------|
182
- | `constructEvent(payload, signature, secret)` | `WebhookEvent` | Verifies and parses a Stripe webhook event |
183
-
184
- ### Types
185
-
186
- | Type | Description |
187
- |------|-------------|
188
- | `FloPayNodeOptions` | Constructor options |
189
- | `CreateSessionParams` | Full session creation parameters (from `@flopay/shared`), including optional `checkoutMetadata: Record<string, string> \| null`, `captureMethod: 'automatic' \| 'manual'`, and caller-managed `idempotencyKey` |
190
- | `CheckoutSessionResult` | `{ status: 201, redirectUrl, nonce }` / `{ status: 204 }` / `{ status: number }` |
191
- | `CheckoutSession` | Normalized session with optional returned `checkoutMetadata`; `status` can be `authorized`, with `paymentId` and `authorizationExpiresAt`, for manual capture |
192
- | `Customer` | `id`, `email`, `firstName?`, `lastName?` |
193
- | `WebhookEvent` | `id`, `type`, `data`, `created` |
181
+ | `checkout.sessions.create(params)` | `Promise<CheckoutSessionResult>` | Creates a session through the billing API (`POST /v1/checkouts/sessions`). Accepts `CreateSessionParams`. |
182
+ | `checkout.sessions.retrieve(id)` | `Promise<CheckoutSession>` | Retrieves and normalizes a Stripe checkout session |
183
+ | `checkout.sessions.expire(id)` | `Promise<CheckoutSession>` | Expires a Stripe checkout session |
184
+ | `checkout.sessions.listLineItems(id)` | `Promise<LineItem[]>` | Lists a Stripe checkout session's line items |
185
+ | `customers.create(params)` | `Promise<Customer>` | Creates a Stripe customer |
186
+ | `customers.retrieve(id)` | `Promise<Customer>` | Retrieves a Stripe customer |
187
+ | `customers.update(id, params)` | `Promise<Customer>` | Updates a Stripe customer |
188
+ | `webhooks.constructEvent(payload, signature, secret)` | `WebhookEvent` | Verifies and parses a Stripe webhook event |
189
+
190
+ The parameter and result types (`CreateSessionParams`, `CheckoutSessionResult`, `CheckoutSession`, `Customer`, `LineItem`, `WebhookEvent`) are exported by `@flopay/shared`.
191
+
192
+ ## Errors
193
+
194
+ SDK validation and billing API failures are `FloPayError`s from `@flopay/shared`; branch on `type` and `code`.
195
+
196
+ | Code | Type | When |
197
+ |------|------|------|
198
+ | `CurrencyRequired` | `validation_error` | `checkout.sessions.create()` could not resolve a session currency. Thrown before any request, with `param: 'currency'`. |
199
+ | `CaptureMethodUnsupportedForSubscription` | `validation_error` | `captureMethod: 'manual'` was requested for a cart with a subscription. Thrown before any request. |
200
+ | The billing API's `code`, or `http_<status>` | `api_error` | The billing API rejected session creation. The error keeps the billing API's message and code, with the HTTP status in `statusCode`. |
201
+ | `MissingCheckoutSessionToken` | `api_error` | The billing API created a session but returned no `nonce`, which means it is too old for this SDK. |
202
+ | `resource_missing` | `api_error` | `customers.retrieve()` found a deleted customer. |
203
+
204
+ Constructing `FloPay` without a secret key throws an `authentication_error`. A create request that exceeds `timeoutMs` is aborted, and network failures reject with the underlying fetch error rather than a `FloPayError`. Stripe operations, including webhook signature verification, reject with Stripe's own errors. Treat a failed `constructEvent()` as an untrusted request.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flopay/node",
3
- "version": "1.9.15",
3
+ "version": "1.9.17",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "publishConfig": {
@@ -27,7 +27,7 @@
27
27
  ],
28
28
  "dependencies": {
29
29
  "stripe": "^22.2.1",
30
- "@flopay/shared": "1.9.15"
30
+ "@flopay/shared": "1.9.17"
31
31
  },
32
32
  "devDependencies": {
33
33
  "tsup": "^8.3.0",