Assets Payouts: Paying Users, Creators, Vendors, and Communities
The Assets Payouts subsystem provides a unified way for Qbix apps to transfer real-world money out of the platform β to creators, vendors, communities, event organizers, and any user with a connected payout account.
Payouts integrate with Stripe Connect, using either Express or Standard connected accounts. Qbix handles:
- Account onboarding (Stripe-hosted OAuth or registration link)
- Storing connected accounts in assets_recipient
- Logging payouts in assets_payout
- Handling asynchronous success/failure via Stripe webhooks
- Supporting multiple payout flows (instant, scheduled, batched)
Developers get a fully event-driven payout engine built on Qbix Streams and the Assets plugin.
Recipient Table: Linked Stripe Accounts
Each user that can receive funds has a record in:
CREATE TABLE assets_recipient (
userId VARBINARY(31) NOT NULL,
payments ENUM('stripe') NOT NULL DEFAULT 'stripe',
recipientId VARBINARY(255) NOT NULL, -- Stripe Connect acct_xxxx
hash VARCHAR(32) NOT NULL,
insertedTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updatedTime TIMESTAMP NULL,
PRIMARY KEY (userId, payments, hash)
);
Here:
- recipientId is the Stripe Connect Account ID, e.g. acct_1NriQKQD...
- hash is the key environment hash (same as Assets Customers)
Onboarding Recipients
To generate a hosted Stripe onboarding link:
$recipient = Assets_Recipient::fetchOrCreate($user->id, 'stripe');
$link = \Stripe\AccountLink::create([
'account' => $recipient->recipientId,
'refresh_url' => Q_Uri::url('/reconnect'),
'return_url' => Q_Uri::url('/connected'),
'type' => 'account_onboarding'
]);
Developers typically call:
Q.req('Assets/payout', 'onboardingLink', {}, function (err, data) {
window.location = data.slots.url;
});
Once completed, Stripe begins returning charges_enabled = true,
which is required before payouts can be sent.
Payout Flow: From Credits β Real Money
Apps typically pay users in two steps:
- User earns credits internally
- User requests real-world payout β system converts credits β USD and sends payout
To initiate a payout:
Assets_Payouts::send($user, [ 'amountCredits' => 500, 'description' => 'Creator revenue share β March' ]);
Behind the scenes:
1. Convert credits β USD using Assets_Credits::convert() 2. Validate user has enough credits 3. Deduct credits immediately (atomic) 4. Create Stripe Transfer or Payout 5. Insert record into assets_payout 6. Webhook updates status asynchronously
Payout Log Table
All payouts are stored in:
CREATE TABLE assets_payout (
id VARBINARY(31) NOT NULL,
userId VARBINARY(31) NOT NULL, -- payout recipient
recipientId VARBINARY(255) NOT NULL, -- Connect acct_xxxx
amount DECIMAL(10,4) NOT NULL, -- USD amount
method VARCHAR(31) NOT NULL, -- 'stripe'
payoutId VARBINARY(255) DEFAULT NULL, -- Stripe payout ID
transferId VARBINARY(255) DEFAULT NULL, -- Stripe transfer ID
attributes VARCHAR(1023),
status ENUM('pending','processing','paid','failed') DEFAULT 'pending',
insertedTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updatedTime TIMESTAMP NULL,
PRIMARY KEY (id)
);
Stripe Transfer β Stripe Payout
Stripe Connect sends funds in two phases:
- Transfer (platform β recipient Stripe account)
- Payout (recipient Stripe β bank account)
Qbix triggers the first phase:
$transfer = \Stripe\Transfer::create([
'amount' => $amountInCents,
'currency' => 'usd',
'destination' => $recipientId, // acct_xxxx
'description' => $description
]);
The second phase happens automatically based on the recipientβs Stripe settings:
- Automatic daily payouts
- Manual on-demand payouts
- Weekly or monthly schedules
Direct Bank Payouts (Instant or Scheduled)
If the connected account has instant payout enabled, apps can offer:
$payout = \Stripe\Payout::create([
'amount' => $cents,
'currency' => 'usd',
'method' => 'instant',
'destination' => $bankAccountId
], [
'stripe_account' => $recipientId
]);
Otherwise Stripe falls back to standard ACH payouts.
Webhook Handling: Update Payout Status
Stripe notifies with:
payout.paid payout.failed payout.canceled transfer.created transfer.failed
The webhook handler locates the payout record:
$p = Assets_Payout::fetchByTransferId($transfer->id); $p->status = 'processing'; $p->save();
Later:
case 'payout.paid':
$p = Assets_Payout::fetchByPayoutId($event->data->object->id);
$p->status = 'paid';
$p->save();
break;
Failures trigger refunding credits:
$p->status = 'failed'; $p->save(); // Restore credits to user Assets_Credits::add($p->userId, $p->amountCredits, 'Payout failed refund');
Creator Revenue / Split Payments
Qbix supports multi-recipient payout splitting using multiple Transfers:
// 70% to creator
Assets_Payouts::send($creator, [
'amountCredits' => $credits * 0.7
]);
// 30% affiliate or community
Assets_Payouts::send($referrer, [
'amountCredits' => $credits * 0.3
]);
Transfers occur independently and webhook results update each payout record.
Streams: Payout Notifications
Each payout posts a message into the userβs credits stream:
$creditsStream->post($communityId, [
'type' => 'Assets/payout',
'content' => Q::interpolate("Paid out {{amount}} USD", [
'amount' => $amount
]),
'instructions' => [ 'status' => 'pending', 'payoutId' => $payoutId ]
]);
Once the webhook finalizes the payout, a second message updates the result.
Security Model
Stripe handles:
- KYC verification
- Bank account validation
- Regulatory checks
- Fraud screening
Qbix handles:
- Identity of the receiving user
- Authorization to request payout
- Ensuring credits are deducted atomically
- Blocking payout attempts until Stripe marks account as verified
Architecture Summary
The payout pipeline works as follows:
User requests payout
β
Check credits β deduct immediately
β
Create Stripe Transfer
β
Save record in assets_payout
β
Stripe webhook (transfer/payout events)
β
Update status to paid or failed
β
If failed β refund credits
The result is a safe, auditable, developer-friendly payout layer compatible with:
- Marketplaces
- Creator platforms
- Event organizers
- Communities splitting revenue
- Any app where users withdraw their balance