@7haven/havenpay
v0.2.3
Published
Official JavaScript SDK for HavenPay
Maintainers
Readme
HavenPay JavaScript SDK
Official JavaScript/TypeScript SDK for HavenPay - Type-safe payment processing for African markets.
Features
- ✅ Type-safe - Full TypeScript support with detailed types
- ✅ Secure - Built-in request signing, response verification, and webhook validation (HMAC-SHA256, constant-time comparison)
- ✅ Reliable - Automatic retry logic with exponential backoff (1s, 2s, 4s)
- ✅ Modern - Uses native
fetch()and Node.jscrypto(zero dependencies, ES2020+) - ✅ Flexible - Both class-based and functional APIs
- ✅ Production-ready - Follows industry best practices (DRY, KISS, single source of truth)
- ✅ Well-tested - Comprehensive test coverage
Installation
npm install @7haven/havenpay
# or
yarn add @7haven/havenpay
# or
pnpm add @7haven/havenpay
# or
bun add @7haven/havenpayQuick Start
Class-based API (Recommended)
import { HavenPay } from '@7haven/havenpay'
const havenpay = new HavenPay({
apiKey: process.env.HAVENPAY_API_KEY!,
signingSecret: process.env.HAVENPAY_SIGNING_SECRET!,
})
// Start a payment
const payment = await havenpay.payments.start({
amount: 1000,
currency: 'USD',
providerId: 'omari', // or 'innbuck'
cartId: 'cart-123',
msisdn: '+263771234567',
})
console.log('Payment reference:', payment.reference)
console.log('OTP reference:', payment.otpReference)
// Verify OTP
const result = await havenpay.payments.verifyOtp(payment.reference, '1234')
if (result.status === 'SUCCESS') {
console.log('Payment successful!')
}Functional API
import { configure, start, verifyOtp } from '@7haven/havenpay'
// Configure once at app initialization
configure({
apiKey: process.env.HAVENPAY_API_KEY!,
signingSecret: process.env.HAVENPAY_SIGNING_SECRET!,
})
// Use anywhere in your app
const payment = await start({
amount: 1000,
currency: 'USD',
providerId: 'omari',
cartId: 'cart-123',
msisdn: '+263771234567',
})
const _result = await verifyOtp(payment.reference, '1234')Configuration
const _havenpay = new HavenPay({
apiKey: 'your-api-key', // Required
signingSecret: 'your-signing-secret-min-32-chars', // Required (min 32 chars, generate: openssl rand -base64 24)
baseUrl: 'https://pay.7haven.online/api', // Optional (default: production API)
timeout: 30000, // Optional (default: 30000ms = 30s)
maxRetries: 3, // Optional (default: 3, exponential backoff: 1s, 2s, 4s)
verifyResponses: true, // Optional (default: true, strongly recommended for security)
})Generating Secrets:
# Generate signingSecret (32 chars minimum, 64+ recommended)
openssl rand -base64 24 # 32 chars
openssl rand -base64 48 # 64 chars (recommended)API Reference
Payments
start(params)
Start a payment and send OTP to customer.
const _payment = await havenpay.payments.start({
amount: 1000, // Amount in minor units (e.g., cents)
currency: 'USD', // or 'ZWG'
providerId: 'omari', // or 'innbuck'
cartId: 'cart-123', // Your cart/order ID
msisdn: '+263771234567', // Customer phone number
webhookUrl: 'https://example.com/webhooks/havenpay', // Optional
})Response:
{
"reference": "pay_abc123",
"otpReference": "otp_xyz789",
"status": "AUTH_SENT",
"message": "OTP sent to customer"
}verifyOtp(reference, otp)
Verify customer OTP and complete payment.
const _result = await havenpay.payments.verifyOtp('pay_abc123', '1234')Response:
{
"reference": "pay_abc123",
"status": "SUCCESS",
"providerPaymentRef": "provider_ref_123",
"message": "Payment completed successfully"
}getStatus(reference)
Get current payment status (includes provider verification).
const _status = await havenpay.payments.getStatus('pay_abc123')Response:
{
"reference": "pay_abc123",
"status": "SUCCESS",
"amount": 1000,
"currency": "USD",
"providerId": "omari",
"providerPaymentRef": "provider_ref_123",
"verified": true,
"verifiedAt": "2025-01-15T10:30:00Z",
"verificationMethod": "query",
"createdAt": "2025-01-15T10:25:00Z",
"updatedAt": "2025-01-15T10:30:00Z"
}getByCart(cartId)
Get latest payment for a cart (for recovery after page reload).
const _payment = await havenpay.payments.getByCart('cart-123')
if (_payment && _payment.status === 'AUTH_SENT') {
// Show OTP input
}cancel(reference)
Cancel/void a payment.
await havenpay.payments.cancel('pay_abc123')resendOtp(reference)
Resend OTP for a payment.
await havenpay.payments.resendOtp('pay_abc123')getProviders(currency?)
Get available payment providers.
const { providers } = await havenpay.payments.getProviders('USD')
providers.forEach((provider) => {
console.log(`${provider.name} (${provider.id}) - ${provider.latencyMs}ms`)
})Webhook Verification
📖 See PAYMENT_FLOW.md for complete webhook implementation guide
Verify webhooks sent from HavenPay to your backend.
import { HavenPay } from '@7haven/havenpay'
// or
import { verifyWebhook as _verifyWebhook } from 'havenpay'
app.post('/webhooks/havenpay', (req, res) => {
const signature = req.headers['x-haven-signature'] as string
const timestamp = req.headers['x-haven-timestamp'] as string
const body = req.rawBody // Raw body string (not parsed JSON)
// Verify signature
const isValid = HavenPay.verifyWebhook(
body,
signature,
timestamp,
process.env.HAVENPAY_WEBHOOK_SECRET!
)
if (!isValid) {
return res.status(401).send('Invalid signature')
}
// Parse and handle event
const _event = JSON.parse(body)
switch (_event.event) {
case 'payment.success':
// Handle successful payment
break
case 'payment.failed':
// Handle failed payment
break
case 'payment.pending':
// Handle pending payment
break
}
res.status(200).send('OK')
})Or use constructWebhookEvent for automatic parsing:
import { HavenPay } from '@7haven/havenpay'
app.post('/webhooks/havenpay', (req, res) => {
try {
const _event = HavenPay.constructWebhookEvent<WebhookPayload>(
req.rawBody,
req.headers['x-haven-signature'] as string,
req.headers['x-haven-timestamp'] as string,
process.env.HAVENPAY_WEBHOOK_SECRET!
)
// Event is verified and parsed
console.log(_event.reference, _event.status)
res.status(200).send('OK')
}
catch (_error) {
console.error('Invalid webhook:', _error)
res.status(401).send('Invalid signature')
}
})Error Handling
The SDK throws specific error types for different failure scenarios:
import {
HavenPayConfigError as _HavenPayConfigError,
HavenPaySignatureError as _HavenPaySignatureError,
HavenPayAPIError,
HavenPayNetworkError,
HavenPayTimeoutError,
} from '@7haven/havenpay'
try {
const _payment = await havenpay.payments.start({ })
} catch (error) {
if (error instanceof HavenPayAPIError) {
console.error('API Error:', error.message)
console.error('Status:', error.statusCode)
console.error('Code:', error.code)
console.error('Details:', error.details)
} else if (error instanceof HavenPayNetworkError) {
console.error('Network Error:', error.message)
} else if (error instanceof HavenPayTimeoutError) {
console.error('Timeout:', error.message)
}
}TypeScript Support
The SDK is written in TypeScript and provides full type definitions:
import type {
Currency as _Currency,
PaymentStatus as _PaymentStatus,
ProviderId as _ProviderId,
StartPaymentParams as _StartPaymentParams,
StartPaymentResponse as _StartPaymentResponse,
// ... all other types
} from '@7haven/havenpay'Security Best Practices
- Never expose your API key or signing secret in client-side code - Server-side only
- Always verify webhook signatures before processing events (HMAC-SHA256, constant-time comparison)
- Use HTTPS for all API requests (enforced by default)
- Store secrets in environment variables, not in code
- Rotate your secrets regularly (industry best practice: every 90 days)
- Enable response verification (enabled by default, protects against MITM attacks)
- Use minimum 32-char secrets (64+ chars recommended for production)
Environment Variables
# Required
HAVENPAY_API_KEY=your-api-key
HAVENPAY_SIGNING_SECRET=<32-char-secret> # Min 32 chars (openssl rand -base64 24)
# Optional
HAVENPAY_WEBHOOK_SECRET=<64-char-secret> # Min 32, recommend 64 chars (openssl rand -base64 48)
HAVENPAY_BASE_URL=https://pay.7haven.online/apiGenerate Secrets:
# Signing secret for API requests/responses (32 chars)
openssl rand -base64 24
# Webhook secret for receiving webhooks (64 chars recommended)
openssl rand -base64 48Examples
Full Payment Flow
import { HavenPay } from '@7haven/havenpay'
const havenpay = new HavenPay({
apiKey: process.env.HAVENPAY_API_KEY!,
signingSecret: process.env.HAVENPAY_SIGNING_SECRET!,
})
async function _processPayment(amount: number, phone: string, cartId: string) {
try {
// 1. Get available providers
const { providers } = await havenpay.payments.getProviders('USD')
const _ecocash = providers.find(p => p.id === 'omari')
if (!_ecocash) {
throw new Error('EcoCash not available')
}
// 2. Start payment
const payment = await havenpay.payments.start({
amount,
currency: 'USD',
providerId: 'omari',
cartId,
msisdn: phone,
})
console.log('OTP sent to customer')
// 3. Customer enters OTP (from your UI)
const otp = await promptForOTP() // Your UI logic
// 4. Verify OTP
const result = await havenpay.payments.verifyOtp(payment.reference, otp)
if (result.status === 'SUCCESS') {
console.log('Payment successful!')
return result
}
else if (result.status === 'PENDING') {
// Check status later or wait for webhook
console.log('Payment pending...')
return result
}
else {
throw new Error('Payment failed')
}
}
catch (error) {
console.error('Payment error:', error)
throw error
}
}Recovery After Page Reload
async function _recoverPayment(cartId: string) {
const payment = await havenpay.payments.getByCart(cartId)
if (!payment) {
// No existing payment, start new one
return null
}
if (payment.status === 'AUTH_SENT') {
// OTP was sent but not verified, show OTP input
return { showOtpInput: true, reference: payment.reference }
}
if (payment.status === 'PENDING') {
// Payment is being processed
return { status: 'processing', reference: payment.reference }
}
if (payment.status === 'SUCCESS') {
// Payment completed
return { status: 'success', reference: payment.reference }
}
return null
}Support
- Documentation: https://pay.7haven.online/docs
- Issues: https://github.com/7haveeen/haven-pay/issues
- Email: [email protected]
License
MIT © 7Haven
