Skip to content
NOSTREL
Payouts

Money leaving is a
different problem
from money arriving.

Nobody loses sleep over a payment that failed to come in. They lose sleep over one that went out and should not have, to a number nobody can quite account for, approved by a person who has since left.

So the controls here are on by default rather than available as an option. It makes sending a single payout fractionally slower than it could be. That is the trade, we made it deliberately, and we would make it again.

create a payoutbash
curl https://api.nostrel.com/v1/api/payouts \  -H "Authorization: Bearer sk_live_••••••••" \  -H "Idempotency-Key: payroll-oct-1042" \  -d phone=254712345678 \  -d amount=1200 \  -d reference=SAL-1042
queued, funds reserved, waiting for a second person
what comes backjson
{  "id": "pay_4k2nd7qzx1",  "state": "queued",  "amount": 1200,  "currency": "KES",  "reference": "SAL-1042",  "approval": "pending",  "created_by": "usr_9f1",  "approvable_by_creator": false}

01Controls

The boring ones, switched on before you arrive

None of this is exciting and all of it is the reason a finance director will sign. Every item here is enforced on the server, which is the only place enforcement counts.

Two people, by default

Whoever creates a payout cannot be the person who releases it. Not a setting somebody has to find and switch on, because the configuration that protects you should not be the one requiring initiative. One compromised laptop is then not enough to move money.

A PIN at the moment of release

Separate from the password, separate from the session. A stolen session should not be sufficient to send money, and a shoulder-surfed password should not be either.

The money is held while it waits

Creating a payout reserves the amount immediately. Your available balance drops before anyone approves anything, so two pending payouts cannot both be covered by the same shillings.

Who did what, in a chain

Created by, approved by, rejected by, with timestamps, written into a hash-chained audit log. Editing an old entry breaks every hash after it, which is the point of chaining them.

Limits that are yours

Per-transaction and monthly ceilings by tier, enforced with an atomic counter rather than a read followed by a write, so a burst of concurrent requests cannot slip past the cap between the two.

Roles, not a shared login

Someone who prepares payouts does not need the permission to release them, and someone reconciling does not need either. Every boundary is enforced on the server rather than by hiding a button.

02Underneath

Reserve, settle, release

Three movements, and the reason your balance is never briefly wrong in a way somebody could exploit.

  1. 01

    Reserve

    The moment a payout is created, the amount moves out of available and into held. Nothing has left the platform yet and nothing can be spent twice. A payout sitting in a queue for two days cannot be quietly double-funded by a payout created tomorrow.

  2. 02

    Settle

    Safaricom confirms. The held amount becomes a real debit, the fee posts as its own entry, and both sides of the movement are written in one database transaction. There is no instant at which one half exists without the other.

  3. 03

    Release

    A rejection, a refusal or a definite failure puts the held amount back into available, by exactly the figure that was taken. Not approximately, and not after a nightly job.

Withdrawing to yourself

Taking your own money out is a payout to a channel you own, so it rides the same rails, the same ledger, the same approval and the same PIN. It is not a separate code path with separate bugs, which is how a withdrawal feature usually becomes the weakest door in the building.

Destinations are verified before they can receive anything, and a bank destination is reached through that bank's own published paybill rather than one we found on an aggregator's list.

Which banks, and where the numbers came from

03State

Six states, and the one in the middle is not a failure

The distinction between a payout that failed and a payout we have not heard about is the difference between a refund and a double payment.

  1. queued

    Created and waiting for a second person. The amount is already reserved against your balance, so it cannot be spent twice while it sits here, and so a payout you forgot about cannot quietly overdraw you later.

    funds heldprocessing, failed
  2. processing

    Approved and handed to Safaricom. From here it is out of our hands and into theirs, which is the honest description of every payout rail anywhere.

    funds heldpaid, failed, timed_out
  3. paid

    Confirmed by the provider. The reservation becomes a real debit, the fee posts, your webhook fires. This is a terminal state.

    balance changedfinal
  4. failed

    Refused, or rejected by the second approver. The reservation is released and your available balance goes back up by exactly the amount that was held. This is a terminal state.

    funds releasedfinal
  5. timed_out

    No result callback arrived in the window. This does not mean it failed, which is the trap: a payout can succeed and the message about it can be lost. We ask the provider using the identifier we kept from the original request rather than guessing.

    funds heldpaid, failed
  6. reversed

    A completed payout was reversed at the provider. Posted as its own ledger entry rather than by editing the original, so the history of what happened stays intact. This is a terminal state.

    funds releasedfinal
Every state a payout can hold. A filled square is terminal: once a record is there it will not change again, which is what makes it safe to act on. A hollow one means something is still owed, by Safaricom, by the payer, or by us.

04At volume

One payout, or ten thousand

Payroll, supplier runs, farmer payments, rider earnings. Upload a CSV, see what it is going to do before it does it, approve once.

  • Validated before anything moves

    Phone numbers checked against the real Safaricom prefixes, amounts parsed as integers, duplicates within the file flagged. A bad row is reported with its line number rather than failing the batch halfway through.

  • Approved as a batch, posted as rows

    One approval for the run. Each payout is still its own ledger movement with its own state, so one failure out of nine hundred is one failure, not a batch you have to unpick.

  • Reserved up front

    The whole run is held against your balance before the first payment leaves, so you find out that you are short at the point of approval rather than at row four hundred.

  • Saved beneficiaries

    The people you pay regularly are a list, not a column somebody retypes every month. Retyping a phone number every month is how a digit changes.

payroll-october.csvtext
recipient,amount,reference,notes254712345678,1200,SAL-1042,October254701234567,1200,SAL-1043,October254733123456,980,SAL-1044,October, part time

Four columns, one of which is optional. A format somebody can produce from the spreadsheet they already keep, rather than one that requires a developer to generate.

05Where it lives

Money leaving has its own screen, and its own second pair of eyes

Available to send, paid out, awaiting approval and platform credit, with every payout's approval state on the row rather than behind a click.

app.nostrel.com/payouts
The NOSTREL payouts screen, listing recipients, queued time, approval state and amount, with balance cards above.
The payouts console, with approval state on every row. Demonstration data from a development environment.

Break it in the sandbox first

Send to a number that refuses, to one that times out, to one with no account. You cannot handle a failure you have never seen, and these are the three you will meet.