@sikka/aps
v0.0.2
Published
A Stripe-like developer-friendly SDK for Amazon Payment Services integration. Supports payment links, hosted checkout, tokenization, webhooks, and payment management.
Downloads
195
Maintainers
Readme
Amazon Payment Services SDK
A Stripe-like developer-friendly SDK for Amazon Payment Services integration
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
npm install @sikka/apsQuick Start
1. Initialize the Client
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:
import type {
APSConfig,
PaymentResponse,
PaymentLinkResponse,
TokenizedCard,
RefundResponse,
CaptureResponse,
VoidResponse,
WebhookEvent,
Order,
Customer,
TransactionStatus,
PaymentMethod
} from '@sikka/aps';2. Create a Payment Link
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: '[email protected]',
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
const checkout = await aps.hostedCheckout.create({
order: {
id: 'order_456',
amount: 25000, // 250.00 SAR
currency: 'SAR',
description: 'E-commerce Purchase',
customer: {
email: '[email protected]', // 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:
// 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:
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.
// 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: '[email protected]'
});How it works:
- Get tokenization form from APS
- Render custom form with your branding
- Customer enters card details
- Form submits to APS (Non-PCI compliant)
- APS returns token to your returnUrl
- Use token to charge payments server-to-server
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
// 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
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:
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:
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: '[email protected]' } },
returnUrl: '/payment-result'
});
redirectToCheckout();
};
}usePayment Hook
Manage payment lifecycle:
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:
import { HostedCheckoutButton } from '@sikka/aps/react';
<HostedCheckoutButton
order={{
id: 'order_123',
amount: 10000,
currency: 'SAR',
customer: { email: '[email protected]' }
}}
returnUrl="https://yoursite.com/result"
>
Pay Now
</HostedCheckoutButton>ErrorDisplay Component
Display APS errors with helpful messages:
import { ErrorDisplay } from '@sikka/aps/react';
<ErrorDisplay
error={error}
onRetry={() => handleRetry()}
onDismiss={() => setError(null)}
/>PaymentStatus Component
Display payment status with icons:
import { PaymentStatus } from '@sikka/aps/react';
<PaymentStatus
status="captured"
amount={10000}
currency="SAR"
transactionId="txn_123"
/>Error Handling
Get Error Details
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
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
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: '[email protected]'
});
if (!result.valid) {
result.errors.forEach(err => console.log(err.field, err.error));
}Testing
Mock Client
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
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
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.
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:
{
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.
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:
{
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_emailis required by APS- Use
returnUrl(notsuccessUrl/failureUrl) - APS redirects to this URL for all outcomes - The response contains a
redirectFormthat should be submitted via POST to redirect the customer
Tokenization Module
Securely tokenize card details for recurring payments.
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:
{
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:
// 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.
// 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):
{
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.
// 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.
// 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:
{
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:
# 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:3000Supported 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
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
- Never expose credentials - Always use environment variables
- Server-side only - All APS API calls must be server-side
- Verify webhooks - Always verify webhook signatures
- Use HTTPS - Required for production
- PCI Compliance - Use hosted checkout or payment links to avoid PCI scope
- Validate amounts - Always validate amounts server-side before creating payments
Testing
Sandbox Environment
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
const aps = new APS({
// ... production credentials
environment: 'production'
});Next.js Integration Example
See the included test app for a complete working example:
# Run the test app
pnpm devVisit http://localhost:3000 to test payment creation and http://localhost:3000/docs for documentation.
License
MIT License - see LICENSE for details.
Support
For APS-specific issues, contact Amazon Payment Services merchant support:
- Email: [email protected]
- Documentation: https://paymentservices.amazon.com/docs
