Embedded Integration Code Examples
Build your own checkout using DigetPay's Server-to-Server (S2S) APIs.
Embedded Integration allows you to collect payment details on your own checkout page, then securely send the transaction from your backend to DigetPay.
PCI DSS RequirementEmbedded Integration requires secure handling of cardholder data (SAQ D or equivalent).
If you don't require full checkout customization, we strongly recommend using Hosted Checkout instead.
Never send card data directly from the browserCard details should always be submitted to your backend, then your backend calls DigetPay APIs.
PrerequisitesBefore starting, make sure you have:
- Sandbox account
- Fin API Key
- Merchant credentials
- Backend server
- HTTPS enabled
- Knowledge of REST APIs
Payment Flow
sequenceDiagram participant Customer participant Merchant Backend participant DigetPay Customer->>Merchant Backend: Submit Card Details Merchant Backend->>Merchant Backend: Generate MD5 Hash Merchant Backend->>DigetPay: POST /payment/s2s/sale DigetPay-->>Merchant Backend: Approved / Declined / 3DS HTML Merchant Backend->>Customer: Show 3DS or Success Page DigetPay->>Merchant Backend: Webhook Notification
Configuration
<?php
$config = [
'environment' => getenv('DIGETPAY_ENV') ?: 'staging',
'api_key' => getenv('DIGETPAY_API_KEY') ?: '',
'environments' => [
'staging' => ['api_base' => 'https://fin-api.digetpay.com/v1'],
'production' => ['api_base' => 'https://api.digetpay.com/v1'],
],
];
</Tab>
<Tab title="Node.js">
```javascript
// config/digetpay.js
const env = process.env.DIGETPAY_ENV || 'staging';
const API_BASES = {
staging: 'https://fin-api.digetpay.com/v1',
production: 'https://api.digetpay.com/v1',
};
export const config = {
environment: env,
apiKey: process.env.DIGETPAY_API_KEY || '',
apiBase: API_BASES[env],
};
if (!config.apiKey) throw new Error('DIGETPAY_API_KEY is not set');# config/digetpay.py
import os
API_BASES = {
"staging": "https://fin-api.digetpay.com/v1",
"production": "https://api.digetpay.com/v1",
}
environment = os.getenv("DIGETPAY_ENV", "staging")
api_key = os.getenv("DIGETPAY_API_KEY", "")
api_base = API_BASES[environment]
if not api_key:
raise RuntimeError("DIGETPAY_API_KEY is not set")// DigetPayConfig.java
public final class DigetPayConfig {
private static final Map<String, String> API_BASES = Map.of(
"staging", "https://fin-api.digetpay.com/v1",
"production", "https://api.digetpay.com/v1"
);
public static String environment() {
return System.getenv().getOrDefault("DIGETPAY_ENV", "staging");
}
public static String apiKey() {
String key = System.getenv("DIGETPAY_API_KEY");
if (key == null || key.isBlank()) throw new IllegalStateException("DIGETPAY_API_KEY is not set");
return key;
}
public static String apiBase() {
return API_BASES.get(environment());
}
}Generate MD5 Request Hash
Every Embedded transaction requires generating a request hash.
Formula:
reverse(email)
+
API_KEY
+
reverse(first6 + last4 of card)
Uppercase the string then calculate the MD5 digest.
<?php
declare(strict_types=1);
function digetpayEmbeddedHash(string $email, string $cardNumber, string $apiKey): string
{
$card = preg_replace('/\s+/', '', $cardNumber);
$reversedEmail = strrev($email);
$reversedCard = strrev(substr($card, 0, 6) . substr($card, -4));
$raw = strtoupper($reversedEmail . $apiKey . $reversedCard);
return md5($raw);
}// src/digetpayHash.js
import crypto from 'crypto';
export function digetpayEmbeddedHash(email, cardNumber, apiKey) {
const card = cardNumber.replace(/\s+/g, '');
const reversedEmail = email.split('').reverse().join('');
const reversedCard = (card.slice(0, 6) + card.slice(-4)).split('').reverse().join('');
const raw = (reversedEmail + apiKey + reversedCard).toUpperCase();
return crypto.createHash('md5').update(raw).digest('hex');
}# digetpay_hash.py
import hashlib
import re
def digetpay_embedded_hash(email: str, card_number: str, api_key: str) -> str:
card = re.sub(r"\s+", "", card_number)
reversed_email = email[::-1]
reversed_card = (card[:6] + card[-4:])[::-1]
raw = (reversed_email + api_key + reversed_card).upper()
return hashlib.md5(raw.encode()).hexdigest()// DigetPayHash.java
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
public final class DigetPayHash {
public static String embeddedHash(String email, String cardNumber, String apiKey) throws Exception {
String card = cardNumber.replaceAll("\\s+", "");
String reversedEmail = new StringBuilder(email).reverse().toString();
String reversedCard = new StringBuilder(card.substring(0, 6) + card.substring(card.length() - 4)).reverse().toString();
String raw = (reversedEmail + apiKey + reversedCard).toUpperCase();
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(raw.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder();
for (byte b : digest) hex.append(String.format("%02x", b));
return hex.toString();
}
}Full explanation: Request Hash (MD5).
Embedded Client
The Embedded client handles communication between your backend and the DigetPay API.
It provides helper methods for:
- Generate request hash
- Direct Sale
- Capture
- Void
- Transaction Status
<?php
declare(strict_types=1);
require_once __DIR__ . '/digetpay-hash.php';
final class DigetPayEmbeddedClient
{
private string $apiBase;
private string $apiKey;
public function __construct(array $config)
{
$env = $config['environment'] ?? 'staging';
$this->apiBase = rtrim($config['environments'][$env]['api_base'], '/');
$this->apiKey = $config['api_key'] ?? '';
}
/** @return array<string, mixed> */
public function directSale(array $order, array $card, array $customer, string $successUrl, string $failureUrl): array
{
$hash = digetpayEmbeddedHash($customer['email'], $card['cardNumber'], $this->apiKey);
$body = [
'orderId' => $order['orderId'],
'amount' => $order['amount'],
'currency' => $order['currency'] ?? 'SAR',
'paymentMethod' => 'card',
'auth' => $order['auth'] ?? 'N',
'card' => [
'cardNumber' => preg_replace('/\s+/', '', $card['cardNumber']),
'cardExpiryMonth' => $card['cardExpiryMonth'],
'cardExpiryYear' => $card['cardExpiryYear'],
'cardCvv' => $card['cardCvv'],
'cardHolder' => $card['cardHolder'],
],
'customer' => $customer,
'successUrl' => $successUrl,
'failureUrl' => $failureUrl,
'hash' => $hash,
];
return $this->request('POST', '/payment/s2s/sale', $body);
}
/** @return array<string, mixed> */
public function capture(string $transactionId, float $amount): array
{
return $this->request('POST', '/payment/s2s/capture', [
'transactionId' => $transactionId,
'amount' => $amount,
]);
}
/** @return array<string, mixed> */
public function voidTransaction(string $transactionId): array
{
return $this->request('POST', '/payment/s2s/void', [
'transactionId' => $transactionId,
]);
}
/** @return array<string, mixed> */
public function getStatus(string $transactionId): array
{
return $this->request('POST', '/payment/s2s/status/' . urlencode($transactionId));
}
/** @return array<string, mixed> */
private function request(string $method, string $path, ?array $body = null): array
{
$ch = curl_init($this->apiBase . $path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'x-api-key: ' . $this->apiKey,
],
CURLOPT_TIMEOUT => 60,
]);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body, JSON_THROW_ON_ERROR));
}
$raw = curl_exec($ch);
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($raw === false || $code >= 400) {
throw new RuntimeException("Embedded HTTP $code: $raw");
}
return json_decode($raw, true) ?: [];
}
}// src/digetpayEmbeddedClient.js
import { config } from '../config/digetpay.js';
import { digetpayEmbeddedHash } from './digetpayHash.js';
async function request(method, path, body) {
const res = await fetch(`${config.apiBase}${path}`, {
method,
headers: { 'Content-Type': 'application/json', 'x-api-key': config.apiKey },
body: body ? JSON.stringify(body) : undefined,
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(`DigetPay HTTP ${res.status}: ${JSON.stringify(data)}`);
return data;
}
export function directSale({ order, card, customer, successUrl, failureUrl }) {
const hash = digetpayEmbeddedHash(customer.email, card.cardNumber, config.apiKey);
return request('POST', '/payment/s2s/sale', {
orderId: order.orderId,
amount: order.amount,
currency: order.currency || 'SAR',
paymentMethod: 'card',
auth: order.auth || 'N',
card: {
cardNumber: card.cardNumber.replace(/\s+/g, ''),
cardExpiryMonth: card.cardExpiryMonth,
cardExpiryYear: card.cardExpiryYear,
cardCvv: card.cardCvv,
cardHolder: card.cardHolder,
},
customer,
successUrl,
failureUrl,
hash,
});
}
export function capture(transactionId, amount) {
return request('POST', '/payment/s2s/capture', { transactionId, amount });
}
export function voidTransaction(transactionId) {
return request('POST', '/payment/s2s/void', { transactionId });
}
export function getStatus(transactionId) {
return request('POST', `/payment/s2s/status/${encodeURIComponent(transactionId)}`);
}# digetpay_embedded_client.py
import requests
from config.digetpay import api_base, api_key
from digetpay_hash import digetpay_embedded_hash
HEADERS = {"Content-Type": "application/json", "x-api-key": api_key}
def _request(method: str, path: str, body: dict | None = None) -> dict:
resp = requests.request(method, f"{api_base}{path}", headers=HEADERS, json=body, timeout=60)
resp.raise_for_status()
return resp.json()
def direct_sale(order: dict, card: dict, customer: dict, success_url: str, failure_url: str) -> dict:
card_number = card["cardNumber"].replace(" ", "")
hash_value = digetpay_embedded_hash(customer["email"], card_number, api_key)
return _request("POST", "/payment/s2s/sale", {
"orderId": order["orderId"],
"amount": order["amount"],
"currency": order.get("currency", "SAR"),
"paymentMethod": "card",
"auth": order.get("auth", "N"),
"card": {
"cardNumber": card_number,
"cardExpiryMonth": card["cardExpiryMonth"],
"cardExpiryYear": card["cardExpiryYear"],
"cardCvv": card["cardCvv"],
"cardHolder": card["cardHolder"],
},
"customer": customer,
"successUrl": success_url,
"failureUrl": failure_url,
"hash": hash_value,
})
def capture(transaction_id: str, amount: float) -> dict:
return _request("POST", "/payment/s2s/capture", {"transactionId": transaction_id, "amount": amount})
def void_transaction(transaction_id: str) -> dict:
return _request("POST", "/payment/s2s/void", {"transactionId": transaction_id})
def get_status(transaction_id: str) -> dict:
return _request("POST", f"/payment/s2s/status/{transaction_id}")// DigetPayEmbeddedClient.java
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.*;
import java.util.*;
public class DigetPayEmbeddedClient {
private final HttpClient http = HttpClient.newHttpClient();
private final ObjectMapper mapper = new ObjectMapper();
private final String apiBase = DigetPayConfig.apiBase();
private final String apiKey = DigetPayConfig.apiKey();
@SuppressWarnings("unchecked")
private Map<String, Object> request(String method, String path, Map<String, Object> body) throws Exception {
HttpRequest.Builder builder = HttpRequest.newBuilder()
.uri(URI.create(apiBase + path))
.header("Content-Type", "application/json")
.header("x-api-key", apiKey);
if (body != null) {
builder.method(method, HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(body)));
} else {
builder.method(method, HttpRequest.BodyPublishers.noBody());
}
HttpResponse<String> resp = http.send(builder.build(), HttpResponse.BodyHandlers.ofString());
if (resp.statusCode() >= 400) throw new RuntimeException("DigetPay HTTP " + resp.statusCode());
return mapper.readValue(resp.body(), Map.class);
}
public Map<String, Object> directSale(Map<String, Object> order, Map<String, String> card,
Map<String, String> customer, String successUrl, String failureUrl) throws Exception {
String cardNumber = card.get("cardNumber").replaceAll("\\s+", "");
String hash = DigetPayHash.embeddedHash(customer.get("email"), cardNumber, apiKey);
Map<String, Object> body = new LinkedHashMap<>();
body.put("orderId", order.get("orderId"));
body.put("amount", order.get("amount"));
body.put("currency", order.getOrDefault("currency", "SAR"));
body.put("paymentMethod", "card");
body.put("auth", order.getOrDefault("auth", "N"));
body.put("card", Map.of(
"cardNumber", cardNumber,
"cardExpiryMonth", card.get("cardExpiryMonth"),
"cardExpiryYear", card.get("cardExpiryYear"),
"cardCvv", card.get("cardCvv"),
"cardHolder", card.get("cardHolder")
));
body.put("customer", customer);
body.put("successUrl", successUrl);
body.put("failureUrl", failureUrl);
body.put("hash", hash);
return request("POST", "/payment/s2s/sale", body);
}
public Map<String, Object> capture(String transactionId, double amount) throws Exception {
return request("POST", "/payment/s2s/capture", Map.of("transactionId", transactionId, "amount", amount));
}
public Map<String, Object> voidTransaction(String transactionId) throws Exception {
return request("POST", "/payment/s2s/void", Map.of("transactionId", transactionId));
}
}Direct Sale Request
The Direct Sale API charges a payment card directly from your backend.
Endpoint
POST /payment/s2s/saleRequired information:
| Field | Description |
|---|---|
| orderId | Merchant order ID |
| amount | Transaction amount |
| currency | Currency code |
| card | Card information |
| customer | Customer information |
| successUrl | 3DS success return URL |
| failureUrl | Failed payment URL |
| hash | MD5 request hash |
Process Payment
Receive card information from your checkout page and call the Direct Sale API.
<?php
declare(strict_types=1);
require_once __DIR__ . '/../src/DigetPayEmbeddedClient.php';
$config = require __DIR__ . '/../config/digetpay.php';
$client = new DigetPayEmbeddedClient($config);
try {
$result = $client->directSale(
order: [
'orderId' => $_POST['order_id'],
'amount' => (float) $_POST['amount'],
'currency' => 'SAR',
'auth' => 'N',
],
card: [
'cardNumber' => $_POST['card_number'],
'cardExpiryMonth' => $_POST['exp_month'],
'cardExpiryYear' => $_POST['exp_year'],
'cardCvv' => $_POST['cvv'],
'cardHolder' => $_POST['card_holder'],
],
customer: [
'name' => $_POST['card_holder'],
'email' => $_POST['email'],
'phone' => $_POST['phone'],
],
successUrl: 'https://yourstore.com/payment/3ds-return',
failureUrl: 'https://yourstore.com/payment/failed'
);
$data = $result['data'] ?? $result;
$html = $data['html'] ?? $data['htmlContent'] ?? null;
if ($html) {
echo $html;
exit;
}
if (($data['status'] ?? '') === 'APPROVED') {
header('Location: /order/complete?paymentId=' . urlencode($data['paymentId'] ?? ''));
exit;
}
header('Location: /payment/failed');
} catch (Throwable $e) {
error_log($e->getMessage());
http_response_code(500);
echo 'Payment failed';
}// routes/payment.js
import express from 'express';
import { directSale } from '../src/digetpayEmbeddedClient.js';
const router = express.Router();
router.post('/process-payment', express.urlencoded({ extended: true }), async (req, res) => {
try {
const result = await directSale({
order: { orderId: req.body.order_id, amount: parseFloat(req.body.amount), auth: 'N' },
card: {
cardNumber: req.body.card_number,
cardExpiryMonth: req.body.exp_month,
cardExpiryYear: req.body.exp_year,
cardCvv: req.body.cvv,
cardHolder: req.body.card_holder,
},
customer: { name: req.body.card_holder, email: req.body.email, phone: req.body.phone },
successUrl: 'https://yourstore.com/payment/3ds-return',
failureUrl: 'https://yourstore.com/payment/failed',
});
const data = result.data ?? result;
const html = data.html ?? data.htmlContent;
if (html) return res.send(html);
if (data.status === 'APPROVED') return res.redirect('/order/complete');
return res.redirect('/payment/failed');
} catch (err) {
console.error(err);
res.status(500).send('Payment failed');
}
});
export default router;# routes/payment.py
from flask import Blueprint, request, redirect, Response
from digetpay_embedded_client import direct_sale
payment_bp = Blueprint("payment", __name__)
@payment_bp.route("/process-payment", methods=["POST"])
def process_payment():
try:
result = direct_sale(
order={"orderId": request.form["order_id"], "amount": float(request.form["amount"]), "auth": "N"},
card={
"cardNumber": request.form["card_number"],
"cardExpiryMonth": request.form["exp_month"],
"cardExpiryYear": request.form["exp_year"],
"cardCvv": request.form["cvv"],
"cardHolder": request.form["card_holder"],
},
customer={"name": request.form["card_holder"], "email": request.form["email"], "phone": request.form["phone"]},
success_url="https://yourstore.com/payment/3ds-return",
failure_url="https://yourstore.com/payment/failed",
)
data = result.get("data") or result
html = data.get("html") or data.get("htmlContent")
if html:
return Response(html, mimetype="text/html")
if data.get("status") == "APPROVED":
return redirect("/order/complete")
return redirect("/payment/failed")
except Exception as e:
return "Payment failed", 500@PostMapping("/process-payment")
public ResponseEntity<?> processPayment(@RequestParam Map<String, String> form) throws Exception {
DigetPayEmbeddedClient client = new DigetPayEmbeddedClient();
Map<String, Object> result = client.directSale(
Map.of("orderId", form.get("order_id"), "amount", Double.parseDouble(form.get("amount")), "auth", "N"),
Map.of("cardNumber", form.get("card_number"), "cardExpiryMonth", form.get("exp_month"),
"cardExpiryYear", form.get("exp_year"), "cardCvv", form.get("cvv"), "cardHolder", form.get("card_holder")),
Map.of("name", form.get("card_holder"), "email", form.get("email"), "phone", form.get("phone")),
"https://yourstore.com/payment/3ds-return",
"https://yourstore.com/payment/failed"
);
// Handle 3DS html or redirect on APPROVED
return ResponseEntity.ok(result);
}Handling the Response
The Direct Sale response can return one of three outcomes.
| Response | Action |
|---|---|
| Approved | Complete the order |
| Declined | Show payment failure |
| 3-D Secure HTML | Render the HTML and allow the customer to authenticate |
Example:
Approved
↓
Order Completed
Declined
↓
Payment Failed
3DS HTML
↓
Render HTML
↓
Customer Authentication
↓
Final Status
Best PracticeDo not mark an order as paid immediately after receiving an Approved response.
Always verify the final transaction using either:
- Transaction Status API
- Webhook Notification
Post-Authorization Operations
After processing an Embedded payment, you can perform additional operations such as:
- Capture an authorized transaction
- Void an authorized transaction
- Check transaction status
- Receive webhook notifications
Capture an Authorized Transaction
Use the Capture API to settle a previously authorized payment.
POST /payment/s2s/captureRequired fields:
| Field | Description |
|---|---|
| transactionId | DigetPay transaction ID |
| amount | Amount to capture |
// After auth-only sale (auth: Y)
$client->capture($transactionId, 10.00);
);import { capture} from '../src/digetpayEmbeddedClient.js';
await capture(
"2232e99b-0257-47d5-bbfd-022c8951767f",
10.0
);
from digetpay_embedded_client import capture
capture(
"2232e99b-0257-47d5-bbfd-022c8951767f",
10.0
)client.capture(
"2232e99b-0257-47d5-bbfd-022c8951767f",
10.0
);Void an Authorized Transaction
Use the Void API to cancel an authorized transaction before settlement.
POST /payment/s2s/void$client->voidTransaction($transactionId);import {voidTransaction } from '../src/digetpayEmbeddedClient.js';
await voidTransaction(transactionId);from digetpay_embedded_client import void_transaction
void_transaction(transaction_id)client.voidTransaction(transactionId);Check Transaction Status
Retrieve the latest transaction status from DigetPay.
POST /payment/s2s/status/{transactionId}$status = $client->getStatus($transactionId);const status =
await getStatus(transactionId);status =
get_status(transaction_id)Map<String,Object> status =
client.getStatus(transactionId);Transaction Status Values
| Status | Meaning |
|---|---|
| APPROVED | Payment completed successfully |
| DECLINED | Payment rejected |
| PENDING | Waiting for completion |
| AUTHORIZED | Authorized but not captured |
| CAPTURED | Funds captured |
| VOIDED | Authorization cancelled |
Receive Webhook Notifications
DigetPay sends asynchronous notifications whenever a payment status changes.
Customer Pays
│
▼
DigetPay
│
▼
POST Webhook
│
▼
Your Backend
│
▼
Update Order StatusWebhook URL example
POST /webhooks/digetpay
Event mapping: Webhook Events.
Webhook handler — see Webhooks Code.
Webhook Best Practices
- Always return HTTP 200 immediately.
- Process webhook events asynchronously.
- Deduplicate events using transactionId.
- Never rely only on browser redirects for payment confirmation.
Recommended FlowFor the most reliable payment confirmation:
- Customer completes payment.
- Receive the webhook.
- Verify the transaction using the Status API.
- Update your order.
- Notify the customer.
Security Best Practices
Building your own checkout gives you full control over the payment experience, but it also means your application is responsible for securely handling sensitive payment information.
Follow these recommendations for every Embedded integration.
Never Log Card DataDo not store or log:
- Card Number (PAN)
- CVV
- Full expiration date
- Request hash
- Raw API requests containing card details
Always Use HTTPSAll customer checkout pages, backend APIs, and callback URLs must use HTTPS.
Never accept card details over an unencrypted connection.
Verify Every PaymentNever trust:
- Browser redirects
- Client-side success messages
Always verify payment using either:
- Transaction Status API
- Webhook Notification
before marking an order as paid.
Complete Embedded Flow
flowchart TD
A[Customer enters card details]
B[Merchant Backend]
C[Generate MD5 Hash]
D[POST /payment/s2s/sale]
E{Response}
F[Approved]
G[3DS HTML]
H[Declined]
I[Customer completes 3DS]
J[Webhook]
K[Transaction Status API]
L[Update Order]
M[Capture or Void if Authorized]
A --> B
B --> C
C --> D
D --> E
E --> F
E --> G
E --> H
G --> I
I --> J
F --> J
J --> K
K --> L
L --> M
Troubleshooting
Invalid Hash
- Verify the MD5 algorithm.
- Reverse the email correctly.
- Use only the first 6 and last 4 card digits.
- Convert the input string to uppercase before hashing.
Transaction Declined
Possible causes include:
- Incorrect card details
- Insufficient funds
- Issuer rejection
- Fraud checks
Use the Transaction Status API for the final payment state.
3-D Secure Not Displayed
Verify that:
- successUrl is valid.
- failureUrl is valid.
- Your application renders the returned HTML without modification.
Duplicate Orders
Always use:
- Webhook deduplication
- transactionId
- Unique orderId
Never process the same payment twice.
Updated about 1 month ago
