pidgeon
Pidgeon is the email inbox API for AI agents. It gives agents their own inboxes so they can work with emails just like a human. This is its Python client.
pip install pidgeon
Create an address, send, receive, reply
import os
from pidgeon import AuthenticatedClient
from pidgeon.api.identities import identities_create
from pidgeon.api.messages import messages_await, messages_reply, messages_send
from pidgeon.models import (
IdentitiesCreateBody,
IdentitiesCreateBodyType,
MessagesReplyBody,
MessagesSendBody,
)
client = AuthenticatedClient(
base_url="https://pidgeon.ai/api/v1",
token=os.environ["PIDGEON_API_KEY"],
)
# 1. An address of its own. Any free name on pidgeon.email works with no DNS.
created = identities_create.sync(
client=client,
body=IdentitiesCreateBody(address="scout@pidgeon.email", type_=IdentitiesCreateBodyType.AGENT),
)
address = created.data
# 2. Send from it. A refusal raises PidgeonError; it never comes back as a value.
messages_send.sync(
client=client,
body=MessagesSendBody(
from_=address.email,
to="ops@example.com",
subject="Staging access",
text="Could you send me the access code?",
),
)
# 3. Receive: one held-open request until the answer arrives, up to 60 seconds.
awaited = messages_await.sync(client=client, identity_id=address.id, timeout=60)
# 4. Reply into the same conversation. Recipients and subject come off the original.
if awaited.data.message:
print(awaited.data.message.extracted.codes)
messages_reply.sync(
awaited.data.message.id,
client=client,
body=MessagesReplyBody(text="Thanks, that worked."),
)
PIDGEON_API_KEY is a pgn_live_… key. Make one at
pidgeon.ai/app/developers/keys, or
let your agent sign itself up: pidgeon.ai/skill.md
is two calls, and the only thing it needs from a person is a six-digit code.
In production, take a signed webhook rather than holding a request open. Documentation: pidgeon.ai/docs · OpenAPI.
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. Every call takes the client;
nothing is global.
Errors
A failure raises PidgeonError. Every call returns the success model or
raises, so a refused send cannot pass for a sent one. code is the API's own
and is what to branch on, message is written for a person and safe to show
one, field names the input at fault, and retry_after is in seconds when a
limit says when to come back:
from pidgeon.errors import PidgeonError
try:
messages_send.sync(client=client, body=body)
except PidgeonError as error:
if error.code == "abuse_blocked":
... # nothing was sent; error.retry_after says when the allowance frees up
raise
Since 1.19.0. Before it, a call returned Error for a failure the
OpenAPI document declared and None for one it did not, and a 403 on a
send was one it did not: code that checked isinstance(result, Error) now
sees the exception instead. raise_on_unexpected_status no longer changes
anything; a status the client cannot read always raises.
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,
IdentitiesCreateBodySendPolicy,
IdentitiesCreateBodyType,
)
identities_create.sync(
client=client,
body=IdentitiesCreateBody(
address="receipts@yourdomain.com",
type_=IdentitiesCreateBodyType.AGENT,
send_policy=IdentitiesCreateBodySendPolicy.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=IdentitiesCreateBodySendPolicy.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.
Contributing
This client is generated from openapi.json,
the contract the API validates against, and published from
pidgeon-ai/pidgeon-python. A
pull request against generated code cannot be merged as written — the fix
belongs in the schema. Open an issue
and say what is wrong, or email support@pidgeon.ai.
MIT licensed.
Metadata
Release files for pidgeon 1.19.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 | |
|---|---|---|---|---|
| pidgeon-1.19.0.tar.gz | 158.9 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pidgeon-1.19.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 597.9 kB
Release files / pidgeon-1.19.0.tar.gz
| Download URL | pidgeon-1.19.0.tar.gz |
|---|---|
| Size | 158.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
00adc9451819c598c732dbdf914b9862fc5031fc33c446bf4d60d3061f7f0239
|
|
BLAKE2b-256 checksum How to use checksums |
97ebe2cea7dcdea8d44dc4eafcc6165e27eb97305406cadb2080fbd6daf7cf9a
|
| 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 20, 2026.
Transparency logRelease files / pidgeon-1.19.0-py3-none-any.whl
| Download URL | pidgeon-1.19.0-py3-none-any.whl |
|---|---|
| Size | 439.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6e986ad26ae62d9f421c2b773b9d8a94cd816d70074da80c316dbad53bf44a58
|
|
BLAKE2b-256 checksum How to use checksums |
4f0a496c05aad2b8e6cae05aa19347f49f54beaf4420fa2f84a98b7e0afaaa47
|
| 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 20, 2026.
Transparency logSecurity for pidgeon 1.19.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.
Attestation for pidgeon-1.19.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 20, 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.