Skip to content

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:

  1. Collect payment details - Customer enters card info or selects bank
  2. Create payment method - Tokenize sensitive data
  3. Create payment - Process the payment on your server
  4. 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 NumberBehavior
4242 4242 4242 4242Succeeds
4000 0027 6000 3184Requires 3D Secure
4000 0000 0000 0002Declined

See all test cards →

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

Released under the MIT License.