Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
"$schema": "../node_modules/.pnpm/@changesets+config@4.0.1/node_modules/@changesets/config/schema.json",
"baseBranch": "main",
"access": "public",
"fixed": [["@payenv/core", "@payenv/connector-fedapay"]],
"fixed": [["@payenv/core", "@payenv/connector-fedapay", "@payenv/connector-kkiapay"]],
"updateInternalDependencies": "patch",
"privatePackages": { "version": false, "tag": false },
"privatePackages": {
"version": false,
"tag": false
},
"ignore": []
}
14 changes: 14 additions & 0 deletions .changeset/kkiapay-widget-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@payenv/core': minor
'@payenv/connector-fedapay': minor
'@payenv/connector-kkiapay': minor
---

Kkiapay, and payments started by the customer in a widget.

- `@payenv/core`: `nextAction.type === 'widget'`, `supportedWidgets` and
`Capability.widget` for providers whose payments start in a widget; `payenv.confirm`
verifies the provider transaction's merchant reference and amount before linking it.
New error code `REFERENCE_MISMATCH`.
- `@payenv/connector-kkiapay` (new): Kkiapay widget payments in Benin (MTN, Moov),
server-side verification, webhook verification and parsing.
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# Copy to .env (git-ignored) to run the sandbox tests (pnpm test) and the demo (pnpm demo).
FEDAPAY_SANDBOX_SECRET_KEY=

# Kkiapay sandbox keys (dashboard → Developers → API keys, test mode).
KKIAPAY_SANDBOX_PUBLIC_KEY=
KKIAPAY_SANDBOX_PRIVATE_KEY=
KKIAPAY_SANDBOX_SECRET_KEY=

# --- Live demo (REAL money). Leave empty unless you really mean it. ---
# DEMO_LIVE=yes
# FEDAPAY_LIVE_SECRET_KEY=
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ Before `1.0.0`, minor versions may contain breaking changes.
## [Unreleased]

### Added
- 2026-10-05 — `@payenv/core`: payments started by the customer in a widget
(`nextAction.type === 'widget'`, `supportedWidgets`, `Capability.widget`) and
`payenv.confirm`, which verifies the provider transaction's merchant reference and
amount (ADR 0005). New error code `REFERENCE_MISMATCH`.
- 2026-10-05 — `@payenv/connector-kkiapay`: Kkiapay widget payments (Benin, MTN and Moov),
server-side verification, webhook verification and parsing.
- 2026-10-05 — demo: Kkiapay widget and a "simulate a FedaPay outage" switch to watch the
fallback.
- 2026-10-05 — Release tooling (Changesets, trusted publishing workflow), getting-started guide.
Packages versioned `0.1.0`; LICENSE, NOTICE and sources shipped in each package.
- 2026-10-05 — `@payenv/core`: `PayenvError.providerCode` keeps the provider's own error
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ packages/
src/
test/
connector-fedapay/ @payenv/connector-fedapay
connector-kkiapay/ @payenv/connector-kkiapay
examples/
fedapay-demo/ runnable demo: pnpm demo
```
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

🇫🇷 [Lire en français](README.fr.md)

**Status:** 🌱 Pre-release `0.1.0` — core engine and FedaPay connector (Benin mobile money). The API may still change. Started on **2026-09-26**.
**Status:** 🌱 Pre-release `0.x` — core engine, FedaPay and Kkiapay connectors (Benin mobile money). The API may still change. Started on **2026-09-26**.
**License:** [Apache 2.0](LICENSE) — free for personal and commercial use, forever.

---
Expand Down
14 changes: 13 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,18 @@ Idempotency:
- Where a provider supports idempotency keys natively, the connector forwards a
derived key per attempt.

## 4b. Payments started by the customer (widgets)

Some providers (Kkiapay, most checkout-style aggregators) cannot start a payment from the
server: the customer starts it in the provider's widget. See
[ADR 0005](adr/0005-customer-started-payments.md).

- Such routes declare `Capability.widget` and are only used when the request lists the
widget in `supportedWidgets` (the front end can open it).
- `collect` returns `requires_action` with `nextAction: { type: 'widget', provider, params }`.
- `payenv.confirm(key, providerRef)` links the provider transaction after verifying with
the provider that it carries this payment's merchant reference and amount.

## 5. Webhooks

- The app exposes one endpoint (or one per provider) and passes the **raw body**
Expand Down Expand Up @@ -197,7 +209,7 @@ The integrator owns the store and its data.
@payenv/core unified model, router, attempt engine, interfaces,
and an in-memory store for development and tests
@payenv/connector-fedapay one package per connector
@payenv/connector-kkiapay
@payenv/connector-kkiapay payments started in the Kkiapay widget, verified server-side
@payenv/store-postgres persistent stores, one package each
```

Expand Down
2 changes: 1 addition & 1 deletion docs/CONNECTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ A connector is an adapter for one payment aggregator. See
| Connector | Region | Collect | Payout | Refund | Webhooks | Status | Maintainer |
|---|---|---|---|---|---|---|---|
| FedaPay | Benin / West Africa | ✅ MTN, Moov, Celtiis | — | — | ✅ Signature | 🟡 In progress | @jules-mahounou |
| Kkiapay | West Africa | — | — | — | — | ⚪ Planned | — |
| Kkiapay | West Africa | ✅ MTN, Moov (widget + server verification) | — | — | ✅ Secret header | 🟡 In progress | @jules-mahounou |
| CinetPay | West & Central Africa | — | — | — | — | ⚪ Planned | — |
| PayDunya | West Africa | — | — | — | — | ⚪ Planned | — |
| Flutterwave | Pan-African | — | — | — | — | ⚪ Planned | — |
Expand Down
15 changes: 15 additions & 0 deletions docs/GETTING_STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,21 @@ const payenv = createPayenv({
});
```

Some providers start payments in a widget on the customer's side (e.g. Kkiapay). They are
used only if your front end can open their widget, and can serve as a fallback:

```ts
const payment = await payenv.collect({ ...request, supportedWidgets: ['kkiapay'] });
if (payment.nextAction?.type === 'widget') {
// Front end: open the widget with payment.nextAction.params, then send the transaction
// id it returns to your server, which calls:
await payenv.confirm(payment.idempotencyKey, transactionId);
}
```

`confirm` verifies with the provider that the transaction belongs to this payment and has
the right amount. See [@payenv/connector-kkiapay](../packages/connector-kkiapay).

More connectors are on the way: see [CONNECTORS.md](CONNECTORS.md).

## 7. Before going live
Expand Down
39 changes: 39 additions & 0 deletions docs/adr/0005-customer-started-payments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# 0005. Payments started by the customer in a widget

- **Status:** Accepted
- **Date:** 2026-10-05

## Context

Some providers cannot start a payment from the server. With Kkiapay (and most
checkout-style aggregators), the customer starts the payment in the provider's widget,
and the server only verifies the resulting transaction. Payenv's first connector,
FedaPay, starts payments from the server (USSD push).

A front end may also be unable to open a given widget (server-only flows, USSD apps,
another platform).

## Decision

1. A connector declares widget routes with `Capability.widget`. They are only eligible
when the request lists that widget in `CollectRequest.supportedWidgets`.
2. `collect` on such a route returns `requires_action` with
`nextAction: { type: 'widget', provider, params }` and a `merchantReference`.
3. `payenv.confirm(idempotencyKey, providerRef)` links the provider transaction **after**
checking with the provider that it carries the payment's merchant reference and
amount. Otherwise it throws `REFERENCE_MISMATCH` and leaves the payment unchanged.
4. Widget routes can serve as a fallback after a server-push provider failed safely
(ADR 0004 still applies: only a proven "no money moved" allows it).

## Consequences

- One API for both kinds of providers; most checkout-style aggregators fit this model.
- Front ends opt in explicitly, so no payment is handed to a widget nobody can open.
- `confirm` closes a classic fraud: confirming an order with another, cheaper transaction.
- Connectors whose provider does not echo a merchant reference cannot support `confirm`.

## Alternatives considered

- **Trusting the transaction id sent by the front end**: rejected, trivially abusable.
- **Calling undocumented widget endpoints from the server**: rejected, fragile and
possibly against the provider's terms.
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ ADR that *supersedes* the old one.
| [0002](0002-library-first-no-custody.md) | Library first, no custody of funds or secrets | Accepted | 2026-09-26 |
| [0003](0003-typescript-first.md) | TypeScript as the first implementation | Accepted | 2026-09-26 |
| [0004](0004-no-fallback-on-ambiguous-state.md) | Never fall back on an ambiguous state | Accepted | 2026-09-26 |
| [0005](0005-customer-started-payments.md) | Payments started by the customer in a widget | Accepted | 2026-10-05 |

To propose one, copy [template.md](template.md) to `NNNN-short-title.md` and open a PR.
14 changes: 14 additions & 0 deletions examples/fedapay-demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ and watch its status change live. No framework, no dependency besides Payenv.
Sandbox test numbers: `+22964000001` and `+22966000001` succeed; any other number
simulates a failed payment.

## Fallback to Kkiapay (sandbox)

Add your Kkiapay sandbox keys to `.env`:
```
KKIAPAY_SANDBOX_PUBLIC_KEY=...
KKIAPAY_SANDBOX_PRIVATE_KEY=...
KKIAPAY_SANDBOX_SECRET_KEY=...
```

The page then shows **"Simuler une panne FedaPay"**. Tick it and pay: FedaPay fails before
any money can move, Payenv falls back to Kkiapay, and the page opens the Kkiapay widget.
Pay with a Kkiapay test number (e.g. `61000000`): the transaction id goes to the server,
which calls `payenv.confirm` to verify it with Kkiapay.

## Live mode (real money)

To check that a real USSD prompt reaches your phone, the demo has an opt-in live mode.
Expand Down
3 changes: 2 additions & 1 deletion examples/fedapay-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
},
"dependencies": {
"@payenv/connector-fedapay": "workspace:*",
"@payenv/core": "workspace:*"
"@payenv/core": "workspace:*",
"@payenv/connector-kkiapay": "workspace:*"
},
"devDependencies": {
"@types/node": "^24.0.0"
Expand Down
Loading
Loading