Take a Commission from Sales
This guide shows you how to automatically collect a platform fee whenever a merchant on your platform receives a payment.
How it works
When a merchant collects a payment, the full amount credits their NextAPI account. To take your commission, use the internal account transfer API to move a portion from the merchant's account to your platform account — instantly, with no external rail needed.
Merchant's customer pays PHP 1,000
↓
Merchant's account credited: +PHP 1,000
↓
Your webhook handler fires
↓
POST /v2/accounts/{merchant_account_id}/transfer
amount: 2000 (PHP 20.00 = 2% commission)
destination: your platform account
↓
Merchant net: PHP 980 / Platform: PHP 20
Prerequisites
- A platform account (your own NextAPI account for receiving commissions)
- Merchants and their accounts set up
- Webhook endpoint configured for
payment_intent.paidandfunding_method.paidevents
Implementation
1. Define your commission structure
// commission.js
const COMMISSION_RULES = {
default: 0.02, // 2% for standard merchants
premium: 0.015, // 1.5% for premium tier
enterprise: 0.01, // 1% for enterprise
};
function calculateCommission(amount, merchantTier = 'default') {
const rate = COMMISSION_RULES[merchantTier] ?? COMMISSION_RULES.default;
// Always round down — never charge more than intended
return Math.floor(amount * rate);
}
2. Handle the payment webhook
const PLATFORM_ACCOUNT_ID = process.env.NEXTAPI_PLATFORM_ACCOUNT_ID;
app.post('/webhooks/nextapi', express.raw({ type: 'application/json' }), async (req, res) => {
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());
// Handle both one-time (payment_intent) and static QR (funding_method) payments
if (event.type === 'payment_intent.paid' || event.type === 'funding_method.paid') {
const { account_id: merchantAccountId, amount, id: eventId } = event.data;
// Idempotency — don't charge commission twice for the same event
if (await isCommissionCharged(eventId)) {
return res.status(200).send('OK');
}
const merchant = await db.merchants.findByAccountId(merchantAccountId);
const commissionAmount = calculateCommission(amount, merchant.tier);
if (commissionAmount > 0) {
await chargeCommission(merchantAccountId, commissionAmount, eventId);
}
await markCommissionCharged(eventId, { merchantAccountId, amount, commissionAmount });
}
res.status(200).send('OK');
});
3. Execute the commission transfer
async function chargeCommission(merchantAccountId, commissionAmount, eventId) {
const auth = 'Basic ' + Buffer.from(
`${process.env.NEXTAPI_CLIENT_ID}:${process.env.NEXTAPI_CLIENT_SECRET}`
).toString('base64');
const response = await fetch(
`https://api.partners.nextpay.world/v2/accounts/${merchantAccountId}/transfer`,
{
method: 'POST',
headers: {
'Authorization': auth,
'Content-Type': 'application/json',
'X-Idempotency-Key': `commission-${eventId}-v1`,
},
body: JSON.stringify({
destination_account_id: PLATFORM_ACCOUNT_ID,
amount: commissionAmount,
description: `Platform commission`,
reference: `commission-${eventId}`,
}),
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(`Commission transfer failed: ${error.message}`);
}
return response.json();
}
Commission models
Flat percentage
// 2% on every transaction
const commission = Math.floor(amount * 0.02);
Tiered by merchant volume
async function getCommissionRate(merchantId) {
const { monthlyVolume } = await getMonthlyVolume(merchantId);
if (monthlyVolume > 10_000_000_00) return 0.01; // > PHP 10M: 1%
if (monthlyVolume > 1_000_000_00) return 0.015; // > PHP 1M: 1.5%
return 0.02; // default: 2%
}
Fixed fee + percentage
function calculateHybridCommission(amount) {
const fixed = 500; // PHP 5.00 flat fee (500 centavos)
const percentage = 0.01; // 1%
return fixed + Math.floor(amount * percentage);
}
Fee cap
function calculateCappedCommission(amount) {
const rate = 0.02;
const cap = 50000; // PHP 500.00 maximum commission
return Math.min(Math.floor(amount * rate), cap);
}
Show commission to merchants
Give merchants visibility into your fee structure. Show commission earned and net received in their dashboard:
app.get('/dashboard/transactions', requireAuth, async (req, res) => {
const user = await db.users.findById(req.user.id);
// Get postings for the merchant's account
const response = await fetch(
`https://api.partners.nextpay.world/v2/accounts/${user.nextapiAccountId}/postings?limit=50`,
{ headers: { 'Authorization': 'Basic ...' } }
);
const { data: postings } = await response.json();
// Join with your commission records for net/gross breakdown
const transactions = await Promise.all(
postings.map(async posting => {
const commission = await db.commissions.findByReference(posting.reference);
return {
...posting,
grossAmount: posting.amount + (commission?.amount || 0),
commissionAmount: commission?.amount || 0,
netAmount: posting.amount,
};
})
);
res.json({ transactions });
});
Edge cases
Zero commission: If commissionAmount is 0 (e.g., very small transaction with rounding), skip the transfer call — a zero-amount transfer is invalid.
Transfer failure: If the commission transfer fails (e.g., merchant doesn't have enough balance due to a concurrent debit), log the failure and retry. The merchant account balance should always be sufficient immediately after a collection webhook.
Commission on refunds: If you implement refunds, decide whether to reverse the commission. The simplest approach: reverse the commission transfer using another internal transfer from your platform account back to the merchant.
Key points
- Internal transfers are instant. No external rails, no fees, no settlement delay.
- Always idempotent. The
X-Idempotency-Keyprevents double-charging if your webhook handler retries. - Round down.
Math.floor()ensures you never charge more commission than intended due to floating-point arithmetic. - Log everything. Store commission records in your database for merchant billing transparency and your own reconciliation.
Related
- Transfer Between Accounts — internal transfer API
- Reconcile Transactions — postings-based reconciliation
- Setup Webhooks — receiving payment events
- Build a SaaS Platform with Wallets — full platform tutorial