TchokoPay
Guides

Disbursing

Sending money from your balance to a mobile money number.

POST /v1/disburse sends money from your TchokoPay balance to any mobile money number. Your balance is funded by what you collect.

Disbursing is not withdrawing

WithdrawalDisbursement
Money goes toYour own accountSomebody else's number
Started fromThe dashboard, by a personYour backend, via the API
WhereDashboard → WithdrawalsPOST /v1/disburse

Moving your own balance to your own number is a withdrawal, and it is not part of the API.

Same currency, always

You pay out of a balance you hold, in the currency you hold it in. There's no conversion at payout time — XAF in your balance goes out as XAF.

If you collect in several currencies you'll have a balance in each, and each has its own limits.

The controls

Because a payout moves real money with no human in the loop, several things bound it. Every one is checked before your balance moves.

These are risk controls, not a budget — they exist so that a mistake, a bug in your own code, or a stolen key stays small.

The key must be allowed to

Disbursing is a separate permission from collecting, chosen when a key is made and never widened afterwards. See Scopes.

The amount must fit your payout limits

A per-payout ceiling, and optionally a rolling daily and weekly one. The per-payout ceiling is the currency's own maximum — the same one that bounds a collection — so there's nothing to set up before your first payout. Daily and weekly ceilings exist only where support has agreed them with you.

Check what is left with GET /v1/account/disburse-limits/:currency, and read enabled rather than the status code. High-volume accounts get ceilings to match their volume.

You must actually have the money

Checked against your live balance at the moment of the write, not a moment before — two payouts racing each other can never both spend the same funds.

Large amounts wait for a person

Above your review threshold, a payout is funded and held rather than dispatched. It arrives with awaitingReview: true and stays PENDING until somebody releases it.

Idempotency is mandatory

Idempotency-Key is required on POST /v1/disburse, unlike on payments.

The two are not symmetrical. A duplicated charge creates an invoice nobody has to pay — a tidy-up. A duplicated payout sends real money twice and cannot be recalled.

// Derive the key from something stable in YOUR system, not a random value
// per attempt — a retry has to produce the same key or it isn't a retry.
const idempotencyKey = `payout-${yourPayoutInstructionId}`;

Send the same key again and you get the original payout back, with its original reference. Nothing moves twice.

When a payout fails

A FAILED payout keeps your money reserved. It does not return automatically.

When a dispatch fails we frequently cannot tell whether the network took the money. Returning funds that may already have gone is the one mistake with no way back — so the money stays put and a person resolves it, either by retrying the rail or returning it explicitly.

You'll get a disburse.failed webhook, and the disbursement will show why in statusReason.

A full example

// `instruction` is whatever your system calls a payment it owes somebody:
// a seller settlement, a user withdrawal, a loan disbursement, a refund.
async function sendPayout(instruction) {
  // Fail early rather than half way through a batch.
  const limits = await get('/v1/account/disburse-limits/XAF');
  if (!limits.enabled) throw new Error(limits.message);
  if (limits.remainingToday !== null && limits.remainingToday < instruction.amount) {
    throw new Error('Would exceed the daily payout limit');
  }

  const payout = await post('/v1/disburse', {
    amount: instruction.amount,
    currency: 'XAF',
    phone: instruction.phone,
    recipientName: instruction.recipientName,
    // Your own id, so you can reconcile without storing ours.
    merchantReference: instruction.id,
  }, {
    // Derived from your id, not random per attempt — a retry has to produce
    // the same key or it is not a retry.
    'Idempotency-Key': `payout-${instruction.id}`,
  });

  if (payout.awaitingReview) {
    // Funded and queued, but a person has to release it. Do not retry —
    // the same key returns this same payout.
    await markAwaitingApproval(instruction.id, payout.reference);
    return;
  }

  await markSent(instruction.id, payout.reference);
}

Then handle disburse.succeeded and disburse.failed in your webhook endpoint rather than polling — see Webhooks.

On this page