• log out

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:

  1. User earns credits internally
  2. 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