paymock
stripe-mock for African payments. A stateful, offline mock server for Paystack and Flutterwave — signed webhooks included, no API keys, no tunnel, no live internet.
If you build with Paystack or Flutterwave, you know the pain: brittle sandboxes, webhooks that need a public tunnel, and no way to test the failure paths (bank downtime, declined cards, duplicate deliveries) without hoping they happen.
pipx install git+https://github.com/mgkvibez/paymock.git
Not on PyPI yet: paymock is reserved for this project and the release workflow is in place,
so pipx install paymock will work as soon as the first tag is pushed.
# your app, unchanged — only base_url changes
import requests
base = "http://localhost:9090/paystack" # was https://api.paystack.co
init = requests.post(f"{base}/transaction/initialize",
json={"email": "me@x.dev", "amount": 50000},
headers={"Authorization": "Bearer sk_test_paymock"}).json()
# force the outcome you want to test, get the exact signed webhook
settle = requests.post(f"{base}/_settle/{init['data']['reference']}",
json={"status": "success"}).json()
# verify the HMAC signature yourself — same scheme as the real gateway
import hmac, hashlib
assert hmac.compare_digest(
settle["webhook_headers"]["X-Paystack-Signature"],
hmac.new(b"sk_test_paymock", settle["webhook_body"].encode(), hashlib.sha512).hexdigest(),
)
What it covers
- Paystack:
POST /transaction/initialize,GET /transaction/verify/:ref, Idempotency-Key replay. - Flutterwave:
POST /v3/payments,GET /v3/transactions/:tx_ref/verify. - Webhooks: exact HMAC signatures —
X-Paystack-Signature(SHA-512) andverif-hash(SHA-256) — with the same payload shapes your handlers expect. - Stateful: transactions persist in memory between requests;
pendinguntil you settle them, like a real checkout. - Failure injection:
POST /_settle/:ref {"status": "failed"}— test your decline and timeout code paths on demand. - Auto-delivery with real retry schedules: register your local URL and paymock POSTs signed webhooks to it, retrying failures exactly like the live gateways do.
- Failure scenarios:
duplicate,delayed,out_of_orderwebhooks, built in.
Webhook auto-delivery and retries
The official sandboxes never show you live retry behavior, so duplicate and late webhooks only surface in production. paymock reproduces it on localhost:
# register where you want webhooks delivered
POST /paystack/_webhooks/register
{"url": "http://localhost:3000/webhooks/paystack"}
# settle a payment and have the webhook auto-delivered, twice
POST /paystack/_settle/<ref>
{"status": "success", "scenario": "duplicate"}
# inspect every delivery attempt (attempt #, status code, timing)
GET /paystack/_webhooks/deliveries
Retry schedules, in gateway-accurate seconds:
Paystack live (per Paystack's docs): every 3 minutes for the first 4 tries, then hourly for 72 hours. Paystack test (same page): hourly, 10 times. Flutterwave: up to 3 retries on non-2xx per Flutterwave's engineering blog. Their intervals are not officially documented — paymock defaults to 60s/300s/900s, marked clearly as our choice, and the schedule is data you can change.
Time compression: delays are gateway seconds divided by time_scale. Default 1.0 (production-faithful waits); the test fixtures use 10000, so a 3-minute retry resolves in under 20ms. Set PAYMOCK_TIME_SCALE or pass it at app creation.
Test fixtures: assert "no double credit"
pytest fixtures ship with the package (entry point pytest11, auto-registered on install):
def test_no_double_credit(paymock, webhook_receiver):
paymock.register_webhook("paystack", webhook_receiver.url)
ref = paymock.initialize("paystack", amount=50000)
paymock.settle("paystack", ref, "success", scenario="duplicate")
events = webhook_receiver.events("charge.success")
assert len(events) == 2 # delivered twice...
assert events[0]["body"] == events[1]["body"] # ...identically
# your app under test must credit this order exactly once
webhook_receiver.fail_times = 2 makes your endpoint fail twice so you can assert the retry schedule from paymock.deliveries().
For Jest (Node 18+), jest/paymock.js provides the same flow against a running paymock:
const collector = webhookCollector(yourExpressApp);
await paymock.register("paystack", collector.url);
await paymock.settle("paystack", ref, "success", "duplicate");
await collector.waitFor(2);
expect(new Set(collector.events("charge.success").map(e => e.data.reference)).size).toBe(1);
Why
stripe-mock proved how much better payment DX can be. Africa's gateways never got one — so every fintech team in Lagos, Nairobi and Accra re-invents the same test scaffolding. This is that scaffolding, done once, MIT-licensed.
Roadmap
Monnify, Interswitch and M-Pesa (Daraja) route mocks, and a small dashboard UI to visualize event flow.
License
MIT.
Fire the failure scenarios (v0.2)
The sandbox never shows you live-mode behavior — Paystack retries live webhooks every 3 minutes for the first 4 tries, then hourly for 72 hours. paymock reproduces that against your local dev server:
# your app runs at localhost:3000/webhook
paymock deliver --scenario duplicate --url http://localhost:3000/webhook --reference T-1
paymock deliver --scenario failed --url http://localhost:3000/webhook --reference T-2 --time-scale 0.001
duplicate delivers the same event 3 times — your handler must credit
exactly once. failed follows the real retry schedule (compressed).
out_of_order sends the stale pending event after the success.
Drop-in test fixtures: fixtures/pytest/ (asserts "no double credit")
and fixtures/jest/. M-Pesa Daraja scenarios: planned, not yet built.
Metadata
Release files for paymock 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| paymock-0.2.0.tar.gz | 23.6 kB | With provenance | Details for paymock-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| paymock-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for paymock-0.2.0-py3-none-any.whl |
Total release size: 44.3 kB
Release files / paymock-0.2.0.tar.gz
| Download URL | paymock-0.2.0.tar.gz |
|---|---|
| Size | 23.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c192d2ffd1731ae2c74e7c22c423a674832b2c37e631b260fff3b1ef9533b6e7
|
|
BLAKE2b-256 checksum How to use checksums |
dae2fdf4760f8e8832ed5c82a1f56d698a5c5d10ef6f32920953ead0d641c4e4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.
Transparency logRelease files / paymock-0.2.0-py3-none-any.whl
| Download URL | paymock-0.2.0-py3-none-any.whl |
|---|---|
| Size | 20.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6ffb6d898f4c8b2ede73eadbb46647350066fdf54f9bb8af12377e524d490c48
|
|
BLAKE2b-256 checksum How to use checksums |
91a0e1d0907d60d06e13b547bbc803b6faa63748c69a0d0a9d589d1ee954828d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.
Transparency logSecurity for paymock 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for paymock-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.