@reevit/node 0.3.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/.github/workflows/ci.yml +35 -0
- package/.github/workflows/publish.yml +32 -0
- package/LICENSE +21 -0
- package/README.md +1385 -0
- package/dist/client.d.ts +72 -0
- package/dist/client.js +117 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +56 -0
- package/dist/services/connections.d.ts +9 -0
- package/dist/services/connections.js +21 -0
- package/dist/services/fraud.d.ts +8 -0
- package/dist/services/fraud.js +17 -0
- package/dist/services/payments.d.ts +10 -0
- package/dist/services/payments.js +30 -0
- package/dist/services/subscriptions.d.ts +8 -0
- package/dist/services/subscriptions.js +17 -0
- package/dist/types.d.ts +118 -0
- package/dist/types.js +2 -0
- package/package.json +26 -0
- package/src/client.ts +202 -0
- package/src/index.ts +48 -0
- package/src/services/connections.ts +21 -0
- package/src/services/fraud.ts +16 -0
- package/src/services/payments.ts +36 -0
- package/src/services/subscriptions.ts +16 -0
- package/src/types.ts +129 -0
- package/tsconfig.json +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,1385 @@
|
|
|
1
|
+
# Reevit TypeScript SDK
|
|
2
|
+
|
|
3
|
+
The official Node.js/TypeScript SDK for [Reevit](https://reevit.io) — a unified payment orchestration platform for Africa.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@reevit/node)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [Installation](#installation)
|
|
12
|
+
- [Quick Start](#quick-start)
|
|
13
|
+
- [Configuration](#configuration)
|
|
14
|
+
- [Payments](#payments)
|
|
15
|
+
- [Create Payment Intent](#create-payment-intent)
|
|
16
|
+
- [Get Payment](#get-payment)
|
|
17
|
+
- [List Payments](#list-payments)
|
|
18
|
+
- [Refund Payment](#refund-payment)
|
|
19
|
+
- [Connections](#connections)
|
|
20
|
+
- [Create Connection](#create-connection)
|
|
21
|
+
- [List Connections](#list-connections)
|
|
22
|
+
- [Test Connection](#test-connection)
|
|
23
|
+
- [Subscriptions](#subscriptions)
|
|
24
|
+
- [Create Subscription](#create-subscription)
|
|
25
|
+
- [List Subscriptions](#list-subscriptions)
|
|
26
|
+
- [Fraud Protection](#fraud-protection)
|
|
27
|
+
- [Get Fraud Policy](#get-fraud-policy)
|
|
28
|
+
- [Update Fraud Policy](#update-fraud-policy)
|
|
29
|
+
- [Error Handling](#error-handling)
|
|
30
|
+
- [TypeScript Types](#typescript-types)
|
|
31
|
+
- [Supported Providers](#supported-providers)
|
|
32
|
+
- [Examples](#examples)
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install @reevit/node
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Or using yarn:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
yarn add @reevit/node
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or using pnpm:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pnpm add @reevit/node
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Quick Start
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { Reevit } from '@reevit/node';
|
|
60
|
+
|
|
61
|
+
// Initialize the client
|
|
62
|
+
const reevit = new Reevit(
|
|
63
|
+
'pfk_live_your_api_key', // Your API key
|
|
64
|
+
'org_your_org_id' // Your organization ID
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Create a payment
|
|
68
|
+
const payment = await reevit.payments.createIntent({
|
|
69
|
+
amount: 5000, // 50.00 GHS (amount in smallest currency unit)
|
|
70
|
+
currency: 'GHS',
|
|
71
|
+
method: 'momo',
|
|
72
|
+
country: 'GH',
|
|
73
|
+
customer_id: 'cust_123'
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
console.log('Payment ID:', payment.id);
|
|
77
|
+
console.log('Status:', payment.status);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Configuration
|
|
83
|
+
|
|
84
|
+
### Basic Configuration
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import { Reevit } from '@reevit/node';
|
|
88
|
+
|
|
89
|
+
const reevit = new Reevit(
|
|
90
|
+
'pfk_live_your_api_key', // API Key (required)
|
|
91
|
+
'org_your_org_id', // Organization ID (required)
|
|
92
|
+
'https://api.reevit.io' // Base URL (optional, defaults to localhost:8080)
|
|
93
|
+
);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Environment Variables (Recommended)
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import { Reevit } from '@reevit/node';
|
|
100
|
+
|
|
101
|
+
const reevit = new Reevit(
|
|
102
|
+
process.env.REEVIT_API_KEY!,
|
|
103
|
+
process.env.REEVIT_ORG_ID!,
|
|
104
|
+
process.env.REEVIT_BASE_URL || 'https://api.reevit.io'
|
|
105
|
+
);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### API Key Types
|
|
109
|
+
|
|
110
|
+
| Key Prefix | Environment | Description |
|
|
111
|
+
|------------|-------------|-------------|
|
|
112
|
+
| `pfk_live_` | Production | Live transactions, real money |
|
|
113
|
+
| `pfk_test_` | Sandbox | Test transactions, no real charges |
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Payments
|
|
118
|
+
|
|
119
|
+
### Create Payment Intent
|
|
120
|
+
|
|
121
|
+
Create a new payment intent to initiate a transaction.
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
const payment = await reevit.payments.createIntent({
|
|
125
|
+
amount: 10000, // 100.00 in smallest currency unit
|
|
126
|
+
currency: 'GHS', // Currency code (GHS, NGN, KES, USD)
|
|
127
|
+
method: 'momo', // Payment method
|
|
128
|
+
country: 'GH', // ISO country code
|
|
129
|
+
customer_id: 'cust_123', // Optional: Your customer reference
|
|
130
|
+
metadata: { // Optional: Custom metadata
|
|
131
|
+
order_id: 'order_456',
|
|
132
|
+
product: 'Premium Plan'
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
#### Payment Methods by Country
|
|
138
|
+
|
|
139
|
+
| Country | Code | Supported Methods |
|
|
140
|
+
|---------|------|-------------------|
|
|
141
|
+
| Ghana | `GH` | `momo`, `card`, `bank_transfer` |
|
|
142
|
+
| Nigeria | `NG` | `card`, `bank_transfer`, `ussd` |
|
|
143
|
+
| Kenya | `KE` | `mpesa`, `card` |
|
|
144
|
+
|
|
145
|
+
#### Full Example with All Options
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
const payment = await reevit.payments.createIntent({
|
|
149
|
+
amount: 25000,
|
|
150
|
+
currency: 'NGN',
|
|
151
|
+
method: 'card',
|
|
152
|
+
country: 'NG',
|
|
153
|
+
customer_id: 'cust_789',
|
|
154
|
+
metadata: {
|
|
155
|
+
order_id: 'ORD-2024-001',
|
|
156
|
+
customer_email: 'customer@example.com',
|
|
157
|
+
description: 'Monthly subscription'
|
|
158
|
+
},
|
|
159
|
+
policy: {
|
|
160
|
+
prefer: ['paystack', 'flutterwave'], // Preferred providers
|
|
161
|
+
max_amount: 100000, // Max transaction amount
|
|
162
|
+
velocity_max_per_minute: 5 // Rate limiting
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
console.log('Payment created:', {
|
|
167
|
+
id: payment.id,
|
|
168
|
+
status: payment.status,
|
|
169
|
+
provider: payment.provider,
|
|
170
|
+
providerRef: payment.provider_ref_id
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Get Payment
|
|
175
|
+
|
|
176
|
+
Retrieve details of a specific payment.
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
const payment = await reevit.payments.get('pay_abc123');
|
|
180
|
+
|
|
181
|
+
console.log('Payment Details:', {
|
|
182
|
+
id: payment.id,
|
|
183
|
+
status: payment.status,
|
|
184
|
+
amount: payment.amount,
|
|
185
|
+
currency: payment.currency,
|
|
186
|
+
fee: payment.fee_amount,
|
|
187
|
+
net: payment.net_amount,
|
|
188
|
+
provider: payment.provider,
|
|
189
|
+
method: payment.method,
|
|
190
|
+
createdAt: payment.created_at
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Check routing attempts (useful for debugging)
|
|
194
|
+
if (payment.route && payment.route.length > 0) {
|
|
195
|
+
console.log('Routing attempts:');
|
|
196
|
+
payment.route.forEach((attempt, index) => {
|
|
197
|
+
console.log(` ${index + 1}. ${attempt.provider}: ${attempt.status}`);
|
|
198
|
+
if (attempt.error) {
|
|
199
|
+
console.log(` Error: ${attempt.error}`);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### List Payments
|
|
206
|
+
|
|
207
|
+
Retrieve a paginated list of payments.
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
// Basic listing (default: 50 payments)
|
|
211
|
+
const payments = await reevit.payments.list();
|
|
212
|
+
|
|
213
|
+
// With pagination
|
|
214
|
+
const page1 = await reevit.payments.list(10, 0); // First 10
|
|
215
|
+
const page2 = await reevit.payments.list(10, 10); // Next 10
|
|
216
|
+
|
|
217
|
+
// Process payments
|
|
218
|
+
payments.forEach(payment => {
|
|
219
|
+
console.log(`${payment.id}: ${payment.status} - ${payment.currency} ${payment.amount / 100}`);
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
#### Pagination Example
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
import { PaymentSummary } from '@reevit/node';
|
|
227
|
+
|
|
228
|
+
async function getAllPayments(): Promise<PaymentSummary[]> {
|
|
229
|
+
const allPayments: PaymentSummary[] = [];
|
|
230
|
+
const pageSize = 50;
|
|
231
|
+
let offset = 0;
|
|
232
|
+
let hasMore = true;
|
|
233
|
+
|
|
234
|
+
while (hasMore) {
|
|
235
|
+
const batch = await reevit.payments.list(pageSize, offset);
|
|
236
|
+
allPayments.push(...batch);
|
|
237
|
+
|
|
238
|
+
if (batch.length < pageSize) {
|
|
239
|
+
hasMore = false;
|
|
240
|
+
} else {
|
|
241
|
+
offset += pageSize;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return allPayments;
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Refund Payment
|
|
250
|
+
|
|
251
|
+
Issue a full or partial refund for a payment.
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
// Full refund
|
|
255
|
+
const fullRefund = await reevit.payments.refund('pay_abc123');
|
|
256
|
+
|
|
257
|
+
// Partial refund
|
|
258
|
+
const partialRefund = await reevit.payments.refund(
|
|
259
|
+
'pay_abc123',
|
|
260
|
+
2500, // Refund 25.00
|
|
261
|
+
'Customer requested' // Reason (optional)
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
console.log('Refund:', {
|
|
265
|
+
id: partialRefund.id,
|
|
266
|
+
paymentId: partialRefund.payment_id,
|
|
267
|
+
amount: partialRefund.amount,
|
|
268
|
+
status: partialRefund.status,
|
|
269
|
+
reason: partialRefund.reason
|
|
270
|
+
});
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## Connections
|
|
276
|
+
|
|
277
|
+
Connections represent your integrations with payment service providers (PSPs).
|
|
278
|
+
|
|
279
|
+
### Create Connection
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
// Paystack Connection (Nigeria)
|
|
283
|
+
const paystackConnection = await reevit.connections.create({
|
|
284
|
+
provider: 'paystack',
|
|
285
|
+
mode: 'live', // 'live' or 'test'
|
|
286
|
+
credentials: {
|
|
287
|
+
secret_key: 'sk_live_xxxxx'
|
|
288
|
+
},
|
|
289
|
+
labels: ['nigeria', 'primary'],
|
|
290
|
+
routing_hints: {
|
|
291
|
+
country_preference: ['NG'],
|
|
292
|
+
method_bias: { card: 'high', bank_transfer: 'medium' },
|
|
293
|
+
fallback_only: false
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Flutterwave Connection (Multi-country)
|
|
298
|
+
const flutterwaveConnection = await reevit.connections.create({
|
|
299
|
+
provider: 'flutterwave',
|
|
300
|
+
mode: 'live',
|
|
301
|
+
credentials: {
|
|
302
|
+
secret_key: 'FLWSECK-xxxxx',
|
|
303
|
+
encryption_key: 'xxxxx'
|
|
304
|
+
},
|
|
305
|
+
labels: ['multi-country', 'backup'],
|
|
306
|
+
routing_hints: {
|
|
307
|
+
country_preference: ['GH', 'NG', 'KE'],
|
|
308
|
+
fallback_only: true
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Hubtel Connection (Ghana)
|
|
313
|
+
const hubtelConnection = await reevit.connections.create({
|
|
314
|
+
provider: 'hubtel',
|
|
315
|
+
mode: 'live',
|
|
316
|
+
credentials: {
|
|
317
|
+
client_id: 'xxxxx',
|
|
318
|
+
client_secret: 'xxxxx'
|
|
319
|
+
},
|
|
320
|
+
labels: ['ghana', 'momo'],
|
|
321
|
+
routing_hints: {
|
|
322
|
+
country_preference: ['GH'],
|
|
323
|
+
method_bias: { momo: 'high' }
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// M-Pesa Connection (Kenya)
|
|
328
|
+
const mpesaConnection = await reevit.connections.create({
|
|
329
|
+
provider: 'mpesa',
|
|
330
|
+
mode: 'live',
|
|
331
|
+
credentials: {
|
|
332
|
+
consumer_key: 'xxxxx',
|
|
333
|
+
consumer_secret: 'xxxxx',
|
|
334
|
+
passkey: 'xxxxx',
|
|
335
|
+
shortcode: '174379',
|
|
336
|
+
initiator_name: 'testapi',
|
|
337
|
+
security_credential: 'xxxxx' // Pre-encrypted
|
|
338
|
+
},
|
|
339
|
+
labels: ['kenya', 'mpesa'],
|
|
340
|
+
routing_hints: {
|
|
341
|
+
country_preference: ['KE'],
|
|
342
|
+
method_bias: { mpesa: 'high' }
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// Monnify Connection (Nigeria)
|
|
347
|
+
const monnifyConnection = await reevit.connections.create({
|
|
348
|
+
provider: 'monnify',
|
|
349
|
+
mode: 'live',
|
|
350
|
+
credentials: {
|
|
351
|
+
api_key: 'xxxxx',
|
|
352
|
+
secret_key: 'xxxxx',
|
|
353
|
+
contract_code: 'xxxxx'
|
|
354
|
+
},
|
|
355
|
+
labels: ['nigeria', 'bank_transfer'],
|
|
356
|
+
routing_hints: {
|
|
357
|
+
country_preference: ['NG'],
|
|
358
|
+
method_bias: { bank_transfer: 'high' }
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### List Connections
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
const connections = await reevit.connections.list();
|
|
367
|
+
|
|
368
|
+
connections.forEach(conn => {
|
|
369
|
+
console.log(`${conn.provider} (${conn.mode}): ${conn.status}`);
|
|
370
|
+
console.log(` Labels: ${conn.labels.join(', ')}`);
|
|
371
|
+
console.log(` Countries: ${conn.routing_hints.country_preference.join(', ')}`);
|
|
372
|
+
});
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Test Connection
|
|
376
|
+
|
|
377
|
+
Verify credentials before creating a connection.
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
const isValid = await reevit.connections.test({
|
|
381
|
+
provider: 'paystack',
|
|
382
|
+
mode: 'live',
|
|
383
|
+
credentials: {
|
|
384
|
+
secret_key: 'sk_live_xxxxx'
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
if (isValid) {
|
|
389
|
+
console.log('Connection credentials are valid');
|
|
390
|
+
} else {
|
|
391
|
+
console.log('Invalid credentials');
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## Subscriptions
|
|
398
|
+
|
|
399
|
+
Manage recurring billing and subscriptions.
|
|
400
|
+
|
|
401
|
+
### Create Subscription
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
// Monthly subscription
|
|
405
|
+
const monthlySubscription = await reevit.subscriptions.create({
|
|
406
|
+
customer_id: 'cust_123',
|
|
407
|
+
plan_id: 'plan_premium',
|
|
408
|
+
amount: 9900, // 99.00 per month
|
|
409
|
+
currency: 'GHS',
|
|
410
|
+
method: 'momo',
|
|
411
|
+
interval: 'monthly',
|
|
412
|
+
metadata: {
|
|
413
|
+
plan_name: 'Premium',
|
|
414
|
+
features: ['unlimited_access', 'priority_support']
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// Yearly subscription
|
|
419
|
+
const yearlySubscription = await reevit.subscriptions.create({
|
|
420
|
+
customer_id: 'cust_456',
|
|
421
|
+
plan_id: 'plan_enterprise',
|
|
422
|
+
amount: 99900, // 999.00 per year
|
|
423
|
+
currency: 'NGN',
|
|
424
|
+
method: 'card',
|
|
425
|
+
interval: 'yearly',
|
|
426
|
+
metadata: {
|
|
427
|
+
plan_name: 'Enterprise',
|
|
428
|
+
discount_applied: '2_months_free'
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
console.log('Subscription created:', {
|
|
433
|
+
id: monthlySubscription.id,
|
|
434
|
+
status: monthlySubscription.status,
|
|
435
|
+
nextRenewal: monthlySubscription.next_renewal_at
|
|
436
|
+
});
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### List Subscriptions
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
const subscriptions = await reevit.subscriptions.list();
|
|
443
|
+
|
|
444
|
+
subscriptions.forEach(sub => {
|
|
445
|
+
console.log(`${sub.id}: ${sub.status}`);
|
|
446
|
+
console.log(` Customer: ${sub.customer_id}`);
|
|
447
|
+
console.log(` Amount: ${sub.currency} ${sub.amount / 100}/${sub.interval}`);
|
|
448
|
+
console.log(` Next renewal: ${sub.next_renewal_at}`);
|
|
449
|
+
});
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## Fraud Protection
|
|
455
|
+
|
|
456
|
+
Configure fraud rules to protect your transactions.
|
|
457
|
+
|
|
458
|
+
### Get Fraud Policy
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
const policy = await reevit.fraud.get();
|
|
462
|
+
|
|
463
|
+
console.log('Current Fraud Policy:', {
|
|
464
|
+
preferredProviders: policy.prefer,
|
|
465
|
+
maxAmount: policy.max_amount,
|
|
466
|
+
blockedBins: policy.blocked_bins,
|
|
467
|
+
allowedBins: policy.allowed_bins,
|
|
468
|
+
velocityLimit: policy.velocity_max_per_minute
|
|
469
|
+
});
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### Update Fraud Policy
|
|
473
|
+
|
|
474
|
+
```typescript
|
|
475
|
+
const updatedPolicy = await reevit.fraud.update({
|
|
476
|
+
prefer: ['paystack', 'flutterwave'],
|
|
477
|
+
max_amount: 500000, // Max 5,000.00
|
|
478
|
+
blocked_bins: ['123456', '654321'], // Block specific card BINs
|
|
479
|
+
allowed_bins: [], // Empty = allow all (except blocked)
|
|
480
|
+
velocity_max_per_minute: 10 // Max 10 transactions per minute
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
console.log('Policy updated successfully');
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
#### Fraud Policy Options
|
|
487
|
+
|
|
488
|
+
| Option | Type | Description |
|
|
489
|
+
|--------|------|-------------|
|
|
490
|
+
| `prefer` | `string[]` | Preferred provider order for routing |
|
|
491
|
+
| `max_amount` | `number` | Maximum transaction amount (in smallest unit) |
|
|
492
|
+
| `blocked_bins` | `string[]` | Card BIN prefixes to block |
|
|
493
|
+
| `allowed_bins` | `string[]` | Only allow these BINs (empty = allow all) |
|
|
494
|
+
| `velocity_max_per_minute` | `number` | Rate limit per customer |
|
|
495
|
+
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
## Error Handling
|
|
499
|
+
|
|
500
|
+
The SDK throws errors for failed API calls. Always wrap calls in try-catch blocks.
|
|
501
|
+
|
|
502
|
+
```typescript
|
|
503
|
+
import { AxiosError } from 'axios';
|
|
504
|
+
|
|
505
|
+
async function createPaymentSafely() {
|
|
506
|
+
try {
|
|
507
|
+
const payment = await reevit.payments.createIntent({
|
|
508
|
+
amount: 5000,
|
|
509
|
+
currency: 'GHS',
|
|
510
|
+
method: 'momo',
|
|
511
|
+
country: 'GH'
|
|
512
|
+
});
|
|
513
|
+
return payment;
|
|
514
|
+
} catch (error) {
|
|
515
|
+
if (error instanceof AxiosError) {
|
|
516
|
+
// API error
|
|
517
|
+
console.error('API Error:', {
|
|
518
|
+
status: error.response?.status,
|
|
519
|
+
message: error.response?.data?.message || error.message,
|
|
520
|
+
code: error.response?.data?.code
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
// Handle specific error codes
|
|
524
|
+
switch (error.response?.status) {
|
|
525
|
+
case 400:
|
|
526
|
+
console.error('Bad request - check your parameters');
|
|
527
|
+
break;
|
|
528
|
+
case 401:
|
|
529
|
+
console.error('Unauthorized - check your API key');
|
|
530
|
+
break;
|
|
531
|
+
case 403:
|
|
532
|
+
console.error('Forbidden - check your permissions');
|
|
533
|
+
break;
|
|
534
|
+
case 404:
|
|
535
|
+
console.error('Resource not found');
|
|
536
|
+
break;
|
|
537
|
+
case 429:
|
|
538
|
+
console.error('Rate limited - slow down requests');
|
|
539
|
+
break;
|
|
540
|
+
case 500:
|
|
541
|
+
console.error('Server error - try again later');
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
} else {
|
|
545
|
+
// Network or other error
|
|
546
|
+
console.error('Unexpected error:', error);
|
|
547
|
+
}
|
|
548
|
+
throw error;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### Retry Logic Example
|
|
554
|
+
|
|
555
|
+
```typescript
|
|
556
|
+
import { Payment, PaymentIntentRequest } from '@reevit/node';
|
|
557
|
+
import { AxiosError } from 'axios';
|
|
558
|
+
|
|
559
|
+
async function createPaymentWithRetry(
|
|
560
|
+
data: PaymentIntentRequest,
|
|
561
|
+
maxRetries = 3,
|
|
562
|
+
delayMs = 1000
|
|
563
|
+
): Promise<Payment> {
|
|
564
|
+
let lastError: Error | undefined;
|
|
565
|
+
|
|
566
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
567
|
+
try {
|
|
568
|
+
return await reevit.payments.createIntent(data);
|
|
569
|
+
} catch (error) {
|
|
570
|
+
lastError = error as Error;
|
|
571
|
+
|
|
572
|
+
if (error instanceof AxiosError) {
|
|
573
|
+
// Don't retry client errors (4xx)
|
|
574
|
+
if (error.response?.status && error.response.status < 500) {
|
|
575
|
+
throw error;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (attempt < maxRetries) {
|
|
580
|
+
console.log(`Attempt ${attempt} failed, retrying in ${delayMs}ms...`);
|
|
581
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
582
|
+
delayMs *= 2; // Exponential backoff
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
throw lastError;
|
|
588
|
+
}
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
## TypeScript Types
|
|
594
|
+
|
|
595
|
+
The SDK exports all types for use in your application.
|
|
596
|
+
|
|
597
|
+
```typescript
|
|
598
|
+
import {
|
|
599
|
+
// Payment types
|
|
600
|
+
Payment,
|
|
601
|
+
PaymentSummary,
|
|
602
|
+
PaymentIntentRequest,
|
|
603
|
+
PaymentRouteAttempt,
|
|
604
|
+
Refund,
|
|
605
|
+
|
|
606
|
+
// Connection types
|
|
607
|
+
Connection,
|
|
608
|
+
ConnectionRequest,
|
|
609
|
+
RoutingHints,
|
|
610
|
+
|
|
611
|
+
// Subscription types
|
|
612
|
+
Subscription,
|
|
613
|
+
SubscriptionRequest,
|
|
614
|
+
|
|
615
|
+
// Fraud types
|
|
616
|
+
FraudPolicy,
|
|
617
|
+
FraudPolicyInput
|
|
618
|
+
} from '@reevit/node';
|
|
619
|
+
|
|
620
|
+
// Use types in your code
|
|
621
|
+
function processPayment(payment: Payment): void {
|
|
622
|
+
console.log(`Processing ${payment.id}`);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function buildPaymentRequest(): PaymentIntentRequest {
|
|
626
|
+
return {
|
|
627
|
+
amount: 5000,
|
|
628
|
+
currency: 'GHS',
|
|
629
|
+
method: 'momo',
|
|
630
|
+
country: 'GH'
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
```
|
|
634
|
+
## Supported Providers
|
|
635
|
+
|
|
636
|
+
Reevit currently supports the following payment service providers:
|
|
637
|
+
|
|
638
|
+
| Provider | Countries | Methods | Features |
|
|
639
|
+
|----------|-----------|---------|----------|
|
|
640
|
+
| **Paystack** | Nigeria, Ghana | Card, Bank Transfer, USSD | Refunds, Webhooks |
|
|
641
|
+
| **Flutterwave** | Nigeria, Ghana, Kenya, +30 | Card, Mobile Money, Bank | Refunds, Webhooks |
|
|
642
|
+
| **Hubtel** | Ghana | Mobile Money | Webhooks |
|
|
643
|
+
| **M-Pesa** | Kenya | M-Pesa (STK Push) | Reversals, Webhooks |
|
|
644
|
+
| **Monnify** | Nigeria | Bank Transfer, Card | Refunds, Webhooks |
|
|
645
|
+
| **Stripe** | Global | Card (incl. Apple Pay/Google Pay via card rails) | Refunds, Webhooks |
|
|
646
|
+
|
|
647
|
+
> Stripe webhooks: configure the Reevit webhook URL in Stripe; store the signing secret in the connection credentials (`stripe_webhook_secret`, `webhook_secret`, or `signing_secret`). Ensure PaymentIntent metadata includes `org_id`, `connection_id`, and `payment_id` so events map correctly.
|
|
648
|
+
|
|
649
|
+
---
|
|
650
|
+
|
|
651
|
+
## Examples
|
|
652
|
+
|
|
653
|
+
### E-commerce Checkout
|
|
654
|
+
|
|
655
|
+
```typescript
|
|
656
|
+
import { Reevit, Payment, PaymentIntentRequest } from '@reevit/node';
|
|
657
|
+
|
|
658
|
+
const reevit = new Reevit(
|
|
659
|
+
process.env.REEVIT_API_KEY!,
|
|
660
|
+
process.env.REEVIT_ORG_ID!,
|
|
661
|
+
process.env.REEVIT_BASE_URL
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
interface CheckoutData {
|
|
665
|
+
orderId: string;
|
|
666
|
+
customerId: string;
|
|
667
|
+
amount: number;
|
|
668
|
+
currency: string;
|
|
669
|
+
country: string;
|
|
670
|
+
paymentMethod: string;
|
|
671
|
+
customerEmail: string;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
async function processCheckout(checkout: CheckoutData): Promise<Payment> {
|
|
675
|
+
const paymentRequest: PaymentIntentRequest = {
|
|
676
|
+
amount: checkout.amount,
|
|
677
|
+
currency: checkout.currency,
|
|
678
|
+
method: checkout.paymentMethod,
|
|
679
|
+
country: checkout.country,
|
|
680
|
+
customer_id: checkout.customerId,
|
|
681
|
+
metadata: {
|
|
682
|
+
order_id: checkout.orderId,
|
|
683
|
+
customer_email: checkout.customerEmail,
|
|
684
|
+
source: 'web_checkout'
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
const payment = await reevit.payments.createIntent(paymentRequest);
|
|
689
|
+
|
|
690
|
+
console.log(`Payment ${payment.id} created for order ${checkout.orderId}`);
|
|
691
|
+
console.log(`Status: ${payment.status}`);
|
|
692
|
+
console.log(`Provider: ${payment.provider}`);
|
|
693
|
+
|
|
694
|
+
return payment;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Usage
|
|
698
|
+
processCheckout({
|
|
699
|
+
orderId: 'ORD-2024-001',
|
|
700
|
+
customerId: 'cust_abc123',
|
|
701
|
+
amount: 15000, // 150.00
|
|
702
|
+
currency: 'GHS',
|
|
703
|
+
country: 'GH',
|
|
704
|
+
paymentMethod: 'momo',
|
|
705
|
+
customerEmail: 'customer@example.com'
|
|
706
|
+
});
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
### Subscription Service
|
|
710
|
+
|
|
711
|
+
```typescript
|
|
712
|
+
import { Reevit, Subscription } from '@reevit/node';
|
|
713
|
+
|
|
714
|
+
const reevit = new Reevit(
|
|
715
|
+
process.env.REEVIT_API_KEY!,
|
|
716
|
+
process.env.REEVIT_ORG_ID!
|
|
717
|
+
);
|
|
718
|
+
|
|
719
|
+
interface Plan {
|
|
720
|
+
id: string;
|
|
721
|
+
name: string;
|
|
722
|
+
monthlyPrice: number;
|
|
723
|
+
yearlyPrice: number;
|
|
724
|
+
currency: string;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const plans: Plan[] = [
|
|
728
|
+
{ id: 'basic', name: 'Basic', monthlyPrice: 1999, yearlyPrice: 19990, currency: 'GHS' },
|
|
729
|
+
{ id: 'pro', name: 'Pro', monthlyPrice: 4999, yearlyPrice: 49990, currency: 'GHS' },
|
|
730
|
+
{ id: 'enterprise', name: 'Enterprise', monthlyPrice: 9999, yearlyPrice: 99990, currency: 'GHS' }
|
|
731
|
+
];
|
|
732
|
+
|
|
733
|
+
async function subscribeToPlan(
|
|
734
|
+
customerId: string,
|
|
735
|
+
planId: string,
|
|
736
|
+
interval: 'monthly' | 'yearly',
|
|
737
|
+
paymentMethod: string
|
|
738
|
+
): Promise<Subscription> {
|
|
739
|
+
const plan = plans.find(p => p.id === planId);
|
|
740
|
+
if (!plan) {
|
|
741
|
+
throw new Error(`Plan ${planId} not found`);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const amount = interval === 'monthly' ? plan.monthlyPrice : plan.yearlyPrice;
|
|
745
|
+
|
|
746
|
+
const subscription = await reevit.subscriptions.create({
|
|
747
|
+
customer_id: customerId,
|
|
748
|
+
plan_id: planId,
|
|
749
|
+
amount,
|
|
750
|
+
currency: plan.currency,
|
|
751
|
+
method: paymentMethod,
|
|
752
|
+
interval,
|
|
753
|
+
metadata: {
|
|
754
|
+
plan_name: plan.name,
|
|
755
|
+
billing_interval: interval
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
console.log(`Subscription created: ${subscription.id}`);
|
|
760
|
+
console.log(`Next renewal: ${subscription.next_renewal_at}`);
|
|
761
|
+
|
|
762
|
+
return subscription;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Usage
|
|
766
|
+
subscribeToPlan('cust_123', 'pro', 'monthly', 'momo');
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
### Multi-Provider Setup
|
|
770
|
+
|
|
771
|
+
```typescript
|
|
772
|
+
import { Reevit } from '@reevit/node';
|
|
773
|
+
|
|
774
|
+
const reevit = new Reevit(
|
|
775
|
+
process.env.REEVIT_API_KEY!,
|
|
776
|
+
process.env.REEVIT_ORG_ID!
|
|
777
|
+
);
|
|
778
|
+
|
|
779
|
+
async function setupProviders() {
|
|
780
|
+
// Primary provider for Nigeria
|
|
781
|
+
await reevit.connections.create({
|
|
782
|
+
provider: 'paystack',
|
|
783
|
+
mode: 'live',
|
|
784
|
+
credentials: {
|
|
785
|
+
secret_key: process.env.PAYSTACK_SECRET_KEY!
|
|
786
|
+
},
|
|
787
|
+
labels: ['nigeria', 'primary'],
|
|
788
|
+
routing_hints: {
|
|
789
|
+
country_preference: ['NG'],
|
|
790
|
+
method_bias: { card: 'high' },
|
|
791
|
+
fallback_only: false
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
// Backup provider for Nigeria
|
|
796
|
+
await reevit.connections.create({
|
|
797
|
+
provider: 'flutterwave',
|
|
798
|
+
mode: 'live',
|
|
799
|
+
credentials: {
|
|
800
|
+
secret_key: process.env.FLUTTERWAVE_SECRET_KEY!,
|
|
801
|
+
encryption_key: process.env.FLUTTERWAVE_ENCRYPTION_KEY!
|
|
802
|
+
},
|
|
803
|
+
labels: ['nigeria', 'backup'],
|
|
804
|
+
routing_hints: {
|
|
805
|
+
country_preference: ['NG'],
|
|
806
|
+
fallback_only: true
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
// Primary provider for Ghana
|
|
811
|
+
await reevit.connections.create({
|
|
812
|
+
provider: 'hubtel',
|
|
813
|
+
mode: 'live',
|
|
814
|
+
credentials: {
|
|
815
|
+
client_id: process.env.HUBTEL_CLIENT_ID!,
|
|
816
|
+
client_secret: process.env.HUBTEL_CLIENT_SECRET!
|
|
817
|
+
},
|
|
818
|
+
labels: ['ghana', 'momo'],
|
|
819
|
+
routing_hints: {
|
|
820
|
+
country_preference: ['GH'],
|
|
821
|
+
method_bias: { momo: 'high' },
|
|
822
|
+
fallback_only: false
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
// Primary provider for Kenya
|
|
827
|
+
await reevit.connections.create({
|
|
828
|
+
provider: 'mpesa',
|
|
829
|
+
mode: 'live',
|
|
830
|
+
credentials: {
|
|
831
|
+
consumer_key: process.env.MPESA_CONSUMER_KEY!,
|
|
832
|
+
consumer_secret: process.env.MPESA_CONSUMER_SECRET!,
|
|
833
|
+
passkey: process.env.MPESA_PASSKEY!,
|
|
834
|
+
shortcode: process.env.MPESA_SHORTCODE!,
|
|
835
|
+
initiator_name: process.env.MPESA_INITIATOR_NAME!,
|
|
836
|
+
security_credential: process.env.MPESA_SECURITY_CREDENTIAL!
|
|
837
|
+
},
|
|
838
|
+
labels: ['kenya', 'mpesa'],
|
|
839
|
+
routing_hints: {
|
|
840
|
+
country_preference: ['KE'],
|
|
841
|
+
method_bias: { mpesa: 'high' },
|
|
842
|
+
fallback_only: false
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
console.log('All providers configured successfully');
|
|
847
|
+
|
|
848
|
+
// List all connections
|
|
849
|
+
const connections = await reevit.connections.list();
|
|
850
|
+
console.log(`Total connections: ${connections.length}`);
|
|
851
|
+
connections.forEach(c => {
|
|
852
|
+
console.log(` - ${c.provider} (${c.mode}): ${c.status}`);
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
setupProviders();
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
### Payment Status Polling
|
|
860
|
+
|
|
861
|
+
```typescript
|
|
862
|
+
import { Reevit, Payment } from '@reevit/node';
|
|
863
|
+
|
|
864
|
+
const reevit = new Reevit(
|
|
865
|
+
process.env.REEVIT_API_KEY!,
|
|
866
|
+
process.env.REEVIT_ORG_ID!
|
|
867
|
+
);
|
|
868
|
+
|
|
869
|
+
async function waitForPaymentCompletion(
|
|
870
|
+
paymentId: string,
|
|
871
|
+
timeoutMs = 300000, // 5 minutes
|
|
872
|
+
pollIntervalMs = 5000
|
|
873
|
+
): Promise<Payment> {
|
|
874
|
+
const startTime = Date.now();
|
|
875
|
+
const terminalStatuses = ['succeeded', 'failed', 'canceled'];
|
|
876
|
+
|
|
877
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
878
|
+
const payment = await reevit.payments.get(paymentId);
|
|
879
|
+
|
|
880
|
+
console.log(`Payment ${paymentId}: ${payment.status}`);
|
|
881
|
+
|
|
882
|
+
if (terminalStatuses.includes(payment.status)) {
|
|
883
|
+
return payment;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
throw new Error(`Payment ${paymentId} did not complete within ${timeoutMs}ms`);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Usage
|
|
893
|
+
async function processAndWait() {
|
|
894
|
+
const payment = await reevit.payments.createIntent({
|
|
895
|
+
amount: 5000,
|
|
896
|
+
currency: 'GHS',
|
|
897
|
+
method: 'momo',
|
|
898
|
+
country: 'GH'
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
console.log(`Created payment: ${payment.id}`);
|
|
902
|
+
|
|
903
|
+
const completedPayment = await waitForPaymentCompletion(payment.id);
|
|
904
|
+
|
|
905
|
+
if (completedPayment.status === 'succeeded') {
|
|
906
|
+
console.log('Payment successful!');
|
|
907
|
+
console.log(`Net amount: ${completedPayment.net_amount}`);
|
|
908
|
+
} else {
|
|
909
|
+
console.log(`Payment ${completedPayment.status}`);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
### Refund Management
|
|
915
|
+
|
|
916
|
+
```typescript
|
|
917
|
+
import { Reevit, Payment, Refund } from '@reevit/node';
|
|
918
|
+
|
|
919
|
+
const reevit = new Reevit(
|
|
920
|
+
process.env.REEVIT_API_KEY!,
|
|
921
|
+
process.env.REEVIT_ORG_ID!
|
|
922
|
+
);
|
|
923
|
+
|
|
924
|
+
interface RefundRequest {
|
|
925
|
+
paymentId: string;
|
|
926
|
+
amount?: number; // Optional for partial refund
|
|
927
|
+
reason: string;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
async function processRefund(request: RefundRequest): Promise<Refund> {
|
|
931
|
+
// First, verify the payment exists and is refundable
|
|
932
|
+
const payment = await reevit.payments.get(request.paymentId);
|
|
933
|
+
|
|
934
|
+
if (payment.status !== 'succeeded') {
|
|
935
|
+
throw new Error(`Cannot refund payment with status: ${payment.status}`);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Validate refund amount
|
|
939
|
+
const refundAmount = request.amount || payment.amount;
|
|
940
|
+
if (refundAmount > payment.amount) {
|
|
941
|
+
throw new Error(`Refund amount (${refundAmount}) exceeds payment amount (${payment.amount})`);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Process the refund
|
|
945
|
+
const refund = await reevit.payments.refund(
|
|
946
|
+
request.paymentId,
|
|
947
|
+
request.amount,
|
|
948
|
+
request.reason
|
|
949
|
+
);
|
|
950
|
+
|
|
951
|
+
console.log(`Refund processed: ${refund.id}`);
|
|
952
|
+
console.log(` Amount: ${refund.amount}`);
|
|
953
|
+
console.log(` Status: ${refund.status}`);
|
|
954
|
+
console.log(` Reason: ${refund.reason}`);
|
|
955
|
+
|
|
956
|
+
return refund;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// Full refund
|
|
960
|
+
processRefund({
|
|
961
|
+
paymentId: 'pay_abc123',
|
|
962
|
+
reason: 'Customer requested cancellation'
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
// Partial refund
|
|
966
|
+
processRefund({
|
|
967
|
+
paymentId: 'pay_abc123',
|
|
968
|
+
amount: 2500, // Refund 25.00
|
|
969
|
+
reason: 'Partial order cancellation'
|
|
970
|
+
});
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
### Express.js Integration
|
|
974
|
+
|
|
975
|
+
```typescript
|
|
976
|
+
import express from 'express';
|
|
977
|
+
import { Reevit, PaymentIntentRequest } from '@reevit/node';
|
|
978
|
+
|
|
979
|
+
const app = express();
|
|
980
|
+
app.use(express.json());
|
|
981
|
+
|
|
982
|
+
const reevit = new Reevit(
|
|
983
|
+
process.env.REEVIT_API_KEY!,
|
|
984
|
+
process.env.REEVIT_ORG_ID!,
|
|
985
|
+
process.env.REEVIT_BASE_URL
|
|
986
|
+
);
|
|
987
|
+
|
|
988
|
+
// Create payment endpoint
|
|
989
|
+
app.post('/api/payments', async (req, res) => {
|
|
990
|
+
try {
|
|
991
|
+
const { amount, currency, method, country, customerId, orderId } = req.body;
|
|
992
|
+
|
|
993
|
+
const payment = await reevit.payments.createIntent({
|
|
994
|
+
amount,
|
|
995
|
+
currency,
|
|
996
|
+
method,
|
|
997
|
+
country,
|
|
998
|
+
customer_id: customerId,
|
|
999
|
+
metadata: { order_id: orderId }
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
res.json({
|
|
1003
|
+
success: true,
|
|
1004
|
+
payment: {
|
|
1005
|
+
id: payment.id,
|
|
1006
|
+
status: payment.status,
|
|
1007
|
+
provider: payment.provider
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
} catch (error: any) {
|
|
1011
|
+
res.status(error.response?.status || 500).json({
|
|
1012
|
+
success: false,
|
|
1013
|
+
error: error.response?.data?.message || error.message
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
// Get payment status endpoint
|
|
1019
|
+
app.get('/api/payments/:id', async (req, res) => {
|
|
1020
|
+
try {
|
|
1021
|
+
const payment = await reevit.payments.get(req.params.id);
|
|
1022
|
+
res.json({ success: true, payment });
|
|
1023
|
+
} catch (error: any) {
|
|
1024
|
+
res.status(error.response?.status || 500).json({
|
|
1025
|
+
success: false,
|
|
1026
|
+
error: error.response?.data?.message || error.message
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
// Refund endpoint
|
|
1032
|
+
app.post('/api/payments/:id/refund', async (req, res) => {
|
|
1033
|
+
try {
|
|
1034
|
+
const { amount, reason } = req.body;
|
|
1035
|
+
const refund = await reevit.payments.refund(req.params.id, amount, reason);
|
|
1036
|
+
res.json({ success: true, refund });
|
|
1037
|
+
} catch (error: any) {
|
|
1038
|
+
res.status(error.response?.status || 500).json({
|
|
1039
|
+
success: false,
|
|
1040
|
+
error: error.response?.data?.message || error.message
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
app.listen(3000, () => {
|
|
1046
|
+
console.log('Server running on port 3000');
|
|
1047
|
+
});
|
|
1048
|
+
```
|
|
1049
|
+
|
|
1050
|
+
### Next.js API Routes
|
|
1051
|
+
|
|
1052
|
+
```typescript
|
|
1053
|
+
// pages/api/payments/create.ts
|
|
1054
|
+
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
1055
|
+
import { Reevit } from '@reevit/node';
|
|
1056
|
+
|
|
1057
|
+
const reevit = new Reevit(
|
|
1058
|
+
process.env.REEVIT_API_KEY!,
|
|
1059
|
+
process.env.REEVIT_ORG_ID!,
|
|
1060
|
+
process.env.REEVIT_BASE_URL
|
|
1061
|
+
);
|
|
1062
|
+
|
|
1063
|
+
export default async function handler(
|
|
1064
|
+
req: NextApiRequest,
|
|
1065
|
+
res: NextApiResponse
|
|
1066
|
+
) {
|
|
1067
|
+
if (req.method !== 'POST') {
|
|
1068
|
+
return res.status(405).json({ error: 'Method not allowed' });
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
try {
|
|
1072
|
+
const { amount, currency, method, country, customerId } = req.body;
|
|
1073
|
+
|
|
1074
|
+
const payment = await reevit.payments.createIntent({
|
|
1075
|
+
amount,
|
|
1076
|
+
currency,
|
|
1077
|
+
method,
|
|
1078
|
+
country,
|
|
1079
|
+
customer_id: customerId
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
res.status(200).json({ payment });
|
|
1083
|
+
} catch (error: any) {
|
|
1084
|
+
res.status(500).json({
|
|
1085
|
+
error: error.response?.data?.message || error.message
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
```
|
|
1090
|
+
|
|
1091
|
+
---
|
|
1092
|
+
|
|
1093
|
+
## Webhook Verification
|
|
1094
|
+
|
|
1095
|
+
Reevit sends webhooks to notify your application of payment events. Always verify webhook signatures to ensure authenticity.
|
|
1096
|
+
|
|
1097
|
+
### Understanding Webhooks
|
|
1098
|
+
|
|
1099
|
+
There are **two types of webhooks** in Reevit:
|
|
1100
|
+
|
|
1101
|
+
1. **Inbound Webhooks (PSP → Reevit)**: Webhooks from payment providers to Reevit. You configure these in the PSP dashboard (e.g., Paystack). Reevit handles these automatically.
|
|
1102
|
+
|
|
1103
|
+
2. **Outbound Webhooks (Reevit → Your App)**: Webhooks from Reevit to your application. You configure these in the Reevit Dashboard and create a handler in your app.
|
|
1104
|
+
|
|
1105
|
+
### Signature Format
|
|
1106
|
+
|
|
1107
|
+
Reevit signs webhooks with HMAC-SHA256:
|
|
1108
|
+
- **Header**: `X-Reevit-Signature: sha256=<hex-signature>`
|
|
1109
|
+
- **Signature**: `HMAC-SHA256(request_body, signing_secret)`
|
|
1110
|
+
|
|
1111
|
+
### Getting Your Signing Secret
|
|
1112
|
+
|
|
1113
|
+
1. Go to **Reevit Dashboard > Developers > Webhooks**
|
|
1114
|
+
2. Configure your webhook endpoint URL
|
|
1115
|
+
3. Copy the signing secret (starts with `whsec_`)
|
|
1116
|
+
4. Add to your environment: `REEVIT_WEBHOOK_SECRET=whsec_xxx...`
|
|
1117
|
+
|
|
1118
|
+
### Next.js App Router Webhook Handler
|
|
1119
|
+
|
|
1120
|
+
```typescript
|
|
1121
|
+
// app/api/webhooks/reevit/route.ts
|
|
1122
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
1123
|
+
import crypto from "crypto";
|
|
1124
|
+
|
|
1125
|
+
// Webhook payload types
|
|
1126
|
+
interface PaymentData {
|
|
1127
|
+
id: string;
|
|
1128
|
+
status: string;
|
|
1129
|
+
amount: number;
|
|
1130
|
+
currency: string;
|
|
1131
|
+
provider: string;
|
|
1132
|
+
customer_id?: string;
|
|
1133
|
+
metadata?: Record<string, string>;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
interface SubscriptionData {
|
|
1137
|
+
id: string;
|
|
1138
|
+
customer_id: string;
|
|
1139
|
+
plan_id: string;
|
|
1140
|
+
status: string;
|
|
1141
|
+
amount: number;
|
|
1142
|
+
currency: string;
|
|
1143
|
+
interval: string;
|
|
1144
|
+
next_renewal_at?: string;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
interface WebhookPayload {
|
|
1148
|
+
id: string;
|
|
1149
|
+
type: string;
|
|
1150
|
+
org_id: string;
|
|
1151
|
+
created_at: string;
|
|
1152
|
+
data?: PaymentData | SubscriptionData;
|
|
1153
|
+
message?: string;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* Verify webhook signature using HMAC-SHA256
|
|
1158
|
+
* Always verify signatures in production to prevent spoofed webhooks
|
|
1159
|
+
*/
|
|
1160
|
+
function verifySignature(payload: string, signature: string, secret: string): boolean {
|
|
1161
|
+
if (!signature.startsWith("sha256=")) return false;
|
|
1162
|
+
|
|
1163
|
+
const expected = crypto
|
|
1164
|
+
.createHmac("sha256", secret)
|
|
1165
|
+
.update(payload)
|
|
1166
|
+
.digest("hex");
|
|
1167
|
+
|
|
1168
|
+
const received = signature.slice(7);
|
|
1169
|
+
if (received.length !== expected.length) return false;
|
|
1170
|
+
|
|
1171
|
+
return crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
export async function POST(request: NextRequest) {
|
|
1175
|
+
try {
|
|
1176
|
+
const rawBody = await request.text();
|
|
1177
|
+
const signature = request.headers.get("x-reevit-signature") || "";
|
|
1178
|
+
const secret = process.env.REEVIT_WEBHOOK_SECRET;
|
|
1179
|
+
|
|
1180
|
+
// Verify signature (required in production)
|
|
1181
|
+
if (secret && !verifySignature(rawBody, signature, secret)) {
|
|
1182
|
+
console.error("[Webhook] Invalid signature");
|
|
1183
|
+
return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
const event: WebhookPayload = JSON.parse(rawBody);
|
|
1187
|
+
console.log(`[Webhook] Received: ${event.type} (${event.id})`);
|
|
1188
|
+
|
|
1189
|
+
// Handle different event types
|
|
1190
|
+
switch (event.type) {
|
|
1191
|
+
// Test event
|
|
1192
|
+
case "reevit.webhook.test":
|
|
1193
|
+
console.log("[Webhook] Test received:", event.message);
|
|
1194
|
+
break;
|
|
1195
|
+
|
|
1196
|
+
// Payment events
|
|
1197
|
+
case "payment.succeeded":
|
|
1198
|
+
await handlePaymentSucceeded(event.data as PaymentData);
|
|
1199
|
+
break;
|
|
1200
|
+
|
|
1201
|
+
case "payment.failed":
|
|
1202
|
+
await handlePaymentFailed(event.data as PaymentData);
|
|
1203
|
+
break;
|
|
1204
|
+
|
|
1205
|
+
case "payment.refunded":
|
|
1206
|
+
await handlePaymentRefunded(event.data as PaymentData);
|
|
1207
|
+
break;
|
|
1208
|
+
|
|
1209
|
+
case "payment.pending":
|
|
1210
|
+
console.log(`[Webhook] Payment pending: ${(event.data as PaymentData)?.id}`);
|
|
1211
|
+
break;
|
|
1212
|
+
|
|
1213
|
+
// Subscription events
|
|
1214
|
+
case "subscription.created":
|
|
1215
|
+
await handleSubscriptionCreated(event.data as SubscriptionData);
|
|
1216
|
+
break;
|
|
1217
|
+
|
|
1218
|
+
case "subscription.renewed":
|
|
1219
|
+
await handleSubscriptionRenewed(event.data as SubscriptionData);
|
|
1220
|
+
break;
|
|
1221
|
+
|
|
1222
|
+
case "subscription.canceled":
|
|
1223
|
+
await handleSubscriptionCanceled(event.data as SubscriptionData);
|
|
1224
|
+
break;
|
|
1225
|
+
|
|
1226
|
+
case "subscription.paused":
|
|
1227
|
+
console.log(`[Webhook] Subscription paused: ${(event.data as SubscriptionData)?.id}`);
|
|
1228
|
+
break;
|
|
1229
|
+
|
|
1230
|
+
default:
|
|
1231
|
+
console.log(`[Webhook] Unhandled event: ${event.type}`);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
return NextResponse.json({ received: true });
|
|
1235
|
+
} catch (error) {
|
|
1236
|
+
console.error("[Webhook] Processing error:", error);
|
|
1237
|
+
return NextResponse.json({ error: "Webhook processing failed" }, { status: 500 });
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// Payment handlers
|
|
1242
|
+
async function handlePaymentSucceeded(data: PaymentData) {
|
|
1243
|
+
const orderId = data.metadata?.order_id;
|
|
1244
|
+
console.log(`[Webhook] Payment succeeded: ${data.id} for order ${orderId}`);
|
|
1245
|
+
|
|
1246
|
+
// TODO: Implement your business logic
|
|
1247
|
+
// - Update order status to "paid"
|
|
1248
|
+
// - Send confirmation email to customer
|
|
1249
|
+
// - Trigger fulfillment process
|
|
1250
|
+
// - Update inventory
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
async function handlePaymentFailed(data: PaymentData) {
|
|
1254
|
+
console.log(`[Webhook] Payment failed: ${data.id}`);
|
|
1255
|
+
|
|
1256
|
+
// TODO: Implement your business logic
|
|
1257
|
+
// - Update order status to "payment_failed"
|
|
1258
|
+
// - Send notification to customer
|
|
1259
|
+
// - Allow retry
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
async function handlePaymentRefunded(data: PaymentData) {
|
|
1263
|
+
const orderId = data.metadata?.order_id;
|
|
1264
|
+
console.log(`[Webhook] Payment refunded: ${data.id} for order ${orderId}`);
|
|
1265
|
+
|
|
1266
|
+
// TODO: Implement your business logic
|
|
1267
|
+
// - Update order status to "refunded"
|
|
1268
|
+
// - Restore inventory if applicable
|
|
1269
|
+
// - Send refund confirmation email
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// Subscription handlers
|
|
1273
|
+
async function handleSubscriptionCreated(data: SubscriptionData) {
|
|
1274
|
+
console.log(`[Webhook] Subscription created: ${data.id} for customer ${data.customer_id}`);
|
|
1275
|
+
|
|
1276
|
+
// TODO: Implement your business logic
|
|
1277
|
+
// - Grant access to subscription features
|
|
1278
|
+
// - Send welcome email
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
async function handleSubscriptionRenewed(data: SubscriptionData) {
|
|
1282
|
+
console.log(`[Webhook] Subscription renewed: ${data.id}`);
|
|
1283
|
+
|
|
1284
|
+
// TODO: Implement your business logic
|
|
1285
|
+
// - Extend access period
|
|
1286
|
+
// - Send renewal confirmation
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
async function handleSubscriptionCanceled(data: SubscriptionData) {
|
|
1290
|
+
console.log(`[Webhook] Subscription canceled: ${data.id}`);
|
|
1291
|
+
|
|
1292
|
+
// TODO: Implement your business logic
|
|
1293
|
+
// - Revoke access at end of billing period
|
|
1294
|
+
// - Send cancellation confirmation
|
|
1295
|
+
}
|
|
1296
|
+
```
|
|
1297
|
+
|
|
1298
|
+
### Express.js Webhook Handler
|
|
1299
|
+
|
|
1300
|
+
```typescript
|
|
1301
|
+
import express from 'express';
|
|
1302
|
+
import crypto from 'crypto';
|
|
1303
|
+
|
|
1304
|
+
const app = express();
|
|
1305
|
+
|
|
1306
|
+
function verifySignature(payload: string, signature: string, secret: string): boolean {
|
|
1307
|
+
if (!signature.startsWith('sha256=')) return false;
|
|
1308
|
+
|
|
1309
|
+
const expected = crypto
|
|
1310
|
+
.createHmac('sha256', secret)
|
|
1311
|
+
.update(payload)
|
|
1312
|
+
.digest('hex');
|
|
1313
|
+
|
|
1314
|
+
const received = signature.slice(7);
|
|
1315
|
+
if (received.length !== expected.length) return false;
|
|
1316
|
+
|
|
1317
|
+
return crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// IMPORTANT: Use raw body for signature verification
|
|
1321
|
+
app.post('/webhooks/reevit', express.raw({ type: 'application/json' }), async (req, res) => {
|
|
1322
|
+
try {
|
|
1323
|
+
const signature = req.headers['x-reevit-signature'] as string;
|
|
1324
|
+
const payload = req.body.toString();
|
|
1325
|
+
const secret = process.env.REEVIT_WEBHOOK_SECRET!;
|
|
1326
|
+
|
|
1327
|
+
if (secret && !verifySignature(payload, signature, secret)) {
|
|
1328
|
+
return res.status(401).json({ error: 'Invalid signature' });
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
const event = JSON.parse(payload);
|
|
1332
|
+
console.log(`[Webhook] Received: ${event.type}`);
|
|
1333
|
+
|
|
1334
|
+
switch (event.type) {
|
|
1335
|
+
case 'reevit.webhook.test':
|
|
1336
|
+
console.log('[Webhook] Test received:', event.message);
|
|
1337
|
+
break;
|
|
1338
|
+
case 'payment.succeeded':
|
|
1339
|
+
// Fulfill order, send confirmation email
|
|
1340
|
+
break;
|
|
1341
|
+
case 'payment.failed':
|
|
1342
|
+
// Notify customer, allow retry
|
|
1343
|
+
break;
|
|
1344
|
+
case 'payment.refunded':
|
|
1345
|
+
// Update order status
|
|
1346
|
+
break;
|
|
1347
|
+
case 'subscription.renewed':
|
|
1348
|
+
// Extend access
|
|
1349
|
+
break;
|
|
1350
|
+
case 'subscription.canceled':
|
|
1351
|
+
// Revoke access
|
|
1352
|
+
break;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
res.status(200).json({ received: true });
|
|
1356
|
+
} catch (error) {
|
|
1357
|
+
console.error('[Webhook] Error:', error);
|
|
1358
|
+
res.status(500).json({ error: 'Webhook processing failed' });
|
|
1359
|
+
}
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1362
|
+
app.listen(3000, () => console.log('Server running on port 3000'));
|
|
1363
|
+
```
|
|
1364
|
+
|
|
1365
|
+
### Environment Variables
|
|
1366
|
+
|
|
1367
|
+
```bash
|
|
1368
|
+
# .env.local
|
|
1369
|
+
REEVIT_API_KEY=pfk_live_xxx
|
|
1370
|
+
REEVIT_ORG_ID=org_xxx
|
|
1371
|
+
REEVIT_WEBHOOK_SECRET=whsec_xxx # Get from Dashboard > Developers > Webhooks
|
|
1372
|
+
```
|
|
1373
|
+
|
|
1374
|
+
---
|
|
1375
|
+
|
|
1376
|
+
## Support
|
|
1377
|
+
|
|
1378
|
+
- **Documentation**: [https://docs.reevit.io](https://docs.reevit.io)
|
|
1379
|
+
- **API Reference**: [https://api.reevit.io/docs](https://api.reevit.io/docs)
|
|
1380
|
+
- **GitHub Issues**: [https://github.com/reevit/reevit-node/issues](https://github.com/reevit/reevit-node/issues)
|
|
1381
|
+
- **Email**: support@reevit.io
|
|
1382
|
+
|
|
1383
|
+
## License
|
|
1384
|
+
|
|
1385
|
+
MIT License - see [LICENSE](LICENSE) for details.
|