Most payment tutorials stop too early.
They show you how to create an invoice, redirect the buyer, receive a webhook, and mark an order as paid.
That is useful, but it is not where many real merchant problems end.
For marketplaces, creator platforms, affiliate programs, course platforms, agencies, reseller networks, and service communities, the real operational question often comes after the customer pays:
Who should receive the money, how much should each party receive, when should payouts happen, and how do we prove what happened later?
That is the business opportunity.
A developer can build a crypto revenue split and payout system for merchants who already have revenue coming in, but do not want to manage spreadsheets, manual wallet transfers, partner balances, payout approvals, and payment disputes by hand.
In this article, I will use OxaPay as the example payment infrastructure because its documentation exposes the payment and payout primitives required for this kind of system: invoice generation, callback URLs, payment webhooks, payment history, payout creation, payout information, payout history, payout status tracking, SDKs, and automation integrations.
This is not an article about creating a new crypto payment gateway.
It is about building a merchant-facing operational product on top of existing crypto payment infrastructure.
The core idea
A crypto revenue split and payout system helps a merchant collect payment from customers and later distribute revenue to the right people.
The developer does not simply build a checkout button.
The developer builds the layer between incoming payments and outgoing payouts.
That layer usually includes:
- payment intake
- order matching
- partner or seller records
- revenue split rules
- internal ledger entries
- partner balances
- payout thresholds
- payout approval flows
- payout execution
- payout status tracking
- audit logs
- finance exports
- support visibility
The merchant buys the system because manual revenue sharing becomes painful as soon as transaction volume grows.
A simple example:
A course platform sells a $100 course.
The platform keeps 20%.
The instructor receives 70%.
An affiliate receives 10%.
The payment arrives in crypto.
Without a system, someone has to calculate every share, update spreadsheets, check whether the payment was fully confirmed, copy wallet addresses, send payouts, record transaction IDs, answer partner questions, and fix mistakes.
With a proper system, the flow becomes:
Customer pays invoice
↓
Payment webhook confirms paid status
↓
System records payment in ledger
↓
Split engine calculates shares
↓
Partner balances are updated
↓
Payout queue is created
↓
Admin approves payout batch
↓
Payout API sends funds
↓
Payout webhook updates final status
↓
Partner dashboard and finance export are updated
That is a real product.
Why merchants would pay for this
Merchants do not pay for code because it is interesting.
They pay when a system removes a recurring operational problem.
Revenue split and payout workflows create several recurring problems:
- partners ask when they will be paid
- sellers dispute their balances
- affiliates want transparent reporting
- finance teams need exportable records
- payouts get delayed by manual review
- wallet addresses change
- a wrong payout can be expensive
- support teams need to understand payment history
- one payment can belong to multiple parties
- one partner can earn from many payments
- one payout batch can contain many partner payments
The value is not only automation.
The deeper value is control.
A merchant wants to know:
- which payment created this payable?
- which rule created this partner share?
- has this payout been approved?
- was the payout sent?
- did the blockchain transaction confirm?
- was the payout rejected?
- who changed the partner wallet address?
- why did the balance change?
- can finance export this month’s payout report?
That is why this can become a paid product or service.
Who can use this kind of product?
This model works best when a business receives money from customers and must distribute money to other people later.
Good target users include:
| Business type | Revenue split problem |
|---|---|
| Marketplaces | Split revenue between platform and sellers |
| Creator platforms | Pay creators after customer purchases |
| Course platforms | Pay instructors and affiliates |
| Agencies | Pay contractors, referrers, and partners |
| Affiliate programs | Track commission and batch payouts |
| Freelancer networks | Pay service providers after customer payment |
| Gaming communities | Share revenue with server owners, teams, or partners |
| Donation platforms | Route donations to campaigns, creators, or beneficiaries |
| Reseller programs | Calculate reseller commission and payout balances |
| SaaS partner programs | Pay referral or integration partners |
The best customers are not tiny merchants with one transaction per week.
The best customers are merchants with recurring transaction flow, multiple partners, and enough operational complexity that spreadsheets start breaking.
The important distinction: split logic is yours, payment infrastructure is OxaPay
This is the first design rule.
OxaPay can help you create invoices, receive webhooks, retrieve payment records, and generate payout requests.
Your application must own the revenue split logic.
That means your system decides:
- who receives a share
- how much each partner receives
- which payment created the balance
- whether funds should be held
- whether the payout requires approval
- whether there is a minimum payout threshold
- whether a partner is allowed to receive payouts
- whether a payout should be delayed, blocked, or retried
This distinction matters.
Do not build a system that receives a payment webhook and immediately sends payouts in the same request.
That is fragile.
A production-grade system should use this pattern:
Payment confirmation → ledger entry → split calculation → payout queue → approval → payout execution
The ledger is the source of truth.
The payout API is the execution layer.
OxaPay primitives used in this system
For this article, we need several OxaPay primitives.
1. Generate Invoice
OxaPay's Generate Invoice endpoint lets a merchant create a new invoice and obtain a payment URL. The request supports fields such as amount, currency, lifetime, fee_paid_by_payer, under_paid_coverage, callback_url, return_url, and order_id.
Docs: https://docs.oxapay.com/api-reference/payment/generate-invoice
In a revenue split system, the invoice represents the customer-facing payment request.
The order_id should reference your internal order, booking, sale, or transaction record.
The callback_url should point to your webhook endpoint.
2. Webhook
OxaPay sends payment status updates to the callback_url. According to the webhook documentation, callbacks are sent as JSON over HTTP POST. The system expects your endpoint to return HTTP 200 with ok, and OxaPay retries webhook delivery up to five times if delivery fails.
Docs: https://docs.oxapay.com/webhook
This is important because revenue split logic should only start after the payment reaches the correct confirmed business state.
In most systems, you should not create partner balances from early or incomplete payment states.
3. HMAC validation
OxaPay webhooks include an HMAC header. The docs state that merchants should validate callbacks using the merchant API key for payment webhooks, and the payout API key for payout webhooks. The signature is generated over the raw POST body using HMAC SHA-512.
Docs: https://docs.oxapay.com/webhook
This is not optional in a payout-related system.
If your application updates balances or payout status from webhooks, signature validation is a core security requirement.
4. Payment History
The Payment History endpoint retrieves payments associated with a merchant account. It supports filters such as track_id, type, status, pay_currency, currency, network, address, date range, amount range, sorting, and pagination.
Docs: https://docs.oxapay.com/api-reference/payment/payment-history
This is useful for reconciliation jobs and admin dashboards.
If a webhook is delayed or your processing job fails, your system can use payment history or payment information to reconcile its internal state.
5. Generate Payout
OxaPay's Generate Payout endpoint creates a cryptocurrency payout request to a specified recipient address. The request uses a payout_api_key and fields such as address, currency, amount, network, callback_url, memo, and description.
Docs: https://docs.oxapay.com/api-reference/payout/generate-payout
This is the execution step after your own system has calculated and approved a payout.
6. Payout Information and Payout History
Payout Information retrieves the details of a specific payout using its track_id. Payout History retrieves payout transactions associated with the account and supports filtering by status, type, currency, network, amount range, date range, sorting, and pagination.
Docs:
- https://docs.oxapay.com/api-reference/payout/payout-information
- https://docs.oxapay.com/api-reference/payout/payout-history
These endpoints are useful for payout dashboards, reconciliation, and support tools.
7. Payout Status Table
The payout status table lists states such as processing, pending, confirming, confirmed, canceled, and rejected.
Docs: https://docs.oxapay.com/api-reference/payout/payout-status-table
Your system should map these external statuses to internal payout states.
8. SDKs and automation tools
OxaPay also provides SDKs and automation integrations. The PHP, Python, and Laravel SDK pages include payment methods and webhook handling notes. The Make integration lists modules such as Generate Invoice, Generate Payout, Get Payment Information, Get Payout Information, Search Payments, Search Payouts, Watch Payment Webhook, and Watch Payout Webhook.
Docs:
- https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/php-sdk
- https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/python-sdk
- https://docs.oxapay.com/welcome-to-oxapay/integrations/sdks/laravel-sdk
- https://apps.make.com/oxapay-crypto-pay-gtw
This gives developers multiple implementation paths: code-first, SDK-based, or workflow-assisted.
What you are actually building
A real revenue split and payout product has five layers.
Layer 1: Payment intake
This layer creates invoices and receives payment webhooks.
It should handle:
- order creation
- invoice generation
- customer redirect
- payment callback
- payment status updates
- duplicate webhook events
- late payment events
- failed or incomplete payment states
Layer 2: Ledger
This is the most important layer.
The ledger records financial movements inside your application.
A common mistake is to only store balances as a single number.
That is not enough.
You need immutable ledger entries.
A partner balance should be calculated from ledger entries, not manually edited as a random number.
Layer 3: Split engine
The split engine decides how revenue is allocated.
It may support:
- fixed percentage splits
- product-owner splits
- affiliate commission
- platform fees
- instructor revenue share
- creator revenue share
- multi-party splits
- reserves or holdbacks
- minimum payout thresholds
- tiered commission rules
Layer 4: Payout queue
Do not execute payouts immediately.
Create payout queue items first.
A payout queue allows:
- review before sending
- minimum threshold checks
- partner address verification
- compliance review where needed
- balance checks
- scheduled payouts
- batch approvals
- retries
- status tracking
Layer 5: Payout execution and tracking
After approval, the system sends payout requests through the payout API.
Then it listens for payout webhooks or checks payout information/history to update payout state.
The goal is not only to send funds.
The goal is to make payouts traceable.
Reference architecture
A practical architecture can look like this:
Customer checkout
↓
Create order in merchant app
↓
Create OxaPay invoice
↓
Customer pays
↓
OxaPay payment webhook
↓
Validate HMAC
↓
Store raw webhook event
↓
Process payment idempotently
↓
Create ledger entries
↓
Apply split rules
↓
Update partner balances
↓
Create payout queue items
↓
Admin reviews payout batch
↓
Call OxaPay Generate Payout
↓
OxaPay payout webhook
↓
Validate payout HMAC
↓
Update payout item status
↓
Update partner dashboard and finance exports
That is the architecture a merchant is paying for.
The database model
You do not need an over-engineered database on day one.
But you do need the right concepts.
Here is a simplified schema.
CREATE TABLE merchants (
id UUID PRIMARY KEY,
name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE partners (
id UUID PRIMARY KEY,
merchant_id UUID NOT NULL REFERENCES merchants(id),
name TEXT NOT NULL,
email TEXT,
status TEXT NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE partner_payout_methods (
id UUID PRIMARY KEY,
partner_id UUID NOT NULL REFERENCES partners(id),
currency TEXT NOT NULL,
network TEXT,
address TEXT NOT NULL,
memo TEXT,
status TEXT NOT NULL DEFAULT 'pending_verification',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
verified_at TIMESTAMPTZ
);
CREATE TABLE orders (
id UUID PRIMARY KEY,
merchant_id UUID NOT NULL REFERENCES merchants(id),
external_order_id TEXT NOT NULL,
amount_decimal NUMERIC(36, 18) NOT NULL,
currency TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'created',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (merchant_id, external_order_id)
);
CREATE TABLE payments (
id UUID PRIMARY KEY,
merchant_id UUID NOT NULL REFERENCES merchants(id),
order_id UUID NOT NULL REFERENCES orders(id),
provider TEXT NOT NULL DEFAULT 'oxapay',
provider_track_id TEXT UNIQUE,
status TEXT NOT NULL,
amount_decimal NUMERIC(36, 18),
currency TEXT,
network TEXT,
raw_payload JSONB,
paid_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE split_rules (
id UUID PRIMARY KEY,
merchant_id UUID NOT NULL REFERENCES merchants(id),
name TEXT NOT NULL,
rule_type TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
config JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE ledger_entries (
id UUID PRIMARY KEY,
merchant_id UUID NOT NULL REFERENCES merchants(id),
partner_id UUID REFERENCES partners(id),
payment_id UUID REFERENCES payments(id),
entry_type TEXT NOT NULL,
amount_decimal NUMERIC(36, 18) NOT NULL,
currency TEXT NOT NULL,
direction TEXT NOT NULL CHECK (direction IN ('credit', 'debit')),
description TEXT,
metadata JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE payout_batches (
id UUID PRIMARY KEY,
merchant_id UUID NOT NULL REFERENCES merchants(id),
status TEXT NOT NULL DEFAULT 'draft',
created_by UUID,
approved_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
approved_at TIMESTAMPTZ
);
CREATE TABLE payout_items (
id UUID PRIMARY KEY,
batch_id UUID REFERENCES payout_batches(id),
merchant_id UUID NOT NULL REFERENCES merchants(id),
partner_id UUID NOT NULL REFERENCES partners(id),
payout_method_id UUID NOT NULL REFERENCES partner_payout_methods(id),
amount_decimal NUMERIC(36, 18) NOT NULL,
currency TEXT NOT NULL,
network TEXT,
status TEXT NOT NULL DEFAULT 'queued',
provider TEXT DEFAULT 'oxapay',
provider_track_id TEXT UNIQUE,
provider_status TEXT,
error_message TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
sent_at TIMESTAMPTZ,
confirmed_at TIMESTAMPTZ
);
CREATE TABLE webhook_events (
id UUID PRIMARY KEY,
provider TEXT NOT NULL,
event_type TEXT NOT NULL,
provider_track_id TEXT,
signature_valid BOOLEAN NOT NULL,
processed BOOLEAN NOT NULL DEFAULT false,
raw_payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (provider, event_type, provider_track_id, raw_payload)
);
CREATE TABLE audit_logs (
id UUID PRIMARY KEY,
merchant_id UUID NOT NULL REFERENCES merchants(id),
actor_id UUID,
action TEXT NOT NULL,
entity_type TEXT NOT NULL,
entity_id UUID,
metadata JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
This schema is not final.
But it captures the key idea:
- payments are not payouts
- balances come from ledger entries
- payout items are queued before execution
- webhook events are stored
- partner payout methods are separate records
- audit logs exist for sensitive actions
Why a ledger matters
Imagine this partner balance:
Partner A balance: 700 USDT
That number alone tells you almost nothing.
A real system needs to explain why the balance is 700 USDT.
A ledger can show:
+70 USDT Course sale #1001 instructor share
+70 USDT Course sale #1002 instructor share
+70 USDT Course sale #1003 instructor share
-100 USDT Payout batch #22
+20 USDT Affiliate bonus correction
The balance is the result of events.
This gives your merchant trust.
It also gives partners visibility.
Without a ledger, every balance dispute becomes a manual investigation.
Split rule examples
A revenue split system should support simple rules first.
Fixed percentage split
{
"platform_fee_percent": 20,
"partner_percent": 80
}
Use case:
A marketplace keeps 20%, seller receives 80%.
Instructor plus affiliate split
{
"platform_fee_percent": 20,
"instructor_percent": 70,
"affiliate_percent": 10
}
Use case:
An online course platform pays the instructor and affiliate after a sale.
Product owner split
{
"platform_fee_percent": 15,
"product_owner_percent": 85
}
Use case:
A digital product marketplace pays the product creator.
Holdback or reserve
{
"platform_fee_percent": 20,
"partner_percent": 70,
"reserve_percent": 10,
"reserve_release_days": 14
}
Use case:
A merchant wants to delay part of partner earnings for support issues, refunds, delivery disputes, or risk management.
Even when crypto payments are operationally different from card payments, merchants may still need internal holds for customer support, refund policies, service delivery windows, or partner disputes.
Example: create an OxaPay invoice
Here is a simplified Node.js example.
import fetch from "node-fetch";
const OXAPAY_MERCHANT_API_KEY = process.env.OXAPAY_MERCHANT_API_KEY;
export async function createInvoiceForOrder(order) {
const response = await fetch("https://api.oxapay.com/v1/payment/invoice", {
method: "POST",
headers: {
"Content-Type": "application/json",
"merchant_api_key": OXAPAY_MERCHANT_API_KEY
},
body: JSON.stringify({
amount: order.amount,
currency: order.currency,
order_id: order.id,
lifetime: 60,
callback_url: "https://yourapp.com/webhooks/oxapay/payment",
return_url: `https://yourapp.com/orders/${order.id}/thank-you`,
description: `Order ${order.id}`
})
});
if (!response.ok) {
throw new Error(`OxaPay invoice request failed: ${response.status}`);
}
const result = await response.json();
return result;
}
In your database, store:
- internal order ID
- OxaPay track ID if returned
- payment URL if returned
- requested amount
- requested currency
- invoice status
- creation time
- expiration time
Do not rely only on the frontend redirect.
The webhook should be the source of payment confirmation.
Example: validate and store a payment webhook
Webhook handling should be fast, verifiable, and idempotent.
Do not perform heavy split calculations before responding.
Store the event, return ok, and process it asynchronously.
import express from "express";
import crypto from "crypto";
const app = express();
// Important: use raw body for HMAC validation.
app.post(
"/webhooks/oxapay/payment",
express.raw({ type: "application/json" }),
async (req, res) => {
const rawBody = req.body;
const receivedHmac = req.header("HMAC");
if (!receivedHmac) {
return res.status(400).send("missing hmac");
}
const isValid = verifyHmac(
rawBody,
receivedHmac,
process.env.OXAPAY_MERCHANT_API_KEY
);
if (!isValid) {
return res.status(401).send("invalid signature");
}
const payload = JSON.parse(rawBody.toString("utf8"));
// Store the raw event first.
const event = await db.webhook_events.insert({
provider: "oxapay",
event_type: "payment",
provider_track_id: String(payload.track_id || ""),
signature_valid: true,
raw_payload: payload
});
// Return exactly what the provider expects.
res.status(200).send("ok");
// Process asynchronously.
await queue.add("process_oxapay_payment_webhook", {
webhook_event_id: event.id
});
}
);
function verifyHmac(rawBody, receivedHmac, secret) {
const expected = crypto
.createHmac("sha512", secret)
.update(rawBody)
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(receivedHmac, "hex");
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}
OxaPay documentation says payment callbacks should be validated using the merchant API key, and payout callbacks should be validated using the payout API key.
So use different secrets for different webhook routes.
Processing the payment webhook idempotently
A webhook can be retried.
Your system must survive duplicates.
This is the minimum logic:
async function processPaymentWebhook(eventId) {
const event = await db.webhook_events.findById(eventId);
const payload = event.raw_payload;
await db.transaction(async (trx) => {
const existingPayment = await trx.payments.findOne({
provider: "oxapay",
provider_track_id: String(payload.track_id)
});
if (existingPayment?.status === "paid") {
await trx.webhook_events.markProcessed(eventId);
return;
}
const payment = await trx.payments.upsertByProviderTrackId({
provider: "oxapay",
provider_track_id: String(payload.track_id),
status: normalizeOxaPayPaymentStatus(payload.status),
amount_decimal: payload.amount,
currency: payload.currency,
network: payload.network,
raw_payload: payload
});
if (payment.status !== "paid") {
await trx.webhook_events.markProcessed(eventId);
return;
}
const order = await trx.orders.findByProviderPayment(payment.id);
if (!order) {
await trx.webhook_events.markProcessed(eventId);
await trx.audit_logs.insert({
merchant_id: payment.merchant_id,
action: "payment_without_order",
entity_type: "payment",
entity_id: payment.id,
metadata: { provider_track_id: payment.provider_track_id }
});
return;
}
if (order.status === "paid") {
await trx.webhook_events.markProcessed(eventId);
return;
}
await trx.orders.update(order.id, { status: "paid" });
await applySplitRules(trx, {
merchant_id: order.merchant_id,
order_id: order.id,
payment_id: payment.id,
amount: order.amount_decimal,
currency: order.currency
});
await trx.webhook_events.markProcessed(eventId);
});
}
function normalizeOxaPayPaymentStatus(status) {
if (!status) return "unknown";
return String(status).toLowerCase();
}
The exact payload fields should be mapped from your tested webhook payloads and the Payment Information response.
The important part is the process:
- verify signature
- store raw event
- deduplicate by provider track ID
- process inside a database transaction
- only apply split rules once
- mark the webhook as processed
Applying split rules
For financial calculations, do not use floating point arithmetic.
Use integer minor units where possible or a decimal library with explicit rounding rules.
For crypto amounts, use a decimal type that can preserve precision.
Here is a simplified split example using decimal logic conceptually.
import Decimal from "decimal.js";
async function applySplitRules(trx, input) {
const { merchant_id, order_id, payment_id, amount, currency } = input;
const order = await trx.orders.findById(order_id);
const rule = await trx.split_rules.findActiveForOrder(order);
const total = new Decimal(amount);
const platformFee = total.mul(rule.config.platform_fee_percent).div(100);
const instructorShare = total.mul(rule.config.instructor_percent).div(100);
const affiliateShare = total.mul(rule.config.affiliate_percent || 0).div(100);
// Optional: keep rounding dust in the platform account.
const allocated = platformFee.plus(instructorShare).plus(affiliateShare);
const roundingRemainder = total.minus(allocated);
await trx.ledger_entries.insertMany([
{
merchant_id,
payment_id,
partner_id: null,
entry_type: "platform_fee",
direction: "credit",
amount_decimal: platformFee.plus(roundingRemainder).toString(),
currency,
description: `Platform fee for order ${order.external_order_id}`
},
{
merchant_id,
payment_id,
partner_id: order.instructor_partner_id,
entry_type: "partner_share",
direction: "credit",
amount_decimal: instructorShare.toString(),
currency,
description: `Instructor share for order ${order.external_order_id}`
}
]);
if (affiliateShare.gt(0) && order.affiliate_partner_id) {
await trx.ledger_entries.insert({
merchant_id,
payment_id,
partner_id: order.affiliate_partner_id,
entry_type: "affiliate_commission",
direction: "credit",
amount_decimal: affiliateShare.toString(),
currency,
description: `Affiliate commission for order ${order.external_order_id}`
});
}
}
In production, you need more controls:
- rule versioning
- effective dates
- product-specific rules
- partner-specific overrides
- minimum payout thresholds
- maximum payout limits
- rounding policy
- reserve policy
- manual adjustments
- approval workflow for corrections
Never silently edit past ledger entries.
If you need to fix something, create a reversing entry or adjustment entry.
That is how you preserve auditability.
Building the payout queue
A payout queue is where earned balances become payable items.
A daily or weekly job can calculate partner balances and create payout items.
async function createPayoutBatchForMerchant(merchantId) {
await db.transaction(async (trx) => {
const batch = await trx.payout_batches.insert({
merchant_id: merchantId,
status: "draft"
});
const partners = await trx.partners.findActiveWithBalances({
merchant_id: merchantId,
min_balance: "25.00"
});
for (const partner of partners) {
const method = await trx.partner_payout_methods.findVerifiedDefault(partner.id);
if (!method) {
await trx.audit_logs.insert({
merchant_id: merchantId,
action: "payout_skipped_no_verified_method",
entity_type: "partner",
entity_id: partner.id,
metadata: { balance: partner.balance }
});
continue;
}
await trx.payout_items.insert({
batch_id: batch.id,
merchant_id: merchantId,
partner_id: partner.id,
payout_method_id: method.id,
amount_decimal: partner.balance,
currency: method.currency,
network: method.network,
status: "queued"
});
}
});
}
This job should not send funds.
It should create a draft batch.
A merchant admin should review it first, especially in early versions of the product.
Approval workflow
Payouts are sensitive.
Your first version should include manual approval.
At minimum:
- payout batch created as
draft - admin reviews partner, amount, currency, network, address
- admin approves batch
- system records
approved_byandapproved_at - execution worker sends payout requests
- payout status is tracked asynchronously
For larger merchants, add stronger controls:
- two-person approval
- payout limits per day
- payout limits per partner
- address change cooldown
- require re-approval after address changes
- role-based permissions
- audit logs for every approval
- blocked partner status
- suspicious payout review queue
This is where your product becomes serious.
Many developers can call a payout API.
Fewer developers build a safe payout approval system.
Example: generate an OxaPay payout
After approval, call the payout API from a backend worker.
Never call payout execution from the frontend.
Never expose the payout API key to client-side code.
import fetch from "node-fetch";
const OXAPAY_PAYOUT_API_KEY = process.env.OXAPAY_PAYOUT_API_KEY;
export async function sendOxaPayPayout(payoutItem) {
const response = await fetch("https://api.oxapay.com/v1/payout", {
method: "POST",
headers: {
"Content-Type": "application/json",
"payout_api_key": OXAPAY_PAYOUT_API_KEY
},
body: JSON.stringify({
address: payoutItem.address,
amount: payoutItem.amount_decimal,
currency: payoutItem.currency,
network: payoutItem.network,
memo: payoutItem.memo || undefined,
callback_url: "https://yourapp.com/webhooks/oxapay/payout",
description: `Payout item ${payoutItem.id}`
})
});
const result = await response.json();
if (!response.ok || result.status >= 400) {
throw new Error(`OxaPay payout failed: ${JSON.stringify(result.error || result)}`);
}
return result;
}
After sending the request, store:
- payout item ID
- provider track ID
- request payload hash
- requested amount
- requested currency
- network
- payout address reference
- provider status
- sent timestamp
Do not delete the payout item if the provider returns an error.
Record the failure and require review.
Payout state machine
Your internal payout state machine should not blindly mirror provider statuses.
Use your own states and map provider statuses into them.
Example:
| Internal status | Meaning | Possible OxaPay mapping |
|---|---|---|
queued |
Created but not approved | Internal only |
approved |
Approved by merchant admin | Internal only |
sending |
Worker is calling payout API | Internal only |
provider_processing |
Provider accepted request | processing |
provider_pending |
Request is in provider queue | pending |
blockchain_confirming |
Transaction created and awaiting confirmation | confirming |
completed |
Payout completed successfully | confirmed |
failed |
Payout failed or rejected | rejected |
canceled |
Payout was canceled | canceled |
needs_review |
Human review needed | Internal only |
This separation makes your system easier to evolve.
If OxaPay changes wording or returns additional states later, you only update the mapping layer.
Example: payout webhook handler
Use a separate route and validate with the payout API key.
app.post(
"/webhooks/oxapay/payout",
express.raw({ type: "application/json" }),
async (req, res) => {
const rawBody = req.body;
const receivedHmac = req.header("HMAC");
if (!receivedHmac) {
return res.status(400).send("missing hmac");
}
const isValid = verifyHmac(
rawBody,
receivedHmac,
process.env.OXAPAY_PAYOUT_API_KEY
);
if (!isValid) {
return res.status(401).send("invalid signature");
}
const payload = JSON.parse(rawBody.toString("utf8"));
const event = await db.webhook_events.insert({
provider: "oxapay",
event_type: "payout",
provider_track_id: String(payload.track_id || ""),
signature_valid: true,
raw_payload: payload
});
res.status(200).send("ok");
await queue.add("process_oxapay_payout_webhook", {
webhook_event_id: event.id
});
}
);
Then process it:
async function processPayoutWebhook(eventId) {
const event = await db.webhook_events.findById(eventId);
const payload = event.raw_payload;
await db.transaction(async (trx) => {
const payoutItem = await trx.payout_items.findByProviderTrackId(
String(payload.track_id)
);
if (!payoutItem) {
await trx.audit_logs.insert({
merchant_id: null,
action: "unknown_payout_webhook",
entity_type: "webhook_event",
entity_id: eventId,
metadata: payload
});
return;
}
const nextStatus = mapOxaPayPayoutStatus(payload.status);
await trx.payout_items.update(payoutItem.id, {
provider_status: payload.status,
status: nextStatus,
confirmed_at: nextStatus === "completed" ? new Date() : payoutItem.confirmed_at
});
if (nextStatus === "completed") {
await trx.ledger_entries.insert({
merchant_id: payoutItem.merchant_id,
partner_id: payoutItem.partner_id,
entry_type: "payout_debit",
direction: "debit",
amount_decimal: payoutItem.amount_decimal,
currency: payoutItem.currency,
description: `Payout completed for item ${payoutItem.id}`,
metadata: {
payout_item_id: payoutItem.id,
provider_track_id: payoutItem.provider_track_id
}
});
}
if (["failed", "canceled", "needs_review"].includes(nextStatus)) {
await trx.audit_logs.insert({
merchant_id: payoutItem.merchant_id,
action: "payout_requires_review",
entity_type: "payout_item",
entity_id: payoutItem.id,
metadata: payload
});
}
});
}
function mapOxaPayPayoutStatus(status) {
switch (String(status).toLowerCase()) {
case "processing":
return "provider_processing";
case "pending":
return "provider_pending";
case "confirming":
return "blockchain_confirming";
case "confirmed":
return "completed";
case "rejected":
return "failed";
case "canceled":
return "canceled";
default:
return "needs_review";
}
}
In a real implementation, make sure the debit ledger entry is also idempotent.
A duplicate payout webhook should not create two payout debits.
Use a unique constraint such as:
CREATE UNIQUE INDEX unique_payout_debit_entry
ON ledger_entries ((metadata->>'payout_item_id'))
WHERE entry_type = 'payout_debit';
Idempotency is not optional
Payment and payout systems are distributed systems.
Requests fail.
Webhooks retry.
Workers crash.
Admins double-click buttons.
Networks timeout.
If your payout system is not idempotent, it can duplicate financial actions.
Use idempotency at several levels:
- unique internal order IDs
- unique payment provider track IDs
- unique webhook event records
- unique payout item IDs
- unique provider payout track IDs
- unique ledger debit per payout item
- database transactions around state transitions
- worker locks when executing payout batches
Stripe's public API docs explain the general idea well: an idempotency key lets a server recognize retries of the same operation and return the original result instead of creating a duplicate operation.
Reference: https://docs.stripe.com/api/idempotent_requests
Even if OxaPay's specific endpoints do not expose a public idempotency-key parameter in the examples, your own application should still implement internal idempotency around all payment and payout state transitions.
Security model
A payout system has a larger blast radius than a checkout integration.
A checkout bug may fail to activate an order.
A payout bug may send funds to the wrong address.
That changes the engineering standard.
Protect API keys
Use separate secrets for merchant payment flows and payout flows.
Store them in a secret manager or encrypted environment configuration.
Never expose them to browser code.
Limit who can access payout configuration.
Verify payout addresses
Partner payout addresses should not go from form input to payout execution immediately.
Use a verification workflow:
- partner submits address
- system marks it
pending_verification - merchant admin reviews it
- optional confirmation email is sent
- address becomes
verified - address changes trigger cooldown or re-approval
Use role-based access control
A partner should only see their own earnings.
An admin should only manage their own merchant account.
A support agent should not be able to approve payouts.
OWASP lists Broken Object Level Authorization as a major API security risk. In a payout product, this is especially important because object IDs often represent sensitive resources such as partner balances, payout methods, payout items, or merchant records.
Reference: https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/
Add payout limits
Start with conservative limits:
- max payout per item
- max payout per partner per day
- max payout per merchant per day
- max payout batch size
- minimum payout threshold
- cooldown after payout address change
These controls protect both the merchant and your product.
Keep audit logs
Every sensitive action should produce an audit log:
- split rule created
- split rule changed
- partner payout address added
- payout address verified
- payout batch created
- payout batch approved
- payout sent
- payout canceled
- manual adjustment created
When money moves, logs are not optional.
Reconciliation jobs
Webhooks are useful, but a production payout product should also reconcile.
Schedule jobs that compare your internal records against provider records.
Example jobs:
Every 15 minutes:
- find payout items in provider_processing, provider_pending, blockchain_confirming
- call Payout Information by provider_track_id
- update stale statuses
Every day:
- retrieve Payout History for the previous day
- compare provider payouts with internal payout items
- flag missing or unknown payouts
Every day:
- retrieve Payment History for the previous day
- compare paid provider payments with internal paid orders
- flag mismatches
This matters because webhooks can fail, infrastructure can be down, and humans can make mistakes.
OxaPay's Payment History and Payout History endpoints are useful for this kind of reconciliation because they allow filtered and paginated retrieval of payment and payout records.
Handling edge cases
A good payout product is built around edge cases.
Duplicate payment webhook
Process the payment once.
Use provider track ID and internal payment status checks.
Payment is paying but not paid
Do not create final partner balances yet.
Wait for the paid state required by your business logic.
Invoice expired but customer claims payment
Use Payment Information or Payment History to investigate.
Give support a payment timeline.
Split percentages do not add to 100%
Reject the rule or assign the remainder to a defined platform rounding account.
Never let the system silently lose money.
Partner address changed before payout
Require re-approval.
Consider a cooldown window.
Payout rejected
Move the payout item to needs_review.
Do not automatically retry forever.
Currency mismatch
Define whether each partner is paid in the same currency as the incoming payment, a preferred payout currency, or a merchant-selected settlement currency.
Document the rule clearly.
Insufficient balance
Keep payout item queued or failed with a clear reason.
Notify the merchant admin.
Rounding dust
Use a consistent policy.
Usually the safest option is to allocate rounding remainder to the platform account or a specific rounding ledger account.
Manual adjustment
Never edit the original ledger entry.
Create a new adjustment entry.
MVP version
The MVP should be intentionally narrow.
Do not start with every marketplace feature.
Build this first:
- one merchant account
- one incoming payment flow
- one currency
- one network
- fixed percentage split rules
- partner records
- verified payout addresses
- immutable ledger entries
- manual payout batch creation
- manual payout approval
- OxaPay payout execution
- payout webhook handling
- partner balance dashboard
- CSV export
- audit logs for payout actions
This is enough to sell to a small marketplace, course platform, agency, or creator platform.
The MVP should prove that merchants trust the numbers.
Not that your UI has every feature.
Production version
A production version can add:
- multi-merchant support
- multi-currency support
- multi-network payout methods
- advanced split rules
- rule versioning
- reserves and release schedules
- partner self-service portal
- payout batch scheduling
- two-person approval
- balance reconciliation
- accounting exports
- webhook replay tools
- API for merchant platforms
- white-label dashboard for agencies
- notification system
- dispute notes
- operational alerts
- role-based permissions
- address change cooldown
- payout risk scoring
This is where the service becomes more than a dev project.
It becomes financial operations software.
Revenue model for developers
There are several ways to monetize this product.
1. Setup fee
Charge for implementation, configuration, and merchant onboarding.
This works well for agencies, course platforms, and marketplaces that need custom integration.
2. Monthly platform fee
Charge a recurring fee for hosting, dashboard access, webhook monitoring, payout queue management, and reporting.
This is better than one-off integration work because the merchant depends on the system every month.
3. Managed payout operations
Some merchants may want you to operate the payout workflow for them.
That can include reviewing payout batches, generating reports, monitoring failed payouts, and coordinating support.
Be careful with responsibility boundaries here.
You should define exactly what you do and what the merchant approves.
4. Usage-based fee
Charge based on:
- number of partners
- number of payout items
- number of payout batches
- number of monthly paid orders
- number of supported merchant accounts
5. White-label license
Sell the system to agencies or platforms that want to offer crypto revenue split tooling under their own brand.
This is harder to build, but potentially stronger if you can reach agencies with existing merchant relationships.
Pricing examples
These are not guaranteed income numbers.
They are pricing structures a developer could test.
| Package | What it includes | Possible pricing model |
|---|---|---|
| Starter setup | One merchant, one split rule, manual payout queue | one-time setup fee |
| Growth setup | Multiple partners, dashboard, CSV export, payout tracking | higher setup fee + monthly support |
| Managed ops | Monitoring, reconciliation, payout issue review | monthly retainer |
| SaaS dashboard | Hosted partner balances and payout queue | monthly subscription |
| Agency license | White-label version for agency clients | license + support fee |
| Enterprise custom | Custom rules, approval workflows, reporting, integrations | project fee + recurring maintenance |
If you are starting alone, the easiest first offer is:
I build a crypto revenue split and payout dashboard for your marketplace, course platform, affiliate program, or creator business so you can calculate partner balances and send payouts without spreadsheets.
That is more concrete than:
I integrate crypto payouts.
What to sell first
Do not sell the full platform first.
Sell a narrow outcome.
For example:
For a course platform
Automatically split every crypto course sale between the platform, instructor, and affiliate, then generate weekly payout batches with partner balance reports.
For an agency
Track revenue from client payments, calculate contractor shares, and create monthly crypto payout batches with approval logs.
For a creator marketplace
Let creators see their earned balance and receive approved crypto payouts after customer payments are confirmed.
For an affiliate program
Calculate affiliate commission from paid crypto invoices and create payout queues after a minimum threshold is reached.
The more specific the outcome, the easier it is to sell.
Technical roadmap: 21-day build plan
Days 1-3: Define the niche
Pick one use case:
- course platform
- affiliate program
- small marketplace
- agency contractor payouts
- creator platform
Define one split rule.
Do not build every split model yet.
Days 4-6: Build payment intake
- create merchant/order model
- create OxaPay invoice
- store invoice response
- add payment webhook endpoint
- validate HMAC
- store raw webhook events
- process paid events idempotently
Days 7-9: Build ledger and split engine
- create ledger schema
- create one split rule type
- apply split after paid payment
- calculate partner balances from ledger entries
- add tests for duplicate webhooks
Days 10-12: Build partner and payout method management
- add partner records
- add payout address records
- add admin verification flow
- add audit logs for address changes
- show partner balances
Days 13-15: Build payout queue
- create payout batches
- add minimum payout threshold
- add manual approval
- create payout items
- show admin review screen
Days 16-18: Execute payouts
- call OxaPay Generate Payout
- store provider track ID
- add payout webhook endpoint
- validate payout HMAC
- update payout status
- create payout debit ledger entries idempotently
Days 19-21: Make it sellable
- add CSV export
- add partner dashboard
- add merchant dashboard
- add failed payout review screen
- add audit log view
- add a demo dataset
- create a landing page
- record a demo video
At the end of 21 days, you should not have a perfect product.
You should have a sellable prototype.
Testing checklist
Before showing this to a merchant, test these cases:
- invoice created successfully
- payment webhook validates HMAC
- invalid webhook signature is rejected
- duplicate payment webhook does not duplicate ledger entries
- payment status before paid does not create partner balances
- paid payment creates correct ledger entries
- split rule percentages are validated
- rounding is handled consistently
- partner balance is calculated from ledger entries
- payout address must be verified before use
- payout batch requires approval
- payout API key is never exposed to frontend
- payout webhook validates with payout API key
- duplicate payout webhook does not duplicate debit entries
- rejected payout moves to review state
- canceled payout moves to review state
- payout history reconciliation detects missing records
- admin actions create audit logs
- partner cannot access another partner's balance
- support can trace payment → split → payout
This checklist is part of the product.
A merchant buying payout infrastructure wants reliability, not just features.
Mistakes to avoid
Mistake 1: Sending payout directly from the payment webhook
This is the biggest mistake.
Payment confirmation and payout execution should be separated by a ledger and payout queue.
Mistake 2: No audit trail
If you cannot explain why a partner balance changed, the merchant will not trust the system.
Mistake 3: Editable balances
Balances should be derived from ledger entries.
Manual correction should be an adjustment entry, not a direct balance edit.
Mistake 4: No approval workflow
Automated payouts sound nice, but most merchants need review controls first.
Mistake 5: Weak authorization
A partner viewing another partner's balance is a serious security issue.
Mistake 6: Ignoring payout address changes
Changing a payout address should be treated as a sensitive event.
Mistake 7: No reconciliation
Webhooks are not a full accounting system.
Use provider history endpoints to detect mismatches.
Mistake 8: Overbuilding before selling
Start with one niche and one split model.
Do not build a general payout platform before validating demand.
Legal, tax, and compliance boundaries
This article is a technical and product-building guide, not legal, tax, or financial advice.
Revenue split and payout systems can touch sensitive obligations depending on jurisdiction, business type, custody model, merchant-of-record structure, tax reporting, sanctions rules, partner onboarding, and the assets involved.
A developer should not casually promise that the software solves compliance.
A safer positioning is:
This product helps merchants calculate, queue, approve, execute, and track crypto payouts using their own payment infrastructure and business rules.
Do not position yourself as the legal owner of funds unless you are prepared for that responsibility.
For many developers, the safer business model is implementation, software, dashboard, automation, and operational tooling for the merchant, where the merchant owns the commercial relationship and payout policy.
Landing page positioning
A weak landing page headline:
Crypto payout API integration.
A stronger headline:
Split crypto revenue between sellers, creators, affiliates, or contractors with a ledger, payout queue, approval flow, and payout tracking.
A simple landing page should include:
- target niche
- revenue split problem
- payment-to-payout flow diagram
- dashboard screenshots
- example split rules
- payout approval workflow
- audit log example
- security controls
- OxaPay infrastructure used
- pricing packages
- demo video
- implementation timeline
The visitor should think:
This developer understands payout operations.
Not:
This developer can call an API.
Final takeaway
A crypto revenue split and payout system is one of the stronger developer business ideas because the merchant problem is painful, recurring, and operational.
The opportunity is not simply sending crypto payouts.
The opportunity is building the layer that turns customer payments into trusted partner balances and controlled payout workflows.
OxaPay provides the useful infrastructure primitives: invoice creation, callback URLs, webhooks, payment history, payout creation, payout information, payout history, payout status tracking, SDKs, and automation modules.
But your product must provide the business logic:
- split rules
- ledger entries
- payout queues
- approval workflows
- partner dashboards
- audit logs
- security controls
- reconciliation
That is what merchants pay for.
Start with one niche.
Pick one split rule.
Build the ledger first.
Queue payouts before sending them.
Validate every webhook.
Keep audit logs.
Make partner balances explainable.
Then turn the pattern into a productized service.
That is how a simple payout integration becomes a real developer business.
References
- OxaPay Generate Invoice
- OxaPay Webhook
- OxaPay Payment Information
- OxaPay Payment History
- OxaPay Payout Service
- OxaPay Generate Payout
- OxaPay Payout Information
- OxaPay Payout History
- OxaPay Payout Status Table
- OxaPay PHP SDK
- OxaPay Python SDK
- OxaPay Laravel SDK
- OxaPay Make Integration
- Stripe Connect: Separate Charges and Transfers
- Stripe API: Idempotent Requests
- OWASP API1:2023 — Broken Object Level Authorization
Top comments (0)