Forhopp

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

Was this page helpful?

Edit this page