@sikka/aps 0.0.2 โ 0.0.4
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 +21 -21
- package/README.md +913 -913
- package/dist/react/index.d.mts +1 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +13 -3
- package/dist/react/index.mjs +3 -3
- package/package.json +75 -75
- package/dist/chunk-FLFFXAST.mjs +0 -408
- package/dist/chunk-TGVOU6OE.mjs +0 -409
package/README.md
CHANGED
|
@@ -1,913 +1,913 @@
|
|
|
1
|
-
# Amazon Payment Services SDK
|
|
2
|
-
|
|
3
|
-
<div align="center">
|
|
4
|
-
|
|
5
|
-
[](https://www.npmjs.com/package/amazon-payment-services)
|
|
6
|
-
[](https://opensource.org/licenses/MIT)
|
|
7
|
-
|
|
8
|
-
**A Stripe-like developer-friendly SDK for Amazon Payment Services integration**
|
|
9
|
-
|
|
10
|
-
</div>
|
|
11
|
-
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
## Features
|
|
15
|
-
|
|
16
|
-
- ๐ **Stripe-like DX** - Intuitive API design inspired by Stripe
|
|
17
|
-
- โ๏ธ **React Hooks** - useAPS, useCheckout, usePayment hooks for seamless React integration
|
|
18
|
-
- ๐งฉ **React Components** - Pre-built components: HostedCheckoutButton, ErrorDisplay, PaymentStatus
|
|
19
|
-
- ๐ **Payment Links** - Generate shareable payment URLs via official APS API
|
|
20
|
-
- ๐ **Hosted Checkout** - Full-featured payment pages with 3DS support
|
|
21
|
-
- ๐ณ **Tokenization** - Secure card storage for recurring payments
|
|
22
|
-
- ๐ **Webhooks** - Real-time payment event notifications with signature verification
|
|
23
|
-
- ๐ฐ **Payment Management** - Capture, refund, void, and query transactions
|
|
24
|
-
- ๐ **Multi-Currency** - Full support for Middle East currencies (SAR, AED, KWD, etc.)
|
|
25
|
-
- ๐ **Secure** - Proper SHA-256 signature calculation per APS documentation
|
|
26
|
-
- ๐ฆ **TypeScript** - Full TypeScript support with comprehensive types
|
|
27
|
-
- ๐งช **Testing Utilities** - Mock client, test cards, and webhook helpers
|
|
28
|
-
- ๐ก๏ธ **Error Handling** - Detailed error messages with suggested actions
|
|
29
|
-
|
|
30
|
-
## Installation
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
npm install @sikka/aps
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## Quick Start
|
|
37
|
-
|
|
38
|
-
### 1. Initialize the Client
|
|
39
|
-
|
|
40
|
-
```typescript
|
|
41
|
-
import APS from '@sikka/aps';
|
|
42
|
-
|
|
43
|
-
const aps = new APS({
|
|
44
|
-
merchantId: process.env.APS_MERCHANT_ID,
|
|
45
|
-
accessCode: process.env.APS_ACCESS_CODE,
|
|
46
|
-
requestSecret: process.env.APS_REQUEST_SECRET,
|
|
47
|
-
responseSecret: process.env.APS_RESPONSE_SECRET,
|
|
48
|
-
environment: 'sandbox' // or 'production'
|
|
49
|
-
});
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
### TypeScript Types
|
|
53
|
-
|
|
54
|
-
All types are exported from the package:
|
|
55
|
-
|
|
56
|
-
```typescript
|
|
57
|
-
import type {
|
|
58
|
-
APSConfig,
|
|
59
|
-
PaymentResponse,
|
|
60
|
-
PaymentLinkResponse,
|
|
61
|
-
TokenizedCard,
|
|
62
|
-
RefundResponse,
|
|
63
|
-
CaptureResponse,
|
|
64
|
-
VoidResponse,
|
|
65
|
-
WebhookEvent,
|
|
66
|
-
Order,
|
|
67
|
-
Customer,
|
|
68
|
-
TransactionStatus,
|
|
69
|
-
PaymentMethod
|
|
70
|
-
} from '@sikka/aps';
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### 2. Create a Payment Link
|
|
74
|
-
|
|
75
|
-
```typescript
|
|
76
|
-
const link = await aps.paymentLinks.create({
|
|
77
|
-
order: {
|
|
78
|
-
id: 'order_123',
|
|
79
|
-
amount: 10000, // 100.00 SAR (in fils/cents)
|
|
80
|
-
currency: 'SAR',
|
|
81
|
-
description: 'Premium Plan Subscription',
|
|
82
|
-
customer: {
|
|
83
|
-
email: 'customer@example.com',
|
|
84
|
-
name: 'Ahmed Al-Saud',
|
|
85
|
-
phone: '+966501234567'
|
|
86
|
-
}
|
|
87
|
-
},
|
|
88
|
-
tokenValidityHours: 24 // Link expires in 24 hours
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
console.log('Payment URL:', link.url);
|
|
92
|
-
// Send this link to your customer via email, SMS, etc.
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### 3. Create Hosted Checkout
|
|
96
|
-
|
|
97
|
-
```typescript
|
|
98
|
-
const checkout = await aps.hostedCheckout.create({
|
|
99
|
-
order: {
|
|
100
|
-
id: 'order_456',
|
|
101
|
-
amount: 25000, // 250.00 SAR
|
|
102
|
-
currency: 'SAR',
|
|
103
|
-
description: 'E-commerce Purchase',
|
|
104
|
-
customer: {
|
|
105
|
-
email: 'customer@example.com', // Required for hosted checkout
|
|
106
|
-
name: 'Ahmed Al-Saud'
|
|
107
|
-
}
|
|
108
|
-
},
|
|
109
|
-
returnUrl: 'https://yoursite.com/api/payment/return', // Use API route to handle POST data
|
|
110
|
-
allowedPaymentMethods: ['card', 'apple_pay', 'mada']
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
// Redirect user to the hosted checkout page
|
|
114
|
-
if (checkout.redirectForm) {
|
|
115
|
-
// Create a form and submit to redirectForm.url
|
|
116
|
-
// The customer will be redirected back to returnUrl after payment
|
|
117
|
-
}
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
**Important:** Hosted Checkout uses `return_url` for all redirects (success, failure, or cancel).
|
|
121
|
-
|
|
122
|
-
**โ ๏ธ POST Data:** APS sends the transaction result as a **POST request** (form data), not GET query parameters. You need an API route to receive the POST data:
|
|
123
|
-
|
|
124
|
-
```typescript
|
|
125
|
-
// app/api/payment/return/route.ts
|
|
126
|
-
export async function POST(request: NextRequest) {
|
|
127
|
-
const formData = await request.formData();
|
|
128
|
-
|
|
129
|
-
// Convert to query parameters
|
|
130
|
-
const params = new URLSearchParams();
|
|
131
|
-
formData.forEach((value, key) => {
|
|
132
|
-
if (typeof value === 'string') params.append(key, value);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// Redirect to result page
|
|
136
|
-
return NextResponse.redirect(
|
|
137
|
-
new URL(`/payment/result?${params.toString()}`, request.url)
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
Then use the API route as your `returnUrl`:
|
|
143
|
-
```typescript
|
|
144
|
-
returnUrl: 'https://yoursite.com/api/payment/return'
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
### 4. Custom Payment Page (Non-PCI)
|
|
148
|
-
|
|
149
|
-
Create a custom payment form while APS handles PCI compliance through tokenization.
|
|
150
|
-
|
|
151
|
-
```typescript
|
|
152
|
-
// Step 1: Get tokenization form (backend)
|
|
153
|
-
const tokenizationForm = aps.customPaymentPage.getTokenizationForm({
|
|
154
|
-
returnUrl: 'https://yoursite.com/api/token-result'
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
// Step 2: Render form in your frontend
|
|
158
|
-
// <form method="POST" action={tokenizationForm.url}>
|
|
159
|
-
// {Object.entries(tokenizationForm.params).map(([key, value]) => (
|
|
160
|
-
// <input type="hidden" name={key} value={value} />
|
|
161
|
-
// ))}
|
|
162
|
-
// <input name="card_number" placeholder="Card Number" required />
|
|
163
|
-
// <input name="expiry_date" placeholder="MM/YY" required />
|
|
164
|
-
// <input name="card_security_code" placeholder="CVV" required />
|
|
165
|
-
// <input name="card_holder_name" placeholder="Card Holder Name" required />
|
|
166
|
-
// <button type="submit">Pay</button>
|
|
167
|
-
// </form>
|
|
168
|
-
|
|
169
|
-
// Step 3: Handle response at returnUrl (backend)
|
|
170
|
-
// Response contains: token_name
|
|
171
|
-
|
|
172
|
-
// Step 4: Charge with token (backend)
|
|
173
|
-
const payment = await aps.customPaymentPage.chargeWithToken({
|
|
174
|
-
order: {
|
|
175
|
-
id: 'order_123',
|
|
176
|
-
amount: 10000, // 100.00 SAR
|
|
177
|
-
currency: 'SAR',
|
|
178
|
-
description: 'Product Purchase'
|
|
179
|
-
},
|
|
180
|
-
tokenName: 'token_from_step_3',
|
|
181
|
-
customerEmail: 'customer@example.com'
|
|
182
|
-
});
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
**How it works:**
|
|
186
|
-
1. Get tokenization form from APS
|
|
187
|
-
2. Render custom form with your branding
|
|
188
|
-
3. Customer enters card details
|
|
189
|
-
4. Form submits to APS (Non-PCI compliant)
|
|
190
|
-
5. APS returns token to your returnUrl
|
|
191
|
-
6. Use token to charge payments server-to-server
|
|
192
|
-
|
|
193
|
-
```typescript
|
|
194
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
195
|
-
import { getAPSClient } from './aps-client';
|
|
196
|
-
|
|
197
|
-
export async function POST(request: NextRequest) {
|
|
198
|
-
const body = await request.json();
|
|
199
|
-
const signature = request.headers.get('x-aps-signature') || '';
|
|
200
|
-
|
|
201
|
-
const aps = getAPSClient();
|
|
202
|
-
|
|
203
|
-
// Verify webhook signature
|
|
204
|
-
const event = aps.webhooks.constructEvent(body, signature);
|
|
205
|
-
|
|
206
|
-
// Handle different event types
|
|
207
|
-
switch (event.type) {
|
|
208
|
-
case 'payment.success':
|
|
209
|
-
// Update order status, send confirmation email, etc.
|
|
210
|
-
console.log('Payment successful:', event.data);
|
|
211
|
-
break;
|
|
212
|
-
case 'payment.failed':
|
|
213
|
-
// Notify customer, retry logic, etc.
|
|
214
|
-
console.log('Payment failed:', event.data);
|
|
215
|
-
break;
|
|
216
|
-
case 'refund.success':
|
|
217
|
-
// Process refund confirmation
|
|
218
|
-
console.log('Refund processed:', event.data);
|
|
219
|
-
break;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return NextResponse.json({ received: true });
|
|
223
|
-
}
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
### 7. Payment Management
|
|
227
|
-
|
|
228
|
-
```typescript
|
|
229
|
-
// Capture an authorized payment (for two-step checkout)
|
|
230
|
-
const capture = await aps.payments.capture({
|
|
231
|
-
transactionId: 'txn_123',
|
|
232
|
-
amount: 10000 // Optional, defaults to full amount
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
// Refund a payment (full or partial)
|
|
236
|
-
const refund = await aps.payments.refund({
|
|
237
|
-
transactionId: 'txn_123',
|
|
238
|
-
amount: 5000, // Partial refund (50.00 SAR)
|
|
239
|
-
reason: 'Customer request'
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
// Void an authorization (before capture)
|
|
243
|
-
const voided = await aps.payments.void({
|
|
244
|
-
transactionId: 'txn_123',
|
|
245
|
-
reason: 'Order cancelled'
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
// Query transaction status
|
|
249
|
-
const transaction = await aps.payments.query({
|
|
250
|
-
transactionId: 'txn_123'
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
console.log('Transaction status:', transaction.status);
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
## React Hooks & Components
|
|
257
|
-
|
|
258
|
-
### Setup Provider
|
|
259
|
-
|
|
260
|
-
```tsx
|
|
261
|
-
import { APSProvider } from '@sikka/aps/react';
|
|
262
|
-
import APS from '@sikka/aps';
|
|
263
|
-
|
|
264
|
-
const aps = new APS({
|
|
265
|
-
merchantId: process.env.APS_MERCHANT_ID!,
|
|
266
|
-
accessCode: process.env.APS_ACCESS_CODE!,
|
|
267
|
-
requestSecret: process.env.APS_REQUEST_SECRET!,
|
|
268
|
-
responseSecret: process.env.APS_RESPONSE_SECRET!,
|
|
269
|
-
environment: 'sandbox'
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
function App() {
|
|
273
|
-
return (
|
|
274
|
-
<APSProvider client={aps}>
|
|
275
|
-
<YourApp />
|
|
276
|
-
</APSProvider>
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
### useAPS Hook
|
|
282
|
-
|
|
283
|
-
Access the APS client from any component:
|
|
284
|
-
|
|
285
|
-
```tsx
|
|
286
|
-
import { useAPS } from '@sikka/aps/react';
|
|
287
|
-
|
|
288
|
-
function MyComponent() {
|
|
289
|
-
const aps = useAPS();
|
|
290
|
-
|
|
291
|
-
const handleClick = async () => {
|
|
292
|
-
const link = await aps.paymentLinks.create({...});
|
|
293
|
-
console.log(link.url);
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
### useCheckout Hook
|
|
299
|
-
|
|
300
|
-
Manage hosted checkout state:
|
|
301
|
-
|
|
302
|
-
```tsx
|
|
303
|
-
import { useCheckout } from '@sikka/aps/react';
|
|
304
|
-
|
|
305
|
-
function CheckoutPage() {
|
|
306
|
-
const { createCheckout, redirectToCheckout, isLoading, error } = useCheckout();
|
|
307
|
-
|
|
308
|
-
const handleCheckout = async () => {
|
|
309
|
-
await createCheckout({
|
|
310
|
-
order: { id: 'order_123', amount: 10000, currency: 'SAR', customer: { email: 'test@example.com' } },
|
|
311
|
-
returnUrl: '/payment-result'
|
|
312
|
-
});
|
|
313
|
-
redirectToCheckout();
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
### usePayment Hook
|
|
319
|
-
|
|
320
|
-
Manage payment lifecycle:
|
|
321
|
-
|
|
322
|
-
```tsx
|
|
323
|
-
import { usePayment } from '@sikka/aps/react';
|
|
324
|
-
|
|
325
|
-
function PaymentPage() {
|
|
326
|
-
const { createPaymentLink, confirmPayment, status } = usePayment();
|
|
327
|
-
|
|
328
|
-
const handlePayment = async () => {
|
|
329
|
-
const link = await createPaymentLink({ order: {...} });
|
|
330
|
-
window.open(link.url, '_blank');
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
// After customer returns
|
|
334
|
-
const finalStatus = await confirmPayment({ transactionId: 'txn_123' });
|
|
335
|
-
}
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
### HostedCheckoutButton Component
|
|
339
|
-
|
|
340
|
-
Complete checkout button with built-in state management:
|
|
341
|
-
|
|
342
|
-
```tsx
|
|
343
|
-
import { HostedCheckoutButton } from '@sikka/aps/react';
|
|
344
|
-
|
|
345
|
-
<HostedCheckoutButton
|
|
346
|
-
order={{
|
|
347
|
-
id: 'order_123',
|
|
348
|
-
amount: 10000,
|
|
349
|
-
currency: 'SAR',
|
|
350
|
-
customer: { email: 'customer@example.com' }
|
|
351
|
-
}}
|
|
352
|
-
returnUrl="https://yoursite.com/result"
|
|
353
|
-
>
|
|
354
|
-
Pay Now
|
|
355
|
-
</HostedCheckoutButton>
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
### ErrorDisplay Component
|
|
359
|
-
|
|
360
|
-
Display APS errors with helpful messages:
|
|
361
|
-
|
|
362
|
-
```tsx
|
|
363
|
-
import { ErrorDisplay } from '@sikka/aps/react';
|
|
364
|
-
|
|
365
|
-
<ErrorDisplay
|
|
366
|
-
error={error}
|
|
367
|
-
onRetry={() => handleRetry()}
|
|
368
|
-
onDismiss={() => setError(null)}
|
|
369
|
-
/>
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
### PaymentStatus Component
|
|
373
|
-
|
|
374
|
-
Display payment status with icons:
|
|
375
|
-
|
|
376
|
-
```tsx
|
|
377
|
-
import { PaymentStatus } from '@sikka/aps/react';
|
|
378
|
-
|
|
379
|
-
<PaymentStatus
|
|
380
|
-
status="captured"
|
|
381
|
-
amount={10000}
|
|
382
|
-
currency="SAR"
|
|
383
|
-
transactionId="txn_123"
|
|
384
|
-
/>
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
## Error Handling
|
|
388
|
-
|
|
389
|
-
### Get Error Details
|
|
390
|
-
|
|
391
|
-
```typescript
|
|
392
|
-
import { getErrorDetails, isRetryableError } from '@sikka/aps';
|
|
393
|
-
|
|
394
|
-
try {
|
|
395
|
-
await aps.paymentLinks.create({...});
|
|
396
|
-
} catch (error) {
|
|
397
|
-
if (error.code) {
|
|
398
|
-
const details = getErrorDetails(error.code);
|
|
399
|
-
console.log(details.message); // Human-readable message
|
|
400
|
-
console.log(details.action); // Suggested action
|
|
401
|
-
console.log(details.category); // Error category
|
|
402
|
-
console.log(details.documentation); // Link to docs
|
|
403
|
-
|
|
404
|
-
if (isRetryableError(error.code)) {
|
|
405
|
-
await retryPayment();
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
```
|
|
410
|
-
|
|
411
|
-
### Response Codes
|
|
412
|
-
|
|
413
|
-
```typescript
|
|
414
|
-
import { ResponseCodes, ErrorCategories, categorizeError } from '@sikka/aps';
|
|
415
|
-
|
|
416
|
-
if (response.response_code === ResponseCodes.SUCCESS) {
|
|
417
|
-
// Payment successful
|
|
418
|
-
} else if (response.response_code === ResponseCodes.INSUFFICIENT_FUNDS) {
|
|
419
|
-
// Handle insufficient funds
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const category = categorizeError(response.response_code);
|
|
423
|
-
```
|
|
424
|
-
|
|
425
|
-
### Validation
|
|
426
|
-
|
|
427
|
-
```typescript
|
|
428
|
-
import { validators } from '@sikka/aps';
|
|
429
|
-
|
|
430
|
-
// Validate individual fields
|
|
431
|
-
validators.isValidMerchantReference('order_123');
|
|
432
|
-
validators.isValidAmount(10000, 'SAR');
|
|
433
|
-
validators.isValidCardNumber('4111111111111111');
|
|
434
|
-
|
|
435
|
-
// Validate all at once
|
|
436
|
-
const result = validators.validatePaymentParams({
|
|
437
|
-
merchant_reference: 'order_123',
|
|
438
|
-
amount: 10000,
|
|
439
|
-
currency: 'SAR',
|
|
440
|
-
customer_email: 'customer@example.com'
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
if (!result.valid) {
|
|
444
|
-
result.errors.forEach(err => console.log(err.field, err.error));
|
|
445
|
-
}
|
|
446
|
-
```
|
|
447
|
-
|
|
448
|
-
## Testing
|
|
449
|
-
|
|
450
|
-
### Mock Client
|
|
451
|
-
|
|
452
|
-
```typescript
|
|
453
|
-
import { mockAPS } from '@sikka/aps/test';
|
|
454
|
-
|
|
455
|
-
const aps = mockAPS({ simulate: 'success', delay: 100 });
|
|
456
|
-
const link = await aps.paymentLinks.create({...});
|
|
457
|
-
|
|
458
|
-
// Simulate errors
|
|
459
|
-
const declinedAps = mockAPS({ simulate: 'declined' });
|
|
460
|
-
await expect(declinedAps.paymentLinks.create({...}))
|
|
461
|
-
.rejects.toThrow('Transaction declined');
|
|
462
|
-
```
|
|
463
|
-
|
|
464
|
-
### Test Cards
|
|
465
|
-
|
|
466
|
-
```typescript
|
|
467
|
-
import { TestCards, testCards } from '@sikka/aps/test';
|
|
468
|
-
|
|
469
|
-
TestCards.VISA.SUCCESS // '4111111111111111'
|
|
470
|
-
TestCards.VISA.DECLINED // '4000000000000002'
|
|
471
|
-
TestCards.MADA.SUCCESS // '5297410000000002'
|
|
472
|
-
|
|
473
|
-
testCards.visa.success;
|
|
474
|
-
testCards.visa.declined;
|
|
475
|
-
```
|
|
476
|
-
|
|
477
|
-
### Mock Webhooks
|
|
478
|
-
|
|
479
|
-
```typescript
|
|
480
|
-
import { createMockWebhookPayload, createMockSignature } from '@sikka/aps/test';
|
|
481
|
-
|
|
482
|
-
const payload = createMockWebhookPayload('payment.success', {
|
|
483
|
-
merchant_reference: 'order_123',
|
|
484
|
-
amount: '10000',
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
const signature = createMockSignature(payload, 'your-secret');
|
|
488
|
-
```
|
|
489
|
-
|
|
490
|
-
## Complete API Reference
|
|
491
|
-
|
|
492
|
-
### Configuration
|
|
493
|
-
|
|
494
|
-
| Parameter | Type | Required | Default | Description |
|
|
495
|
-
|-----------|------|----------|---------|-------------|
|
|
496
|
-
| `merchantId` | string | โ | - | Your APS merchant ID from dashboard |
|
|
497
|
-
| `accessCode` | string | โ | - | API access code from Integration Settings |
|
|
498
|
-
| `requestSecret` | string | โ | - | SHA Request Phrase for signing |
|
|
499
|
-
| `responseSecret` | string | โ | - | SHA Response Phrase for verification |
|
|
500
|
-
| `environment` | string | | `'sandbox'` | `'sandbox'` or `'production'` |
|
|
501
|
-
| `currency` | string | | `'USD'` | Default currency code (e.g., `'SAR'`, `'AED'`) |
|
|
502
|
-
| `language` | string | | `'en'` | Interface language: `'en'` or `'ar'` |
|
|
503
|
-
|
|
504
|
-
### Payment Links Module
|
|
505
|
-
|
|
506
|
-
Create payment links that can be shared via email, SMS, WhatsApp, etc.
|
|
507
|
-
|
|
508
|
-
```typescript
|
|
509
|
-
await aps.paymentLinks.create(options: PaymentLinkOptions)
|
|
510
|
-
```
|
|
511
|
-
|
|
512
|
-
**Options:**
|
|
513
|
-
|
|
514
|
-
| Field | Type | Required | Description |
|
|
515
|
-
|-------|------|----------|-------------|
|
|
516
|
-
| `order.id` | string | | Unique order reference (auto-generated if not provided) |
|
|
517
|
-
| `order.amount` | number | โ | Amount in **fils/cents** (e.g., 10000 = 100.00 SAR) |
|
|
518
|
-
| `order.currency` | string | โ | Three-letter ISO currency code |
|
|
519
|
-
| `order.description` | string | | Order description shown to customer |
|
|
520
|
-
| `order.customer.email` | string | โ | Customer email for notifications |
|
|
521
|
-
| `order.customer.name` | string | | Customer full name |
|
|
522
|
-
| `order.customer.phone` | string | | Customer phone (international format) |
|
|
523
|
-
| `tokenValidityHours` | number | | Link expiry in hours (default: 24) |
|
|
524
|
-
| `recurring` | boolean | | Enable for recurring/subscription payments |
|
|
525
|
-
| `allowedPaymentMethods` | array | | Restrict to specific methods: `['card', 'mada', 'apple_pay']` |
|
|
526
|
-
| `metadata` | object | | Custom key-value pairs to attach to order |
|
|
527
|
-
|
|
528
|
-
**Returns:**
|
|
529
|
-
|
|
530
|
-
```typescript
|
|
531
|
-
{
|
|
532
|
-
url: string; // Payment link URL to share
|
|
533
|
-
linkId: string; // APS payment link ID
|
|
534
|
-
orderId: string; // Your order reference
|
|
535
|
-
expiresAt?: Date; // Expiry timestamp
|
|
536
|
-
rawResponse: object; // Full APS API response
|
|
537
|
-
}
|
|
538
|
-
```
|
|
539
|
-
|
|
540
|
-
### Hosted Checkout Module
|
|
541
|
-
|
|
542
|
-
Redirect customers to a secure APS-hosted payment page.
|
|
543
|
-
|
|
544
|
-
```typescript
|
|
545
|
-
await aps.hostedCheckout.create(options: HostedCheckoutOptions)
|
|
546
|
-
```
|
|
547
|
-
|
|
548
|
-
**Options:**
|
|
549
|
-
|
|
550
|
-
| Field | Type | Required | Description |
|
|
551
|
-
|-------|------|----------|-------------|
|
|
552
|
-
| `order.id` | string | | Unique order reference (auto-generated if not provided) |
|
|
553
|
-
| `order.amount` | number | โ | Amount in **fils/cents** (e.g., 10000 = 100.00 SAR) |
|
|
554
|
-
| `order.currency` | string | โ | Three-letter ISO currency code |
|
|
555
|
-
| `order.description` | string | | Order description shown to customer |
|
|
556
|
-
| `order.customer.email` | string | โ | **Required** - Customer email for payment page |
|
|
557
|
-
| `order.customer.name` | string | | Customer full name |
|
|
558
|
-
| `order.customer.phone` | string | | Customer phone (international format) |
|
|
559
|
-
| `returnUrl` | string | โ | Redirect URL after payment (success, failure, or cancel) |
|
|
560
|
-
| `tokenize` | boolean | | Allow card saving for future payments (`remember_me`) |
|
|
561
|
-
| `allowedPaymentMethods` | array | | Limit payment methods: `['card', 'mada', 'apple_pay']` |
|
|
562
|
-
| `hideShipping` | boolean | | Hide shipping information fields |
|
|
563
|
-
|
|
564
|
-
**Returns:**
|
|
565
|
-
|
|
566
|
-
```typescript
|
|
567
|
-
{
|
|
568
|
-
transactionId: string;
|
|
569
|
-
orderId: string;
|
|
570
|
-
status: 'pending' | 'authorized' | 'captured' | 'failed';
|
|
571
|
-
amount: number;
|
|
572
|
-
currency: string;
|
|
573
|
-
redirectForm: {
|
|
574
|
-
url: string; // APS payment page URL
|
|
575
|
-
method: 'POST';
|
|
576
|
-
params: Record<string, string>; // All form parameters including signature
|
|
577
|
-
};
|
|
578
|
-
rawResponse: object;
|
|
579
|
-
}
|
|
580
|
-
```
|
|
581
|
-
|
|
582
|
-
**Important Notes:**
|
|
583
|
-
- `customer_email` is **required** by APS
|
|
584
|
-
- Use `returnUrl` (not `successUrl`/`failureUrl`) - APS redirects to this URL for all outcomes
|
|
585
|
-
- The response contains a `redirectForm` that should be submitted via POST to redirect the customer
|
|
586
|
-
|
|
587
|
-
### Tokenization Module
|
|
588
|
-
|
|
589
|
-
Securely tokenize card details for recurring payments.
|
|
590
|
-
|
|
591
|
-
```typescript
|
|
592
|
-
await aps.tokens.create(options: TokenizeCardOptions)
|
|
593
|
-
```
|
|
594
|
-
|
|
595
|
-
**Options:**
|
|
596
|
-
|
|
597
|
-
| Field | Type | Required | Description |
|
|
598
|
-
|-------|------|----------|-------------|
|
|
599
|
-
| `cardNumber` | string | โ | Full card number |
|
|
600
|
-
| `expiryMonth` | string | โ | 2-digit month (MM) |
|
|
601
|
-
| `expiryYear` | string | โ | 2-digit year (YY) |
|
|
602
|
-
| `cvv` | string | โ | Card security code |
|
|
603
|
-
| `cardholderName` | string | | Name as shown on card |
|
|
604
|
-
|
|
605
|
-
**Returns:**
|
|
606
|
-
|
|
607
|
-
```typescript
|
|
608
|
-
{
|
|
609
|
-
token: string; // Card token for future use
|
|
610
|
-
last4: string; // Last 4 digits
|
|
611
|
-
brand: string; // Card brand (visa, mastercard, mada, etc.)
|
|
612
|
-
expiryMonth: string;
|
|
613
|
-
expiryYear: string;
|
|
614
|
-
}
|
|
615
|
-
```
|
|
616
|
-
|
|
617
|
-
**Additional Methods:**
|
|
618
|
-
|
|
619
|
-
```typescript
|
|
620
|
-
// Verify if token is still valid
|
|
621
|
-
const isValid = await aps.tokens.verify('tok_xxxxx');
|
|
622
|
-
|
|
623
|
-
// Delete/invalidate a token
|
|
624
|
-
await aps.tokens.delete('tok_xxxxx');
|
|
625
|
-
```
|
|
626
|
-
|
|
627
|
-
### Custom Payment Page Module (Non-PCI)
|
|
628
|
-
|
|
629
|
-
Create custom payment forms while maintaining PCI compliance through tokenization.
|
|
630
|
-
|
|
631
|
-
```typescript
|
|
632
|
-
// Get tokenization form
|
|
633
|
-
const form = aps.customPaymentPage.getTokenizationForm({
|
|
634
|
-
returnUrl: 'https://yoursite.com/api/token-result',
|
|
635
|
-
merchantReference: 'order_123'
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
// Render form in your frontend with custom styling
|
|
639
|
-
// Form submits to APS endpoint for secure tokenization
|
|
640
|
-
|
|
641
|
-
// After receiving token, charge with:
|
|
642
|
-
await aps.customPaymentPage.chargeWithToken({
|
|
643
|
-
order: {
|
|
644
|
-
id: string,
|
|
645
|
-
amount: number,
|
|
646
|
-
currency: string,
|
|
647
|
-
description?: string
|
|
648
|
-
},
|
|
649
|
-
tokenName: string, // Token from tokenization response
|
|
650
|
-
customerEmail: string, // Required
|
|
651
|
-
customerName?: string,
|
|
652
|
-
customerPhone?: string,
|
|
653
|
-
returnUrl?: string
|
|
654
|
-
});
|
|
655
|
-
```
|
|
656
|
-
|
|
657
|
-
**Returns (chargeWithToken):**
|
|
658
|
-
|
|
659
|
-
```typescript
|
|
660
|
-
{
|
|
661
|
-
transactionId: string;
|
|
662
|
-
orderId: string;
|
|
663
|
-
status: 'captured' | 'authorized' | 'pending' | 'failed';
|
|
664
|
-
amount: number;
|
|
665
|
-
currency: string;
|
|
666
|
-
paymentMethod?: string;
|
|
667
|
-
authenticationUrl?: string; // 3DS URL if required
|
|
668
|
-
redirectForm?: { ... }; // For 3DS redirect
|
|
669
|
-
message?: string;
|
|
670
|
-
rawResponse: object;
|
|
671
|
-
}
|
|
672
|
-
```
|
|
673
|
-
|
|
674
|
-
**Important Notes:**
|
|
675
|
-
- Tokenization form collects card details securely (Non-PCI compliant)
|
|
676
|
-
- Card data submits directly to APS, never touches your server
|
|
677
|
-
- Token received at returnUrl can be used for immediate or future charges
|
|
678
|
-
- Use `chargeWithToken()` for server-to-server payment processing
|
|
679
|
-
|
|
680
|
-
### Payments Module (Management)
|
|
681
|
-
|
|
682
|
-
Manage existing transactions.
|
|
683
|
-
|
|
684
|
-
```typescript
|
|
685
|
-
// Capture authorized payment
|
|
686
|
-
await aps.payments.capture({
|
|
687
|
-
transactionId: string,
|
|
688
|
-
amount?: number // Optional, full capture if not specified
|
|
689
|
-
});
|
|
690
|
-
|
|
691
|
-
// Refund payment
|
|
692
|
-
await aps.payments.refund({
|
|
693
|
-
transactionId: string,
|
|
694
|
-
amount?: number, // Optional, full refund if not specified
|
|
695
|
-
reason?: string
|
|
696
|
-
});
|
|
697
|
-
|
|
698
|
-
// Void authorization
|
|
699
|
-
await aps.payments.void({
|
|
700
|
-
transactionId: string,
|
|
701
|
-
reason?: string
|
|
702
|
-
});
|
|
703
|
-
|
|
704
|
-
// Query transaction
|
|
705
|
-
await aps.payments.query({
|
|
706
|
-
transactionId?: string,
|
|
707
|
-
orderId?: string
|
|
708
|
-
});
|
|
709
|
-
```
|
|
710
|
-
|
|
711
|
-
### Webhooks Module
|
|
712
|
-
|
|
713
|
-
Verify and parse webhook events.
|
|
714
|
-
|
|
715
|
-
```typescript
|
|
716
|
-
// In your API route handler
|
|
717
|
-
const signature = request.headers.get('x-aps-signature') || '';
|
|
718
|
-
const payload = request.body;
|
|
719
|
-
|
|
720
|
-
// Verify signature
|
|
721
|
-
const isValid = aps.webhooks.verifySignature(
|
|
722
|
-
JSON.stringify(payload),
|
|
723
|
-
signature
|
|
724
|
-
);
|
|
725
|
-
|
|
726
|
-
if (!isValid) {
|
|
727
|
-
return res.status(401).send('Invalid signature');
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
// Construct event
|
|
731
|
-
const event = aps.webhooks.constructEvent(payload);
|
|
732
|
-
|
|
733
|
-
// Handle by type
|
|
734
|
-
switch (event.type) {
|
|
735
|
-
case 'payment.success':
|
|
736
|
-
case 'payment.failed':
|
|
737
|
-
case 'payment.pending':
|
|
738
|
-
case 'refund.success':
|
|
739
|
-
case 'refund.failed':
|
|
740
|
-
case 'chargeback.created':
|
|
741
|
-
case 'subscription.renewed':
|
|
742
|
-
case 'subscription.cancelled':
|
|
743
|
-
}
|
|
744
|
-
```
|
|
745
|
-
|
|
746
|
-
**Webhook Event Structure:**
|
|
747
|
-
|
|
748
|
-
```typescript
|
|
749
|
-
{
|
|
750
|
-
id: string;
|
|
751
|
-
type: WebhookEventType;
|
|
752
|
-
timestamp: Date;
|
|
753
|
-
data: {
|
|
754
|
-
transactionId: string;
|
|
755
|
-
orderId: string;
|
|
756
|
-
amount: number;
|
|
757
|
-
currency: string;
|
|
758
|
-
status: TransactionStatus;
|
|
759
|
-
paymentMethod?: string;
|
|
760
|
-
metadata?: Record<string, any>;
|
|
761
|
-
};
|
|
762
|
-
rawPayload: object;
|
|
763
|
-
}
|
|
764
|
-
```
|
|
765
|
-
|
|
766
|
-
## Supported Payment Methods
|
|
767
|
-
|
|
768
|
-
| Method | Regions | Description |
|
|
769
|
-
|--------|---------|-------------|
|
|
770
|
-
| `card` | All | Visa, Mastercard, American Express |
|
|
771
|
-
| `apple_pay` | All | Apple Pay |
|
|
772
|
-
| `mada` | Saudi Arabia | MADA debit cards |
|
|
773
|
-
| `stc_pay` | Saudi Arabia | STC Pay wallet |
|
|
774
|
-
| `knet` | Kuwait | KNET payment |
|
|
775
|
-
| `naps` | Qatar | NAPS payment |
|
|
776
|
-
| `fawry` | Egypt | Fawry cash payment |
|
|
777
|
-
| `meeza` | Egypt | Meeza cards |
|
|
778
|
-
| `sadad` | Saudi Arabia | Sadad payment |
|
|
779
|
-
| `aman` | Egypt | Aman installment |
|
|
780
|
-
|
|
781
|
-
## Environment Variables
|
|
782
|
-
|
|
783
|
-
Create a `.env.local` file:
|
|
784
|
-
|
|
785
|
-
```bash
|
|
786
|
-
# APS Merchant Credentials (from your APS dashboard)
|
|
787
|
-
APS_MERCHANT_ID=your_merchant_id
|
|
788
|
-
APS_ACCESS_CODE=your_access_code
|
|
789
|
-
APS_REQUEST_SECRET=your_request_secret
|
|
790
|
-
APS_RESPONSE_SECRET=your_response_secret
|
|
791
|
-
|
|
792
|
-
# APS Configuration
|
|
793
|
-
APS_ENVIRONMENT=sandbox # Use 'production' for live
|
|
794
|
-
APS_CURRENCY=SAR # Default currency
|
|
795
|
-
APS_LANGUAGE=en # Default language
|
|
796
|
-
|
|
797
|
-
# Your App URL
|
|
798
|
-
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
799
|
-
```
|
|
800
|
-
|
|
801
|
-
## Supported Currencies
|
|
802
|
-
|
|
803
|
-
- **SAR** - Saudi Riyal
|
|
804
|
-
- **AED** - UAE Dirham
|
|
805
|
-
- **KWD** - Kuwaiti Dinar
|
|
806
|
-
- **QAR** - Qatari Riyal
|
|
807
|
-
- **BHD** - Bahraini Dinar
|
|
808
|
-
- **OMR** - Omani Rial
|
|
809
|
-
- **JOD** - Jordanian Dinar
|
|
810
|
-
- **EGP** - Egyptian Pound
|
|
811
|
-
- **USD** - US Dollar
|
|
812
|
-
- **EUR** - Euro
|
|
813
|
-
|
|
814
|
-
## Error Handling
|
|
815
|
-
|
|
816
|
-
```typescript
|
|
817
|
-
import { APSException, APSError } from '@sikka/aps';
|
|
818
|
-
|
|
819
|
-
try {
|
|
820
|
-
const link = await aps.paymentLinks.create({ ... });
|
|
821
|
-
} catch (error) {
|
|
822
|
-
if (error instanceof APSException) {
|
|
823
|
-
console.error('Error Code:', error.code);
|
|
824
|
-
console.error('Message:', error.message);
|
|
825
|
-
console.error('Status:', error.statusCode);
|
|
826
|
-
console.error('Details:', error.rawResponse);
|
|
827
|
-
|
|
828
|
-
// Handle specific errors
|
|
829
|
-
switch (error.code) {
|
|
830
|
-
case 'PAYMENT_LINK_ERROR':
|
|
831
|
-
// Handle payment link creation failure
|
|
832
|
-
break;
|
|
833
|
-
case 'SIGNATURE_ERROR':
|
|
834
|
-
// Handle signature mismatch
|
|
835
|
-
break;
|
|
836
|
-
case 'INVALID_CREDENTIALS':
|
|
837
|
-
// Handle authentication failure
|
|
838
|
-
break;
|
|
839
|
-
}
|
|
840
|
-
} else {
|
|
841
|
-
console.error('Unexpected error:', error);
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
```
|
|
845
|
-
|
|
846
|
-
## Common Response Codes
|
|
847
|
-
|
|
848
|
-
| Code | Message | Description |
|
|
849
|
-
|------|---------|-------------|
|
|
850
|
-
| `00000` | Success | Transaction successful |
|
|
851
|
-
| `48000` | Success | Payment link created successfully |
|
|
852
|
-
| `10030` | Authentication failed | 3DS authentication failed |
|
|
853
|
-
| `00008` | Signature mismatch | Invalid signature |
|
|
854
|
-
| `00002` | Invalid parameter | Parameter format error |
|
|
855
|
-
| `14000` | Declined | Card declined by issuer |
|
|
856
|
-
|
|
857
|
-
## Security Best Practices
|
|
858
|
-
|
|
859
|
-
1. **Never expose credentials** - Always use environment variables
|
|
860
|
-
2. **Server-side only** - All APS API calls must be server-side
|
|
861
|
-
3. **Verify webhooks** - Always verify webhook signatures
|
|
862
|
-
4. **Use HTTPS** - Required for production
|
|
863
|
-
5. **PCI Compliance** - Use hosted checkout or payment links to avoid PCI scope
|
|
864
|
-
6. **Validate amounts** - Always validate amounts server-side before creating payments
|
|
865
|
-
|
|
866
|
-
## Testing
|
|
867
|
-
|
|
868
|
-
### Sandbox Environment
|
|
869
|
-
|
|
870
|
-
```typescript
|
|
871
|
-
const aps = new APS({
|
|
872
|
-
// ... credentials
|
|
873
|
-
environment: 'sandbox'
|
|
874
|
-
});
|
|
875
|
-
```
|
|
876
|
-
|
|
877
|
-
**Test Cards** (use in sandbox):
|
|
878
|
-
|
|
879
|
-
| Card Number | Type | CVV | Expiry |
|
|
880
|
-
|-------------|------|-----|--------|
|
|
881
|
-
| 4111 1111 1111 1111 | Visa | 123 | 12/25 |
|
|
882
|
-
| 5297 4100 0000 0002 | MADA | 123 | 12/25 |
|
|
883
|
-
| 5100 0000 0000 0008 | Mastercard | 123 | 12/25 |
|
|
884
|
-
|
|
885
|
-
### Production
|
|
886
|
-
|
|
887
|
-
```typescript
|
|
888
|
-
const aps = new APS({
|
|
889
|
-
// ... production credentials
|
|
890
|
-
environment: 'production'
|
|
891
|
-
});
|
|
892
|
-
```
|
|
893
|
-
|
|
894
|
-
## Next.js Integration Example
|
|
895
|
-
|
|
896
|
-
See the included test app for a complete working example:
|
|
897
|
-
|
|
898
|
-
```bash
|
|
899
|
-
# Run the test app
|
|
900
|
-
pnpm dev
|
|
901
|
-
```
|
|
902
|
-
|
|
903
|
-
Visit `http://localhost:3000` to test payment creation and `http://localhost:3000/docs` for documentation.
|
|
904
|
-
|
|
905
|
-
## License
|
|
906
|
-
|
|
907
|
-
MIT License - see [LICENSE](LICENSE) for details.
|
|
908
|
-
|
|
909
|
-
## Support
|
|
910
|
-
|
|
911
|
-
For APS-specific issues, contact Amazon Payment Services merchant support:
|
|
912
|
-
- Email: merchantsupport-ps@amazon.com
|
|
913
|
-
- Documentation: https://paymentservices.amazon.com/docs
|
|
1
|
+
# Amazon Payment Services SDK
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/amazon-payment-services)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
**A Stripe-like developer-friendly SDK for Amazon Payment Services integration**
|
|
9
|
+
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- ๐ **Stripe-like DX** - Intuitive API design inspired by Stripe
|
|
17
|
+
- โ๏ธ **React Hooks** - useAPS, useCheckout, usePayment hooks for seamless React integration
|
|
18
|
+
- ๐งฉ **React Components** - Pre-built components: HostedCheckoutButton, ErrorDisplay, PaymentStatus
|
|
19
|
+
- ๐ **Payment Links** - Generate shareable payment URLs via official APS API
|
|
20
|
+
- ๐ **Hosted Checkout** - Full-featured payment pages with 3DS support
|
|
21
|
+
- ๐ณ **Tokenization** - Secure card storage for recurring payments
|
|
22
|
+
- ๐ **Webhooks** - Real-time payment event notifications with signature verification
|
|
23
|
+
- ๐ฐ **Payment Management** - Capture, refund, void, and query transactions
|
|
24
|
+
- ๐ **Multi-Currency** - Full support for Middle East currencies (SAR, AED, KWD, etc.)
|
|
25
|
+
- ๐ **Secure** - Proper SHA-256 signature calculation per APS documentation
|
|
26
|
+
- ๐ฆ **TypeScript** - Full TypeScript support with comprehensive types
|
|
27
|
+
- ๐งช **Testing Utilities** - Mock client, test cards, and webhook helpers
|
|
28
|
+
- ๐ก๏ธ **Error Handling** - Detailed error messages with suggested actions
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install @sikka/aps
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
### 1. Initialize the Client
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import APS from '@sikka/aps';
|
|
42
|
+
|
|
43
|
+
const aps = new APS({
|
|
44
|
+
merchantId: process.env.APS_MERCHANT_ID,
|
|
45
|
+
accessCode: process.env.APS_ACCESS_CODE,
|
|
46
|
+
requestSecret: process.env.APS_REQUEST_SECRET,
|
|
47
|
+
responseSecret: process.env.APS_RESPONSE_SECRET,
|
|
48
|
+
environment: 'sandbox' // or 'production'
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### TypeScript Types
|
|
53
|
+
|
|
54
|
+
All types are exported from the package:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import type {
|
|
58
|
+
APSConfig,
|
|
59
|
+
PaymentResponse,
|
|
60
|
+
PaymentLinkResponse,
|
|
61
|
+
TokenizedCard,
|
|
62
|
+
RefundResponse,
|
|
63
|
+
CaptureResponse,
|
|
64
|
+
VoidResponse,
|
|
65
|
+
WebhookEvent,
|
|
66
|
+
Order,
|
|
67
|
+
Customer,
|
|
68
|
+
TransactionStatus,
|
|
69
|
+
PaymentMethod
|
|
70
|
+
} from '@sikka/aps';
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 2. Create a Payment Link
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
const link = await aps.paymentLinks.create({
|
|
77
|
+
order: {
|
|
78
|
+
id: 'order_123',
|
|
79
|
+
amount: 10000, // 100.00 SAR (in fils/cents)
|
|
80
|
+
currency: 'SAR',
|
|
81
|
+
description: 'Premium Plan Subscription',
|
|
82
|
+
customer: {
|
|
83
|
+
email: 'customer@example.com',
|
|
84
|
+
name: 'Ahmed Al-Saud',
|
|
85
|
+
phone: '+966501234567'
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
tokenValidityHours: 24 // Link expires in 24 hours
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
console.log('Payment URL:', link.url);
|
|
92
|
+
// Send this link to your customer via email, SMS, etc.
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 3. Create Hosted Checkout
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
const checkout = await aps.hostedCheckout.create({
|
|
99
|
+
order: {
|
|
100
|
+
id: 'order_456',
|
|
101
|
+
amount: 25000, // 250.00 SAR
|
|
102
|
+
currency: 'SAR',
|
|
103
|
+
description: 'E-commerce Purchase',
|
|
104
|
+
customer: {
|
|
105
|
+
email: 'customer@example.com', // Required for hosted checkout
|
|
106
|
+
name: 'Ahmed Al-Saud'
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
returnUrl: 'https://yoursite.com/api/payment/return', // Use API route to handle POST data
|
|
110
|
+
allowedPaymentMethods: ['card', 'apple_pay', 'mada']
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Redirect user to the hosted checkout page
|
|
114
|
+
if (checkout.redirectForm) {
|
|
115
|
+
// Create a form and submit to redirectForm.url
|
|
116
|
+
// The customer will be redirected back to returnUrl after payment
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Important:** Hosted Checkout uses `return_url` for all redirects (success, failure, or cancel).
|
|
121
|
+
|
|
122
|
+
**โ ๏ธ POST Data:** APS sends the transaction result as a **POST request** (form data), not GET query parameters. You need an API route to receive the POST data:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// app/api/payment/return/route.ts
|
|
126
|
+
export async function POST(request: NextRequest) {
|
|
127
|
+
const formData = await request.formData();
|
|
128
|
+
|
|
129
|
+
// Convert to query parameters
|
|
130
|
+
const params = new URLSearchParams();
|
|
131
|
+
formData.forEach((value, key) => {
|
|
132
|
+
if (typeof value === 'string') params.append(key, value);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Redirect to result page
|
|
136
|
+
return NextResponse.redirect(
|
|
137
|
+
new URL(`/payment/result?${params.toString()}`, request.url)
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Then use the API route as your `returnUrl`:
|
|
143
|
+
```typescript
|
|
144
|
+
returnUrl: 'https://yoursite.com/api/payment/return'
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### 4. Custom Payment Page (Non-PCI)
|
|
148
|
+
|
|
149
|
+
Create a custom payment form while APS handles PCI compliance through tokenization.
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
// Step 1: Get tokenization form (backend)
|
|
153
|
+
const tokenizationForm = aps.customPaymentPage.getTokenizationForm({
|
|
154
|
+
returnUrl: 'https://yoursite.com/api/token-result'
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Step 2: Render form in your frontend
|
|
158
|
+
// <form method="POST" action={tokenizationForm.url}>
|
|
159
|
+
// {Object.entries(tokenizationForm.params).map(([key, value]) => (
|
|
160
|
+
// <input type="hidden" name={key} value={value} />
|
|
161
|
+
// ))}
|
|
162
|
+
// <input name="card_number" placeholder="Card Number" required />
|
|
163
|
+
// <input name="expiry_date" placeholder="MM/YY" required />
|
|
164
|
+
// <input name="card_security_code" placeholder="CVV" required />
|
|
165
|
+
// <input name="card_holder_name" placeholder="Card Holder Name" required />
|
|
166
|
+
// <button type="submit">Pay</button>
|
|
167
|
+
// </form>
|
|
168
|
+
|
|
169
|
+
// Step 3: Handle response at returnUrl (backend)
|
|
170
|
+
// Response contains: token_name
|
|
171
|
+
|
|
172
|
+
// Step 4: Charge with token (backend)
|
|
173
|
+
const payment = await aps.customPaymentPage.chargeWithToken({
|
|
174
|
+
order: {
|
|
175
|
+
id: 'order_123',
|
|
176
|
+
amount: 10000, // 100.00 SAR
|
|
177
|
+
currency: 'SAR',
|
|
178
|
+
description: 'Product Purchase'
|
|
179
|
+
},
|
|
180
|
+
tokenName: 'token_from_step_3',
|
|
181
|
+
customerEmail: 'customer@example.com'
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**How it works:**
|
|
186
|
+
1. Get tokenization form from APS
|
|
187
|
+
2. Render custom form with your branding
|
|
188
|
+
3. Customer enters card details
|
|
189
|
+
4. Form submits to APS (Non-PCI compliant)
|
|
190
|
+
5. APS returns token to your returnUrl
|
|
191
|
+
6. Use token to charge payments server-to-server
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
195
|
+
import { getAPSClient } from './aps-client';
|
|
196
|
+
|
|
197
|
+
export async function POST(request: NextRequest) {
|
|
198
|
+
const body = await request.json();
|
|
199
|
+
const signature = request.headers.get('x-aps-signature') || '';
|
|
200
|
+
|
|
201
|
+
const aps = getAPSClient();
|
|
202
|
+
|
|
203
|
+
// Verify webhook signature
|
|
204
|
+
const event = aps.webhooks.constructEvent(body, signature);
|
|
205
|
+
|
|
206
|
+
// Handle different event types
|
|
207
|
+
switch (event.type) {
|
|
208
|
+
case 'payment.success':
|
|
209
|
+
// Update order status, send confirmation email, etc.
|
|
210
|
+
console.log('Payment successful:', event.data);
|
|
211
|
+
break;
|
|
212
|
+
case 'payment.failed':
|
|
213
|
+
// Notify customer, retry logic, etc.
|
|
214
|
+
console.log('Payment failed:', event.data);
|
|
215
|
+
break;
|
|
216
|
+
case 'refund.success':
|
|
217
|
+
// Process refund confirmation
|
|
218
|
+
console.log('Refund processed:', event.data);
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return NextResponse.json({ received: true });
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### 7. Payment Management
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
// Capture an authorized payment (for two-step checkout)
|
|
230
|
+
const capture = await aps.payments.capture({
|
|
231
|
+
transactionId: 'txn_123',
|
|
232
|
+
amount: 10000 // Optional, defaults to full amount
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Refund a payment (full or partial)
|
|
236
|
+
const refund = await aps.payments.refund({
|
|
237
|
+
transactionId: 'txn_123',
|
|
238
|
+
amount: 5000, // Partial refund (50.00 SAR)
|
|
239
|
+
reason: 'Customer request'
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Void an authorization (before capture)
|
|
243
|
+
const voided = await aps.payments.void({
|
|
244
|
+
transactionId: 'txn_123',
|
|
245
|
+
reason: 'Order cancelled'
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Query transaction status
|
|
249
|
+
const transaction = await aps.payments.query({
|
|
250
|
+
transactionId: 'txn_123'
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
console.log('Transaction status:', transaction.status);
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## React Hooks & Components
|
|
257
|
+
|
|
258
|
+
### Setup Provider
|
|
259
|
+
|
|
260
|
+
```tsx
|
|
261
|
+
import { APSProvider } from '@sikka/aps/react';
|
|
262
|
+
import APS from '@sikka/aps';
|
|
263
|
+
|
|
264
|
+
const aps = new APS({
|
|
265
|
+
merchantId: process.env.APS_MERCHANT_ID!,
|
|
266
|
+
accessCode: process.env.APS_ACCESS_CODE!,
|
|
267
|
+
requestSecret: process.env.APS_REQUEST_SECRET!,
|
|
268
|
+
responseSecret: process.env.APS_RESPONSE_SECRET!,
|
|
269
|
+
environment: 'sandbox'
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
function App() {
|
|
273
|
+
return (
|
|
274
|
+
<APSProvider client={aps}>
|
|
275
|
+
<YourApp />
|
|
276
|
+
</APSProvider>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### useAPS Hook
|
|
282
|
+
|
|
283
|
+
Access the APS client from any component:
|
|
284
|
+
|
|
285
|
+
```tsx
|
|
286
|
+
import { useAPS } from '@sikka/aps/react';
|
|
287
|
+
|
|
288
|
+
function MyComponent() {
|
|
289
|
+
const aps = useAPS();
|
|
290
|
+
|
|
291
|
+
const handleClick = async () => {
|
|
292
|
+
const link = await aps.paymentLinks.create({...});
|
|
293
|
+
console.log(link.url);
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### useCheckout Hook
|
|
299
|
+
|
|
300
|
+
Manage hosted checkout state:
|
|
301
|
+
|
|
302
|
+
```tsx
|
|
303
|
+
import { useCheckout } from '@sikka/aps/react';
|
|
304
|
+
|
|
305
|
+
function CheckoutPage() {
|
|
306
|
+
const { createCheckout, redirectToCheckout, isLoading, error } = useCheckout();
|
|
307
|
+
|
|
308
|
+
const handleCheckout = async () => {
|
|
309
|
+
await createCheckout({
|
|
310
|
+
order: { id: 'order_123', amount: 10000, currency: 'SAR', customer: { email: 'test@example.com' } },
|
|
311
|
+
returnUrl: '/payment-result'
|
|
312
|
+
});
|
|
313
|
+
redirectToCheckout();
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### usePayment Hook
|
|
319
|
+
|
|
320
|
+
Manage payment lifecycle:
|
|
321
|
+
|
|
322
|
+
```tsx
|
|
323
|
+
import { usePayment } from '@sikka/aps/react';
|
|
324
|
+
|
|
325
|
+
function PaymentPage() {
|
|
326
|
+
const { createPaymentLink, confirmPayment, status } = usePayment();
|
|
327
|
+
|
|
328
|
+
const handlePayment = async () => {
|
|
329
|
+
const link = await createPaymentLink({ order: {...} });
|
|
330
|
+
window.open(link.url, '_blank');
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// After customer returns
|
|
334
|
+
const finalStatus = await confirmPayment({ transactionId: 'txn_123' });
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### HostedCheckoutButton Component
|
|
339
|
+
|
|
340
|
+
Complete checkout button with built-in state management:
|
|
341
|
+
|
|
342
|
+
```tsx
|
|
343
|
+
import { HostedCheckoutButton } from '@sikka/aps/react';
|
|
344
|
+
|
|
345
|
+
<HostedCheckoutButton
|
|
346
|
+
order={{
|
|
347
|
+
id: 'order_123',
|
|
348
|
+
amount: 10000,
|
|
349
|
+
currency: 'SAR',
|
|
350
|
+
customer: { email: 'customer@example.com' }
|
|
351
|
+
}}
|
|
352
|
+
returnUrl="https://yoursite.com/result"
|
|
353
|
+
>
|
|
354
|
+
Pay Now
|
|
355
|
+
</HostedCheckoutButton>
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### ErrorDisplay Component
|
|
359
|
+
|
|
360
|
+
Display APS errors with helpful messages:
|
|
361
|
+
|
|
362
|
+
```tsx
|
|
363
|
+
import { ErrorDisplay } from '@sikka/aps/react';
|
|
364
|
+
|
|
365
|
+
<ErrorDisplay
|
|
366
|
+
error={error}
|
|
367
|
+
onRetry={() => handleRetry()}
|
|
368
|
+
onDismiss={() => setError(null)}
|
|
369
|
+
/>
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### PaymentStatus Component
|
|
373
|
+
|
|
374
|
+
Display payment status with icons:
|
|
375
|
+
|
|
376
|
+
```tsx
|
|
377
|
+
import { PaymentStatus } from '@sikka/aps/react';
|
|
378
|
+
|
|
379
|
+
<PaymentStatus
|
|
380
|
+
status="captured"
|
|
381
|
+
amount={10000}
|
|
382
|
+
currency="SAR"
|
|
383
|
+
transactionId="txn_123"
|
|
384
|
+
/>
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## Error Handling
|
|
388
|
+
|
|
389
|
+
### Get Error Details
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
import { getErrorDetails, isRetryableError } from '@sikka/aps';
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
await aps.paymentLinks.create({...});
|
|
396
|
+
} catch (error) {
|
|
397
|
+
if (error.code) {
|
|
398
|
+
const details = getErrorDetails(error.code);
|
|
399
|
+
console.log(details.message); // Human-readable message
|
|
400
|
+
console.log(details.action); // Suggested action
|
|
401
|
+
console.log(details.category); // Error category
|
|
402
|
+
console.log(details.documentation); // Link to docs
|
|
403
|
+
|
|
404
|
+
if (isRetryableError(error.code)) {
|
|
405
|
+
await retryPayment();
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Response Codes
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
import { ResponseCodes, ErrorCategories, categorizeError } from '@sikka/aps';
|
|
415
|
+
|
|
416
|
+
if (response.response_code === ResponseCodes.SUCCESS) {
|
|
417
|
+
// Payment successful
|
|
418
|
+
} else if (response.response_code === ResponseCodes.INSUFFICIENT_FUNDS) {
|
|
419
|
+
// Handle insufficient funds
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const category = categorizeError(response.response_code);
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### Validation
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
import { validators } from '@sikka/aps';
|
|
429
|
+
|
|
430
|
+
// Validate individual fields
|
|
431
|
+
validators.isValidMerchantReference('order_123');
|
|
432
|
+
validators.isValidAmount(10000, 'SAR');
|
|
433
|
+
validators.isValidCardNumber('4111111111111111');
|
|
434
|
+
|
|
435
|
+
// Validate all at once
|
|
436
|
+
const result = validators.validatePaymentParams({
|
|
437
|
+
merchant_reference: 'order_123',
|
|
438
|
+
amount: 10000,
|
|
439
|
+
currency: 'SAR',
|
|
440
|
+
customer_email: 'customer@example.com'
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
if (!result.valid) {
|
|
444
|
+
result.errors.forEach(err => console.log(err.field, err.error));
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
## Testing
|
|
449
|
+
|
|
450
|
+
### Mock Client
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
import { mockAPS } from '@sikka/aps/test';
|
|
454
|
+
|
|
455
|
+
const aps = mockAPS({ simulate: 'success', delay: 100 });
|
|
456
|
+
const link = await aps.paymentLinks.create({...});
|
|
457
|
+
|
|
458
|
+
// Simulate errors
|
|
459
|
+
const declinedAps = mockAPS({ simulate: 'declined' });
|
|
460
|
+
await expect(declinedAps.paymentLinks.create({...}))
|
|
461
|
+
.rejects.toThrow('Transaction declined');
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### Test Cards
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
467
|
+
import { TestCards, testCards } from '@sikka/aps/test';
|
|
468
|
+
|
|
469
|
+
TestCards.VISA.SUCCESS // '4111111111111111'
|
|
470
|
+
TestCards.VISA.DECLINED // '4000000000000002'
|
|
471
|
+
TestCards.MADA.SUCCESS // '5297410000000002'
|
|
472
|
+
|
|
473
|
+
testCards.visa.success;
|
|
474
|
+
testCards.visa.declined;
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### Mock Webhooks
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
import { createMockWebhookPayload, createMockSignature } from '@sikka/aps/test';
|
|
481
|
+
|
|
482
|
+
const payload = createMockWebhookPayload('payment.success', {
|
|
483
|
+
merchant_reference: 'order_123',
|
|
484
|
+
amount: '10000',
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const signature = createMockSignature(payload, 'your-secret');
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
## Complete API Reference
|
|
491
|
+
|
|
492
|
+
### Configuration
|
|
493
|
+
|
|
494
|
+
| Parameter | Type | Required | Default | Description |
|
|
495
|
+
|-----------|------|----------|---------|-------------|
|
|
496
|
+
| `merchantId` | string | โ | - | Your APS merchant ID from dashboard |
|
|
497
|
+
| `accessCode` | string | โ | - | API access code from Integration Settings |
|
|
498
|
+
| `requestSecret` | string | โ | - | SHA Request Phrase for signing |
|
|
499
|
+
| `responseSecret` | string | โ | - | SHA Response Phrase for verification |
|
|
500
|
+
| `environment` | string | | `'sandbox'` | `'sandbox'` or `'production'` |
|
|
501
|
+
| `currency` | string | | `'USD'` | Default currency code (e.g., `'SAR'`, `'AED'`) |
|
|
502
|
+
| `language` | string | | `'en'` | Interface language: `'en'` or `'ar'` |
|
|
503
|
+
|
|
504
|
+
### Payment Links Module
|
|
505
|
+
|
|
506
|
+
Create payment links that can be shared via email, SMS, WhatsApp, etc.
|
|
507
|
+
|
|
508
|
+
```typescript
|
|
509
|
+
await aps.paymentLinks.create(options: PaymentLinkOptions)
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
**Options:**
|
|
513
|
+
|
|
514
|
+
| Field | Type | Required | Description |
|
|
515
|
+
|-------|------|----------|-------------|
|
|
516
|
+
| `order.id` | string | | Unique order reference (auto-generated if not provided) |
|
|
517
|
+
| `order.amount` | number | โ | Amount in **fils/cents** (e.g., 10000 = 100.00 SAR) |
|
|
518
|
+
| `order.currency` | string | โ | Three-letter ISO currency code |
|
|
519
|
+
| `order.description` | string | | Order description shown to customer |
|
|
520
|
+
| `order.customer.email` | string | โ | Customer email for notifications |
|
|
521
|
+
| `order.customer.name` | string | | Customer full name |
|
|
522
|
+
| `order.customer.phone` | string | | Customer phone (international format) |
|
|
523
|
+
| `tokenValidityHours` | number | | Link expiry in hours (default: 24) |
|
|
524
|
+
| `recurring` | boolean | | Enable for recurring/subscription payments |
|
|
525
|
+
| `allowedPaymentMethods` | array | | Restrict to specific methods: `['card', 'mada', 'apple_pay']` |
|
|
526
|
+
| `metadata` | object | | Custom key-value pairs to attach to order |
|
|
527
|
+
|
|
528
|
+
**Returns:**
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
{
|
|
532
|
+
url: string; // Payment link URL to share
|
|
533
|
+
linkId: string; // APS payment link ID
|
|
534
|
+
orderId: string; // Your order reference
|
|
535
|
+
expiresAt?: Date; // Expiry timestamp
|
|
536
|
+
rawResponse: object; // Full APS API response
|
|
537
|
+
}
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### Hosted Checkout Module
|
|
541
|
+
|
|
542
|
+
Redirect customers to a secure APS-hosted payment page.
|
|
543
|
+
|
|
544
|
+
```typescript
|
|
545
|
+
await aps.hostedCheckout.create(options: HostedCheckoutOptions)
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
**Options:**
|
|
549
|
+
|
|
550
|
+
| Field | Type | Required | Description |
|
|
551
|
+
|-------|------|----------|-------------|
|
|
552
|
+
| `order.id` | string | | Unique order reference (auto-generated if not provided) |
|
|
553
|
+
| `order.amount` | number | โ | Amount in **fils/cents** (e.g., 10000 = 100.00 SAR) |
|
|
554
|
+
| `order.currency` | string | โ | Three-letter ISO currency code |
|
|
555
|
+
| `order.description` | string | | Order description shown to customer |
|
|
556
|
+
| `order.customer.email` | string | โ | **Required** - Customer email for payment page |
|
|
557
|
+
| `order.customer.name` | string | | Customer full name |
|
|
558
|
+
| `order.customer.phone` | string | | Customer phone (international format) |
|
|
559
|
+
| `returnUrl` | string | โ | Redirect URL after payment (success, failure, or cancel) |
|
|
560
|
+
| `tokenize` | boolean | | Allow card saving for future payments (`remember_me`) |
|
|
561
|
+
| `allowedPaymentMethods` | array | | Limit payment methods: `['card', 'mada', 'apple_pay']` |
|
|
562
|
+
| `hideShipping` | boolean | | Hide shipping information fields |
|
|
563
|
+
|
|
564
|
+
**Returns:**
|
|
565
|
+
|
|
566
|
+
```typescript
|
|
567
|
+
{
|
|
568
|
+
transactionId: string;
|
|
569
|
+
orderId: string;
|
|
570
|
+
status: 'pending' | 'authorized' | 'captured' | 'failed';
|
|
571
|
+
amount: number;
|
|
572
|
+
currency: string;
|
|
573
|
+
redirectForm: {
|
|
574
|
+
url: string; // APS payment page URL
|
|
575
|
+
method: 'POST';
|
|
576
|
+
params: Record<string, string>; // All form parameters including signature
|
|
577
|
+
};
|
|
578
|
+
rawResponse: object;
|
|
579
|
+
}
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
**Important Notes:**
|
|
583
|
+
- `customer_email` is **required** by APS
|
|
584
|
+
- Use `returnUrl` (not `successUrl`/`failureUrl`) - APS redirects to this URL for all outcomes
|
|
585
|
+
- The response contains a `redirectForm` that should be submitted via POST to redirect the customer
|
|
586
|
+
|
|
587
|
+
### Tokenization Module
|
|
588
|
+
|
|
589
|
+
Securely tokenize card details for recurring payments.
|
|
590
|
+
|
|
591
|
+
```typescript
|
|
592
|
+
await aps.tokens.create(options: TokenizeCardOptions)
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
**Options:**
|
|
596
|
+
|
|
597
|
+
| Field | Type | Required | Description |
|
|
598
|
+
|-------|------|----------|-------------|
|
|
599
|
+
| `cardNumber` | string | โ | Full card number |
|
|
600
|
+
| `expiryMonth` | string | โ | 2-digit month (MM) |
|
|
601
|
+
| `expiryYear` | string | โ | 2-digit year (YY) |
|
|
602
|
+
| `cvv` | string | โ | Card security code |
|
|
603
|
+
| `cardholderName` | string | | Name as shown on card |
|
|
604
|
+
|
|
605
|
+
**Returns:**
|
|
606
|
+
|
|
607
|
+
```typescript
|
|
608
|
+
{
|
|
609
|
+
token: string; // Card token for future use
|
|
610
|
+
last4: string; // Last 4 digits
|
|
611
|
+
brand: string; // Card brand (visa, mastercard, mada, etc.)
|
|
612
|
+
expiryMonth: string;
|
|
613
|
+
expiryYear: string;
|
|
614
|
+
}
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
**Additional Methods:**
|
|
618
|
+
|
|
619
|
+
```typescript
|
|
620
|
+
// Verify if token is still valid
|
|
621
|
+
const isValid = await aps.tokens.verify('tok_xxxxx');
|
|
622
|
+
|
|
623
|
+
// Delete/invalidate a token
|
|
624
|
+
await aps.tokens.delete('tok_xxxxx');
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
### Custom Payment Page Module (Non-PCI)
|
|
628
|
+
|
|
629
|
+
Create custom payment forms while maintaining PCI compliance through tokenization.
|
|
630
|
+
|
|
631
|
+
```typescript
|
|
632
|
+
// Get tokenization form
|
|
633
|
+
const form = aps.customPaymentPage.getTokenizationForm({
|
|
634
|
+
returnUrl: 'https://yoursite.com/api/token-result',
|
|
635
|
+
merchantReference: 'order_123'
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
// Render form in your frontend with custom styling
|
|
639
|
+
// Form submits to APS endpoint for secure tokenization
|
|
640
|
+
|
|
641
|
+
// After receiving token, charge with:
|
|
642
|
+
await aps.customPaymentPage.chargeWithToken({
|
|
643
|
+
order: {
|
|
644
|
+
id: string,
|
|
645
|
+
amount: number,
|
|
646
|
+
currency: string,
|
|
647
|
+
description?: string
|
|
648
|
+
},
|
|
649
|
+
tokenName: string, // Token from tokenization response
|
|
650
|
+
customerEmail: string, // Required
|
|
651
|
+
customerName?: string,
|
|
652
|
+
customerPhone?: string,
|
|
653
|
+
returnUrl?: string
|
|
654
|
+
});
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
**Returns (chargeWithToken):**
|
|
658
|
+
|
|
659
|
+
```typescript
|
|
660
|
+
{
|
|
661
|
+
transactionId: string;
|
|
662
|
+
orderId: string;
|
|
663
|
+
status: 'captured' | 'authorized' | 'pending' | 'failed';
|
|
664
|
+
amount: number;
|
|
665
|
+
currency: string;
|
|
666
|
+
paymentMethod?: string;
|
|
667
|
+
authenticationUrl?: string; // 3DS URL if required
|
|
668
|
+
redirectForm?: { ... }; // For 3DS redirect
|
|
669
|
+
message?: string;
|
|
670
|
+
rawResponse: object;
|
|
671
|
+
}
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
**Important Notes:**
|
|
675
|
+
- Tokenization form collects card details securely (Non-PCI compliant)
|
|
676
|
+
- Card data submits directly to APS, never touches your server
|
|
677
|
+
- Token received at returnUrl can be used for immediate or future charges
|
|
678
|
+
- Use `chargeWithToken()` for server-to-server payment processing
|
|
679
|
+
|
|
680
|
+
### Payments Module (Management)
|
|
681
|
+
|
|
682
|
+
Manage existing transactions.
|
|
683
|
+
|
|
684
|
+
```typescript
|
|
685
|
+
// Capture authorized payment
|
|
686
|
+
await aps.payments.capture({
|
|
687
|
+
transactionId: string,
|
|
688
|
+
amount?: number // Optional, full capture if not specified
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
// Refund payment
|
|
692
|
+
await aps.payments.refund({
|
|
693
|
+
transactionId: string,
|
|
694
|
+
amount?: number, // Optional, full refund if not specified
|
|
695
|
+
reason?: string
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
// Void authorization
|
|
699
|
+
await aps.payments.void({
|
|
700
|
+
transactionId: string,
|
|
701
|
+
reason?: string
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
// Query transaction
|
|
705
|
+
await aps.payments.query({
|
|
706
|
+
transactionId?: string,
|
|
707
|
+
orderId?: string
|
|
708
|
+
});
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
### Webhooks Module
|
|
712
|
+
|
|
713
|
+
Verify and parse webhook events.
|
|
714
|
+
|
|
715
|
+
```typescript
|
|
716
|
+
// In your API route handler
|
|
717
|
+
const signature = request.headers.get('x-aps-signature') || '';
|
|
718
|
+
const payload = request.body;
|
|
719
|
+
|
|
720
|
+
// Verify signature
|
|
721
|
+
const isValid = aps.webhooks.verifySignature(
|
|
722
|
+
JSON.stringify(payload),
|
|
723
|
+
signature
|
|
724
|
+
);
|
|
725
|
+
|
|
726
|
+
if (!isValid) {
|
|
727
|
+
return res.status(401).send('Invalid signature');
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Construct event
|
|
731
|
+
const event = aps.webhooks.constructEvent(payload);
|
|
732
|
+
|
|
733
|
+
// Handle by type
|
|
734
|
+
switch (event.type) {
|
|
735
|
+
case 'payment.success':
|
|
736
|
+
case 'payment.failed':
|
|
737
|
+
case 'payment.pending':
|
|
738
|
+
case 'refund.success':
|
|
739
|
+
case 'refund.failed':
|
|
740
|
+
case 'chargeback.created':
|
|
741
|
+
case 'subscription.renewed':
|
|
742
|
+
case 'subscription.cancelled':
|
|
743
|
+
}
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
**Webhook Event Structure:**
|
|
747
|
+
|
|
748
|
+
```typescript
|
|
749
|
+
{
|
|
750
|
+
id: string;
|
|
751
|
+
type: WebhookEventType;
|
|
752
|
+
timestamp: Date;
|
|
753
|
+
data: {
|
|
754
|
+
transactionId: string;
|
|
755
|
+
orderId: string;
|
|
756
|
+
amount: number;
|
|
757
|
+
currency: string;
|
|
758
|
+
status: TransactionStatus;
|
|
759
|
+
paymentMethod?: string;
|
|
760
|
+
metadata?: Record<string, any>;
|
|
761
|
+
};
|
|
762
|
+
rawPayload: object;
|
|
763
|
+
}
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
## Supported Payment Methods
|
|
767
|
+
|
|
768
|
+
| Method | Regions | Description |
|
|
769
|
+
|--------|---------|-------------|
|
|
770
|
+
| `card` | All | Visa, Mastercard, American Express |
|
|
771
|
+
| `apple_pay` | All | Apple Pay |
|
|
772
|
+
| `mada` | Saudi Arabia | MADA debit cards |
|
|
773
|
+
| `stc_pay` | Saudi Arabia | STC Pay wallet |
|
|
774
|
+
| `knet` | Kuwait | KNET payment |
|
|
775
|
+
| `naps` | Qatar | NAPS payment |
|
|
776
|
+
| `fawry` | Egypt | Fawry cash payment |
|
|
777
|
+
| `meeza` | Egypt | Meeza cards |
|
|
778
|
+
| `sadad` | Saudi Arabia | Sadad payment |
|
|
779
|
+
| `aman` | Egypt | Aman installment |
|
|
780
|
+
|
|
781
|
+
## Environment Variables
|
|
782
|
+
|
|
783
|
+
Create a `.env.local` file:
|
|
784
|
+
|
|
785
|
+
```bash
|
|
786
|
+
# APS Merchant Credentials (from your APS dashboard)
|
|
787
|
+
APS_MERCHANT_ID=your_merchant_id
|
|
788
|
+
APS_ACCESS_CODE=your_access_code
|
|
789
|
+
APS_REQUEST_SECRET=your_request_secret
|
|
790
|
+
APS_RESPONSE_SECRET=your_response_secret
|
|
791
|
+
|
|
792
|
+
# APS Configuration
|
|
793
|
+
APS_ENVIRONMENT=sandbox # Use 'production' for live
|
|
794
|
+
APS_CURRENCY=SAR # Default currency
|
|
795
|
+
APS_LANGUAGE=en # Default language
|
|
796
|
+
|
|
797
|
+
# Your App URL
|
|
798
|
+
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
## Supported Currencies
|
|
802
|
+
|
|
803
|
+
- **SAR** - Saudi Riyal
|
|
804
|
+
- **AED** - UAE Dirham
|
|
805
|
+
- **KWD** - Kuwaiti Dinar
|
|
806
|
+
- **QAR** - Qatari Riyal
|
|
807
|
+
- **BHD** - Bahraini Dinar
|
|
808
|
+
- **OMR** - Omani Rial
|
|
809
|
+
- **JOD** - Jordanian Dinar
|
|
810
|
+
- **EGP** - Egyptian Pound
|
|
811
|
+
- **USD** - US Dollar
|
|
812
|
+
- **EUR** - Euro
|
|
813
|
+
|
|
814
|
+
## Error Handling
|
|
815
|
+
|
|
816
|
+
```typescript
|
|
817
|
+
import { APSException, APSError } from '@sikka/aps';
|
|
818
|
+
|
|
819
|
+
try {
|
|
820
|
+
const link = await aps.paymentLinks.create({ ... });
|
|
821
|
+
} catch (error) {
|
|
822
|
+
if (error instanceof APSException) {
|
|
823
|
+
console.error('Error Code:', error.code);
|
|
824
|
+
console.error('Message:', error.message);
|
|
825
|
+
console.error('Status:', error.statusCode);
|
|
826
|
+
console.error('Details:', error.rawResponse);
|
|
827
|
+
|
|
828
|
+
// Handle specific errors
|
|
829
|
+
switch (error.code) {
|
|
830
|
+
case 'PAYMENT_LINK_ERROR':
|
|
831
|
+
// Handle payment link creation failure
|
|
832
|
+
break;
|
|
833
|
+
case 'SIGNATURE_ERROR':
|
|
834
|
+
// Handle signature mismatch
|
|
835
|
+
break;
|
|
836
|
+
case 'INVALID_CREDENTIALS':
|
|
837
|
+
// Handle authentication failure
|
|
838
|
+
break;
|
|
839
|
+
}
|
|
840
|
+
} else {
|
|
841
|
+
console.error('Unexpected error:', error);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
## Common Response Codes
|
|
847
|
+
|
|
848
|
+
| Code | Message | Description |
|
|
849
|
+
|------|---------|-------------|
|
|
850
|
+
| `00000` | Success | Transaction successful |
|
|
851
|
+
| `48000` | Success | Payment link created successfully |
|
|
852
|
+
| `10030` | Authentication failed | 3DS authentication failed |
|
|
853
|
+
| `00008` | Signature mismatch | Invalid signature |
|
|
854
|
+
| `00002` | Invalid parameter | Parameter format error |
|
|
855
|
+
| `14000` | Declined | Card declined by issuer |
|
|
856
|
+
|
|
857
|
+
## Security Best Practices
|
|
858
|
+
|
|
859
|
+
1. **Never expose credentials** - Always use environment variables
|
|
860
|
+
2. **Server-side only** - All APS API calls must be server-side
|
|
861
|
+
3. **Verify webhooks** - Always verify webhook signatures
|
|
862
|
+
4. **Use HTTPS** - Required for production
|
|
863
|
+
5. **PCI Compliance** - Use hosted checkout or payment links to avoid PCI scope
|
|
864
|
+
6. **Validate amounts** - Always validate amounts server-side before creating payments
|
|
865
|
+
|
|
866
|
+
## Testing
|
|
867
|
+
|
|
868
|
+
### Sandbox Environment
|
|
869
|
+
|
|
870
|
+
```typescript
|
|
871
|
+
const aps = new APS({
|
|
872
|
+
// ... credentials
|
|
873
|
+
environment: 'sandbox'
|
|
874
|
+
});
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
**Test Cards** (use in sandbox):
|
|
878
|
+
|
|
879
|
+
| Card Number | Type | CVV | Expiry |
|
|
880
|
+
|-------------|------|-----|--------|
|
|
881
|
+
| 4111 1111 1111 1111 | Visa | 123 | 12/25 |
|
|
882
|
+
| 5297 4100 0000 0002 | MADA | 123 | 12/25 |
|
|
883
|
+
| 5100 0000 0000 0008 | Mastercard | 123 | 12/25 |
|
|
884
|
+
|
|
885
|
+
### Production
|
|
886
|
+
|
|
887
|
+
```typescript
|
|
888
|
+
const aps = new APS({
|
|
889
|
+
// ... production credentials
|
|
890
|
+
environment: 'production'
|
|
891
|
+
});
|
|
892
|
+
```
|
|
893
|
+
|
|
894
|
+
## Next.js Integration Example
|
|
895
|
+
|
|
896
|
+
See the included test app for a complete working example:
|
|
897
|
+
|
|
898
|
+
```bash
|
|
899
|
+
# Run the test app
|
|
900
|
+
pnpm dev
|
|
901
|
+
```
|
|
902
|
+
|
|
903
|
+
Visit `http://localhost:3000` to test payment creation and `http://localhost:3000/docs` for documentation.
|
|
904
|
+
|
|
905
|
+
## License
|
|
906
|
+
|
|
907
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
908
|
+
|
|
909
|
+
## Support
|
|
910
|
+
|
|
911
|
+
For APS-specific issues, contact Amazon Payment Services merchant support:
|
|
912
|
+
- Email: merchantsupport-ps@amazon.com
|
|
913
|
+
- Documentation: https://paymentservices.amazon.com/docs
|