Skip to main content

Status Overview

A payout moves through several statuses from creation to final settlement. Tracking these statuses lets you confirm delivery, detect failures early, and provide accurate feedback to your users.

Status Flow

Status Descriptions

StatusDescriptionSuggested User Message
CREATEDThe payout has been received and persisted. It is awaiting entry into the processing queue.”Payout submitted”
PENDINGThe payout is queued and waiting to be dispatched to the payment provider.”Processing soon”
PROCESSINGThe disbursement has been submitted to the payment provider and is awaiting confirmation.”Transfer in progress”
COMPLETEDThe payment provider has confirmed successful delivery of funds to the beneficiary.”Payment delivered!”
ERRORAn internal system error occurred. No funds were disbursed. Contact support if this persists.”Internal error — contact support”
FAILEDThe disbursement was declined due to invalid or incorrect beneficiary account details.”Transfer failed — check beneficiary details”
REJECTEDThe payout was blocked by a compliance review and will not be processed.”Transfer rejected — compliance issue”
REFUNDEDThe disbursed funds have been returned to the pre-fund balance.”Funds refunded to balance”

Getting Payout Status

GET /api/v2/payouts/{id}
const getPayoutStatus = async (payoutId) => {
  const response = await fetch(
    `https://sandbox.killb.app/api/v2/payouts/${payoutId}`,
    {
      headers: { 'Authorization': `Bearer ${token}` }
    }
  );

  const payout = await response.json();
  return payout.status;
};

Polling for Status

Use polling as a fallback when webhooks are unavailable:
const waitForPayout = async (payoutId, timeoutMs = 300000) => {
  const terminalStatuses = ['COMPLETED', 'ERROR', 'FAILED', 'REJECTED', 'REFUNDED'];
  const start = Date.now();

  while (Date.now() - start < timeoutMs) {
    const response = await fetch(
      `https://sandbox.killb.app/api/v2/payouts/${payoutId}`,
      { headers: { 'Authorization': `Bearer ${token}` } }
    );

    const payout = await response.json();
    console.log('Status:', payout.status);

    if (terminalStatuses.includes(payout.status)) {
      return payout;
    }

    // Wait 15 seconds before next check
    await new Promise(resolve => setTimeout(resolve, 15000));
  }

  throw new Error(`Payout ${payoutId} did not settle within timeout`);
};

const result = await waitForPayout('payout-abc123');

if (result.status === 'COMPLETED') {
  console.log('Funds delivered successfully');
} else {
  console.error('Payout ended with status:', result.status);
}
Use webhooks as the primary notification method. Polling every 15–30 seconds is appropriate as a backup.

Webhooks for Status Updates

Subscribe to the PAYOUT event type to receive real-time notifications on every status change:
// Subscribe to payout events
await fetch('/api/v2/webhooks', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    url: 'https://your-app.com/webhooks/killb',
    secret: 'your-webhook-secret',
    events: ['PAYOUT'],
    active: true
  })
});
Handle incoming payout webhook events:
app.post('/webhooks/killb', (req, res) => {
  const { event, data } = req.body;

  switch (event) {
    case 'payout.pending':
      updatePayoutStatus(data.id, 'PENDING');
      break;

    case 'payout.processing':
      updatePayoutStatus(data.id, 'PROCESSING');
      break;

    case 'payout.completed':
      updatePayoutStatus(data.id, 'COMPLETED');
      notifyRecipient(data);
      break;

    case 'payout.error':
      // Internal system error — no funds disbursed
      updatePayoutStatus(data.id, 'ERROR');
      alertOpsTeam(data);
      break;

    case 'payout.failed':
      // Invalid or incorrect beneficiary details
      updatePayoutStatus(data.id, 'FAILED');
      notifyInvalidBeneficiary(data);
      break;

    case 'payout.rejected':
      // Blocked by compliance review
      updatePayoutStatus(data.id, 'REJECTED');
      escalateToCompliance(data);
      break;

    case 'payout.refunded':
      // Funds returned to pre-fund balance
      updatePayoutStatus(data.id, 'REFUNDED');
      reconcileBalance(data);
      break;
  }

  res.status(200).json({ received: true });
});

Best Practices

Always configure a webhook endpoint for PAYOUT events. This gives you instant status updates without any polling overhead and reduces unnecessary API calls.
If your webhook delivery fails, poll GET /api/v2/payouts/{id} every 15–30 seconds. Set a reasonable timeout (e.g., 5 minutes) and alert your team if a payout stays in PROCESSING longer than expected.
Each non-success terminal status has a distinct cause and the appropriate remediation differs:
  • ERROR — An internal system issue on KillB’s side. No action is needed on the beneficiary details; contact support if the issue recurs.
  • FAILED — The beneficiary’s bank declined the transfer due to invalid or incorrect account details. Review and correct the beneficiary information before retrying.
  • REJECTED — The payout was blocked by a compliance review. Do not retry automatically; escalate internally for review.
Pull all payouts for the previous day via GET /api/v2/payouts and reconcile against your internal ledger. Compare expected vs. actual terminal statuses to catch any discrepancies early.
const { payouts } = await listPayouts({ limit: 100 });
const needsAttention = payouts.filter(p =>
  ['ERROR', 'FAILED', 'REJECTED'].includes(p.status)
);

if (needsAttention.length > 0) {
  console.warn(`${needsAttention.length} payouts need attention`, needsAttention);
}

Next Steps

Webhooks Setup

Configure and secure webhook notifications

Error Handling

Handle API errors and retries gracefully