Skip to main content

Build a SaaS Platform with User Wallets

This tutorial shows you how to build a multi-tenant SaaS platform where each of your users gets their own wallet — they can collect payments from their customers, and your platform automatically earns a commission on every transaction.

Time: ~45 minutes Prerequisites: Complete Your First Sub-Merchant, Your First Collection, and Setup Webhooks.


What you'll build

A platform where:

  1. Each user is a merchant with their own NextAPI account
  2. Users generate QRPH codes for their own payment collection
  3. When a user collects a payment, your platform automatically takes a commission
  4. Users can request a payout of their balance to their bank

Architecture

User signs up on your platform

Your platform creates: Merchant + Account (via NextAPI)

User generates QRPH code (via NextAPI Funding Method)

User's customer scans and pays

NextAPI webhook → your platform receives it

Your platform splits: net → user account, commission → platform account

User requests payout → your platform calls POST /v2/payout-requests

Step 1: Merchant onboarding

When a user registers on your platform, automatically create their NextAPI merchant and account:

async function onboardUser(user) {
const auth = 'Basic ' + Buffer.from(
`${process.env.NEXTAPI_CLIENT_ID}:${process.env.NEXTAPI_CLIENT_SECRET}`
).toString('base64');

// Create merchant
const merchantRes = await fetch('https://api.partners.nextpay.world/v2/merchants', {
method: 'POST',
headers: { 'Authorization': auth, 'Content-Type': 'application/json' },
body: JSON.stringify({
name: user.businessName,
external_id: `user-${user.id}`,
contact_email: user.email,
}),
});
const merchant = await merchantRes.json();

// Create account
const accountRes = await fetch('https://api.partners.nextpay.world/v2/accounts', {
method: 'POST',
headers: { 'Authorization': auth, 'Content-Type': 'application/json' },
body: JSON.stringify({
merchant_id: merchant.id,
name: 'Main',
external_id: `account-user-${user.id}`,
}),
});
const account = await accountRes.json();

// Generate permanent QR code
const qrRes = await fetch(
`https://api.partners.nextpay.world/v2/accounts/${account.id}/funding-methods`,
{
method: 'POST',
headers: { 'Authorization': auth, 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'qrph', external_id: `qr-user-${user.id}` }),
}
);
const fundingMethod = await qrRes.json();

// Store in your database
await db.users.update(user.id, {
nextapiMerchantId: merchant.id,
nextapiAccountId: account.id,
nextapiFundingMethodId: fundingMethod.id,
qrImageUrl: fundingMethod.qr_image_url,
});

return { merchant, account, fundingMethod };
}

Step 2: Show users their QR code

Each user's QR code is available from the stored qr_image_url:

app.get('/dashboard/qr', requireAuth, async (req, res) => {
const user = await db.users.findById(req.user.id);

res.json({
qrImageUrl: user.qrImageUrl,
accountId: user.nextapiAccountId,
});
});

In your dashboard UI:

<div class="payment-qr">
<h3>Your Payment QR Code</h3>
<img src="{{ user.qrImageUrl }}" alt="Scan to pay {{ user.businessName }}" width="256" height="256" />
<p>Customers scan this with GCash, Maya, or any bank app</p>
</div>

Step 3: Handle payments and take commission

When a user's customer pays, you receive a webhook. At this point, apply your commission split:

const PLATFORM_COMMISSION_RATE = 0.02; // 2%
const PLATFORM_ACCOUNT_ID = process.env.NEXTAPI_PLATFORM_ACCOUNT_ID;

app.post('/webhooks/nextapi', express.raw({ type: 'application/json' }), async (req, res) => {
// Verify signature (always first)
const signature = req.headers['x-nextpay-signature'];
if (!verifySignature(req.body, signature, process.env.NEXTAPI_WEBHOOK_SECRET)) {
return res.status(401).send('Unauthorized');
}

const event = JSON.parse(req.body.toString());

if (event.type === 'funding_method.paid') {
const { account_id, amount, id: eventId } = event.data;

// Idempotency check
if (await isEventProcessed(eventId)) {
return res.status(200).send('OK');
}

// Look up the user who owns this account
const user = await db.users.findByNextapiAccountId(account_id);
if (!user) {
console.error(`No user found for account ${account_id}`);
return res.status(200).send('OK');
}

// Calculate commission
const commissionCentavos = Math.floor(amount * PLATFORM_COMMISSION_RATE);

if (commissionCentavos > 0) {
// Transfer commission from user account to platform account
await fetch(
`https://api.partners.nextpay.world/v2/accounts/${account_id}/transfer`,
{
method: 'POST',
headers: {
'Authorization': 'Basic ...',
'Content-Type': 'application/json',
'X-Idempotency-Key': `commission-${eventId}`,
},
body: JSON.stringify({
destination_account_id: PLATFORM_ACCOUNT_ID,
amount: commissionCentavos,
description: `Platform commission - ${eventId}`,
reference: `commission-${eventId}`,
}),
}
);
}

// Record in your database
await db.transactions.create({
userId: user.id,
type: 'collection',
grossAmount: amount,
commissionAmount: commissionCentavos,
netAmount: amount - commissionCentavos,
eventId,
createdAt: new Date(),
});

await markEventProcessed(eventId);
console.log(`User ${user.id} collected PHP ${amount / 100}, commission: PHP ${commissionCentavos / 100}`);
}

res.status(200).send('OK');
});

Step 4: Show user balance

Display the user's current balance from NextAPI:

app.get('/dashboard/balance', requireAuth, async (req, res) => {
const user = await db.users.findById(req.user.id);

const response = await fetch(
`https://api.partners.nextpay.world/v2/accounts/${user.nextapiAccountId}/balances`,
{
headers: { 'Authorization': 'Basic ...' },
}
);

const balance = await response.json();

res.json({
available: balance.available,
availableFormatted: `PHP ${(balance.available / 100).toFixed(2)}`,
pending: balance.pending,
});
});

Step 5: Handle user payout requests

When a user wants to withdraw their balance to their bank:

app.post('/dashboard/withdraw', requireAuth, async (req, res) => {
const { amountCentavos, bankCode, accountNumber, accountName } = req.body;
const user = await db.users.findById(req.user.id);

// Verify sufficient balance
const balanceRes = await fetch(
`https://api.partners.nextpay.world/v2/accounts/${user.nextapiAccountId}/balances`,
{ headers: { 'Authorization': 'Basic ...' } }
);
const balance = await balanceRes.json();

if (balance.available < amountCentavos) {
return res.status(400).json({ error: 'Insufficient balance' });
}

const withdrawalId = `withdrawal-${user.id}-${Date.now()}`;

const payoutRes = await fetch('https://api.partners.nextpay.world/v2/payout-requests', {
method: 'POST',
headers: {
'Authorization': 'Basic ...',
'Content-Type': 'application/json',
'X-Idempotency-Key': `${withdrawalId}-v1`,
},
body: JSON.stringify({
account_id: user.nextapiAccountId,
amount: amountCentavos,
recipient: {
type: 'bank_account',
bank_code: bankCode,
account_number: accountNumber,
account_name: accountName,
},
description: `Withdrawal - ${user.businessName}`,
reference: withdrawalId,
}),
});

const payoutRequest = await payoutRes.json();

// Store withdrawal record
await db.withdrawals.create({
userId: user.id,
payoutRequestId: payoutRequest.id,
amount: amountCentavos,
status: 'initiated',
reference: withdrawalId,
});

res.json({
withdrawalId,
payoutRequestId: payoutRequest.id,
status: payoutRequest.status,
message: 'Withdrawal initiated. Funds will arrive within 30 seconds (InstaPay) or by end of day (PESONet).',
});
});

Complete flow summary

StepWhat happensYour code
User signs upCreate Merchant + Account + Funding MethodonboardUser()
User's customer paysNextAPI fires funding_method.paidWebhook handler
Commission splitTransfer from user account to platform accountPOST /accounts/{id}/transfer
User checks balanceShow available from NextAPIGET /accounts/{id}/balances
User withdrawsCreate payout requestPOST /payout-requests
Withdrawal confirmedNextAPI fires payout_request.completedWebhook handler

What's next?