REST-style JSON APIs, widget session tokens, and iframe embedding for the DirhamClub wallet in your web application.
# DirhamClub Wallet Integration Guide
This guide explains how to integrate the DirhamClub centralized wallet into **any web application**. It is **not** tied to a specific host product: you register a client app, store each end user’s dirhamUserId in your own user model, call DirhamClub’s HTTP APIs from your backend, and optionally embed the hosted wallet UI in an iframe.
DirhamClub is a multi-tenant wallet platform: balances, ledger transactions, and Stripe top-ups live in DirhamClub; your app orchestrates identity, linking, and business rules.
---
---
DirhamClub provides:
dirhamUserId**: Identifier linking your app's user to a DirhamClub user (not unique - one DirhamClub account can be linked to multiple app users)---
┌─────────────────┐
│ Your App │
│ (Frontend) │
│ │
│ ┌───────────┐ │
│ │ DirhamClub│ │
│ │ Widget │ │◄──────┐
│ │ (iframe) │ │ │
│ └───────────┘ │ │
│ │ │ │
└───────┼─────────┘ │
│ │
│ HTTP │ postMessage
│ │
┌───────┼─────────┐ │
│ Your App │ │
│ (Backend) │ │
│ │ │
│ ┌───────────┐ │ │
│ │ DirhamClub│ │◄──────┘
│ │ Service │ │
│ └───────────┘ │
└───────┼─────────┘
│
│ HTTPS
│
┌───────┼─────────┐
│ DirhamClub │
│ (Next.js App) │
│ │
│ • Widget APIs │
│ • Server APIs │
│ • Hosted auth │
│ (in widget) │
└─────────────────┘---
http://localhost:3000 (development)/admin/client-apps)clientId and clientSecretallowedOrigins (your frontend URL)allowedIframeParents (your frontend URL)scopes: wallet:read, wallet:writedirhamUserId Field**dirhamUserId field (String, optional, sparse - NOT unique, to allow one DirhamClub account for multiple users)---
Paths are relative to your DirhamClub base URL (e.g. https://wallet.example.com).
For a normal integration you only use **(A) server-to-server wallet APIs** from your backend and **(B) widget session + widget data/top-up APIs** (session created on your server; browser talks to DirhamClub with ?session=). End-user signup/login runs **inside the iframe** (or DirhamClub pages), not via extra REST calls from your app—so auth, profile, admin, and JWT-wallet routes are omitted here.
**Client app setup:** Create the app and copy clientId / clientSecret in the DirhamClub admin UI (/admin/client-apps). No need to call admin HTTP APIs for integration.
---
Authenticate with **clientId** + **clientSecret**. Never send the secret to the browser.
**GET (query string)**
| Method | Path | Query parameters | |--------|------|------------------| | GET | /api/server/wallet | clientId, clientSecret, dirhamUserId | | GET | /api/server/wallet/check-balance | clientId, clientSecret, dirhamUserId, requiredAmount |
**GET /api/server/wallet response:** { "balance", "transactions", "isActive" } **GET check-balance response:** { "balance", "requiredAmount", "hasSufficientBalance" } User unknown in DirhamClub → 401, often with requiresAuth / code: USER_NOT_FOUND.
**POST (JSON body)**
| Method | Path | Body | |--------|------|------| | POST | /api/server/wallet/debit | clientId, clientSecret, dirhamUserId, amount, referenceType, referenceId, optional description | | POST | /api/server/wallet/credit | Same fields as debit |
**Success:** { "balance", "message" }. **Errors:** 401 bad client credentials; 404 user not found; 400 insufficient balance (debit).
Debit/credit are **idempotent** on the pair (referenceType, referenceId).
---
**1. Create session — call from your server**
| Method | Path | Body (JSON) | |--------|------|-------------| | POST | /api/widget/session | clientId, clientSecret, dirhamUserId, optional parentOrigin |
**Response:** { "sessionToken", "expiresAt" } (short-lived, typically ~5 minutes).
**2. Calls from the browser (widget uses these; your iframe loads hosted pages below)**
| Method | Path | Query / body | |--------|------|----------------| | GET | /api/widget/wallet | ?session=<token> | | POST | /api/widget/topup/create-intent | ?session=<token>, body { "amount": number } | | POST | /api/widget/topup/confirm | ?session=<token>, body { "paymentIntentId": string } |
**GET /api/widget/wallet:** balance, last **10** transactions, isActive, user: { firstName, lastName }.
**Iframe URLs (src)**
/widget/wallet?session=<token> — omit session to show embedded login/signup first/widget/topup?session=<token>---
Listen on window for message; in production, verify event.origin is your DirhamClub host.
| event.data.type | Meaning | |-------------------|--------| | DIRHAM_WALLET_UPDATED | Refresh UI / reload iframe | | DIRHAM_WALLET_LOADED | Widget ready | | DIRHAM_OPEN_TOPUP | Open top-up (e.g. new window with /widget/topup?session=...) | | DIRHAM_AUTH_REQUIRED | User must complete DirhamClub auth | | DIRHAM_AUTH_SUCCESS | Includes dirhamUserId — persist on your user, then create a new widget session | | DIRHAM_LOGOUT_REQUEST | Clear link / reload iframe without session if switching accounts |
---
Add to your .env file:
# DirhamClub Configuration
DIRHAMCLUB_BASE_URL=http://localhost:3000
DIRHAMCLUB_CLIENT_ID=your_client_id_here
DIRHAMCLUB_CLIENT_SECRET=your_client_secret_hereAdd dirhamUserId field to your User schema:
// Example: MongoDB/Mongoose
const userSchema = new Schema({
// ... existing fields
dirhamUserId: {
type: String,
default: null,
sparse: true // NOT unique - allows one DirhamClub account to be linked to multiple app users
}
});**Important**: The dirhamUserId field should NOT have a unique: true constraint if you want several of your app’s users to share one DirhamClub wallet (e.g. family or team accounts). If you require strictly one app user per DirhamClub user, you may enforce uniqueness in your own schema instead.
**Migration Note**: If you have an existing unique index on dirhamUserId, you need to drop it. See the migration section below.
Create a service file to communicate with DirhamClub APIs:
**File: services/dirhamclub_service/dirhamclub.service.js**
const axios = require('axios');
const DIRHAMCLUB_BASE_URL = process.env.DIRHAMCLUB_BASE_URL || 'http://localhost:3000';
const DIRHAMCLUB_CLIENT_ID = process.env.DIRHAMCLUB_CLIENT_ID;
const DIRHAMCLUB_CLIENT_SECRET = process.env.DIRHAMCLUB_CLIENT_SECRET;
/**
* Create widget session token for iframe embedding
*/
async function createWidgetSession(dirhamUserId, parentOrigin) {
try {
if (!DIRHAMCLUB_CLIENT_ID || !DIRHAMCLUB_CLIENT_SECRET) {
throw new Error('DirhamClub client credentials not configured');
}
const response = await axios.post(
`${DIRHAMCLUB_BASE_URL}/api/widget/session`,
{
clientId: DIRHAMCLUB_CLIENT_ID,
clientSecret: DIRHAMCLUB_CLIENT_SECRET,
dirhamUserId,
parentOrigin,
},
{
headers: { 'Content-Type': 'application/json' },
timeout: 10000,
}
);
return response.data;
} catch (error) {
if (error.response) {
const errorData = error.response.data || {};
const apiError = new Error(errorData.error || 'DirhamClub API error');
if (errorData.requiresAuth) {
apiError.requiresAuth = true;
}
if (errorData.code) {
apiError.code = errorData.code;
}
throw apiError;
}
throw error;
}
}
/**
* Get wallet from DirhamClub (server-to-server)
*/
async function getWallet(dirhamUserId) {
try {
const response = await axios.get(
`${DIRHAMCLUB_BASE_URL}/api/server/wallet`,
{
params: {
clientId: DIRHAMCLUB_CLIENT_ID,
clientSecret: DIRHAMCLUB_CLIENT_SECRET,
dirhamUserId,
},
timeout: 10000,
}
);
return response.data;
} catch (error) {
// Handle errors (preserve requiresAuth flag)
if (error.response) {
const errorData = error.response.data || {};
const apiError = new Error(errorData.error || 'DirhamClub API error');
if (errorData.requiresAuth) {
apiError.requiresAuth = true;
}
throw apiError;
}
throw error;
}
}
/**
* Check wallet balance (server-to-server)
*/
async function checkWalletBalance(dirhamUserId, requiredAmount) {
try {
const response = await axios.get(
`${DIRHAMCLUB_BASE_URL}/api/server/wallet/check-balance`,
{
params: {
clientId: DIRHAMCLUB_CLIENT_ID,
clientSecret: DIRHAMCLUB_CLIENT_SECRET,
dirhamUserId,
requiredAmount,
},
timeout: 10000,
}
);
return response.data;
} catch (error) {
if (error.response) {
const errorData = error.response.data || {};
const apiError = new Error(errorData.error || 'DirhamClub API error');
if (errorData.requiresAuth) {
apiError.requiresAuth = true;
}
throw apiError;
}
throw error;
}
}
/**
* Debit wallet (server-to-server)
*/
async function debitWallet(dirhamUserId, amount, referenceType, referenceId, description) {
try {
const response = await axios.post(
`${DIRHAMCLUB_BASE_URL}/api/server/wallet/debit`,
{
clientId: DIRHAMCLUB_CLIENT_ID,
clientSecret: DIRHAMCLUB_CLIENT_SECRET,
dirhamUserId,
amount,
referenceType,
referenceId,
description,
},
{
headers: { 'Content-Type': 'application/json' },
timeout: 10000,
}
);
return response.data;
} catch (error) {
if (error.response) {
const errorData = error.response.data || {};
const apiError = new Error(errorData.error || 'DirhamClub API error');
if (errorData.requiresAuth) {
apiError.requiresAuth = true;
}
throw apiError;
}
throw error;
}
}
/**
* Credit wallet (server-to-server)
*/
async function creditWallet(dirhamUserId, amount, referenceType, referenceId, description) {
try {
const response = await axios.post(
`${DIRHAMCLUB_BASE_URL}/api/server/wallet/credit`,
{
clientId: DIRHAMCLUB_CLIENT_ID,
clientSecret: DIRHAMCLUB_CLIENT_SECRET,
dirhamUserId,
amount,
referenceType,
referenceId,
description,
},
{
headers: { 'Content-Type': 'application/json' },
timeout: 10000,
}
);
return response.data;
} catch (error) {
if (error.response) {
const errorData = error.response.data || {};
const apiError = new Error(errorData.error || 'DirhamClub API error');
if (errorData.requiresAuth) {
apiError.requiresAuth = true;
}
throw apiError;
}
throw error;
}
}
module.exports = {
createWidgetSession,
getWallet,
checkWalletBalance,
debitWallet,
creditWallet,
};Expose **only** what your frontend needs. Paths are yours to choose; keep them behind your own session/auth.
Typical endpoints:
| Your route (example) | DirhamClub call | Purpose | |---------------------|-----------------|---------| | POST /api/integrations/dirhamclub/widget-session | POST /api/widget/session with clientId, clientSecret, dirhamUserId, parentOrigin | Short-lived iframe token | | POST /api/integrations/dirhamclub/link-account | *(none — update your DB)* | Persist dirhamUserId after widget DIRHAM_AUTH_SUCCESS | | POST /api/integrations/dirhamclub/unlink-account | *(none — update your DB)* | Clear link when user switches DirhamClub accounts |
**Widget session handler (Express-style sketch)**
// POST /api/integrations/dirhamclub/widget-session
router.post('/widget-session', authenticateUser, async (req, res) => {
let parentOrigin;
try {
parentOrigin =
req.headers.origin ||
(req.headers.referer ? new URL(req.headers.referer).origin : undefined);
} catch {
parentOrigin = undefined;
}
const appUser = await loadUser(req.user.id); // your ORM / DB
if (!appUser?.dirhamUserId) {
return res.status(400).json({
error: 'DirhamClub account not linked',
requiresAuth: true,
});
}
try {
const session = await createWidgetSession(appUser.dirhamUserId, parentOrigin);
return res.json(session);
} catch (error) {
const status = error.requiresAuth || error.code === 'USER_NOT_FOUND' ? 400 : 500;
return res.status(status).json({
error: error.message || 'Failed to create widget session',
requiresAuth: !!error.requiresAuth,
code: error.code,
});
}
});**Link / unlink (sketch)**
// POST /api/integrations/dirhamclub/link-account body: { dirhamUserId }
router.post('/link-account', authenticateUser, async (req, res) => {
const { dirhamUserId } = req.body;
if (!dirhamUserId) return res.status(400).json({ error: 'dirhamUserId is required' });
let user = await loadUser(req.user.id);
if (!user) return res.status(404).json({ error: 'User not found' });
if (user.dirhamUserId && user.dirhamUserId !== dirhamUserId) {
await clearDirhamLink(req.user.id); // e.g. MongoDB $unset for sparse indexes
user = await loadUser(req.user.id);
}
user.dirhamUserId = dirhamUserId;
await saveUser(user);
return res.json({ success: true, dirhamUserId });
});
// POST /api/integrations/dirhamclub/unlink-account
router.post('/unlink-account', authenticateUser, async (req, res) => {
const user = await loadUser(req.user.id);
const prev = user?.dirhamUserId;
await clearDirhamLink(req.user.id);
return res.json({ success: true, unlinkedDirhamUserId: prev });
});Use **let** (not const) for user if you reassign after refetch. Prefer **removing** the field ($unset in MongoDB) over setting null when using sparse unique indexes.
Where you previously debited a local wallet, call **POST /api/server/wallet/debit** (via your dirhamclub.service helpers) with:
referenceType** for your domain (ORDER_PAYMENT, BOOKING_HOLD, etc.)referenceId per business attempt** (see Idempotency)Refunds or goodwill credits use **POST /api/server/wallet/credit** with their own referenceType / referenceId pair.
Read-only flows: **GET /api/server/wallet** and **GET /api/server/wallet/check-balance**.
---
The following is a **reference pattern** (React + Vite-style). Adapt fetch helpers, routing, and styling to your stack. The important parts are: (1) never put clientSecret in the browser, (2) load the iframe from your DirhamClub base URL with a session from your backend, (3) handle postMessage from the widget.
# DirhamClub deployment (iframe src + postMessage origin checks)
VITE_DIRHAMCLUB_URL=https://your-dirhamclub-host
# Your own backend base URL (must expose widget-session + link/unlink)
VITE_YOUR_API_BASE_URL=https://api.yourapp.comPoint these at the bridge routes you implemented under Backend integration:
const API = import.meta.env.VITE_YOUR_API_BASE_URL;
export const DIRHAM_WIDGET_SESSION_URL = `${API}/api/integrations/dirhamclub/widget-session`;
export const DIRHAM_LINK_ACCOUNT_URL = `${API}/api/integrations/dirhamclub/link-account`;
export const DIRHAM_UNLINK_ACCOUNT_URL = `${API}/api/integrations/dirhamclub/unlink-account`;**Example file:** components/DirhamWalletModal.jsx (name as you like)
import React, { useState, useEffect, useRef } from 'react';
import { MdClose } from 'react-icons/md';
import styles from './DirhamWalletModal.module.css';
import doApi from '../../utils/doApi';
import {
DIRHAM_WIDGET_SESSION_URL,
DIRHAM_LINK_ACCOUNT_URL,
DIRHAM_UNLINK_ACCOUNT_URL,
} from '../../config/dirhamEndpoints';
import toast from 'react-hot-toast';
const DIRHAMCLUB_BASE_URL = import.meta.env.VITE_DIRHAMCLUB_URL || 'http://localhost:3000';
const DirhamWalletModal = ({ isOpen, onClose }) => {
const [sessionToken, setSessionToken] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const iframeRef = useRef(null);
useEffect(() => {
if (isOpen && !sessionToken) {
createWidgetSession();
}
// Listen for messages from widget
const handleMessage = async (event) => {
// In production, verify event.origin matches DirhamClub domain
if (event.data.type === 'DIRHAM_WALLET_UPDATED') {
toast.success('Wallet updated');
// Reload iframe
if (iframeRef.current) {
iframeRef.current.src = iframeRef.current.src;
}
} else if (event.data.type === 'DIRHAM_WALLET_LOADED') {
console.log('Wallet loaded:', event.data);
} else if (event.data.type === 'DIRHAM_OPEN_TOPUP') {
openTopupModal();
} else if (event.data.type === 'DIRHAM_AUTH_REQUIRED') {
toast.error('Please login to DirhamClub to access your wallet');
} else if (event.data.type === 'DIRHAM_AUTH_SUCCESS') {
// User authenticated, link the account and retry session creation
if (event.data.dirhamUserId) {
try {
await linkDirhamClubAccount(event.data.dirhamUserId);
await createWidgetSession();
} catch (error) {
console.error('Error in auth success flow:', error);
try {
await createWidgetSession();
} catch (sessionError) {
console.error('Error creating session after auth:', sessionError);
}
}
}
} else if (event.data.type === 'DIRHAM_LOGOUT_REQUEST') {
// User wants to logout and switch account
handleLogout();
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [isOpen, sessionToken]);
const linkDirhamClubAccount = async (dirhamUserId) => {
try {
const [response, err] = await doApi({
url: DIRHAM_LINK_ACCOUNT_URL,
method: 'POST',
data: { dirhamUserId },
});
if (err) {
throw new Error(err.error || 'Failed to link account');
}
toast.success('DirhamClub account linked successfully!');
} catch (error) {
console.error('Error linking account:', error);
toast.error(error.message || 'Failed to link account');
throw error;
}
};
const handleLogout = async () => {
try {
// Unlink the account from backend
const [response, err] = await doApi({
url: DIRHAM_UNLINK_ACCOUNT_URL,
method: 'POST',
});
if (err) {
console.error('Error unlinking account:', err);
// Still proceed with logout even if unlink fails
} else {
toast.success('Logged out from DirhamClub. You can now login with a different account.');
}
// Clear session token and reload widget to show auth UI
setSessionToken(null);
setError('');
// Reload iframe to show login/signup form
if (iframeRef.current) {
iframeRef.current.src = `${DIRHAMCLUB_BASE_URL}/widget/wallet`;
}
} catch (error) {
console.error('Error during logout:', error);
// Still proceed with logout
setSessionToken(null);
setError('');
if (iframeRef.current) {
iframeRef.current.src = `${DIRHAMCLUB_BASE_URL}/widget/wallet`;
}
}
};
const createWidgetSession = async () => {
setLoading(true);
setError('');
try {
const [response, err] = await doApi({
url: DIRHAM_WIDGET_SESSION_URL,
method: 'POST',
setLoading,
});
if (err) {
// If auth required, load widget without session - it will show auth UI
if (err.requiresAuth || err.error?.includes('not linked')) {
setSessionToken(null);
setError('');
return;
}
throw new Error(err.error || 'Failed to create widget session');
}
if (!response || !response.sessionToken) {
throw new Error('Invalid response from server');
}
setSessionToken(response.sessionToken);
} catch (error) {
console.error('Error creating widget session:', error);
if (error.message?.includes('not linked')) {
setError('AUTH_REQUIRED');
} else {
setError(error.message || 'Failed to load wallet');
toast.error(error.message || 'Failed to load wallet');
}
} finally {
setLoading(false);
}
};
const openTopupModal = () => {
if (!sessionToken) return;
const topupUrl = `${DIRHAMCLUB_BASE_URL}/widget/topup?session=${sessionToken}`;
const topupWindow = window.open(
topupUrl,
'DirhamClub Topup',
'width=500,height=700,resizable=yes,scrollbars=yes'
);
// Listen for messages from topup window
const handleTopupMessage = (event) => {
if (event.data.type === 'DIRHAM_WALLET_UPDATED') {
toast.success('Wallet topped up successfully!');
if (iframeRef.current) {
iframeRef.current.src = iframeRef.current.src;
}
topupWindow?.close();
window.removeEventListener('message', handleTopupMessage);
}
};
window.addEventListener('message', handleTopupMessage);
};
if (!isOpen) return null;
const widgetUrl = `${DIRHAMCLUB_BASE_URL}/widget/wallet${sessionToken ? `?session=${sessionToken}` : ''}`;
return (
<div className={styles.overlay} onClick={onClose}>
<div className={styles.modal} onClick={(e) => e.stopPropagation()}>
<div className={styles.header}>
<div className={styles.title}>
<h2>Wallet</h2>
</div>
<button className={styles.closeButton} onClick={onClose}>
<MdClose size={20} />
</button>
</div>
{loading && (
<div className={styles.loading}>
Loading wallet...
</div>
)}
{error && error !== 'AUTH_REQUIRED' && (
<div className={styles.error}>
{error}
<button onClick={createWidgetSession} className={styles.retryButton}>
Retry
</button>
</div>
)}
{(widgetUrl || !sessionToken) && !loading && error !== 'AUTH_REQUIRED' && (
<iframe
ref={iframeRef}
src={widgetUrl}
className={styles.iframe}
title="DirhamClub Wallet"
allow="payment *;"
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox"
/>
)}
</div>
</div>
);
};
export default DirhamWalletModal;**Example:** DirhamWalletModal.module.css
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
padding: 20px;
}
.modal {
background: white;
border-radius: 12px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
max-width: 600px;
width: 100%;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24px;
border-bottom: 1px solid #e5e7eb;
background: linear-gradient(135deg, #075e54 0%, #0a3b83 100%);
color: white;
flex-shrink: 0;
}
.title h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
}
.closeButton {
background: none;
border: none;
color: white;
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background-color 0.2s;
}
.closeButton:hover {
background-color: rgba(255, 255, 255, 0.2);
}
.loading {
padding: 2rem;
text-align: center;
color: #666;
}
.error {
padding: 2rem;
text-align: center;
color: #dc2626;
display: flex;
flex-direction: column;
gap: 1rem;
align-items: center;
}
.retryButton {
padding: 0.5rem 1rem;
background-color: #2563eb;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
}
.retryButton:hover {
background-color: #1d4ed8;
}
.iframe {
width: 100%;
flex: 1;
border: none;
min-height: 500px;
}
@media (max-width: 768px) {
.modal {
max-width: 100%;
max-height: 100vh;
border-radius: 0;
}
.overlay {
padding: 0;
}
.iframe {
min-height: 400px;
}
}import DirhamWalletModal from '../components/DirhamWalletModal';
const [showWallet, setShowWallet] = useState(false);
<DirhamWalletModal isOpen={showWallet} onClose={() => setShowWallet(false)} />;---
DIRHAM_AUTH_SUCCESS message with dirhamUserIdPOST /api/integrations/dirhamclub/link-account) to persist dirhamUserIduser.dirhamUserId existsdirhamUserId is linked to your user, it persistsdirhamUserId (no uniqueness on that field)$unset operator to remove the field (not set to null) to avoid sparse index conflictsDirhamClub treats **referenceType + referenceId** as an idempotency key for debits/credits. If your product allows users to **retry** the same checkout or payment flow, generate a **new** referenceId for each new attempt so the ledger records a new charge. Reusing the same pair returns the original outcome without deducting again.
Your own business rules (e.g. whether a merchant can re-accept a cancelled order) are **outside** DirhamClub; wallet balance is always authoritative via DirhamClub APIs.
---
ws_<uuid> (e.g., ws_a1b2c3d4e5f6...)1. User opens wallet modal
↓
2. Frontend requests session from backend
↓
3. Backend calls DirhamClub API with clientId/clientSecret
↓
4. DirhamClub validates credentials and user
↓
5. DirhamClub creates session token (5min expiry)
↓
6. Frontend embeds widget with session token in URL
↓
7. Widget validates session token on each API call
↓
8. After 5 minutes, session expires
↓
9. User needs to reopen modal to get new session---
#### 1. Get Wallet
const wallet = await getDirhamClubWallet(dirhamUserId);
// Returns: { balance, transactions, isActive }#### 2. Check Balance
const result = await checkDirhamClubBalance(dirhamUserId, requiredAmount);
// Returns: { balance, requiredAmount, hasSufficientBalance }#### 3. Debit Wallet (Payment)
// IMPORTANT: new referenceId per new payable attempt (idempotency)
const uniqueReferenceId = `${orderId}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
const result = await debitDirhamClubWallet(
dirhamUserId,
amount,
'ORDER_PAYMENT', // your domain-specific referenceType
uniqueReferenceId, // new id per attempt
'Payment description'
);
// Returns: { balance, message } from HTTP API**Note**: Reusing the same referenceType + referenceId replays the first outcome (no second debit). See Idempotency, linking, and data model notes.
#### 4. Credit Wallet (Refund)
const result = await creditDirhamClubWallet(
dirhamUserId,
amount,
'ORDER_REFUND',
`${orderId}_refund`, // distinct referenceId from original debit unless intentional idempotent replay
'Refund description'
);
// Returns: { balance, message }referenceType is a **string label** for your ledger. DirhamClub does not enforce an enum for server APIs—use consistent names in your integration, for example:
ORDER_PAYMENT, SUBSCRIPTION_CHARGE, MARKETPLACE_ESCROWORDER_REFUND, CHARGEBACK_CREDITTOPUP, BONUS, etc. (often created inside DirhamClub services)---
// Handle requiresAuth error
if (err.requiresAuth) {
// Show wallet modal - it will display auth UI
setShowWallet(true);
}
// Handle account not linked
if (err.code === 'ACCOUNT_NOT_LINKED') {
// Widget will show login/signup form
setShowWallet(true);
}
// Handle other errors
toast.error(err.message || 'An error occurred');try {
const result = await checkDirhamClubBalance(dirhamUserId, amount);
return result;
} catch (error) {
// Preserve requiresAuth flag
if (error.requiresAuth) {
error.requiresAuth = true;
}
throw error;
}USER_NOT_FOUND: User doesn't exist in DirhamClub (needs authentication)ACCOUNT_NOT_LINKED: User hasn't linked DirhamClub accountINSUFFICIENT_BALANCE: Not enough balance for operationINVALID_CLIENT: Client credentials invalidSESSION_EXPIRED: Widget session token expired---
clientSecret in frontend codeallowedOrigins and allowedIframeParents in DirhamClubevent.origin matches DirhamClub domainconst handleMessage = (event) => {
const DIRHAMCLUB_ORIGIN = 'https://dirhamclub.ae';
if (event.origin !== DIRHAMCLUB_ORIGIN) {
return; // Ignore messages from untrusted origins
}
// Process message
};dirhamUserId should be treated as sensitivedirhamUserId in frontend logs or URLs---
If dirhamUserId is optional and you use a **sparse unique** index, avoid setting the field to null when unlinking—use **$unset** so the index does not see duplicate nulls. That pattern is common when storing the link on a users collection.
If your product needs it, omit a uniqueness constraint on dirhamUserId in **your** database so multiple rows can hold the same value. If you previously had unique: true on that field, drop that index in a migration (for example db.users.dropIndex("dirhamUserId_1") in MongoDB).
const uniqueReferenceId = `${businessId}_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;Use a **new** referenceId whenever the user starts a **new** payable attempt. Reuse the same pair only when you intentionally want the API to return the same result without double-charging (e.g. safe retries of the same HTTP request).
The embedded wallet can send DIRHAM_LOGOUT_REQUEST. Clear the stored dirhamUserId for the current app user (or only in session, depending on your policy), call your unlink API if you use one, then reload the iframe **without** a session token so the user can authenticate as a different DirhamClub user.
---
DIRHAMCLUB_BASE_URL, DIRHAMCLUB_CLIENT_ID, DIRHAMCLUB_CLIENT_SECRET)dirhamUserId (or equivalent) to your user/account model; decide on uniqueness vs sharingPOST /api/server/wallet/debit and .../credit with correct idempotency keysrequiresAuth / USER_NOT_FOUND to your product UXDIRHAM_LOGOUT_REQUEST on the client if you support account switchingVITE_DIRHAMCLUB_URL or equivalent)/widget/wallet (and optional /widget/topup) with session query param from your APIpostMessage handling and strict event.origin checks in productionallowedOrigins (your frontend URL)allowedIframeParents (your frontend URL)wallet:read, wallet:writeclientId and clientSecret---
/admin/client-apps on your DirhamClub deploymentdirhamclub/README.md; architecture: dirhamclub/docs/---
For issues or questions:
---
**Last updated**: March 2025 **Version**: 1.1.0 (general integration + full API reference)
