Unified, framework-agnostic payment SDK for Bun & TypeScript. Seamlessly integrate Moyasar, PayPal, Paymob, Stripe, Tabby, Tamara and other payment gateways with type-safe lifecycle hooks and normalized webhooks.
- π Multi-Gateway Support: Moyasar, PayPal, Paymob, Stripe, Tabby, Tamara
- πͺ Lifecycle Hooks: Before, after, and error hooks for all operations
- π Type-Safe: Full TypeScript support with strict types
- π Framework-Agnostic: Works with Elysia, Express, Hono, or vanilla
- Gateways
- Core Concepts
packages/payments/
βββ src/
β βββ index.ts # Main exports
β βββ client.ts # PaymentClient orchestrator
β βββ errors.ts # Custom error classes
β βββ types/ # Type definitions
β βββ hooks/ # Lifecycle hooks
β βββ gateways/ # Gateway implementations
βββ dist/ # Built output
βββ docs/ # Documentation
βββ resources/ # Resources
βββ package.json
βββ README.md
βββ tsconfig.json
bun add @abshahin/payments-sdkimport { PaymentClient } from '@abshahin/payments-sdk';
const client = new PaymentClient({
moyasar: {
secretKey: process.env.MOYASAR_SECRET_KEY!,
webhookSecret: process.env.MOYASAR_WEBHOOK_SECRET,
},
defaultGateway: 'moyasar',
});
// Create a payment
const result = await client.createPayment({
amount: 100,
currency: 'SAR',
orderId: 'order_123',
callbackUrl: 'https://example.com/callback',
moyasarSource: {
type: 'token',
token: 'token_xxx'
},
});
if (result.status === 'failed') {
// Do not mark the order paid.
} else if (result.redirectUrl) {
// Redirect customer for 3DS verification
}const client = new PaymentClient({
moyasar: { secretKey: '...' },
paypal: { clientId: '...', clientSecret: '...' },
paymob: {
secretKey: '...',
publicKey: '...',
hmacSecret: '...',
integrationId: 123456,
authIntegrationId: 456789, // for capture: false auth/capture flows
region: 'ksa',
timeoutMs: 30000,
},
stripe: {
secretKey: 'sk_...',
publishableKey: 'pk_...',
webhookSecret: 'whsec_...',
},
tabby: {
secretKey: 'sk_...',
merchantCode: 'your_merchant_code',
sandbox: true,
},
tamara: {
apiToken: 'your_api_token',
notificationToken: 'your_notification_token',
sandbox: true,
},
defaultGateway: 'moyasar',
});
// Use default gateway
await client.createPayment({ ... });
// Specify gateway explicitly
await client.createPayment({ ... }, 'paypal');
// Stripe Checkout Example
const stripe = client.gateway('stripe');
const session = await stripe.createCheckoutSession({
successUrl: 'https://example.com/success',
cancelUrl: 'https://example.com/cancel',
mode: 'payment',
metadata: { paymentId: 'order_123' },
lineItems: [
{
priceData: {
currency: 'USD',
productData: {
name: 'T-Shirt',
},
amount: 20,
},
quantity: 10,
}
]
});For Stripe webhooks, you MUST pass the raw request body to verifyWebhook. If your framework parses JSON automatically, you need to access the raw body buffer or string before parsing; Buffer payloads are verified using their original bytes.
Stripe webhook verification fails closed when webhookSecret is not configured.
Stripe webhook parsing expects snapshot events with data.object; hydrate thin events before passing them to parseWebhookEvent. Checkout, invoice, and subscription webhooks normalize gatewayPaymentId to the related PaymentIntent, SetupIntent, or Subscription when Stripe includes one.
// Example using Elysia
app.post('/webhook/stripe', async ({ request }) => {
const signature = request.headers.get('stripe-signature');
const rawBody = await Bun.readableStreamToText(request.body); // Get raw body
const isValid = client.gateway('stripe').verifyWebhook(
rawBody,
signature
);
});import {
PaymentError,
PaymentAbortedError,
GatewayNotConfiguredError,
InvalidWebhookError,
GatewayApiError,
CardDeclinedError,
InsufficientFundsError,
RateLimitError,
} from '@abshahin/payments-sdk';
try {
await client.createPayment({ ... });
} catch (error) {
if (error instanceof PaymentAbortedError) {
// Aborted by a hook
console.log('Aborted:', error.message);
} else if (error instanceof GatewayApiError) {
// Gateway API returned an error
console.log('Gateway error:', error.rawError);
} else if (error instanceof PaymentError) {
// Other payment error
console.log('Error code:', error.code);
}
}MIT