Skip to main content

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.paid and funding_method.paid events

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-Key prevents 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.