Skip to main content

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

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)

Source distribution for pytest-webhooks-cc 0.1.0
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)

Table of built distributions (wheels) for pytest-webhooks-cc 0.1.0
File Interpreter ABI Platform Actions
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 log

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

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

Select from 2 files

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