Custom UI
This guide walks you through implementing your own card payment form with custom HTML code. We recommend following this guide only if the integration provided by our payment form does not meet your requirements, For more information about the payment form please checkout Basic Integration.
Overview
In this guide, we'll see how to set up a basic payment form to accept credit/debit card payments. The payment flow will be as follows:
- Tokenize card information temporarely.
- Use generated token to start the payment.
- Redirect user to 3DS page.
- Validate payment result.
Before Starting
Before you start the integration process, make sure you complete these steps:
- Sign up for a Moyasar test account at Moyasar's Dashboard.
- Get your API Key To authenticate your API request.
For more details about used APIs in this tutorial, please refer to Moyasar API Docs
Due to security considerations, you are not allowed to post user details to your own server and the tokenization process must be started from the client browser directly against Moyasar API.
Integrate Your Payment Form
We need to create a simple HTML form having two different sets of input fields:
Hidden
input fields are required to authenticate the tokenization request.- Visible input fields for user-related data.
Here is the specification of the required data and fields to be included in the submission:
Step 1: Connect the HTML Payment Form
First, set your form’s action attribute to payment create endpoint URL, and set the method attribute to POST
<form action="https://api.moyasar.com/v1/tokens" method="POST">
<button type="submit">Pay</button>
</form>
You are not allowed to use any other action other than the one indicated earlier.
Step 2: Include Authentication Fields
Next, we need to include some required hidden
fields for the tokenization process, which are:
publishable_api_key
: used to identify the merchant account and is safe to include in frontend code. To learn more, check out the Authentication guide.save_only
: this fields must be set totrue
to indicate for the server that this is a checkout token.
Here are the HTML elements need to be added:
<input type="hidden" name="publishable_api_key" value="your_publishable_api_key_here" />
<input type="hidden" name="save_only" value="true" />
Step 3: Include Payment Details Fields
Finally, we need a few visible elements to allow users to enter their payment details. Here are the required fields for credit card payments:
name
for the card holder name.number
for the card number.month
for the card expiry month.year
for the card expiry year.cvc
for the card security value CVC/CVV.
Final Code
<form
id="moyasar-token-form"
accept-charset="UTF-8"
action="https://api.moyasar.com/v1/tokens"
method="POST">
<input type="hidden" name="publishable_api_key" value="your_publishable_api_key_here" />
<input type="hidden" name="save_only" value="true" />
<input type="text" name="name" />
<input type="text" name="number" />
<input type="text" name="month" />
<input type="text" name="year" />
<input type="text" name="cvc" />
<button id="moyasar-payment-button" type="submit">Purchase</button>
</form>
Starting Payment
To start the payment process, we must control the HTML form to create the token then start the payment process from the merchant backend.
The following Javascript code will create the token then pass it to the backend code to start the payment:
async function initiatePayment(event) {
// This line is crucial to prevent the default form behaviour
event.preventDefault();
const form = document.getElementById('moyasar-token-form');
const tokenData = Object.fromEntries(new FormData(form));
// Create temporary checkout token
let response = await fetch('https://api.moyasar.com/v1/tokens', {
method: 'post',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(tokenData),
});
response = await response.json();
// Send token to merchant backend
let backendResponse = await fetch('https://mystore.example.com/initiate-payment', {
method: 'post',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
// You can add any other information related to your business logic here.
token: response.token,
}),
});
backendResponse = await backendResponse.json();
// Redirect the user to payment page
window.location.href = backendResponse.transaction_url;
}
document.addEventListener('DOMContentLoaded', () => {
document
.getElementById('moyasar-payment-button')
.addEventListener('click', (e) => initiatePayment(e));
});
The previous Javascript code assumes you have your backend on https://mystore.example.com
. Your backend must provide
an endpoint (e.g. /initiate-payment
) for starting the payment process as follows:
- Laravel
- Ruby on Rails
- Express JS
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
class PaymentController extends Controller
{
public function initiatePayment(Request $request)
{
// Assuming you have an Order model where we will check if the order exists first
$order = Order::where('number', $request->input('order_number'))->first();
if (! ($order && $order-payable())) {
return response()
->json(['message' => 'Invalid order'])
->setStatusCode(400);
}
$response = Http::asJson()
->withBasicAuth('pk_live_123', '')
->post('https://api.moyasar.com/v1/payments', [
'amount' => 100,
'currency' => 'SAR',
'description' => 'Test Payment',
'callback_url' => 'https://mystore.example.com/payment-return',
'source' => [
'type' => 'token',
'token' => $request->input('token')
]
])
->throw()
->json();
if ($response['status'] != 'initiated') {
return response()
->json(['message' => 'Payment cannot be initiated: ' . $response['source']['message']])
->setStatusCode(400);
}
return response()->json([
'transaction_url' => $response['source']['transaction_url']
]);
}
}
require 'mint_http'
class PaymentsController < ApplicationController
def initiate_payment
# Assuming you have an Order model where we will check if the order exists first
order = Order.find_by(number: params[:order_number])
unless order && order.payable?
render json: { message: 'Invalid order' }, status: :bad_request and return
end
response = MintHttp
.accept_json
.basic_auth('pk_live_123')
.post('https://api.moyasar.com/v1/payments', {
amount: 100,
currency: 'SAR',
description: 'Test Payment',
callback_url: 'https://mystore.example.com/payment-return',
source: {
type: 'token',
token: params[:token]
}
})
.raise!
.json
if response['status'] != 'initiated'
render json: { message: "Payment cannot be initiated: #{response['source']['message']}" }, status: :bad_request
else
render json: { transaction_url: response['source']['transaction_url'] }
end
end
end
const axios = require('axios');
const initiatePayment = async (req, res) => {
const { order_number, token } = req.body;
// Assuming Order is a mongoose model or equivalent ORM
const order = await Order.findOne({ number: order_number });
if (! (order && order.isPayable())) {
return res.status(400).json({ message: 'Invalid order' });
}
try {
const response = await axios.post('https://api.moyasar.com/v1/payments', {
amount: 100,
currency: 'SAR',
description: 'Test Payment',
callback_url: 'https://mystore.example.com/payment-return',
source: {
type: 'token',
token: token,
},
}, {
auth: {
username: 'pk_live_123',
password: '',
},
headers: {
'Content-Type': 'application/json',
},
});
if (response.data.status !== 'initiated') {
return res.status(400).json({
message: `Payment cannot be initiated: ${response.data.source.message}`,
});
}
res.json({ transaction_url: response.data.source.transaction_url });
} catch (error) {
res.status(500).json({ message: 'Something went wrong', error: error.message });
}
};
module.exports = { initiatePayment };
Backend code provided here is only an example and might not be the same backend you are using. You may need to change or adjust the code in order for it to run within your environment.
Error handling is intentionally left out of the previous code examples and it is your responsibility to handle error cases that might arise.
Here is a non-exhaustive list of error cases:
- Creating token might return a validation or business error 4xx.
- Creating payment might return a validation or business error 4xx or might return a payment that has status
failed
.
Verify the Payment
Once the user has completed the payment, they are redirected back to your website or app using the
callback_url
you provided earlier and the following HTTP query parameters will be appended:
- id
- status
- message
https://www.my-store.com/payments_redirect?id=79cced57-9deb-4c4b-8f48-59c124f79688&status=paid&message=Succeeded
Fetch the payment using its ID through the API, and verify its status, amount, and currency before accepting or placing any user's purchase or, using the ID we have saved in the Javascript example, perform the same verifications.