Metadata-Version: 2.4
Name: pidgeon
Version: 1.2.0
Summary: A client library for accessing Pidgeon API
Keywords: pidgeon,email,webhooks,api-client,agents
Author: Pidgeon
License-Expression: MIT
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Communications :: Email
Classifier: Typing :: Typed
Requires-Dist: httpx>=0.23.1,<0.29.0
Requires-Dist: attrs>=22.2.0
Requires-Python: >=3.11
Project-URL: Homepage, https://pidgeon.ai
Project-URL: Documentation, https://pidgeon.ai/docs
Project-URL: Repository, https://github.com/patrickcneuhaus/pidgeon
Description-Content-Type: text/markdown

# pidgeon

The Python client for the [Pidgeon](https://pidgeon.ai) 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.

```sh
pip install pidgeon
```

## Sending

An API key goes in `PIDGEON_API_KEY`. Every call takes the client; nothing is
global.

```python
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.

```python
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

```python
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.

```python
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](https://pidgeon.ai/docs). MIT.
