Starship Rewards API

Create Payout API

Initiate money transfers to beneficiaries via bank transfers, mobile money, and other payment methods

Create Payout API

Create a new payout to transfer money to a beneficiary. The payout amount is immediately deducted from your wallet, and the funds are sent through the appropriate payment provider.

Endpoint

POST /api/v1/payouts

Authentication: Bearer token required

Request Headers

Authorization: Bearer <access_token>
Content-Type: application/json

Request Body

Required Fields

{
  "reference_id": "PAY_2024_001",
  "wallet_id": 1234,
  "amount": 100.00,
  "currency": "USD",
  "beneficiary_email": "john.doe@example.com",
  "beneficiary_first_name": "John",
  "beneficiary_last_name": "Doe"
}

Complete Request Schema

{
  "reference_id": "PAY_2024_001",
  "wallet_id": 1234,
  "amount": 100.00,
  "currency": "USD",
  "beneficiary_id": "ben_abc123xyz",
  "beneficiary_email": "john.doe@example.com",
  "beneficiary_first_name": "John",
  "beneficiary_last_name": "Doe",
  "beneficiary_country": "US",
  "beneficiary_phone": "+1234567890",
  "beneficiary_identifiers": {
    "account_number": "123456789",
    "routing_number": "987654321"
  },
  "description": "Q1 2024 Bonus Payment",
  "metadata": {
    "employee_id": "EMP001",
    "department": "Engineering"
  },
  "notify_beneficiary": true
}

Field Descriptions

FieldTypeRequiredDescription
reference_idstringYesUnique client reference for idempotency
wallet_idnumberYesSource wallet ID for the payout
amountnumberYesPayout amount (must be > 0)
currencystringYesCurrency code (ISO 4217)
beneficiary_idstringNoID of saved beneficiary (alternative to inline info)
beneficiary_emailstringYes*Beneficiary email (*required if no beneficiary_id)
beneficiary_first_namestringYes*First name (*required if no beneficiary_id)
beneficiary_last_namestringYes*Last name (*required if no beneficiary_id)
beneficiary_countrystringNoCountry code (ISO 3166-1 alpha-2)
beneficiary_phonestringNoPhone number with country code
beneficiary_identifiersobjectNoPayment identifiers (bank account, mobile number, etc.)
descriptionstringNoInternal description for the payout
metadataobjectNoCustom key-value pairs for your reference
notify_beneficiarybooleanNoSend email notification to beneficiary (default: false)

Validation Rules

  • reference_id: Must be unique across all your payouts (idempotency key)
  • amount: Must be positive and within provider min/max limits
  • currency: Must be supported by an available payout provider
  • wallet_id: Must belong to your client and have sufficient balance (including fees)
  • beneficiary_email: Must be valid email format when provided

Response

Success Response

Status Code: 201 Created

{
  "id": "pyt_xyz789abc",
  "reference_id": "PAY_2024_001",
  "status": "created",
  "amount": 100.00,
  "currency": "USD",
  "fees": 1.50,
  "total_amount": 101.50,
  "beneficiary": {
    "email": "john.doe@example.com",
    "first_name": "John",
    "last_name": "Doe",
    "country": "US",
    "phone": "+1234567890"
  },
  "description": "Q1 2024 Bonus Payment",
  "created_at": "2024-01-15T10:30:00Z",
  "queued_at": null,
  "submitted_at": null,
  "completed_at": null,
  "failed_at": null,
  "cancelled_at": null
}

Response Fields

FieldTypeDescription
idstringUnique payout identifier (use for tracking)
reference_idstringYour client reference
statusstringCurrent payout status
amountnumberPayout amount to beneficiary
currencystringCurrency code
feesnumberFees charged for this payout
total_amountnumberTotal deducted from wallet (amount + fees)
beneficiaryobjectBeneficiary information
failure_codestringError code if failed (nullable)
failure_reasonstringError description if failed (nullable)
descriptionstringPayout description (nullable)
created_atstringCreation timestamp (ISO 8601)
queued_atstringWhen queued for processing (nullable)
submitted_atstringWhen submitted to provider (nullable)
completed_atstringWhen completed successfully (nullable)
failed_atstringWhen failed (nullable)
cancelled_atstringWhen cancelled (nullable)

Payout Statuses

StatusDescriptionWallet Impact
createdPayout created and queuedDeducted
queuedIn queue for processingDeducted
processingBeing processed by providerDeducted
settlingFunds settling to beneficiaryDeducted
action_requiredNeeds additional actionDeducted
completedSuccessfully deliveredDeducted
failedProcessing failedRefunded
cancelledCancelled by clientRefunded
expiredExpired before completionRefunded

Error Responses

400 Bad Request - Validation Error

{
  "error": "ValidationException",
  "message": "reference_id is required"
}

400 Bad Request - Invalid Amount

{
  "error": "ValidationException",
  "message": "amount must be greater than 0"
}

400 Bad Request - Insufficient Funds

{
  "error": "InternalServerError",
  "message": "insufficient wallet balance"
}

400 Bad Request - Duplicate Reference

{
  "error": "InternalServerError",
  "message": "duplicate reference_id"
}

401 Unauthorized

{
  "error": "UnauthorizedAccess",
  "message": "Authentication required"
}

500 Internal Server Error

{
  "error": "InternalServerError",
  "message": "Failed to create payout"
}

Examples

Basic Payout with Inline Beneficiary

curl -X POST "{{host}}/api/v1/payouts" \
  -H "Authorization: Bearer your_access_token" \
  -H "Content-Type: application/json" \
  -d '{
    "reference_id": "PAY_2024_001",
    "wallet_id": 1234,
    "amount": 100.00,
    "currency": "USD",
    "beneficiary_email": "john.doe@example.com",
    "beneficiary_first_name": "John",
    "beneficiary_last_name": "Doe",
    "beneficiary_country": "US",
    "beneficiary_identifiers": {
      "account_number": "123456789",
      "routing_number": "987654321"
    },
    "description": "Bonus Payment",
    "notify_beneficiary": true
  }'
<?php
function createPayout($accessToken, $data) {
    $url = '{{host}}/api/v1/payouts';

    $options = [
        'http' => [
            'header' => [
                "Content-Type: application/json",
                "Authorization: Bearer $accessToken"
            ],
            'method' => 'POST',
            'content' => json_encode($data)
        ]
    ];

    $context = stream_context_create($options);
    $result = file_get_contents($url, false, $context);

    if ($result === false) {
        throw new Exception('Failed to create payout');
    }

    return json_decode($result, true);
}

// Usage
$payout = createPayout('your_access_token', [
    'reference_id' => 'PAY_2024_001',
    'wallet_id' => 1234,
    'amount' => 100.00,
    'currency' => 'USD',
    'beneficiary_email' => 'john.doe@example.com',
    'beneficiary_first_name' => 'John',
    'beneficiary_last_name' => 'Doe',
    'beneficiary_country' => 'US',
    'beneficiary_identifiers' => [
        'account_number' => '123456789',
        'routing_number' => '987654321'
    ],
    'description' => 'Bonus Payment',
    'notify_beneficiary' => true
]);

echo "Payout ID: " . $payout['id'] . "\n";
echo "Status: " . $payout['status'] . "\n";
echo "Total Deducted: " . $payout['total_amount'] . " " . $payout['currency'] . "\n";
?>

Payout with Saved Beneficiary

curl -X POST "{{host}}/api/v1/payouts" \
  -H "Authorization: Bearer your_access_token" \
  -H "Content-Type: application/json" \
  -d '{
    "reference_id": "PAY_2024_002",
    "wallet_id": 1234,
    "amount": 50.00,
    "currency": "USD",
    "beneficiary_id": "ben_abc123xyz",
    "description": "Recurring payment"
  }'

Mobile Money Payout

curl -X POST "{{host}}/api/v1/payouts" \
  -H "Authorization: Bearer your_access_token" \
  -H "Content-Type: application/json" \
  -d '{
    "reference_id": "MM_2024_001",
    "wallet_id": 5678,
    "amount": 1000.00,
    "currency": "KES",
    "beneficiary_email": "jane@example.com",
    "beneficiary_first_name": "Jane",
    "beneficiary_last_name": "Wanjiku",
    "beneficiary_country": "KE",
    "beneficiary_phone": "+254712345678",
    "beneficiary_identifiers": {
      "mobile_number": "+254712345678",
      "provider": "mpesa"
    },
    "notify_beneficiary": true
  }'

Error Handling Example

async function createPayoutWithRetry(payoutData, maxRetries = 3) {
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
        try {
            const response = await fetch('/api/v1/payouts', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${getAccessToken()}`
                },
                body: JSON.stringify(payoutData)
            });

            const result = await response.json();

            if (response.status === 201) {
                return result;
            }

            // Don't retry client errors (4xx)
            if (response.status >= 400 && response.status < 500) {
                throw new Error(result.message || 'Validation error');
            }

            // Retry server errors (5xx)
            if (attempt < maxRetries) {
                await sleep(Math.pow(2, attempt) * 1000); // Exponential backoff
                continue;
            }

            throw new Error(result.message || 'Server error');

        } catch (error) {
            if (attempt === maxRetries) {
                throw error;
            }
        }
    }
}

// Usage
try {
    const payout = await createPayoutWithRetry({
        reference_id: `PAY_${Date.now()}`,
        wallet_id: 1234,
        amount: 100.00,
        currency: 'USD',
        beneficiary_email: 'recipient@example.com',
        beneficiary_first_name: 'Recipient',
        beneficiary_last_name: 'Name'
    });

    console.log('Payout created:', payout.id);
} catch (error) {
    console.error('Failed to create payout:', error.message);
}

Idempotency

The reference_id field serves as an idempotency key. If you submit a payout with a reference_id that already exists:

  • The request will fail with a duplicate reference error
  • The original payout is not modified

This prevents accidental duplicate payouts due to network retries or application errors.

Best Practice

Generate unique reference IDs using a combination of:

  • Timestamp or sequential counter
  • Random component
  • Business context (e.g., invoice number)
// Example reference ID generation
const referenceId = `PAY_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;

Wallet Deduction

The payout amount plus fees are deducted immediately when the payout is created:

  1. Validation - Check wallet balance covers amount + fees
  2. Deduction - Immediately deduct total amount from wallet
  3. Processing - Payout enters processing queue
  4. Outcome - If failed/cancelled, amount is refunded to wallet

Important: Ensure your wallet has sufficient balance for both the payout amount and any applicable fees before creating a payout.

Best Practices

Before Creating Payouts

  1. Check provider availability - Verify a provider supports the target currency/country
  2. Calculate total cost - Account for payout amount + fees
  3. Validate wallet balance - Ensure sufficient funds before submission
  4. Verify beneficiary details - Validate bank/mobile details are correct

During Processing

  1. Store payout IDs - Save the returned id for tracking
  2. Monitor status - Poll the Get Payout API for updates
  3. Handle failures gracefully - Check failure_code and failure_reason for details

Production Recommendations

  1. Use webhooks - Set up webhooks for real-time status updates (when available)
  2. Implement reconciliation - Regularly compare your records with API data
  3. Set up alerts - Monitor for unusual failure rates
  4. Test thoroughly - Use sandbox environment before production