ForhoppPay Connect — Payouts
Feature Reference: FR-26, FR-27 | Service:
MerchantPayoutService,ConnectFeeCalculationService
1. Overview
ForhoppPay Connect automatically transfers merchant earnings according to their configured payout schedule. Payouts are calculated as captured charges minus platform fees, refunds, and disputed amounts. A minimum threshold of $25 must be met before a payout is initiated.
2. Payout ID Format
po_[a-zA-Z0-9]{32}
Example: po_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
3. Payout Schedules
| Schedule | Cron Expression | Timing |
|---|---|---|
| Daily | 0 0 0 * * * |
Every day at 00:00 UTC |
| Weekly | 0 0 0 * * MON |
Every Monday at 00:00 UTC |
| Monthly | 0 0 0 1 * * |
1st of each month at 00:00 UTC |
Merchants configure their schedule in merchant_profiles.payout_schedule.
4. Payout Calculation
4.1 Formula
Payout Amount = Captured Charges − Platform Fees − Refunds − Disputed Amounts
All values are calculated since the last paid payout. If no previous payout exists, all historical data is included.
4.2 Detailed Breakdown
public record PayoutCalculation(
Long grossAmount, // Sum of captured charge amounts
Long fees, // Sum of platform fees
Long refunds, // Sum of refund amounts
Long disputedAmount, // Sum of active dispute amounts (held)
Long netAmount, // grossAmount - fees - refunds - disputedAmount
LocalDateTime sinceDate // Date of last payout
) {}
4.3 Example
Since last payout:
Captured charges: $500.00 (50000 cents)
Platform fees: -$14.80 (1480 cents) [2.9% + $0.30 × 4 charges]
Refunds: -$25.00 (2500 cents)
Active disputes: -$50.00 (5000 cents) [held, not deducted permanently]
─────────────────────────────────────
Net payout: $410.20 (41020 cents)
5. Minimum Threshold
MINIMUM_PAYOUT_THRESHOLD = $25.00 (2500 cents)
If the calculated net amount is below $25, the payout is deferred to the next cycle. The balance accumulates until the threshold is met.
6. Fee Structure
6.1 Transaction Fee
fee = round(amount_cents × 0.029 + 30)
| Charge Amount | Percentage (2.9%) | Fixed ($0.30) | Total Fee | Net |
|---|---|---|---|---|
| $10.00 | $0.29 | $0.30 | $0.59 | $9.41 |
| $25.00 | $0.73 | $0.30 | $1.03 | $23.98 |
| $50.00 | $1.45 | $0.30 | $1.75 | $48.25 |
| $100.00 | $2.90 | $0.30 | $3.20 | $96.80 |
| $500.00 | $14.50 | $0.30 | $14.80 | $485.20 |
| $1,000.00 | $29.00 | $0.30 | $29.30 | $970.70 |
6.2 Currency Conversion Fee
1% above mid-market exchange rate
Applied when charge currency differs from merchant's payout currency.
6.3 Dispute Fee
$15.00 per dispute (refunded if merchant wins)
7. Payout Processing Flow
Scheduled Trigger (daily/weekly/monthly)
│
▼
MerchantPayoutService.processPayoutsForSchedule(schedule)
│
├─ Query merchant_profiles WHERE payout_schedule = {schedule}
│ AND connect_enabled = true
│
For each merchant:
│
├─ calculatePendingPayout(merchantAccountId)
│ ├─ Get last payout date
│ ├─ Sum captured charges since last payout
│ ├─ Sum fees since last payout
│ ├─ Sum refunds since last payout
│ ├─ Sum active disputed amounts
│ └─ Calculate net = captured - fees - refunds - disputed
│
├─ Check net >= $25 minimum threshold
│ └─ If below: skip, log, continue to next merchant
│
├─ Create MerchantPayout entity
│ ├─ Generate unique payout ID (po_xxx)
│ ├─ Set status = PENDING
│ ├─ Set arrival_date = +3 business days
│ └─ Persist
│
└─ Dispatch payout.created webhook
8. Payout Statuses
| Status | Description | Terminal |
|---|---|---|
PENDING |
Payout scheduled, awaiting processing | No |
IN_TRANSIT |
Funds being transferred | No |
PAID |
Payout completed successfully | Yes |
FAILED |
Payout failed (see failure_reason) | Yes |
CANCELED |
Payout cancelled | Yes |
9. Arrival Date Calculation
Estimated arrival: 3 business days from payout creation.
// Skip weekends
LocalDate arrivalDate = today;
int daysToAdd = 3;
while (daysToAdd > 0) {
arrivalDate = arrivalDate.plusDays(1);
if (arrivalDate.getDayOfWeek() != SATURDAY &&
arrivalDate.getDayOfWeek() != SUNDAY) {
daysToAdd--;
}
}
10. Dispute Impact on Payouts
Per FR-29.4, disputed amounts are held from payouts:
- When a dispute is opened, the disputed amount is excluded from payout calculations
disputeRepository.sumActiveDisputedAmount(merchantAccountId)returns the total held amount- If the merchant wins the dispute, the amount becomes available for the next payout
- If the merchant loses, the amount is permanently deducted
11. Manual Payouts
Merchants can trigger a manual payout via the dashboard:
MerchantPayoutService.triggerManualPayout(merchantAccountId)
Same calculation and threshold rules apply. Returns Optional.empty() if below threshold.
12. Balance API
Merchants can check their available balance at any time:
GET /api/v1/connect/balance
Authorization: Bearer sk_live_xxx
Response:
{
"object": "balance",
"available": { "amount_cents": 41020, "currency": "USD" },
"pending": { "amount_cents": 50000, "currency": "USD" },
"fees_cents": 1480,
"refunds_cents": 2500,
"disputed_cents": 5000,
"minimum_payout_cents": 2500,
"payout_eligible": true,
"last_payout_at": 1750000000,
"livemode": true
}
| Field | Description |
|---|---|
available.amount_cents |
Net amount available for payout |
pending.amount_cents |
Gross captured since last payout |
fees_cents |
Platform fees deducted |
refunds_cents |
Refunds deducted |
disputed_cents |
Amounts held due to active disputes |
payout_eligible |
Whether net >= $25 minimum |
last_payout_at |
Unix timestamp of last paid payout (null if none) |
12. Database Schema
CREATE TABLE forhopppay.merchant_payouts (
id BIGSERIAL PRIMARY KEY,
payout_id VARCHAR(50) NOT NULL UNIQUE,
merchant_account_id BIGINT NOT NULL REFERENCES forhopppay.forhopppay_accounts(id),
amount_cents BIGINT NOT NULL,
fee_cents BIGINT DEFAULT 0,
net_amount_cents BIGINT NOT NULL,
currency VARCHAR(3) NOT NULL DEFAULT 'USD',
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
payout_method VARCHAR(30) NOT NULL,
payout_details JSONB,
external_reference VARCHAR(255),
arrival_date DATE,
failure_reason VARCHAR(500),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP
);
13. Correctness Properties
| # | Property | Validates |
|---|---|---|
| 33 | Daily at 00:00 UTC, weekly Monday 00:00, monthly 1st 00:00 | FR-26.1, FR-26.2, FR-26.3 |
| 34 | No payout if balance < $25 | FR-26.4 |
| 35 | Amount = captured - fees - refunds since last payout | FR-26.5, FR-26.6 |
| 36 | Fee = (amount × 0.029) + 30 cents | FR-27.1, FR-27.3 |
| 38 | Disputed amount held until resolution | FR-29.4 |
Last Updated: June 2026
