Skip to content

feat: Daraja 3.0 disbursements (B2B and Business To Pochi) - #63

Merged
newtonmunene99 merged 14 commits into
devfrom
feature/daraja-disbursements_20261007
Oct 7, 2026
Merged

newtonmunene99 merged 14 commits into
devfrom
feature/daraja-disbursements_20261007

Conversation

@newtonmunene99

Copy link
Copy Markdown
Owner

Adds the five Daraja 3.0 disbursement products, which all act through the initiator.

Method Daraja product Endpoint CommandID
b2b.payBill Business Pay Bill /mpesa/b2b/v1/paymentrequest BusinessPayBill
b2b.buyGoods Business Buy Goods /mpesa/b2b/v1/paymentrequest BusinessBuyGoods
b2b.topUpB2C B2C Account Top Up /mpesa/b2b/v1/paymentrequest BusinessPayToBulk
b2b.remitTax Tax Remittance /mpesa/b2b/v1/remittax PayTaxToKRA
b2c.payToPochi Business To Pochi /mpesa/b2pochi/v1/paymentrequest BusinessPayToPochi

How it works

  • The B2B methods share a private b2bRequest built on initiatorRequest. It sends SenderIdentifierType/RecieverIdentifierType "4" (Daraja's spelling) and no OriginatorConversationID, and it reports every invalid field in one ValidationError.
  • Each method sends only the optional fields its portal page lists:
    • remitTax always sends KRA's 572572 and takes no partyB.
    • Top Up never sends Occassion.
  • payToPochi uses the B2C body and rules: a generated OriginatorConversationID, Ksh 10 to 250 000. It shares one helper with b2c.pay, and pay's existing tests are unchanged.
  • Results are read with parseResult. The portal's success and failure samples for all five are now fixtures, and the parser reads them with no changes.

Docs: a new B2B page (live after release) and a Business To Pochi section on the B2C page. The configuration, certificates, callbacks, limits, exports, home and migration pages, the README, the bug-report dropdown and the API instructions now include the new methods. The examples are type-checked through docs-examples.ts.

Live sandbox run (2026-10-07):

  • The four B2B requests were accepted, and their result webhooks arrived within about a second, all 8006 because the shared testapi initiator is still locked.
  • Business To Pochi got 401.002.01: the sandbox app doesn't have that product.
  • The redacted captures are pinned in sandbox-captures.spec.ts. The fixture redaction now also masks the gateway's requestId.

Release: a minor changeset; the next version PR gives 4.0.0-alpha.1.

Checks: vp check, vp test (378 passed), vp pack, test:dist, lint:pkg and docs:build all pass. A separate reviewer approved each phase; 39 of 40 mutations were killed, and the survivor makes no difference on the wire.

Transcribed the success and failure result samples for Business Pay Bill,
Business Buy Goods, B2C Account Top Up, Tax Remittance and Business To
Pochi from the Daraja portal, repairing only the portal's invalid JSON
(each fix is listed in the fixtures README and noted on its test).
parseResult reads all ten without changes: B2B completion times become
Dates, Top Up's numeric BillReferenceNumber is kept, and its valueless
Occassion reference is skipped.
Daraja 3.0 brings business-to-business payments back as separate
products. b2b.payBill pays a paybill from your shortcode: a private
b2bRequest builds the shared B2B body (SenderIdentifierType and
RecieverIdentifierType 4, Daraja's spelling kept) and sends it through
initiatorRequest, so every problem is still reported in one
ValidationError before anything is sent. accountReference is required
(up to 13 characters); requester and occasion are optional.
Pays a till, merchant store or merchant head office from your shortcode
(CommandID BusinessBuyGoods on the same B2B endpoint as Pay Bill). Unlike
payBill, the account reference is optional; requester and occasion are
optional too, and none of the three is sent when not given.
Moves money from your working account to a B2C shortcode's utility
account (CommandID BusinessPayToBulk). The account reference and
requester are optional, and Occassion is never sent: Top Up's parameter
table doesn't include it.
Tax Remittance posts PayTaxToKRA to /mpesa/b2b/v1/remittax. KRA's
shortcode 572572 is the only PartyB Daraja accepts, so the method sends
it itself and takes no partyB. The account reference is the PRN KRA
issues and is required; Requester and Occassion are never sent.
Business To Pochi has its own endpoint (/mpesa/b2pochi/v1/paymentrequest)
but B2C's body and rules: InitiatorName, a generated or given
OriginatorConversationID, Ksh 10 to 250 000 and 2 to 100 character
remarks. pay and payToPochi now share one send helper; pay's commandId
check runs first inside it, so the order of its validation issues is
unchanged and its 20 existing tests pass as they were.
B2BApi, B2BPayBillInput, B2BBuyGoodsInput, B2BTopUpInput, B2BTaxInput
and B2CPochiInput are exported from the package root, and the type test
pins Mpesa['b2b'] and each new method's input and return type.
A new B2B page covers payBill, buyGoods, topUpB2C and remitTax with a
field table each, the B2C page gains a Business To Pochi section, and
every list of initiator APIs (configuration, certificates, callbacks,
limits, exports, home page, README, migration table, the bug report
dropdown and the API instructions) now includes them. The examples are
mirrored in docs-examples.ts, so vp check type-checks them.
The partyB rule now runs only for the three methods that take one, rather
than passing trivially for remitTax, and Top Up's and Tax's account
reference limit and Buy Goods' requester and occasion rules get their
own tests. The fixture comments now list the same hand fixes as the
fixtures README, and the B2B JSDoc names the shortcode length and the
requester formats.
One opt-in sandbox run on 2026-10-07 sent each method from the sandbox
org shortcode through a tunnel. Daraja accepted all four B2B requests
(ResponseCode 0) and posted their results within seconds, each 8006
because the shared test initiator is still locked. Business To Pochi got
401.002.01 twice (the SDK refreshes once): the sandbox app doesn't have
that product. The redacted acknowledgements, the Pochi refusal and the
four results are pinned, including that Buy Goods and Top Up results
carry no BillReferenceNumber.
The migration guide's initiator and derived-field lists include B2B and
Business To Pochi, the B2B page names the portal's org API roles and the
requester formats, and its Results section says which balance field
parseBalances splits and that only Pay Bill and Tax results echo the
account reference (Buy Goods and Top Up didn't in the sandbox).
Daraja's error bodies carry a requestId trace ID that the fixture
redaction kept; it is now masked like the conversation IDs, and the
Business To Pochi capture is re-redacted. The B2B docs only claim what
the sandbox run showed: Buy Goods didn't echo the account reference it
was sent, and Top Up wasn't sent one.
@newtonmunene99

Copy link
Copy Markdown
Owner Author

@claude review

@claude

claude Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Claude finished @newtonmunene99's task in 21s —— View job


Review of PR #63: Daraja 3.0 disbursements

I found no blocking issues. I read the source changes (src/apis/b2b.ts, the b2c.ts refactor, and the initiator, shared, client, index and callback doc edits) and the changeset. I did not read the 52-file diff's tests, fixtures or docs line by line, and I did not run vp check or vp test. The PR description reports them passing.

  • Read the source changes and the supporting validators
  • Post the review

What works well

  • b2bRequest builds on initiatorRequest, so every invalid field comes back in one ValidationError and nothing is sent when validation fails.
  • The b2c.pay refactor into send keeps the commandId check first (check?.(issues) runs before the other checks). The error order and the generated OriginatorConversationID behaviour for pay are therefore unchanged, which matches the PR's claim that pay's existing tests are untouched.
  • Each product sends only the optional fields its portal page lists. topUpB2C and remitTax drop Occassion (and Requester for tax) by destructuring before extraFields, and remitTax hard-codes KRA's 572572.
  • The Reciever/Occassion misspellings are Daraja's, and the code comments say so.
  • The exports, the Mpesa interface docs and the DarajaResult and parseResult docs are all updated consistently.

Minor suggestions (non-blocking)

  1. Silently ignored fields for untyped callers (src/apis/b2b.ts:195-218). A JS caller who passes occasion to topUpB2C, or requester to remitTax, has the field dropped with no error. TypeScript rejects this at compile time, so it only matters for untyped callers. The docs could state it, for example "Top Up and Tax Remittance ignore occasion".
  2. Validation order (src/apis/b2b.ts:125-136). Issues come out as amount, shortCode, partyB, remarks, then the extras (accountReference, occasion, requester). That is deterministic and fine, but a test pinning the full multi-error order would stop it drifting. The PR says each field rule is tested, so this may already be covered.
  3. No upper bound on B2B amount (src/apis/b2b.ts:125). It only checks an integer of at least 1, and docs/guide/limits.md was updated, so I assume this follows the portal. If Daraja documents a per-transaction cap for any B2B product, validating it client-side would give a clearer error than the result-callback failure.
  4. requester truthiness check (src/apis/b2b.ts:165). input.requester ? checkPhone(...) : '' treats '' as "not provided", which matches how checkLength treats empty strings. Whitespace-only input goes to checkPhone and is rejected, which seems right.
  5. Sandbox gaps. The four B2B result callbacks are pinned as locked-initiator (8006) captures, and payToPochi got 401.002.01 in the sandbox, so none of the five has a live success capture. The portal-sample fixtures cover the success path. The PR already says this, but a follow-up once the initiator is unlocked would close it.

Release

  • A minor changeset giving 4.0.0-alpha.1 looks right for the prerelease line.

From the PR review: a test pins the order in which payBill reports every
invalid field at once, and the B2B page says that Top Up and Tax
Remittance ignore the fields they don't take when untyped JavaScript
passes them.
@newtonmunene99
newtonmunene99 merged commit a2dd435 into dev Oct 7, 2026
4 checks passed
@newtonmunene99
newtonmunene99 deleted the feature/daraja-disbursements_20261007 branch October 7, 2026 13:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant