pytest-webhooks-cc
pytest fixtures for testing the webhooks and emails your app sends, with webhooks.cc.
webhooks.cc gives each test a real URL (and a real email address) that captures whatever is sent to it. Your test points the code under test at that URL, then reads what arrived: method, headers, body, signature. No mock server to write, no tunnel to keep open, and it works the same on your machine and in CI.
pip install pytest-webhooks-cc
That is the whole setup: no conftest, no import. The fixtures are in every test as soon as the package is installed. Python 3.10 or later, pytest 8 or later.
Create an API key on your API keys page (the free plan is enough for a small suite) and set it as WHK_API_KEY. Without it, tests that use the fixtures are skipped, so the suite still passes on a machine without a key.
Test a webhook your app sends
webhooks_cc_endpoint is a new capture URL for each test, deleted when the test ends, whether it passed or failed. webhooks_cc is the client that reads what the URL received.
import httpx
from webhooks_cc import match_header
def send_order_webhook(url: str, order: dict) -> None:
"""Stands in for your app: the code under test."""
httpx.post(url, json=order, headers={"X-Event": "order.paid"})
def test_sends_order_paid(webhooks_cc, webhooks_cc_endpoint):
send_order_webhook(webhooks_cc_endpoint.url, {"id": "ord_1", "total": 4999})
request = webhooks_cc.requests.wait_for(
webhooks_cc_endpoint.slug,
timeout="30s",
match=match_header("x-event", "order.paid"),
)
assert request.method == "POST"
assert request.json() == {"id": "ord_1", "total": 4999}
assert request.headers["content-type"] == "application/json"
wait_for polls until a matching request arrives and raises webhooks_cc.TimeoutError when the time runs out. A request that arrived before the wait started still counts. Matchers such as match_body_path("data.status", "paid") pick one request out of several.
Test a sign-up email
Every endpoint also receives email, at {slug}@mailhooks.cc. Sign up with that address and read the confirmation code or link out of the email your app sent:
from webhooks_cc import extract_code, extract_link
from myapp.accounts import sign_up # your code: creates the account, sends the email
def test_sign_up_sends_a_confirmation_email(webhooks_cc, webhooks_cc_endpoint):
address = webhooks_cc.emails.address(webhooks_cc_endpoint)
sign_up(email=address, password="correct horse battery staple")
email = webhooks_cc.emails.wait_for(
webhooks_cc_endpoint.slug, subject="Confirm your email", timeout="60s"
)
assert email.email.to[0].address == address
assert len(extract_code(email)) == 6 # "482913"
assert "/confirm?token=" in extract_link(email) # the link to click
The codes and links are found on the server, the same ones the webhooks.cc dashboard shows. email.email holds the parsed message: subject, from_, to, text, html, attachments, and the SPF, DKIM and DMARC results. To try the test before your app can send mail, webhooks_cc.emails.send_test(webhooks_cc_endpoint.slug) delivers a sample email with a code and a link.
Fixtures
| Fixture | Scope | What you get |
|---|---|---|
webhooks_cc |
session | A WebhooksCC client configured from the environment. Tests that use it are skipped when WHK_API_KEY is not set. |
webhooks_cc_endpoint |
function | A new Endpoint named after the test and deleted when it ends. .url captures HTTP requests, .slug names it in the client's calls. |
For an endpoint that answers with something other than 200 (to test your retries, say), write a fixture of your own with temporary_endpoint, which deletes the endpoint the same way:
import pytest
from webhooks_cc import temporary_endpoint
@pytest.fixture
def failing_endpoint(webhooks_cc):
with temporary_endpoint(
webhooks_cc, "always 503", mock_response={"status": 503, "body": "", "headers": {}}
) as endpoint:
yield endpoint
Settings
The fixtures read the environment; there is nothing to put in pytest.ini.
| Variable | Default | What it does |
|---|---|---|
WHK_API_KEY |
none | The API key the client uses. Unset or empty: the tests are skipped. |
WHK_BASE_URL |
https://webhooks.cc |
Where the API is. |
WHK_WEBHOOK_URL |
https://go.webhooks.cc |
Where endpoints capture, the host in webhooks_cc_endpoint.url. |
pytest's own options do the rest:
| Option | What it does |
|---|---|
-rs |
Lists each skipped test with its reason. |
-p no:webhooks_cc |
Turns the fixtures off for a run. |
-p no:pytest_webhooks_cc |
Keeps the fixtures and drops the line about skipped tests. |
-p pytest_webhooks_cc with PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 |
Loads the plugin, fixtures included, when pytest is told not to load installed plugins. |
When the key is missing, the plugin says so once, under pytest's "collected" line, because a skip is otherwise easy to miss:
collected 14 items
webhooks.cc: WHK_API_KEY is not set, so 3 tests that use the webhooks.cc fixtures will be skipped
Under pytest-xdist the workers collect and the main process does not, so the line is not shown there; -rs still lists the skipped tests.
In CI
Add the key as a secret and pass it to the test step. On GitHub Actions:
jobs:
test:
runs-on: ubuntu-latest
env:
WHK_API_KEY: ${{ secrets.WHK_API_KEY }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- run: pip install -e . pytest-webhooks-cc
- run: pytest
Pull requests from forks get no secrets, so their webhooks.cc tests are skipped while the rest of the suite runs. With pytest-xdist (pytest -n auto) each worker gets its own client and every test its own endpoint, so parallel tests never see each other's requests.
How it relates to webhooks-cc
The fixtures are defined once, in the webhooks-cc SDK, which this package depends on. If you already install webhooks-cc, you already have them; this package is the pytest-shaped way in, and adds the line about skipped tests. Installing both registers the fixtures once. Everything else the SDK does is there too: signed test requests for Stripe, GitHub and 32 other providers with build_template_request (no key and no network needed), verify_signature, mock responses and response rules.
Documentation
- Testing with pytest: a whole suite, with handler tests that need no network, retries, emails and CI
- Python SDK reference
- Matchers and signature verification
- Source
License
MIT
Metadata
Release files for pytest-webhooks-cc 0.1.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 |
|---|---|---|---|---|
| pytest_webhooks_cc-0.1.0.tar.gz | 9.5 kB | With provenance | Details for pytest_webhooks_cc-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| pytest_webhooks_cc-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for pytest_webhooks_cc-0.1.0-py3-none-any.whl |
Total release size: 17.0 kB
Release files / pytest_webhooks_cc-0.1.0.tar.gz
| Download URL | pytest_webhooks_cc-0.1.0.tar.gz |
|---|---|
| Size | 9.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b99b2cd9db00a4e220e42e8c863713391a8c842780dfe4cf36e79c84939fb967
|
|
BLAKE2b-256 checksum How to use checksums |
757f681cad38cd6dab84844b0343c81e336914fa6342530401237d79a901ab48
|
| 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 Nov 12, 2026.
Transparency logRelease files / pytest_webhooks_cc-0.1.0-py3-none-any.whl
| Download URL | pytest_webhooks_cc-0.1.0-py3-none-any.whl |
|---|---|
| Size | 7.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c1ab6ecd78a537e61a05f2eab73aa26d93b4d81f8d6ac78dc9e9b6f768211802
|
|
BLAKE2b-256 checksum How to use checksums |
c6c0ea256e222b8c52ba9395198f84d4b278db63937bd8c590949a150f3f509f
|
| 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 Nov 12, 2026.
Transparency logSecurity for pytest-webhooks-cc 0.1.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
Both files in this release have been uploaded with provenance metadata.
Attestation for pytest_webhooks_cc-0.1.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 Nov 12, 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.