@ojire/payment-gateway-sdk
v1.0.1
Published
JavaScript SDK for Ojire Payment Gateway
Readme
OPG JavaScript SDK
JavaScript SDK for Ojire Payment Gateway (OPG) - enables secure payment collection via QR Code (QRIS), Virtual Accounts (VA), and Credit Cards (CC).
Features
- 🔒 PCI-DSS Compliant: Credit card data handled through secure Hosted Fields (iFrames)
- 🛡️ Dual-Key Authentication: Separate keys for frontend and backend operations
- 💳 Multiple Payment Methods: QRIS, Virtual Accounts, Credit Cards
- 🎨 Customizable UI: Modal or embedded, light/dark themes
- 📱 Mobile Responsive: Works seamlessly on all devices
- 🔄 Automatic Retries: Built-in retry logic with exponential backoff
- 🎯 TypeScript Support: Full type definitions included
- 📦 Multiple Formats: ES modules, CommonJS, and UMD bundles
Installation
NPM
npm install @ojire/opg-sdkYarn
yarn add @ojire/opg-sdkCDN (UMD)
<!-- Production (minified) -->
<script src="https://cdn.ojire.com/opg-sdk/1.0.0/index.umd.min.js"></script>
<link rel="stylesheet" href="https://cdn.ojire.com/opg-sdk/1.0.0/index.umd.min.css">
<!-- Development -->
<script src="https://cdn.ojire.com/opg-sdk/1.0.0/index.umd.js"></script>
<link rel="stylesheet" href="https://cdn.ojire.com/opg-sdk/1.0.0/index.umd.css">Quick Start
1. Create Payment Intent (Backend)
First, create a Payment Intent from your backend using your Secret Server Key:
// Backend (Node.js example)
const response = await fetch('https://api.ojire.com/v1/payment-intents', {
method: 'POST',
headers: {
'Authorization': 'Bearer opg_live_sk_v1_xxxxx', // Secret Server Key
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: {
value: '150000.00',
currency: 'IDR'
},
order_id: 'ORDER-123',
customer: {
name: 'John Doe',
email: '[email protected]',
phone: '+6281234567890'
},
description: 'Order #123 - Product Purchase'
})
});
const { payment_intent } = await response.json();
const clientSecret = payment_intent.client_secret;
// Send clientSecret to your frontend2. Initialize SDK (Frontend)
import { OPGClient } from '@ojire/opg-sdk';
// Initialize with your Public Client Key
const opg = new OPGClient({
publicKey: 'opg_test_pk_v1_xxxxx', // Public Client Key
locale: 'en', // 'en' or 'id'
theme: 'light' // 'light' or 'dark'
});
// Initialize and check connectivity
await opg.init();3. Process Payment
Option A: Promise-based API
try {
const result = await opg.pay(clientSecret, {
modal: true, // Show as modal overlay
onPending: (result) => {
console.log('Payment pending:', result);
// Display QR code or VA number to user
}
});
console.log('Payment successful:', result);
// Redirect to success page
window.location.href = '/order/success';
} catch (error) {
console.error('Payment failed:', error);
// Handle error
}Option B: Callback-based API
const session = await opg.mount({
clientSecret: clientSecret,
modal: true,
onSuccess: (result) => {
console.log('Payment successful:', result);
// Update UI, redirect to success page
window.location.href = '/order/success';
},
onPending: (result) => {
console.log('Payment pending:', result);
// Show QR code or VA instructions
},
onFailure: (error) => {
console.error('Payment failed:', error);
// Show error message to user
alert(`Payment failed: ${error.message}`);
},
onClose: () => {
console.log('Payment UI closed');
// User closed the payment modal
}
});Option C: Embedded in Container
<div id="payment-container"></div>const session = await opg.mount({
clientSecret: clientSecret,
container: '#payment-container', // or document.getElementById('payment-container')
modal: false, // Embed in container instead of modal
onSuccess: (result) => {
console.log('Payment successful:', result);
}
});API Reference
OPGClient
Constructor
new OPGClient(config: OPGClientConfig | string)Parameters:
config: Configuration object or public key stringpublicKey(required): Your Public Client Keylocale(optional): UI language -'en'or'id'(default:'en')theme(optional): UI theme -'light'or'dark'(default:'light')baseUrl(optional): Custom API base URL (for testing)
Example:
// Simple initialization
const opg = new OPGClient('opg_test_pk_v1_xxxxx');
// With options
const opg = new OPGClient({
publicKey: 'opg_test_pk_v1_xxxxx',
locale: 'id',
theme: 'dark'
});Methods
init(): Promise<void>
Initialize the SDK and validate connectivity to OPG servers.
await opg.init();Throws:
NetworkError: If connectivity check fails
pay(clientSecret: string, options?: PaymentOptions): Promise<PaymentResult>
Shorthand method to mount payment UI and wait for result (Promise-based).
const result = await opg.pay(clientSecret, {
modal: true,
idempotencyKey: 'unique-key-123', // Optional
onPending: (result) => {
// Handle pending state
}
});Returns: Promise<PaymentResult | PaymentActionRequired>
mount(options: PaymentOptions): Promise<PaymentSession>
Mount payment UI and return a PaymentSession for manual control.
const session = await opg.mount({
clientSecret: clientSecret,
container: '#payment-container',
modal: false,
onSuccess: (result) => { /* ... */ },
onFailure: (error) => { /* ... */ }
});Returns: Promise<PaymentSession>
destroy(): void
Cleanup and unmount the SDK.
opg.destroy();Properties
version: string
SDK version number.
console.log(opg.version); // "1.0.0"PaymentOptions
Configuration for payment UI and event handlers.
interface PaymentOptions {
clientSecret: string; // Required: Client secret from Payment Intent
container?: string | HTMLElement; // DOM selector or element for embedded mode
modal?: boolean; // Show as modal (default: true)
idempotencyKey?: string; // Optional: For safe retries
onSuccess?: (result: PaymentResult) => void;
onPending?: (result: PendingResult) => void;
onFailure?: (error: PaymentError) => void;
onClose?: () => void;
}PaymentResult
Result object returned on successful payment.
interface PaymentResult {
id: string; // Payment ID
payment_intent_id: string; // Payment Intent ID
status: 'success'; // Terminal status
amount: Amount; // Payment amount
payment_method: string; // 'VA' | 'QRIS' | 'CREDIT_CARD'
// VA-specific fields
va_number?: string;
va_bank_code?: string;
// QRIS-specific fields
qr_code?: string; // Base64 encoded QR image
// Credit Card-specific fields
card_last4?: string;
card_network?: string;
auth_code?: string;
created_at: string;
updated_at: string;
}PendingResult
Result object for pending payments (QR/VA).
interface PendingResult {
id: string;
payment_intent_id: string;
status: 'pending';
amount: Amount;
payment_method: string;
// Display data for user
va_number?: string;
qr_code?: string;
expires_at?: string;
}PaymentError
Error object with details.
interface PaymentError {
message: string;
code: string;
details?: Record<string, any>;
}PaymentSession
Active payment session with control methods.
class PaymentSession {
readonly paymentIntentId: string;
readonly status: PaymentStatus;
// Confirm payment with selected method
confirm(method: PaymentMethodSelection): Promise<PaymentResult>;
// Get current status
getStatus(): Promise<PaymentStatus>;
// Start/stop polling for status updates
startPolling(intervalMs?: number): void;
stopPolling(): void;
// Close the payment UI
close(): void;
// Event listeners
on(event: PaymentEvent, handler: EventHandler): void;
off(event: PaymentEvent, handler: EventHandler): void;
}Event Handling
The SDK emits events at key points in the payment lifecycle:
Event Types
success: Payment completed successfullypending: Payment requires user action (scan QR, transfer to VA)failure: Payment failedclose: User closed the payment UI
Event Data
Each event includes:
{
payment_intent_id: string;
payment_id?: string; // Available after confirmation
status: PaymentStatus;
// ... additional payment details
}Example
const session = await opg.mount({ clientSecret });
session.on('success', (result) => {
console.log('Payment ID:', result.payment_id);
console.log('Amount:', result.amount);
// Update your UI
});
session.on('pending', (result) => {
if (result.payment_method === 'QRIS') {
console.log('Show QR code:', result.qr_code);
} else if (result.payment_method === 'VA') {
console.log('VA Number:', result.va_number);
}
});
session.on('failure', (error) => {
console.error('Error code:', error.code);
console.error('Message:', error.message);
});Error Codes
Common error codes you may encounter:
| Code | Description | Action |
|------|-------------|--------|
| INVALID_PUBLIC_KEY | Public key format is invalid | Check your public key format |
| CONNECTIVITY_ERROR | Cannot connect to OPG servers | Check network connection |
| INVALID_CLIENT_SECRET | Client secret is invalid or expired | Create a new Payment Intent |
| PAYMENT_EXPIRED | Payment Intent has expired | Create a new Payment Intent |
| AMOUNT_MISMATCH | Payment amount doesn't match intent | Check for tampering |
| PAYMENT_FAILED | Payment processing failed | Check payment details |
| NETWORK_ERROR | Network request failed | Retry the operation |
| VALIDATION_ERROR | Input validation failed | Check input data |
| CARD_DECLINED | Card was declined | Try another card |
| INSUFFICIENT_FUNDS | Insufficient funds | Use another payment method |
| 3DS_REQUIRED | 3D Secure authentication required | Complete 3DS challenge |
Payment Methods
QRIS (QR Code)
const result = await opg.pay(clientSecret, {
onPending: (result) => {
// Display QR code to user
const qrImage = document.getElementById('qr-image');
qrImage.src = result.qr_code; // Base64 encoded image
// Show expiry time
console.log('Expires at:', result.expires_at);
}
});The SDK automatically polls for payment status after QR is displayed.
Virtual Account (VA)
const result = await opg.pay(clientSecret, {
onPending: (result) => {
// Display VA number to user
console.log('Bank:', result.va_bank_code);
console.log('VA Number:', result.va_number);
console.log('Amount:', result.amount.value);
// Add copy button
navigator.clipboard.writeText(result.va_number);
}
});The SDK automatically polls for payment status after VA is displayed.
Credit Card
Credit card payments use Hosted Fields for PCI-DSS compliance:
import { HostedFields } from '@ojire/opg-sdk';
// Create hosted fields
const hostedFields = new HostedFields({
cardNumber: {
container: '#card-number',
placeholder: '1234 5678 9012 3456'
},
expiryDate: {
container: '#expiry-date',
placeholder: 'MM/YY'
},
cvv: {
container: '#cvv',
placeholder: '123'
},
styles: {
base: {
fontSize: '16px',
color: '#333'
},
invalid: {
color: '#e74c3c'
}
}
});
// Mount the fields
await hostedFields.mount();
// Listen for validation events
hostedFields.on('change', (event) => {
console.log('Field:', event.field);
console.log('Valid:', event.valid);
});
// Tokenize and pay
const { token } = await hostedFields.tokenize();
const result = await opg.pay(clientSecret, {
// Token is automatically used for card payments
});Security Best Practices
1. Never Expose Secret Server Key
❌ DON'T use Secret Server Key in frontend:
// WRONG - Never do this!
const opg = new OPGClient('opg_live_sk_v1_xxxxx'); // Secret key!✅ DO use Public Client Key in frontend:
// CORRECT
const opg = new OPGClient('opg_live_pk_v1_xxxxx'); // Public key2. Create Payment Intents Server-Side
Always create Payment Intents from your backend to lock the amount:
// Backend only
const intent = await createPaymentIntent({
amount: calculateOrderTotal(order), // Server-side calculation
order_id: order.id
});
// Send only client_secret to frontend
res.json({ clientSecret: intent.client_secret });3. Verify Webhooks
Always verify webhook signatures:
// Backend webhook handler
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const hmac = crypto.createHmac('sha256', secret);
hmac.update(JSON.stringify(payload));
const expectedSignature = hmac.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
app.post('/webhook', (req, res) => {
const signature = req.headers['x-opg-signature'];
const isValid = verifyWebhookSignature(
req.body,
signature,
process.env.WEBHOOK_SECRET
);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
// Process webhook
const { event_type, payment } = req.body;
// Update order status in database
res.status(200).send('OK');
});4. Use HTTPS
Always serve your website over HTTPS in production.
5. Implement CSP Headers
Add Content Security Policy headers to prevent XSS:
Content-Security-Policy: frame-src https://api.ojire.com https://api-staging.ojire.com;Testing
Test Mode
Use test keys for development:
const opg = new OPGClient('opg_test_pk_v1_xxxxx');Test Cards
Use these test card numbers:
| Card Number | Network | Result | |-------------|---------|--------| | 4111111111111111 | Visa | Success | | 5555555555554444 | Mastercard | Success | | 378282246310005 | Amex | Success | | 4000000000000002 | Visa | Declined | | 4000000000009995 | Visa | Insufficient Funds |
Test VA Banks
Available test banks:
- BCA:
014 - BNI:
009 - Mandiri:
008 - BRI:
002
Test QRIS
In test mode, QRIS payments are automatically marked as successful after 5 seconds.
Browser Support
- Chrome 90+
- Firefox 88+
- Safari 14+
- Edge 90+
- Mobile browsers (iOS Safari 14+, Chrome Mobile 90+)
TypeScript Support
The SDK is written in TypeScript and includes full type definitions:
import {
OPGClient,
PaymentResult,
PaymentError,
OPGClientConfig
} from '@ojire/opg-sdk';
const config: OPGClientConfig = {
publicKey: 'opg_test_pk_v1_xxxxx',
locale: 'en',
theme: 'light'
};
const opg = new OPGClient(config);
try {
const result: PaymentResult = await opg.pay(clientSecret);
console.log(result.payment_id);
} catch (error) {
const paymentError = error as PaymentError;
console.error(paymentError.code);
}Migration Guide
From v0.x to v1.0
- Update initialization:
// Old
const opg = new OPG({ apiKey: 'xxx' });
// New
const opg = new OPGClient({ publicKey: 'opg_test_pk_v1_xxx' });
await opg.init();- Update payment method:
// Old
opg.createPayment({ amount: 150000 });
// New
// Create Payment Intent from backend first
const result = await opg.pay(clientSecret);Examples
See the /examples directory for complete examples:
Support
- Documentation: https://docs.ojire.com
- API Reference: https://api-docs.ojire.com
- Support Email: [email protected]
- GitHub Issues: https://github.com/ojire/opg-sdk/issues
Development
# Install dependencies
npm install
# Run tests
npm test
# Run tests in watch mode
npm run test:watch
# Build
npm run build
# Type check
npm run typecheck
# Lint
npm run lintContributing
We welcome contributions! Please see CONTRIBUTING.md for details.
License
MIT License - see LICENSE for details.
Changelog
See CHANGELOG.md for version history.
Made with ❤️ by Ojire
