HeldSway

Payouts REST API — Documentation

Date: 2026-05-07 Status: Implemented Module: AffiliateSupport Scope: REST API endpoints for payout management, payout requests, and payout settings


1. Objective

Expose the full functionality of the Payouts management system as external REST API endpoints. These endpoints let third-party systems:

  • List payouts with filtering, searching, and pagination
  • Retrieve a single payout with full details
  • View aggregated payout statistics
  • Transition payout status (pending → processing → completed/failed)
  • Bulk process multiple payouts at once
  • Manage affiliate payout requests (approve/reject)
  • Export payouts as CSV
  • Download PDF payout statements
  • Read and update payout settings (frequency, minimum, early requests)

2. Authentication & Scopes

All endpoints require a Bearer token issued via the Access Token system.

2.1 Scopes

Scope Access
payouts:read List, show, stats, requests, export, statement
payouts:write Status transitions, bulk actions, approve/reject requests
settings:read Read payout settings
settings:write Update payout settings

2.2 Authentication Header

Authorization: Bearer <access_token>

2.3 Error Responses

HTTP 401 — Unauthorized (missing or invalid token)

{
  "success": false,
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Invalid or missing access token."
  }
}

HTTP 403 — Forbidden (token lacks required scope)

{
  "success": false,
  "error": {
    "code": "FORBIDDEN",
    "message": "Insufficient scope."
  }
}

3. Endpoint Overview

3.1 Payout Management

Method Path Scope Throttle Description
GET /v1/payouts payouts:read 120/min List payouts (paginated, filterable)
GET /v1/payouts/stats payouts:read 120/min Business payout statistics
GET /v1/payouts/requests payouts:read 120/min List affiliate payout requests
GET /v1/payouts/export payouts:read 30/min Download CSV export
GET /v1/payouts/{id} payouts:read 120/min Single payout detail
GET /v1/payouts/{id}/statement payouts:read 30/min Download PDF statement
POST /v1/payouts/{id}/processing payouts:write 60/min Mark payout as processing
POST /v1/payouts/{id}/complete payouts:write 60/min Mark payout as completed
POST /v1/payouts/{id}/fail payouts:write 60/min Mark payout as failed
POST /v1/payouts/bulk-processing payouts:write 60/min Bulk mark as processing
POST /v1/payouts/bulk-complete payouts:write 60/min Bulk mark as completed
POST /v1/payouts/requests/{id}/approve payouts:write 60/min Approve a payout request
POST /v1/payouts/requests/{id}/reject payouts:write 60/min Reject a payout request

3.2 Payout Settings

Method Path Scope Throttle Description
GET /v1/settings/payouts settings:read 120/min Read payout configuration
PUT /v1/settings/payouts settings:write 60/min Update payout configuration

4. Payout Status Lifecycle

                    ┌──────────┐
                    │ Pending  │
                    └────┬─────┘
                         │
              ┌──────────┼──────────┐
              ▼          ▼          ▼
        ┌──────────┐  ┌─────────┐  ┌────────┐
        │Processing│  │Completed│  │ Failed │
        └────┬─────┘  └─────────┘  └────────┘
             │
        ┌────┼────┐
        ▼         ▼
  ┌─────────┐  ┌────────┐
  │Completed│  │ Failed │
  └─────────┘  └────────┘

Valid transitions:

From To Endpoint
pending processing POST /v1/payouts/{id}/processing
pending completed POST /v1/payouts/{id}/complete
pending failed POST /v1/payouts/{id}/fail
processing completed POST /v1/payouts/{id}/complete
processing failed POST /v1/payouts/{id}/fail

Invalid transitions return HTTP 409 with error code INVALID_STATUS.


5. Detailed Endpoint Specifications

5.1 List Payouts — GET /v1/payouts

Scope: payouts:read

Query Parameters

Parameter Type Required Default Constraints Description
search string No max:255 Search by affiliate name or email
status string No pending, processing, completed, failed Filter by status
affiliate_id integer No Filter by affiliate ID
from date No YYYY-MM-DD, <= to Start of date range (created_at >=)
to date No YYYY-MM-DD, >= from End of date range (created_at <=)
per_page integer No 15 min:1, max:100 Items per page
page integer No 1 min:1 Page number

Success Response — HTTP 200

{
  "success": true,
  "message": "OK",
  "data": {
    "items": [
      {
        "id": 42,
        "affiliate": {
          "id": 7,
          "name": "Jane Smith",
          "referral_code": "JANE2026"
        },
        "amount": "250.00",
        "currency": "USD",
        "method": "bank_transfer",
        "status": "pending",
        "period_start": "2026-04-01",
        "period_end": "2026-04-30",
        "reference": null,
        "paid_at": null,
        "batch_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "created_at": "2026-05-01T00:00:00.000Z"
      }
    ],
    "meta": {
      "current_page": 1,
      "per_page": 15,
      "total": 1,
      "last_page": 1
    }
  }
}

Notes:

  • affiliate is loaded from the affiliate’s user record. Contains id, name, and referral_code.
  • method is extracted from the affiliate’s payment details. May be null if not set.
  • batch_id groups payouts generated together by the automated scheduler. May be null for manual payouts.
  • All monetary values are strings with 2 decimal places.
  • Results are ordered by created_at DESC.

5.2 Get Payout Stats — GET /v1/payouts/stats

Scope: payouts:read

Success Response — HTTP 200

{
  "success": true,
  "message": "OK",
  "data": {
    "total_pending_amount": 1250.00,
    "total_pending_count": 5,
    "completed_this_month": 3400.00,
    "failed_count": 1,
    "pending_requests_count": 2
  }
}

Field descriptions:

Field Description
total_pending_amount Sum of all pending payout amounts
total_pending_count Number of pending payouts
completed_this_month Sum of completed payouts in the current calendar month
failed_count Number of failed payouts
pending_requests_count Number of pending affiliate payout requests awaiting review

5.3 Get Payout Detail — GET /v1/payouts/{id}

Scope: payouts:read

Path Parameters

Parameter Type Description
id integer Payout record ID

Success Response — HTTP 200

{
  "success": true,
  "message": "OK",
  "data": {
    "id": 42,
    "affiliate": {
      "id": 7,
      "name": "Jane Smith",
      "referral_code": "JANE2026"
    },
    "amount": "250.00",
    "currency": "USD",
    "method": "bank_transfer",
    "status": "completed",
    "period_start": "2026-04-01",
    "period_end": "2026-04-30",
    "reference": "TXN-12345",
    "paid_at": "2026-05-02T14:30:00.000Z",
    "batch_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "created_at": "2026-05-01T00:00:00.000Z",
    "notes": "Paid via bank transfer",
    "updated_at": "2026-05-02T14:30:00.000Z",
    "payout_request": {
      "id": 10,
      "requested_amount": "250.00",
      "status": "approved",
      "notes": "Need early payout for expenses",
      "created_at": "2026-04-28T10:00:00.000Z"
    }
  }
}

Notes:

  • Detail view returns additional fields not in the list: notes, updated_at, and linked payout_request.
  • payout_request is present only if this payout was created from an affiliate’s early payout request. Otherwise omitted.

Error — HTTP 404

{
  "success": false,
  "error": {
    "code": "NOT_FOUND",
    "message": "Payout not found."
  }
}

5.4 List Payout Requests — GET /v1/payouts/requests

Scope: payouts:read

Query Parameters

Parameter Type Required Default Constraints Description
status string No pending, approved, rejected Filter by request status
affiliate_id integer No Filter by affiliate ID
per_page integer No 15 min:1, max:100 Items per page
page integer No 1 min:1 Page number

Success Response — HTTP 200

{
  "success": true,
  "message": "OK",
  "data": {
    "items": [
      {
        "id": 10,
        "affiliate": {
          "id": 7,
          "name": "Jane Smith",
          "referral_code": "JANE2026"
        },
        "requested_amount": "250.00",
        "notes": "Need early payout for expenses",
        "status": "pending",
        "rejection_reason": null,
        "payout_id": null,
        "reviewed_at": null,
        "created_at": "2026-04-28T10:00:00.000Z"
      }
    ],
    "meta": {
      "current_page": 1,
      "per_page": 15,
      "total": 1,
      "last_page": 1
    }
  }
}

Notes:

  • payout_id is populated after the request is approved and a payout is created.
  • rejection_reason is populated only for rejected requests.
  • reviewed_at is the timestamp when the request was approved or rejected.

5.5 Export Payouts CSV — GET /v1/payouts/export

Scope: payouts:read Throttle: 30/min (heavy operation)

Query Parameters

Parameter Type Required Description
status string No Filter by payout status
affiliate_id integer No Filter by affiliate ID
from date No Start of date range
to date No End of date range

Success Response — HTTP 200

Content-Type: text/csv Content-Disposition: attachment; filename="payouts-export-2026-05-07.csv"

Affiliate,Email,Amount,Currency,Method,Status,Period Start,Period End,Reference,Paid At,Created At
Jane Smith,jane@example.com,250.00,USD,bank_transfer,completed,2026-04-01,2026-04-30,TXN-12345,2026-05-02 14:30:00,2026-05-01 00:00:00

5.6 Download Payout Statement — GET /v1/payouts/{id}/statement

Scope: payouts:read Throttle: 30/min (PDF generation)

Path Parameters

Parameter Type Description
id integer Payout record ID

Success Response — HTTP 200

Content-Type: application/pdf Content-Disposition: attachment; filename="payout-statement-42.pdf"

Returns a PDF document containing:

  • Payout details (amount, currency, period, reference)
  • Conversions included in the payout period
  • Conversion count, total order value, total commission

Error — HTTP 404

{
  "success": false,
  "error": {
    "code": "NOT_FOUND",
    "message": "Payout not found."
  }
}

5.7 Mark Payout as Processing — POST /v1/payouts/{id}/processing

Scope: payouts:write

Path Parameters

Parameter Type Description
id integer Payout record ID

Success Response — HTTP 200

{
  "success": true,
  "message": "Payout marked as processing.",
  "data": {
    "id": 42,
    "affiliate": { "id": 7, "name": "Jane Smith", "referral_code": "JANE2026" },
    "amount": "250.00",
    "currency": "USD",
    "method": "bank_transfer",
    "status": "processing",
    "period_start": "2026-04-01",
    "period_end": "2026-04-30",
    "reference": null,
    "paid_at": null,
    "batch_id": null,
    "created_at": "2026-05-01T00:00:00.000Z"
  }
}

Error — HTTP 409

{
  "success": false,
  "error": {
    "code": "INVALID_STATUS",
    "message": "Cannot mark payout #42 as processing: current status is completed."
  }
}

5.8 Mark Payout as Completed — POST /v1/payouts/{id}/complete

Scope: payouts:write

Path Parameters

Parameter Type Description
id integer Payout record ID

Body Parameters

Field Type Required Constraints Description
reference string No max:255 Payment reference (e.g., bank transfer ID)
notes string No max:1000 Optional notes

Success Response — HTTP 200

{
  "success": true,
  "message": "Payout marked as completed.",
  "data": {
    "id": 42,
    "status": "completed",
    "reference": "TXN-12345",
    "paid_at": "2026-05-07T14:30:00.000Z",
    "..."
  }
}

Side effect: Sends a PayoutCompletedNotification email to the affiliate.

Error — HTTP 409

Same as 5.7 — returned when current status is completed or failed.


5.9 Mark Payout as Failed — POST /v1/payouts/{id}/fail

Scope: payouts:write

Body Parameters

Field Type Required Constraints Description
notes string No max:1000 Failure reason

Success Response — HTTP 200

{
  "success": true,
  "message": "Payout marked as failed.",
  "data": {
    "id": 42,
    "status": "failed",
    "..."
  }
}

Side effect: Sends a PayoutFailedNotification email to the affiliate.


5.10 Bulk Mark as Processing — POST /v1/payouts/bulk-processing

Scope: payouts:write

Body Parameters

Field Type Required Constraints Description
ids integer[] Yes min:1, max:100, each distinct Payout IDs to transition

Success Response — HTTP 200

{
  "success": true,
  "message": "3 payout(s) marked as processing.",
  "data": {
    "processed_count": 3,
    "requested_count": 5,
    "skipped_count": 2
  }
}

Notes:

  • Only pending payouts are transitioned. Non-pending IDs are silently skipped.
  • skipped_count = requested_count - processed_count.

5.11 Bulk Mark as Completed — POST /v1/payouts/bulk-complete

Scope: payouts:write

Body Parameters

Field Type Required Constraints Description
ids integer[] Yes min:1, max:100, each distinct Payout IDs to complete
reference string No max:255 Payment reference (applied to all)

Success Response — HTTP 200

{
  "success": true,
  "message": "3 payout(s) marked as completed.",
  "data": {
    "completed_count": 3,
    "requested_count": 3,
    "skipped_count": 0
  }
}

Notes:

  • Only pending and processing payouts are transitioned. Others are skipped.
  • The reference is applied to all completed payouts in the batch.

5.12 Approve Payout Request — POST /v1/payouts/requests/{id}/approve

Scope: payouts:write

Path Parameters

Parameter Type Description
id integer Payout request ID

Success Response — HTTP 200

{
  "success": true,
  "message": "Payout request approved.",
  "data": {
    "payout_request": {
      "id": 10,
      "affiliate": { "id": 7, "name": "Jane Smith", "referral_code": "JANE2026" },
      "requested_amount": "250.00",
      "status": "approved",
      "payout_id": 42,
      "reviewed_at": "2026-05-07T14:30:00.000Z",
      "created_at": "2026-04-28T10:00:00.000Z"
    },
    "payout": {
      "id": 42,
      "amount": "250.00",
      "currency": "USD",
      "status": "pending",
      "created_at": "2026-05-07T14:30:00.000Z"
    }
  }
}

Side effects:

  • Creates a new pending payout linked to the request.
  • Sends a PayoutRequestApprovedNotification email to the affiliate.

Error — HTTP 409

{
  "success": false,
  "error": {
    "code": "INVALID_STATUS",
    "message": "Cannot approve request: current status is approved."
  }
}

5.13 Reject Payout Request — POST /v1/payouts/requests/{id}/reject

Scope: payouts:write

Path Parameters

Parameter Type Description
id integer Payout request ID

Body Parameters

Field Type Required Constraints Description
reason string Yes max:1000 Rejection reason (required)

Success Response — HTTP 200

{
  "success": true,
  "message": "Payout request rejected.",
  "data": {
    "id": 10,
    "affiliate": { "id": 7, "name": "Jane Smith", "referral_code": "JANE2026" },
    "requested_amount": "250.00",
    "status": "rejected",
    "rejection_reason": "Insufficient documentation provided.",
    "reviewed_at": "2026-05-07T14:30:00.000Z",
    "created_at": "2026-04-28T10:00:00.000Z"
  }
}

Side effect: Sends a PayoutRequestRejectedNotification email to the affiliate.

Validation Error — HTTP 422

{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "details": {
      "reason": ["The reason field is required."]
    }
  }
}

6. Payout Settings Endpoints

6.1 Get Payout Settings — GET /v1/settings/payouts

Scope: settings:read

Success Response — HTTP 200

{
  "success": true,
  "message": "OK",
  "data": {
    "payout_frequency": "monthly",
    "payout_day": 15,
    "min_payout": "100.00",
    "currency": "USD",
    "allow_early_requests": true,
    "auto_create_payouts": true,
    "next_payout_date": "2026-06-15"
  }
}

Field descriptions:

Field Type Description
payout_frequency string weekly or monthly
payout_day integer Day of week (1-7, ISO) for weekly; day of month (1-31) for monthly
min_payout string Minimum balance required to trigger a scheduled payout
currency string Currency code (ISO 4217)
allow_early_requests boolean Whether affiliates can request early payouts
auto_create_payouts boolean Whether the system automatically creates scheduled payouts
next_payout_date string|null Estimated next payout date. null if auto_create_payouts is disabled

6.2 Update Payout Settings — PUT /v1/settings/payouts

Scope: settings:write

Body Parameters

Field Type Required Constraints Description
payout_frequency string Yes weekly, monthly Payout schedule frequency
payout_day integer Yes 1-7 (weekly) or 1-31 (monthly) Day of payout
min_payout number Yes min:0 Minimum balance threshold
allow_early_requests boolean Yes Allow affiliate early payout requests
auto_create_payouts boolean Yes Enable automatic scheduled payouts

Note: payout_day max depends on payout_frequency:

  • Weekly: max is 7 (ISO day of week: 1=Monday, 7=Sunday)
  • Monthly: max is 31 (capped to actual days in month during generation)

Success Response — HTTP 200

{
  "success": true,
  "message": "Payout settings updated.",
  "data": {
    "payout_frequency": "weekly",
    "payout_day": 5,
    "min_payout": "25.00",
    "currency": "USD",
    "allow_early_requests": true,
    "auto_create_payouts": false,
    "next_payout_date": null
  }
}

7. Database Schema

7.1 payouts Table

Column Type Notes
id BIGINT PK Auto-increment
business_id FK → businesses Multi-tenant scope, cascade delete
affiliate_id FK → affiliates Cascade delete
amount DECIMAL(12,2) Payout amount
currency VARCHAR(3) Default USD
method VARCHAR(50), nullable Payment method from affiliate profile
status ENUM pending, processing, completed, failed
reference VARCHAR(255), nullable Payment reference (e.g., bank transfer ID)
notes TEXT, nullable Optional notes
paid_at TIMESTAMP, nullable When the payout was completed
period_start DATE, nullable Start of commission period
period_end DATE, nullable End of commission period
payout_request_id FK → payout_requests, nullable Linked early request
batch_id VARCHAR(36), nullable UUID grouping scheduled payouts
created_at, updated_at TIMESTAMPS Standard

Indexes: (business_id, affiliate_id), (business_id, paid_at), (business_id, status), BRIN (created_at).

7.2 payout_requests Table

Column Type Notes
id BIGINT PK Auto-increment
business_id FK → businesses Multi-tenant scope, cascade delete
affiliate_id FK → affiliates Cascade delete
requested_amount DECIMAL(12,2) Amount requested
notes TEXT, nullable Affiliate’s reason for early request
status ENUM pending, approved, rejected
rejection_reason TEXT, nullable Reason for rejection
payout_id FK → payouts, nullable Created payout (after approval)
reviewed_at TIMESTAMP, nullable When approved/rejected
created_at, updated_at TIMESTAMPS Standard

Indexes: (business_id, affiliate_id), (business_id, status).

7.3 business_payout_settings Table

Column Type Notes
id BIGINT PK Auto-increment
business_id FK → businesses Unique, cascade delete
payout_frequency VARCHAR(20) weekly or monthly (default: monthly)
payout_day SMALLINT Default: 1
min_payout DECIMAL(12,2) Default: 50.00
currency VARCHAR(3) Default: USD
allow_early_requests BOOLEAN Default: false
auto_create_payouts BOOLEAN Default: true
created_at, updated_at TIMESTAMPS Standard

8. Notification Side Effects

Write operations trigger email notifications to affiliates and business owners:

Endpoint Notification Recipient
POST /v1/payouts/{id}/complete PayoutCompletedNotification Affiliate
POST /v1/payouts/{id}/fail PayoutFailedNotification Affiliate
POST /v1/payouts/requests/{id}/approve PayoutRequestApprovedNotification Affiliate
POST /v1/payouts/requests/{id}/reject PayoutRequestRejectedNotification Affiliate

All notifications are queued (implement ShouldQueue) and sent via the mail channel.


9. Security Considerations

Concern Mitigation
Multi-tenant isolation All queries scoped via forBusiness() — never expose cross-business data
Scope enforcement Read operations require payouts:read, write operations require payouts:write
Rate limiting Read at 120/min, write at 60/min, export/PDF at 30/min
Idempotency Invalid status transitions return 409 (not 500) — safe to retry
Bulk action cap Maximum 100 IDs per bulk request to prevent abuse
Audit trail All status transitions logged via AuditService (Auditable trait on models)
Input validation All inputs validated via dedicated FormRequest classes

10. Data Flow

External Client
    │
    ▼
┌─────────────────────────────────────────────────┐
│  API Domain (api.token middleware)               │
│  ├── Validates Bearer token                     │
│  ├── Resolves Business from token               │
│  ├── Enforces scope (payouts:read/write)        │
│  └── Sets request->attributes->business         │
└────────────────────┬────────────────────────────┘
                     │
                     ▼
┌─────────────────────────────────────────────────┐
│  PayoutApiController (extends BaseApiController)│
│  ├── Validates input via FormRequest            │
│  ├── Delegates to PayoutService                 │
│  ├── Formats response via formatPayout()        │
│  └── Returns ApiResponse envelope               │
└────────────────────┬────────────────────────────┘
                     │
                     ▼
┌─────────────────────────────────────────────────┐
│  PayoutService (EXISTING)                       │
│  ├── businessPayouts() → paginated query        │
│  ├── businessPayoutStats() → aggregated stats   │
│  ├── markProcessing/Completed/Failed()          │
│  ├── bulkMarkProcessing/Completed()             │
│  ├── approveRequest()/rejectRequest()           │
│  └── AuditService::log() → audit trail          │
└────────────────────┬────────────────────────────┘
                     │
                     ▼
┌─────────────────────────────────────────────────┐
│  PostgreSQL — payouts, payout_requests,         │
│  business_payout_settings tables                │
│  (scoped by business_id, indexed)               │
└─────────────────────────────────────────────────┘

11. Implementation Files

Created

File Purpose
app/Core/Enums/ApiKeyScope.php Added PayoutsRead, PayoutsWrite enum cases
database/migrations/2026_05_07_000001_backfill_payout_scopes_to_api_keys.php Backfill scopes to existing API keys
modules/AffiliateSupport/Http/Controllers/Api/PayoutApiController.php Controller with 13 endpoints
modules/AffiliateSupport/Http/Requests/Api/ListPayoutsApiRequest.php List validation
modules/AffiliateSupport/Http/Requests/Api/ListPayoutRequestsApiRequest.php Requests list validation
modules/AffiliateSupport/Http/Requests/Api/MarkPayoutCompletedApiRequest.php Complete action validation
modules/AffiliateSupport/Http/Requests/Api/MarkPayoutFailedApiRequest.php Fail action validation
modules/AffiliateSupport/Http/Requests/Api/RejectPayoutRequestApiRequest.php Reject action validation
modules/AffiliateSupport/Http/Requests/Api/BulkPayoutActionApiRequest.php Bulk action validation
modules/AffiliateSupport/Http/Requests/Api/UpdatePayoutSettingsApiRequest.php Settings update validation
tests/Feature/Api/PayoutApiTest.php 29 feature tests

Modified

File Change
modules/AffiliateSupport/Routes/api.php Added payout route groups + settings routes
modules/AffiliateSupport/Http/Controllers/Api/BusinessSettingsApiController.php Added showPayoutSettings(), updatePayoutSettings()

12. Test Coverage

29 tests, 133 assertions — all passing.

Category Tests
List payouts (paginated, filtered, tenant-isolated) 5
Show payout (detail, 404 cases) 3
Stats 1
List payout requests 1
Export CSV 1
Status transitions (processing, complete, fail) 5
Bulk operations 3
Payout request approve/reject 4
Auth & scope enforcement 3
Payout settings (read, update, validation) 3