Skip to main content

paymock

ci license: MIT python: 3.9+

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

  1. Paystack: POST /transaction/initialize, GET /transaction/verify/:ref, Idempotency-Key replay.
  2. Flutterwave: POST /v3/payments, GET /v3/transactions/:tx_ref/verify.
  3. Webhooks: exact HMAC signatures — X-Paystack-Signature (SHA-512) and verif-hash (SHA-256) — with the same payload shapes your handlers expect.
  4. Stateful: transactions persist in memory between requests; pending until you settle them, like a real checkout.
  5. Failure injection: POST /_settle/:ref {"status": "failed"} — test your decline and timeout code paths on demand.
  6. 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.
  7. Failure scenarios: duplicate, delayed, out_of_order webhooks, 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)

Source distribution for paymock 0.2.0
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)

Table of built distributions (wheels) for paymock 0.2.0
File Interpreter ABI Platform Actions
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page