Skip to main content

Status Overview

A payout moves through several phases from creation to final settlement. Each phase has three statuses — Pending, Processing, and Completed — giving you granular visibility at every step of the pipeline.

Status Flow

During the Cash In phase, FAILED and ERROR are terminal statuses — no funds were collected, so no refund is issued. For all subsequent phases (KYT Out, Cash Out), FAILED and ERROR are transient: the payout transitions to the refund flow (REFUND_PENDINGREFUND_PROCESSINGREFUNDED). REJECTED also triggers the refund flow since it only occurs after Cash In.

Status Descriptions

StatusPhaseDescriptionSuggested User Message
CREATEDInitialPayout received and persisted, awaiting queue entry”Payout submitted”
CASH_IN_PENDINGCash InIncoming fund collection queued”Processing payment”
CASH_IN_PROCESSINGCash InCollecting funds from pre-fund balance”Collecting funds”
CASH_IN_COMPLETEDCash InFunds collected successfully”Funds collected”
KYT_OUT_PENDINGKYT OutOutgoing transaction queued for compliance check”Outbound check pending”
KYT_OUT_PROCESSINGKYT OutCompliance review in progress (outgoing)“Outbound check in progress”
KYT_OUT_COMPLETEDKYT OutOutgoing compliance check passed”Ready for transfer”
CASH_OUT_PENDINGCash OutDisbursement to beneficiary queued”Transfer queued”
CASH_OUT_PROCESSINGCash OutFunds submitted to payment provider”Transfer in progress”
CASH_OUT_COMPLETEDCash OutPayment provider confirmed delivery”Transfer delivered”
COMPLETEDTerminalPayout fully settled”Payment delivered!”
REVIEW_NEEDEDReviewTransaction flagged for manual compliance review”Under review — we’ll notify you”
FAILEDError StateOperation failed. Terminal if during Cash In (no funds collected). Transitions to refund flow if during KYT Out or Cash Out.”Transfer failed — check beneficiary details”
REJECTEDError StatePayout blocked by compliance review. Always transitions to refund flow since it only occurs after Cash In.”Transfer rejected — compliance issue”
ERRORError StateInternal system error. Terminal if during Cash In (no funds collected). Transitions to refund flow if during KYT Out or Cash Out.”Internal error — contact support”
REFUND_PENDINGRefundRefund to pre-fund balance queued”Refund initiated”
REFUND_PROCESSINGRefundRefund in progress”Refund in progress”
REFUNDEDTerminalFunds returned to 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', 'FAILED', 'REJECTED', 'ERROR', '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) {
    // Phase progress events — update your UI status indicator
    case 'payout.cash_in_pending':
    case 'payout.cash_in_processing':
    case 'payout.cash_in_completed':
    case 'payout.kyt_out_pending':
    case 'payout.kyt_out_processing':
    case 'payout.kyt_out_completed':
    case 'payout.cash_out_pending':
    case 'payout.cash_out_processing':
    case 'payout.cash_out_completed':
    case 'payout.refund_pending':
    case 'payout.refund_processing': {
      const status = event.replace('payout.', '').toUpperCase();
      updatePayoutStatus(data.id, status);
      break;
    }

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

    case 'payout.review_needed':
      // Transaction flagged for manual compliance review — no action required, payout will resume or be rejected
      updatePayoutStatus(data.id, 'REVIEW_NEEDED');
      alertComplianceTeam(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 remains in any processing state longer than expected.
Each non-success status has a distinct cause and remediation. Critically, the phase in which FAILED or ERROR occurs determines whether a refund is issued:
  • FAILED / ERROR during Cash In — Terminal. No funds were collected, so no refund is issued. For ERROR, contact support if it recurs.
  • FAILED / ERROR after Cash In (KYT Out, Cash Out) — Transient. Because funds were already collected, the payout automatically transitions to REFUND_PENDINGREFUND_PROCESSINGREFUNDED.
  • REJECTED — Blocked by compliance review. Do not retry automatically; escalate internally. Always leads to the refund flow since funds were already collected.
  • REVIEW_NEEDED — A KYT check flagged the transaction for manual review. The payout resumes automatically or transitions to REJECTED after review; no action required on your side.
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