Starship Rewards API

Cancel Payout API

Cancel a pending payout and receive a refund to your wallet

Cancel Payout API

Cancel a payout that has not yet been completed. Cancelled payouts are refunded to your wallet.

Endpoint

POST /api/v1/payouts/:id/cancel

Authentication: Bearer token required

Request Headers

Authorization: Bearer <access_token>

Path Parameters

ParameterTypeDescription
idstringPayout ID to cancel (e.g., pyt_xyz789abc)

Cancellation Rules

A payout can only be cancelled when it's in certain states:

StatusCan Cancel?Notes
createdYesImmediately cancellable
queuedYesCan cancel before processing starts
processingMaybeDepends on provider - may already be submitted
settlingNoFunds already with beneficiary
action_requiredYesCan cancel while waiting for action
completedNoAlready delivered
failedNoAlready failed (refund automatic)
cancelledNoAlready cancelled
expiredNoAlready expired

Response

Success Response

Status Code: 200 OK

Returns the updated payout with cancelled status:

{
  "id": "pyt_xyz789abc",
  "reference_id": "PAY_2024_001",
  "status": "cancelled",
  "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"
  },
  "description": "Bonus Payment",
  "created_at": "2024-01-15T10:30:00Z",
  "cancelled_at": "2024-01-15T10:32:00Z",
  "events": [
    {
      "event_type": "status_change",
      "from_status": null,
      "to_status": "created",
      "actor_type": "system",
      "message": "Payout created",
      "created_at": "2024-01-15T10:30:00Z"
    },
    {
      "event_type": "status_change",
      "from_status": "created",
      "to_status": "cancelled",
      "actor_type": "client",
      "message": "Payout cancelled by client",
      "created_at": "2024-01-15T10:32:00Z"
    }
  ]
}

Error Responses

400 Bad Request - Cannot Cancel

{
  "error": "BadRequest",
  "message": "payout cannot be cancelled in current status"
}

400 Bad Request - Already Completed

{
  "error": "BadRequest",
  "message": "payout has already been completed"
}

400 Bad Request - Missing ID

{
  "error": "BadRequest",
  "message": "Payout ID is required"
}

404 Not Found

{
  "error": "NotFound",
  "message": "Payout not found"
}

401 Unauthorized

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

Examples

Basic Cancellation

curl -X POST "{{host}}/api/v1/payouts/pyt_xyz789abc/cancel" \
  -H "Authorization: Bearer your_access_token"
<?php
function cancelPayout($accessToken, $payoutId) {
    $url = "{{host}}/api/v1/payouts/$payoutId/cancel";

    $options = [
        'http' => [
            'header' => "Authorization: Bearer $accessToken",
            'method' => 'POST'
        ]
    ];

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

    if ($result === false) {
        // Check response headers for error details
        throw new Exception('Failed to cancel payout');
    }

    return json_decode($result, true);
}

// Usage
try {
    $payout = cancelPayout('your_access_token', 'pyt_xyz789abc');

    if ($payout['status'] === 'cancelled') {
        echo "Payout cancelled successfully!\n";
        echo "Refund amount: " . $payout['total_amount'] . " " . $payout['currency'] . "\n";
    }
} catch (Exception $e) {
    echo "Error: " . $e->getMessage() . "\n";
}
?>

Safe Cancellation with Status Check

async function safeCancelPayout(payoutId) {
    // First, check current status
    const checkResponse = await fetch(`/api/v1/payouts/${payoutId}`, {
        headers: {
            'Authorization': `Bearer ${getAccessToken()}`
        }
    });

    const payout = await checkResponse.json();

    // Check if cancellation is possible
    const cancellableStatuses = ['created', 'queued', 'action_required'];
    const maybeCancellable = ['processing'];

    if (!cancellableStatuses.includes(payout.status) &&
        !maybeCancellable.includes(payout.status)) {
        throw new Error(`Payout cannot be cancelled - status is ${payout.status}`);
    }

    if (maybeCancellable.includes(payout.status)) {
        console.warn('Payout is processing - cancellation may not succeed');
    }

    // Attempt cancellation
    const cancelResponse = await fetch(`/api/v1/payouts/${payoutId}/cancel`, {
        method: 'POST',
        headers: {
            'Authorization': `Bearer ${getAccessToken()}`
        }
    });

    if (!cancelResponse.ok) {
        const error = await cancelResponse.json();
        throw new Error(error.message || 'Failed to cancel payout');
    }

    return await cancelResponse.json();
}

// Usage
try {
    const cancelledPayout = await safeCancelPayout('pyt_xyz789abc');
    console.log('Payout cancelled:', cancelledPayout.id);
    console.log('Refund amount:', cancelledPayout.total_amount, cancelledPayout.currency);
} catch (error) {
    console.error('Cancellation failed:', error.message);
}

Batch Cancellation

async function cancelMultiplePayouts(payoutIds) {
    const results = {
        cancelled: [],
        failed: []
    };

    for (const payoutId of payoutIds) {
        try {
            const payout = await safeCancelPayout(payoutId);
            results.cancelled.push({
                id: payoutId,
                refund: payout.total_amount
            });
        } catch (error) {
            results.failed.push({
                id: payoutId,
                reason: error.message
            });
        }
    }

    return results;
}

// Usage
const results = await cancelMultiplePayouts([
    'pyt_abc123',
    'pyt_def456',
    'pyt_ghi789'
]);

console.log(`Cancelled: ${results.cancelled.length}`);
console.log(`Failed: ${results.failed.length}`);

results.failed.forEach(f => {
    console.log(`  ${f.id}: ${f.reason}`);
});

User Confirmation Flow

async function cancelPayoutWithConfirmation(payoutId) {
    // Fetch payout details
    const payout = await getPayout(payoutId);

    // Show confirmation dialog
    const confirmed = await showConfirmDialog({
        title: 'Cancel Payout?',
        message: `Are you sure you want to cancel this payout?`,
        details: [
            `Amount: ${payout.amount} ${payout.currency}`,
            `Beneficiary: ${payout.beneficiary.first_name} ${payout.beneficiary.last_name}`,
            `Reference: ${payout.reference_id}`
        ],
        confirmText: 'Yes, Cancel Payout',
        cancelText: 'No, Keep It'
    });

    if (!confirmed) {
        return null;
    }

    // Proceed with cancellation
    const cancelledPayout = await cancelPayout(payoutId);

    // Show success message
    showNotification({
        type: 'success',
        message: `Payout cancelled. ${cancelledPayout.total_amount} ${cancelledPayout.currency} refunded to wallet.`
    });

    return cancelledPayout;
}

Refund Process

When a payout is cancelled:

  1. Status Update - Payout status changes to cancelled
  2. Wallet Credit - The total amount (payout amount + fees) is refunded to the source wallet
  3. Event Logged - A cancellation event is added to the payout history
  4. Timestamp Set - The cancelled_at field is populated

Refund Timeline

  • Immediate: Cancellation of created or queued payouts
  • May take time: Cancellation of processing payouts (depends on provider)

Verify Refund

After cancellation, verify the refund in your wallet:

async function verifyCancellationRefund(payoutId) {
    // Get cancelled payout
    const payout = await getPayout(payoutId);

    if (payout.status !== 'cancelled') {
        throw new Error('Payout was not cancelled');
    }

    // Get wallet transactions
    const transactions = await getWalletTransactions({
        wallet_id: payout.wallet_id,
        type: 'refund',
        reference: payoutId
    });

    const refund = transactions.find(t =>
        t.reference_id === payoutId &&
        t.type === 'PAYOUT_REFUND'
    );

    if (refund) {
        console.log('Refund confirmed:', refund);
        return refund;
    } else {
        console.log('Refund pending or not found');
        return null;
    }
}

Error Handling

Handle Common Errors

async function handleCancellation(payoutId) {
    try {
        const payout = await cancelPayout(payoutId);
        return {
            success: true,
            payout
        };
    } catch (error) {
        const errorMessage = error.message.toLowerCase();

        if (errorMessage.includes('not found')) {
            return {
                success: false,
                error: 'PAYOUT_NOT_FOUND',
                message: 'The payout does not exist or you do not have access to it'
            };
        }

        if (errorMessage.includes('cannot be cancelled') ||
            errorMessage.includes('already been completed')) {
            return {
                success: false,
                error: 'CANNOT_CANCEL',
                message: 'This payout cannot be cancelled in its current state'
            };
        }

        if (errorMessage.includes('unauthorized')) {
            return {
                success: false,
                error: 'UNAUTHORIZED',
                message: 'You are not authorized to cancel this payout'
            };
        }

        return {
            success: false,
            error: 'UNKNOWN_ERROR',
            message: error.message
        };
    }
}

Best Practices

  1. Check status first - Verify the payout is in a cancellable state before attempting
  2. Act quickly - Cancel as soon as possible; once processing starts, cancellation may fail
  3. Confirm with users - Always show a confirmation dialog for user-initiated cancellations
  4. Verify refunds - Check wallet balance or transaction history to confirm refund
  5. Log cancellations - Keep records of all cancellation attempts and outcomes

Use Cases

Duplicate Detection

async function handleDuplicatePayout(newReferenceId, duplicatePayoutId) {
    console.log(`Duplicate detected: ${duplicatePayoutId}`);

    // Check if the duplicate can be cancelled
    const duplicate = await getPayout(duplicatePayoutId);

    if (['created', 'queued'].includes(duplicate.status)) {
        // Cancel the duplicate
        await cancelPayout(duplicatePayoutId);
        console.log('Duplicate cancelled');
    } else {
        console.log('Duplicate already processing - manual review needed');
    }
}

Fraud Prevention

async function emergencyCancel(clientId) {
    // Get all pending payouts for a client
    const payouts = await listPayouts({
        status: 'created',
        client_id: clientId
    });

    // Cancel all pending payouts
    const results = await cancelMultiplePayouts(
        payouts.map(p => p.id)
    );

    console.log(`Emergency cancel complete:
        Cancelled: ${results.cancelled.length}
        Failed: ${results.failed.length}`);

    return results;
}