npm package: @sikka/aps
Description: A Stripe-like developer-friendly SDK for Amazon Payment Services integration. Supports payment links, hosted checkout, tokenization, webhooks, and payment management.
Latest version: 0.0.7

# Amazon Payment Services SDK

<div align="center">

[![npm version](https://img.shields.io/npm/v/amazon-payment-services.svg)](https://www.npmjs.com/package/amazon-payment-services)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

**A Stripe-like developer-friendly SDK for Amazon Payment Services integration**

</div>

---

## Features

- 🚀 **Stripe-like DX** - Intuitive API design inspired by Stripe
- ⚛️ **React Hooks** - useAPS, useCheckout, usePayment hooks for seamless React integration
- 🧩 **React Components** - Pre-built components: HostedCheckoutButton, ErrorDisplay, PaymentStatus
- 🔗 **Payment Links** - Generate shareable payment URLs via official APS API
- 🛒 **Hosted Checkout** - Full-featured payment pages with 3DS support
- 💳 **Tokenization** - Secure card storage for recurring payments
- 🔔 **Webhooks** - Real-time payment event notifications with signature verification
- 💰 **Payment Management** - Capture, refund, void, and query transactions
- 🌍 **Multi-Currency** - Full support for Middle East currencies (SAR, AED, KWD, etc.)
- 🔒 **Secure** - Proper SHA-256 signature calculation per APS documentation
- 📦 **TypeScript** - Full TypeScript support with comprehensive types
- 🧪 **Testing Utilities** - Mock client, test cards, and webhook helpers
- 🛡️ **Error Handling** - Detailed error messages with suggested actions

## Installation

```bash
npm install @sikka/aps
```

## Quick Start

### 1. Initialize the Client

```typescript
import APS from '@sikka/aps';

const aps = new APS({
  merchantId: process.env.APS_MERCHANT_ID,
  accessCode: process.env.APS_ACCESS_CODE,
  requestSecret: process.env.APS_REQUEST_SECRET,
  responseSecret: process.env.APS_RESPONSE_SECRET,
  environment: 'sandbox' // or 'production'
});
```

### TypeScript Types

All types are exported from the package:

```typescript
import type {
  APSConfig,
  PaymentResponse,
  PaymentLinkResponse,
  TokenizedCard,
  RefundResponse,
  CaptureResponse,
  VoidResponse,
  WebhookEvent,
  Order,
  Customer,
  TransactionStatus,
  PaymentMethod
} from '@sikka/aps';
```

### 2. Create a Payment Link

```typescript
const link = await aps.paymentLinks.create({
  order: {
    id: 'order_123',
    amount: 10000, // 100.00 SAR (in fils/cents)
    currency: 'SAR',
    description: 'Premium Plan Subscription',
    customer: {
      email: 'customer@example.com',
      name: 'Ahmed Al-Saud',
      phone: '+966501234567'
    }
  },
  tokenValidityHours: 24 // Link expires in 24 hours
});

console.log('Payment URL:', link.url);
// Send this link to your customer via email, SMS, etc.
```

### 3. Create Hosted Checkout

```typescript
const checkout = await aps.hostedCheckout.create({
  order: {
    id: 'order_456',
    amount: 25000, // 250.00 SAR
    currency: 'SAR',
    description: 'E-commerce Purchase',
    customer: {
      email: 'customer@example.com', // Required for hosted checkout
      name: 'Ahmed Al-Saud'
    }
  },
  returnUrl: 'https://yoursite.com/api/payment/return', // Use API route to handle POST data
  allowedPaymentMethods: ['card', 'apple_pay', 'mada']
});

// Redirect user to the hosted checkout page
if (checkout.redirectForm) {
  // Create a form and submit to redirectForm.url
  // The customer will be redirected back to returnUrl after payment
}
```

**Important:** Hosted Checkout uses `return_url` for all redirects (success, failure, or cancel). 

**⚠️ 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:

```typescript
// app/api/payment/return/route.ts
export async function POST(request: NextRequest) {
  const formData = await request.formData();
  
  // Convert to query parameters
  const params = new URLSearchParams();
  formData.forEach((value, key) => {
    if (typeof value === 'string') params.append(key, value);
  });
  
  // Redirect to result page
  return NextResponse.redirect(
    new URL(`/payment/result?${params.toString()}`, request.url)
  );
}
```

Then use the API route as your `returnUrl`:
```typescript
returnUrl: 'https://yoursite.com/api/payment/return'
```

### 4. Custom Payment Page (Non-PCI)

Create a custom payment form while APS handles PCI compliance through tokenization.

```typescript
// Step 1: Get tokenization form (backend)
const tokenizationForm = aps.customPaymentPage.getTokenizationForm({
  returnUrl: 'https://yoursite.com/api/token-result'
});

// Step 2: Render form in your frontend
// <form method="POST" action={tokenizationForm.url}>
//   {Object.entries(tokenizationForm.params).map(([key, value]) => (
//     <input type="hidden" name={key} value={value} />
//   ))}
//   <input name="card_number" placeholder="Card Number" required />
//   <input name="expiry_date" placeholder="MM/YY" required />
//   <input name="card_security_code" placeholder="CVV" required />
//   <input name="card_holder_name" placeholder="Card Holder Name" required />
//   <button type="submit">Pay</button>
// </form>

// Step 3: Handle response at returnUrl (backend)
// Response contains: token_name

// Step 4: Charge with token (backend)
const payment = await aps.customPaymentPage.chargeWithToken({
  order: {
    id: 'order_123',
    amount: 10000, // 100.00 SAR
    currency: 'SAR',
    description: 'Product Purchase'
  },
  tokenName: 'token_from_step_3',
  customerEmail: 'customer@example.com'
});
```

**How it works:**
1. Get tokenization form from APS
2. Render custom form with your branding
3. Customer enters card details
4. Form submits to APS (Non-PCI compliant)
5. APS returns token to your returnUrl
6. Use token to charge payments server-to-server

```typescript
import { NextRequest, NextResponse } from 'next/server';
import { getAPSClient } from './aps-client';

export async function POST(request: NextRequest) {
  const body = await request.json();
  const signature = request.headers.get('x-aps-signature') || '';
  
  const aps = getAPSClient();
  
  // Verify webhook signature
  const event = aps.webhooks.constructEvent(body, signature);
  
  // Handle different event types
  switch (event.type) {
    case 'payment.success':
      // Update order status, send confirmation email, etc.
      console.log('Payment successful:', event.data);
      break;
    case 'payment.failed':
      // Notify customer, retry logic, etc.
      console.log('Payment failed:', event.data);
      break;
    case 'refund.success':
      // Process refund confirmation
      console.log('Refund processed:', event.data);
      break;
  }
  
  return NextResponse.json({ received: true });
}
```

### 7. Payment Management

```typescript
// Capture an authorized payment (for two-step checkout)
const capture = await aps.payments.capture({
  transactionId: 'txn_123',
  amount: 10000 // Optional, defaults to full amount
});

// Refund a payment (full or partial)
const refund = await aps.payments.refund({
  transactionId: 'txn_123',
  amount: 5000, // Partial refund (50.00 SAR)
  reason: 'Customer request'
});

// Void an authorization (before capture)
const voided = await aps.payments.void({
  transactionId: 'txn_123',
  reason: 'Order cancelled'
});

// Query transaction status
const transaction = await aps.payments.query({
  transactionId: 'txn_123'
});

console.log('Transaction status:', transaction.status);
```

## React Hooks & Components

### Setup Provider

```tsx
import { APSProvider } from '@sikka/aps/react';
import APS from '@sikka/aps';

const aps = new APS({
  merchantId: process.env.APS_MERCHANT_ID!,
  accessCode: process.env.APS_ACCESS_CODE!,
  requestSecret: process.env.APS_REQUEST_SECRET!,
  responseSecret: process.env.APS_RESPONSE_SECRET!,
  environment: 'sandbox'
});

function App() {
  return (
    <APSProvider client={aps}>
      <YourApp />
    </APSProvider>
  );
}
```

### useAPS Hook

Access the APS client from any component:

```tsx
import { useAPS } from '@sikka/aps/react';

function MyComponent() {
  const aps = useAPS();

  const handleClick = async () => {
    const link = await aps.paymentLinks.create({...});
    console.log(link.url);
  };
}
```

### useCheckout Hook

Manage hosted checkout state:

```tsx
import { useCheckout } from '@sikka/aps/react';

function CheckoutPage() {
  const { createCheckout, redirectToCheckout, isLoading, error } = useCheckout();

  const handleCheckout = async () => {
    await createCheckout({
      order: { id: 'order_123', amount: 10000, currency: 'SAR', customer: { email: 'test@example.com' } },
      returnUrl: '/payment-result'
    });
    redirectToCheckout();
  };
}
```

### usePayment Hook

Manage payment lifecycle:

```tsx
import { usePayment } from '@sikka/aps/react';

function PaymentPage() {
  const { createPaymentLink, confirmPayment, status } = usePayment();

  const handlePayment = async () => {
    const link = await createPaymentLink({ order: {...} });
    window.open(link.url, '_blank');
  };

  // After customer returns
  const finalStatus = await confirmPayment({ transactionId: 'txn_123' });
}
```

### HostedCheckoutButton Component

Complete checkout button with built-in state management:

```tsx
import { HostedCheckoutButton } from '@sikka/aps/react';

<HostedCheckoutButton
  order={{
    id: 'order_123',
    amount: 10000,
    currency: 'SAR',
    customer: { email: 'customer@example.com' }
  }}
  returnUrl="https://yoursite.com/result"
>
  Pay Now
</HostedCheckoutButton>
```

### ErrorDisplay Component

Display APS errors with helpful messages:

```tsx
import { ErrorDisplay } from '@sikka/aps/react';

<ErrorDisplay
  error={error}
  onRetry={() => handleRetry()}
  onDismiss={() => setError(null)}
/>
```

### PaymentStatus Component

Display payment status with icons:

```tsx
import { PaymentStatus } from '@sikka/aps/react';

<PaymentStatus
  status="captured"
  amount={10000}
  currency="SAR"
  transactionId="txn_123"
/>
```

## Error Handling

### Get Error Details

```typescript
import { getErrorDetails, isRetryableError } from '@sikka/aps';

try {
  await aps.paymentLinks.create({...});
} catch (error) {
  if (error.code) {
    const details = getErrorDetails(error.code);
    console.log(details.message);      // Human-readable message
    console.log(details.action);       // Suggested action
    console.log(details.category);     // Error category
    console.log(details.documentation); // Link to docs

    if (isRetryableError(error.code)) {
      await retryPayment();
    }
  }
}
```

### Response Codes

```typescript
import { ResponseCodes, ErrorCategories, categorizeError } from '@sikka/aps';

if (response.response_code === ResponseCodes.SUCCESS) {
  // Payment successful
} else if (response.response_code === ResponseCodes.INSUFFICIENT_FUNDS) {
  // Handle insufficient funds
}

const category = categorizeError(response.response_code);
```

### Validation

```typescript
import { validators } from '@sikka/aps';

// Validate individual fields
validators.isValidMerchantReference('order_123');
validators.isValidAmount(10000, 'SAR');
validators.isValidCardNumber('4111111111111111');

// Validate all at once
const result = validators.validatePaymentParams({
  merchant_reference: 'order_123',
  amount: 10000,
  currency: 'SAR',
  customer_email: 'customer@example.com'
});

if (!result.valid) {
  result.errors.forEach(err => console.log(err.field, err.error));
}
```

## Testing

### Mock Client

```typescript
import { mockAPS } from '@sikka/aps/test';

const aps = mockAPS({ simulate: 'success', delay: 100 });
const link = await aps.paymentLinks.create({...});

// Simulate errors
const declinedAps = mockAPS({ simulate: 'declined' });
await expect(declinedAps.paymentLinks.create({...}))
  .rejects.toThrow('Transaction declined');
```

### Test Cards

```typescript
import { TestCards, testCards } from '@sikka/aps/test';

TestCards.VISA.SUCCESS        // '4111111111111111'
TestCards.VISA.DECLINED       // '4000000000000002'
TestCards.MADA.SUCCESS        // '5297410000000002'

testCards.visa.success;
testCards.visa.declined;
```

### Mock Webhooks

```typescript
import { createMockWebhookPayload, createMockSignature } from '@sikka/aps/test';

const payload = createMockWebhookPayload('payment.success', {
  merchant_reference: 'order_123',
  amount: '10000',
});

const signature = createMockSignature(payload, 'your-secret');
```

## Complete API Reference

### Configuration

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `merchantId` | string | ✓ | - | Your APS merchant ID from dashboard |
| `accessCode` | string | ✓ | - | API access code from Integration Settings |
| `requestSecret` | string | ✓ | - | SHA Request Phrase for signing |
| `responseSecret` | string | ✓ | - | SHA Response Phrase for verification |
| `environment` | string | | `'sandbox'` | `'sandbox'` or `'production'` |
| `currency` | string | | `'USD'` | Default currency code (e.g., `'SAR'`, `'AED'`) |
| `language` | string | | `'en'` | Interface language: `'en'` or `'ar'` |

### Payment Links Module

Create payment links that can be shared via email, SMS, WhatsApp, etc.

```typescript
await aps.paymentLinks.create(options: PaymentLinkOptions)
```

**Options:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `order.id` | string | | Unique order reference (auto-generated if not provided) |
| `order.amount` | number | ✓ | Amount in **fils/cents** (e.g., 10000 = 100.00 SAR) |
| `order.currency` | string | ✓ | Three-letter ISO currency code |
| `order.description` | string | | Order description shown to customer |
| `order.customer.email` | string | ✓ | Customer email for notifications |
| `order.customer.name` | string | | Customer full name |
| `order.customer.phone` | string | | Customer phone (international format) |
| `tokenValidityHours` | number | | Link expiry in hours (default: 24) |
| `recurring` | boolean | | Enable for recurring/subscription payments |
| `allowedPaymentMethods` | array | | Restrict to specific methods: `['card', 'mada', 'apple_pay']` |
| `metadata` | object | | Custom key-value pairs to attach to order |

**Returns:**

```typescript
{
  url: string;              // Payment link URL to share
  linkId: string;           // APS payment link ID
  orderId: string;          // Your order reference
  expiresAt?: Date;         // Expiry timestamp
  rawResponse: object;      // Full APS API response
}
```

### Hosted Checkout Module

Redirect customers to a secure APS-hosted payment page.

```typescript
await aps.hostedCheckout.create(options: HostedCheckoutOptions)
```

**Options:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `order.id` | string | | Unique order reference (auto-generated if not provided) |
| `order.amount` | number | ✓ | Amount in **fils/cents** (e.g., 10000 = 100.00 SAR) |
| `order.currency` | string | ✓ | Three-letter ISO currency code |
| `order.description` | string | | Order description shown to customer |
| `order.customer.email` | string | ✓ | **Required** - Customer email for payment page |
| `order.customer.name` | string | | Customer full name |
| `order.customer.phone` | string | | Customer phone (international format) |
| `returnUrl` | string | ✓ | Redirect URL after payment (success, failure, or cancel) |
| `tokenize` | boolean | | Allow card saving for future payments (`remember_me`) |
| `allowedPaymentMethods` | array | | Limit payment methods: `['card', 'mada', 'apple_pay']` |
| `hideShipping` | boolean | | Hide shipping information fields |

**Returns:**

```typescript
{
  transactionId: string;
  orderId: string;
  status: 'pending' | 'authorized' | 'captured' | 'failed';
  amount: number;
  currency: string;
  redirectForm: {
    url: string;        // APS payment page URL
    method: 'POST';
    params: Record<string, string>;  // All form parameters including signature
  };
  rawResponse: object;
}
```

**Important Notes:**
- `customer_email` is **required** by APS
- Use `returnUrl` (not `successUrl`/`failureUrl`) - APS redirects to this URL for all outcomes
- The response contains a `redirectForm` that should be submitted via POST to redirect the customer

### Tokenization Module

Securely tokenize card details for recurring payments.

```typescript
await aps.tokens.create(options: TokenizeCardOptions)
```

**Options:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `cardNumber` | string | ✓ | Full card number |
| `expiryMonth` | string | ✓ | 2-digit month (MM) |
| `expiryYear` | string | ✓ | 2-digit year (YY) |
| `cvv` | string | ✓ | Card security code |
| `cardholderName` | string | | Name as shown on card |

**Returns:**

```typescript
{
  token: string;        // Card token for future use
  last4: string;        // Last 4 digits
  brand: string;        // Card brand (visa, mastercard, mada, etc.)
  expiryMonth: string;
  expiryYear: string;
}
```

**Additional Methods:**

```typescript
// Verify if token is still valid
const isValid = await aps.tokens.verify('tok_xxxxx');

// Delete/invalidate a token
await aps.tokens.delete('tok_xxxxx');
```

### Custom Payment Page Module (Non-PCI)

Create custom payment forms while maintaining PCI compliance through tokenization.

```typescript
// Get tokenization form
const form = aps.customPaymentPage.getTokenizationForm({
  returnUrl: 'https://yoursite.com/api/token-result',
  merchantReference: 'order_123'
});

// Render form in your frontend with custom styling
// Form submits to APS endpoint for secure tokenization

// After receiving token, charge with:
await aps.customPaymentPage.chargeWithToken({
  order: {
    id: string,
    amount: number,
    currency: string,
    description?: string
  },
  tokenName: string,        // Token from tokenization response
  customerEmail: string,    // Required
  customerName?: string,
  customerPhone?: string,
  returnUrl?: string
});
```

**Returns (chargeWithToken):**

```typescript
{
  transactionId: string;
  orderId: string;
  status: 'captured' | 'authorized' | 'pending' | 'failed';
  amount: number;
  currency: string;
  paymentMethod?: string;
  authenticationUrl?: string;  // 3DS URL if required
  redirectForm?: { ... };      // For 3DS redirect
  message?: string;
  rawResponse: object;
}
```

**Important Notes:**
- Tokenization form collects card details securely (Non-PCI compliant)
- Card data submits directly to APS, never touches your server
- Token received at returnUrl can be used for immediate or future charges
- Use `chargeWithToken()` for server-to-server payment processing

### Payments Module (Management)

Manage existing transactions.

```typescript
// Capture authorized payment
await aps.payments.capture({
  transactionId: string,
  amount?: number  // Optional, full capture if not specified
});

// Refund payment
await aps.payments.refund({
  transactionId: string,
  amount?: number,  // Optional, full refund if not specified
  reason?: string
});

// Void authorization
await aps.payments.void({
  transactionId: string,
  reason?: string
});

// Query transaction
await aps.payments.query({
  transactionId?: string,
  orderId?: string
});
```

### Webhooks Module

Verify and parse webhook events.

```typescript
// In your API route handler
const signature = request.headers.get('x-aps-signature') || '';
const payload = request.body;

// Verify signature
const isValid = aps.webhooks.verifySignature(
  JSON.stringify(payload),
  signature
);

if (!isValid) {
  return res.status(401).send('Invalid signature');
}

// Construct event
const event = aps.webhooks.constructEvent(payload);

// Handle by type
switch (event.type) {
  case 'payment.success':
  case 'payment.failed':
  case 'payment.pending':
  case 'refund.success':
  case 'refund.failed':
  case 'chargeback.created':
  case 'subscription.renewed':
  case 'subscription.cancelled':
}
```

**Webhook Event Structure:**

```typescript
{
  id: string;
  type: WebhookEventType;
  timestamp: Date;
  data: {
    transactionId: string;
    orderId: string;
    amount: number;
    currency: string;
    status: TransactionStatus;
    paymentMethod?: string;
    metadata?: Record<string, any>;
  };
  rawPayload: object;
}
```

## Supported Payment Methods

| Method | Regions | Description |
|--------|---------|-------------|
| `card` | All | Visa, Mastercard, American Express |
| `apple_pay` | All | Apple Pay |
| `mada` | Saudi Arabia | MADA debit cards |
| `stc_pay` | Saudi Arabia | STC Pay wallet |
| `knet` | Kuwait | KNET payment |
| `naps` | Qatar | NAPS payment |
| `fawry` | Egypt | Fawry cash payment |
| `meeza` | Egypt | Meeza cards |
| `sadad` | Saudi Arabia | Sadad payment |
| `aman` | Egypt | Aman installment |

## Environment Variables

Create a `.env.local` file:

```bash
# APS Merchant Credentials (from your APS dashboard)
APS_MERCHANT_ID=your_merchant_id
APS_ACCESS_CODE=your_access_code
APS_REQUEST_SECRET=your_request_secret
APS_RESPONSE_SECRET=your_response_secret

# APS Configuration
APS_ENVIRONMENT=sandbox          # Use 'production' for live
APS_CURRENCY=SAR                 # Default currency
APS_LANGUAGE=en                  # Default language

# Your App URL
NEXT_PUBLIC_APP_URL=http://localhost:3000
```

## Supported Currencies

- **SAR** - Saudi Riyal
- **AED** - UAE Dirham
- **KWD** - Kuwaiti Dinar
- **QAR** - Qatari Riyal
- **BHD** - Bahraini Dinar
- **OMR** - Omani Rial
- **JOD** - Jordanian Dinar
- **EGP** - Egyptian Pound
- **USD** - US Dollar
- **EUR** - Euro

## Error Handling

```typescript
import { APSException, APSError } from '@sikka/aps';

try {
  const link = await aps.paymentLinks.create({ ... });
} catch (error) {
  if (error instanceof APSException) {
    console.error('Error Code:', error.code);
    console.error('Message:', error.message);
    console.error('Status:', error.statusCode);
    console.error('Details:', error.rawResponse);
    
    // Handle specific errors
    switch (error.code) {
      case 'PAYMENT_LINK_ERROR':
        // Handle payment link creation failure
        break;
      case 'SIGNATURE_ERROR':
        // Handle signature mismatch
        break;
      case 'INVALID_CREDENTIALS':
        // Handle authentication failure
        break;
    }
  } else {
    console.error('Unexpected error:', error);
  }
}
```

## Common Response Codes

| Code | Message | Description |
|------|---------|-------------|
| `00000` | Success | Transaction successful |
| `48000` | Success | Payment link created successfully |
| `10030` | Authentication failed | 3DS authentication failed |
| `00008` | Signature mismatch | Invalid signature |
| `00002` | Invalid parameter | Parameter format error |
| `14000` | Declined | Card declined by issuer |

## Security Best Practices

1. **Never expose credentials** - Always use environment variables
2. **Server-side only** - All APS API calls must be server-side
3. **Verify webhooks** - Always verify webhook signatures
4. **Use HTTPS** - Required for production
5. **PCI Compliance** - Use hosted checkout or payment links to avoid PCI scope
6. **Validate amounts** - Always validate amounts server-side before creating payments

## Testing

### Sandbox Environment

```typescript
const aps = new APS({
  // ... credentials
  environment: 'sandbox'
});
```

**Test Cards** (use in sandbox):

| Card Number | Type | CVV | Expiry |
|-------------|------|-----|--------|
| 4111 1111 1111 1111 | Visa | 123 | 12/25 |
| 5297 4100 0000 0002 | MADA | 123 | 12/25 |
| 5100 0000 0000 0008 | Mastercard | 123 | 12/25 |

### Production

```typescript
const aps = new APS({
  // ... production credentials
  environment: 'production'
});
```

## Next.js Integration Example

See the included test app for a complete working example:

```bash
# Run the test app
pnpm dev
```

Visit `http://localhost:3000` to test payment creation and `http://localhost:3000/docs` for documentation.

## License

MIT License - see [LICENSE](LICENSE) for details.

## Support

For APS-specific issues, contact Amazon Payment Services merchant support:
- Email: merchantsupport-ps@amazon.com
- Documentation: https://paymentservices.amazon.com/docs