Accept a Payment
This guide walks you through accepting a payment from start to finish, including both client-side and server-side code.
Overview
The payment flow consists of four main steps:
- Collect payment details - Customer enters card info or selects bank
- Create payment method - Tokenize sensitive data
- Create payment - Process the payment on your server
- Handle result - Show success/failure to customer
Step 1: Set Up Your Frontend
HTML Form
Create a payment form with a secure card element:
html
<!DOCTYPE html>
<html>
<head>
<title>Checkout</title>
<script src="https://js.salam.com/v1/salam.js"></script>
<style>
.payment-form {
max-width: 400px;
margin: 50px auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
#card-element {
padding: 12px;
border: 1px solid #ccc;
border-radius: 4px;
margin-bottom: 16px;
}
#card-errors {
color: #f93e3e;
margin-bottom: 16px;
}
button {
width: 100%;
padding: 12px;
background: #6772e5;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>
</head>
<body>
<form id="payment-form" class="payment-form">
<h2>Complete your payment</h2>
<p>Amount: <strong>RM 100.00</strong></p>
<div id="card-element"></div>
<div id="card-errors"></div>
<button id="submit">Pay RM 100.00</button>
</form>
<script src="/payment.js"></script>
</body>
</html>JavaScript
javascript
// payment.js
const salam = Salam('pk_test_xxxxx'); // Your publishable key
const elements = salam.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');
// Handle real-time validation errors
cardElement.on('change', (event) => {
const displayError = document.getElementById('card-errors');
if (event.error) {
displayError.textContent = event.error.message;
} else {
displayError.textContent = '';
}
});
// Handle form submission
const form = document.getElementById('payment-form');
const submitButton = document.getElementById('submit');
form.addEventListener('submit', async (event) => {
event.preventDefault();
submitButton.disabled = true;
submitButton.textContent = 'Processing...';
try {
// Step 1: Create payment method
const { paymentMethod, error } = await salam.createPaymentMethod({
type: 'card',
card: cardElement,
});
if (error) {
throw error;
}
// Step 2: Send to your server
const response = await fetch('/create-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
payment_method: paymentMethod.id,
amount: 10000, // RM 100.00
}),
});
const result = await response.json();
// Step 3: Handle result
if (result.status === 'succeeded') {
showSuccess();
} else if (result.status === 'requires_action') {
// Handle 3D Secure
const { error: confirmError } = await salam.confirmPayment(
result.client_secret
);
if (confirmError) {
showError(confirmError.message);
} else {
showSuccess();
}
} else {
showError(result.error || 'Payment failed');
}
} catch (error) {
showError(error.message);
} finally {
submitButton.disabled = false;
submitButton.textContent = 'Pay RM 100.00';
}
});
function showSuccess() {
document.getElementById('payment-form').innerHTML = `
<div style="text-align: center; padding: 40px;">
<div style="font-size: 60px;">✓</div>
<h2>Payment Successful!</h2>
<p>Thank you for your purchase.</p>
</div>
`;
}
function showError(message) {
const errorDiv = document.getElementById('card-errors');
errorDiv.textContent = message;
}Step 2: Set Up Your Backend
Express.js
javascript
const express = require('express');
const Salam = require('@salam/salam-node');
const app = express();
const salam = new Salam(process.env.SALAM_SECRET_KEY);
app.use(express.json());
app.use(express.static('public')); // Serve your HTML/JS files
app.post('/create-payment', async (req, res) => {
const { payment_method, amount } = req.body;
try {
const payment = await salam.payments.create({
amount,
currency: 'MYR',
payment_method,
description: 'Order payment',
metadata: {
customer_email: 'customer@example.com',
},
});
res.json({
status: payment.status,
client_secret: payment.client_secret,
});
} catch (error) {
res.status(500).json({
error: error.message,
});
}
});
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});NestJS
typescript
// payment.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { PaymentService } from './payment.service';
@Controller('payment')
export class PaymentController {
constructor(private readonly paymentService: PaymentService) {}
@Post('create')
async createPayment(@Body() body: { payment_method: string; amount: number }) {
return this.paymentService.createPayment(body);
}
}typescript
// payment.service.ts
import { Injectable } from '@nestjs/common';
import Salam from '@salam/salam-node';
@Injectable()
export class PaymentService {
private salam: Salam;
constructor() {
this.salam = new Salam(process.env.SALAM_SECRET_KEY);
}
async createPayment(data: { payment_method: string; amount: number }) {
const payment = await this.salam.payments.create({
amount: data.amount,
currency: 'MYR',
payment_method: data.payment_method,
description: 'Order payment',
});
return {
status: payment.status,
client_secret: payment.client_secret,
};
}
}Step 3: Handle Webhooks
Set up a webhook endpoint to receive payment notifications:
javascript
app.post('/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const sig = req.headers['x-salam-signature'];
const webhookSecret = process.env.WEBHOOK_SECRET;
try {
const event = salam.webhooks.constructEvent(
req.body,
sig,
webhookSecret
);
switch (event.type) {
case 'payment.succeeded':
const payment = event.data.object;
console.log(`✓ Payment ${payment.id} succeeded`);
// Fulfill the order
fulfillOrder(payment);
break;
case 'payment.failed':
console.log(`✗ Payment ${event.data.object.id} failed`);
// Send failure notification
break;
}
res.json({ received: true });
} catch (err) {
console.error('Webhook error:', err.message);
res.status(400).send(`Webhook Error: ${err.message}`);
}
}
);
function fulfillOrder(payment) {
// Your order fulfillment logic
// - Update database
// - Send confirmation email
// - Trigger shipping
console.log('Fulfilling order for payment:', payment.id);
}Webhook Configuration
Configure your webhook endpoint in the Dashboard at: Developers → Webhooks → Add Endpoint
Step 4: Handle Different Scenarios
3D Secure Authentication
Some cards require 3D Secure authentication:
javascript
if (result.status === 'requires_action') {
const { error } = await salam.confirmPayment(result.client_secret);
if (error) {
// Authentication failed
showError(error.message);
} else {
// Authentication succeeded
showSuccess();
}
}Save Card for Future Use
javascript
const payment = await salam.payments.create({
amount: 10000,
currency: 'MYR',
payment_method: paymentMethod.id,
customer: 'cus_xxxxx', // Existing customer
save_payment_method: true, // Save for future use
});Handle Errors
javascript
try {
const payment = await salam.payments.create({...});
} catch (error) {
if (error.type === 'card_error') {
// Card was declined
console.log('Card declined:', error.message);
} else if (error.type === 'invalid_request_error') {
// Invalid parameters
console.log('Invalid request:', error.message);
} else {
// Other error
console.log('Error:', error.message);
}
}Testing
Test your integration using test cards:
| Card Number | Behavior |
|---|---|
4242 4242 4242 4242 | Succeeds |
4000 0027 6000 3184 | Requires 3D Secure |
4000 0000 0000 0002 | Declined |
Production Checklist
Before going live:
- [ ] Switch to live API keys
- [ ] Set up webhook endpoint
- [ ] Verify webhook signatures
- [ ] Handle all error scenarios
- [ ] Test 3D Secure flow
- [ ] Implement logging
- [ ] Set up monitoring
- [ ] Review security best practices
Next Steps
- FPX Payments - Accept bank transfers
- Handle Webhooks - Production webhook setup
- Save Payment Methods - Let customers save cards
- Subscription Payments - Recurring payments