pidgeon
The Python client for the Pidgeon email API — mailboxes your code creates, on a domain you own.
Generated from openapi.json, the same document the TypeScript client is
generated from, so an operation has the same name in both:
messages_send here is messagesSend there.
pip install pidgeon
Sending
An API key goes in PIDGEON_API_KEY. Every call takes the client; nothing is
global.
import os
from pidgeon import AuthenticatedClient
from pidgeon.api.messages import messages_send
from pidgeon.models import Error, MessagesSendBody
client = AuthenticatedClient(
base_url="https://pidgeon.ai/api/v1",
token=os.environ["PIDGEON_API_KEY"],
)
result = messages_send.sync(
client=client,
body=MessagesSendBody(
from_="hello@yourdomain.com",
to=["priya@example.com"],
subject="Your invoice",
text="Attached, payable within 30 days.",
),
)
if isinstance(result, Error):
raise RuntimeError(result.error.message)
Errors come back, they are not raised. Every call returns the success
model or Error or None, so an isinstance check is the shape of a
correct caller. Error.error carries the API's own code, a message
written for a person and safe to show one, and a field when one input was at
fault.
from_ and type_ carry a trailing underscore, because from and type
are reserved in Python. The wire names are unchanged.
Every operation also has asyncio, and sync_detailed / asyncio_detailed
variants that return the whole Response with its status and headers.
Receiving
Mail arriving at an address your code owns becomes an event, and your endpoint is called with a signed payload. Verify before you parse — an unverified payload is an unauthenticated POST that claims a message arrived.
from pidgeon.webhooks import verify_webhook
event = verify_webhook(
raw_body=request.body, # the exact bytes, before any json.loads
headers=request.headers,
secret=os.environ["PIDGEON_WEBHOOK_SECRET"],
)
raw_body must be the bytes we sent. Parse the JSON after verifying —
re-serialising changes key order and the signature will not match.
verify_webhook raises rather than returning a boolean, rejects a timestamp
outside five minutes before it looks at the signature, and compares in
constant time.
The event carries ids, never a body. Exchange one for the content with
messages_get.
Creating an address
from pidgeon.api.identities import identities_create
from pidgeon.models import IdentitiesCreateBody
identities_create.sync(
client=client,
body=IdentitiesCreateBody(
address="receipts@yourdomain.com",
type_="agent",
send_policy="reply_only",
),
)
Say where with address, domain or domain_id — one of the three. Every
address you already have carries its domain_id, which is what to use when
you are on the shared domain and own nothing to list.
send_policy="reply_only" makes an address structurally unable to write to
anybody who has not written to it first, which is the right default for
software.
Pagination
A list returns a page, not an iterator — deliberately. An iterator that hides pagination also hides the rate limit pagination is subject to.
from pidgeon.api.events import events_list
cursor = None
while True:
page = events_list.sync(client=client, cursor=cursor)
for event in page.data:
handle(event)
cursor = page.next_cursor
if cursor is None:
break
What this deliberately does not do
No retry policy, no queue, no caching, no auto-paginating iterator. Those are decisions that belong to your application — an SDK that makes them for you is one you eventually work around.
Versioning
Versioned independently of the API. The API is /v1; this package is 1.x,
and semver applies to its own surface.
Full reference: pidgeon.ai/docs. MIT.
Metadata
Release files for pidgeon 1.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 | |
|---|---|---|---|
| pidgeon-1.2.0.tar.gz | 123.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pidgeon-1.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 485.4 kB
Release files / pidgeon-1.2.0.tar.gz
| Download URL | pidgeon-1.2.0.tar.gz |
|---|---|
| Size | 123.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b55947624e0c9e310ce54c0108f1e15294107c686f79044d49b701fc6c649289
|
|
BLAKE2b-256 checksum How to use checksums |
ff72b181e13dfcd3bee9b1113987bf4ff5d0a67620efc90749f994f867fea476
|
| 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 Oct 14, 2026.
Transparency logRelease files / pidgeon-1.2.0-py3-none-any.whl
| Download URL | pidgeon-1.2.0-py3-none-any.whl |
|---|---|
| Size | 361.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d241370d533a0050c22087179aebc4bb4b3c89d36d092b7ed87ea71a8ed287a2
|
|
BLAKE2b-256 checksum How to use checksums |
2c80659e5f8d6e92430fce73821789a0fd42d36e08f5ecd51d88e309f419a1f4
|
| 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 Oct 14, 2026.
Transparency log