@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/README.md CHANGED
@@ -1,913 +1,913 @@
1
- # Amazon Payment Services SDK
2
-
3
- <div align="center">
4
-
5
- [![npm version](https://img.shields.io/npm/v/amazon-payment-services.svg)](https://www.npmjs.com/package/amazon-payment-services)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ [![npm version](https://img.shields.io/npm/v/amazon-payment-services.svg)](https://www.npmjs.com/package/amazon-payment-services)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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