Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

pulsepigeon

Official PulsePigeon API client for Python.

Install

pip install pulsepigeon

Quickstart

import os
from pulsepigeon_client import PulsePigeonClient

client = PulsePigeonClient(api_key=os.environ["PULSEPIGEON_API_KEY"])

client.send_message(
    {
        "project_id": "proj_123",
        "from_email": "support@example.com",
        "to": [{"email": "user@example.com"}],
        "subject": "Reset your password",
        "text": "Use the secure reset link in this email.",
    },
    idempotency_key="password-reset-user-123-event-456",
)

Every send should carry an idempotency_key — retrying the same key with the same body returns the original result instead of sending a duplicate.

Personalized batches use per-item idempotency and partial-success results:

result = client.send_batch(
    [
        {
            "idempotency_key": "invoice-123-user-456",
            "message": {
                "project_id": "proj_123",
                "from_email": "billing@example.com",
                "to": [{"email": "user@example.com"}],
                "subject": "Your invoice",
                "text": "Invoice 123 is ready.",
            },
        }
    ]
)

The API accepts at most 100 items and 10 MB of decoded JSON per request. Accepted items replay their original message IDs. Failed items return an HTTP-style status and error code in their own result.

An AsyncPulsePigeonClient with the same methods is available for asyncio-based applications (each call runs the sync request on a worker thread — see the module docstring for why that's the right trade-off for a stdlib-only client):

from pulsepigeon_client import AsyncPulsePigeonClient

client = AsyncPulsePigeonClient(api_key=os.environ["PULSEPIGEON_API_KEY"])
await client.send_message({...}, idempotency_key="...")

API

Every method requires an API key carrying the scope listed below (request scopes when creating the key — POST /v1/api-keys, scopes field — a closed vocabulary: messages:send, reports:read, domains:*, webhooks:*, suppressions:*, contacts:*). Your own session's role bounds which scopes you can request: minting a key with contacts:*, suppressions:*, or webhooks:* requires an owner, admin, or operator session (those scopes reach owner-only routes like GDPR erasure and suppression-list export/import); messages:send, reports:read, and domains:* require only the ordinary role permission their routes already need of a session caller.

Project and workspace scoping. A project key is issued against exactly one project. list_messages, search_message_logs, list_events, list_subscriber_lists, and create_subscriber_list are filtered to that project only — a key never sees or creates data in another project. request_subscriber_opt_in additionally checks the target list's own project before dispatching mail, and requires messages:send on top of contacts:* (it sends a real confirmation email — "manage contacts" must not imply "send mail").

Tenant-wide methods require an owner-created workspace key. Project keys continue to reject those calls with 403 api_key_route_not_project_scoped. Workspace keys require an explicit live or test environment and can restrict source IP networks, sender domains, and expiration. The server enforces each restriction on every request.

Method and reference Scope
send_message(payload, idempotency_key=None) messages:send
send_batch(items) messages:send
list_messages() reports:read (project-scoped)
search_message_logs(**filters) reports:read (project-scoped)
list_events(message_id=None) reports:read (project-scoped)
list_domains() reports:read (workspace key)
check_domain(domain_id) domains:* (workspace key)
get_dmarc_ramp(domain_id) / list_dmarc_ramps() reports:read (workspace key)
list_dmarc_sources() reports:read (workspace key)
list_webhook_subscriptions() webhooks:* (workspace key)
upsert_webhook_subscription(endpoint_url=, secret=, enabled=, merge=) webhooks:* (workspace key)
delete_webhook_subscription(subscription_id) webhooks:* (workspace key)
list_webhook_deliveries() webhooks:* (workspace key)
suppress_recipient(email, reason) suppressions:* (workspace key)
export_suppressions() returns CSV text suppressions:* (workspace key)
import_suppressions(csv_text) suppressions:* (workspace key)
create_subscriber_list(name, project_id=None) contacts:* (project-scoped)
list_subscriber_lists() contacts:* (project-scoped)
request_subscriber_opt_in(list_id, email, jurisdiction="US") contacts:* + messages:send (project-scoped)
delete_contact(email, reason) (GDPR-style erasure) contacts:* (workspace key)
verify_webhook_signature(payload, header, secret, tolerance_seconds=3600) n/a (local, no request)

Errors raise one of PulsePigeonAuthenticationError (401), PulsePigeonPermissionError (403), PulsePigeonNotFoundError (404), PulsePigeonConflictError (409), PulsePigeonValidationError (400/422), PulsePigeonRateLimitError (429, has retry_after_seconds), or PulsePigeonServerError (5xx) — all subclasses of PulsePigeonError (status, body, code). Catch the base class as a fallback.

Verifying webhook deliveries

PulsePigeon-Signature: t=<unix>,v1=<hmac-sha256 hex> is sent with every status-webhook delivery. Verify it before trusting the payload — this is the single most common thing to get wrong by hand: the signature covers a canonical re-serialization of the JSON body (sorted keys, no whitespace), not the raw bytes received over the wire. The signed timestamp is when the event was enqueued for delivery, not when the HTTP request is actually sent — tolerance_seconds (default 3600) has to budget for realistic delivery-queue delay, not just clock skew, or a delayed-but-authentic delivery gets spuriously rejected.

from pulsepigeon_client import verify_webhook_signature

payload = json.loads(raw_body)
if not verify_webhook_signature(payload, signature_header, secret):
    raise ValueError("invalid PulsePigeon webhook signature")

Auth

Create an API key from the PulsePigeon console under API keys (or POST /v1/api-keys with a scopes list) and pass it as api_key. Keys are sent as Authorization: Bearer <key> — never log or commit a real key.

Integration test

tests/test_bearer_auth_integration.py proves the packaged client round-trips against a real, running instance of the API (not mocked) — see that file and its TypeScript counterpart, sdk/typescript/test/integration.test.ts, for what it verifies and its prerequisites (a reachable Postgres). tests/test_client_unit.py covers request-building and error-mapping without a server. Run with pytest -q.

Release files for pulsepigeon 0.1.0b1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pulsepigeon 0.1.0b1
File Size Uploaded
pulsepigeon-0.1.0b1.tar.gz 12.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pulsepigeon 0.1.0b1
File Interpreter ABI Platform
pulsepigeon-0.1.0b1-py3-none-any.whl Python 3 none any Details

Total release size: 25.5 kB

Release files / pulsepigeon-0.1.0b1.tar.gz

Download URL pulsepigeon-0.1.0b1.tar.gz
Size 12.7 kB
Tags Source
SHA-256 checksum
How to use checksums
d756a03a5b82769012dc3f2ebb9253127e18f113eb40155354360a385abb4785
BLAKE2b-256 checksum
How to use checksums
9f7aaa30edc0895411e5dec51d7891cb20eacbe8cb46e6324217c7dd05823168
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Sep 24, 2026.

Transparency log

Release files / pulsepigeon-0.1.0b1-py3-none-any.whl

Download URL pulsepigeon-0.1.0b1-py3-none-any.whl
Size 12.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a35ed416fd8e61a6904e085d6b30e6402742d9ed9b60d29f913e73418c84cebe
BLAKE2b-256 checksum
How to use checksums
d2eb5e6c87187ad5f66270acc5662b66f493b80b6a1a4f240048e04d5f367238
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Sep 24, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0b1 This release

2 release files

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