Official Primitive Python SDK for webhook handling and API access
Project description
primitivedotdev
Official Primitive Python SDK.
The default root module is intentionally small and centered on inbound/outbound email automations:
primitive.receive(...)primitive.client(...)client.send(...)client.reply(...)client.forward(...)
The generated HTTP API, raw webhook helpers, and lower-level types still remain available for advanced use cases.
Requirements
- Python
>=3.10
Installation
pip install primitivedotdev
Basic usage
Receive and reply
import primitive
client = primitive.client(api_key="prim_test")
def webhook_handler(body: bytes, headers: dict[str, str]) -> dict[str, object]:
email = primitive.receive(
body=body,
headers=headers,
secret="whsec_...",
)
client.reply(email, "Thank you for your email.")
return {"ok": True}
Send a new email
import primitive
client = primitive.client(api_key="prim_test")
result = client.send(
from_email="Support <support@example.com>",
to="alice@example.com",
subject="Hello",
body_text="Hi there",
# Use a unique key per logical send. Reusing a key returns the original
# response from the first send, which is how retries are deduplicated.
idempotency_key="customer-key-abc123",
wait=True,
wait_timeout_ms=5000,
)
print(result.id, result.status, result.queue_id, result.delivery_status)
send, reply, and forward keep the HTTP request open until Primitive's
downstream SMTP transaction completes. In production, configure the client with
a request timeout long enough for SMTP delivery, typically 30-60 seconds:
client = primitive.client(api_key="prim_test", timeout=60.0)
About wait mode
When wait=True, the call returns the first downstream SMTP outcome (or
wait_timeout_ms, default 30000). Possible terminal delivery_status values:
deliveredaccepted by the receiving MTAbouncedrejected by the receiving MTA (the response is still 200 OK)deferredtemporary failure, the receiving MTA may retrywait_timeoutno outcome was observed in time. Treat as "outcome unknown." The send may still complete after the response returns.
Reply from a different address
reply() defaults the From address to the inbound recipient (the address that
received the email). When your verified outbound domain differs from your
inbound domain, pass from_email explicitly:
client.reply(
email,
"Thanks for your email.",
from_email="notifications@outbound.example.com",
)
HTML replies and waiting on the delivery outcome
reply() accepts a dict with html as a sibling of text, plus the same
wait flag the top-level send() takes:
client.reply(
email,
{
"text": "Thanks for your email.",
"html": "<p>Thanks for your email.</p>",
"wait": True,
},
)
subject is intentionally not accepted on reply(). Gmail's Conversation View
needs both a References match and a normalized-subject match to thread, so a
custom subject silently breaks the thread for half the recipient population.
Use client.send(...) if you need full subject control.
If the inbound row is not in a state we can reply to (no Message-Id recorded,
or content was discarded), the API returns inbound_not_repliable (HTTP 422)
and the SDK raises.
Forward an inbound email
client.forward(
email,
to="ops@example.com",
body_text="Can you take this one?",
)
Per-call request options
send, reply, forward (and their a* async variants) accept the same
three per-call kwargs:
timeout(seconds, float). Overrides the client-level timeout.extra_headers(dict). Merged on top of client headers.idempotency_key(str). Sent as theIdempotency-Keyheader.
# Per-call timeout
client.send(
from_email="support@example.com",
to="alice@example.com",
subject="Hello",
body_text="Hi there",
timeout=15.0,
)
# Per-call idempotency key
client.send(
from_email="support@example.com",
to="alice@example.com",
subject="Hello",
body_text="Hi there",
idempotency_key="my-key",
)
Use client.with_options(...) to clone the client with new defaults applied
to every subsequent call. Per-call kwargs still win over these defaults.
fast = client.with_options(timeout=5.0)
fast.send(
from_email="support@example.com",
to="alice@example.com",
subject="Hello",
body_text="Hi there",
)
with_options accepts timeout and extra_headers only. idempotency_key
is a per-call concern and is rejected as a client default.
The normalized email object
primitive.receive(...) returns a normalized inbound email object:
email.sender.address
email.sender.name
email.received_by
email.received_by_all
email.reply_target.address
email.reply_subject
email.forward_subject
email.subject
email.text
email.thread.message_id
email.thread.references
email.raw
Advanced usage
Generated API module
Use primitive.api when you need the full generated HTTP API surface.
from primitive.api import create_client
from primitive.api.api.account.get_account import sync as get_account
client = create_client("prim_test")
account = get_account(client=client)
Lower-level webhook helpers
You can still use the raw helpers directly:
handle_webhook(...)parse_webhook_event(...)verify_webhook_signature(...)validate_email_received_event(...)
Development
From sdks/sdk-python:
uv sync --dev
uv run python scripts/generate_schema_module.py
uv run python scripts/generate_models.py
uv run python scripts/generate_api_client.py
uv run pytest
uv run ruff check .
uv run basedpyright
Or from repo root sdks/:
make python-generate
make python-check
make python-build
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file primitivedotdev-0.19.0.tar.gz.
File metadata
- Download URL: primitivedotdev-0.19.0.tar.gz
- Upload date:
- Size: 132.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3e0b35404e404c61ee43166de9aeb5c00bc562d891100718c02f8ff72842610d
|
|
| MD5 |
9d1f4fd3b50cd577c0f0ca234d62344e
|
|
| BLAKE2b-256 |
94fe44c02e20dbdee91a56558d94a2524f294ba68b1b4f9d37b0556eede6708a
|
Provenance
The following attestation bundles were made for primitivedotdev-0.19.0.tar.gz:
Publisher:
python-release.yml on primitivedotdev/sdks
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
primitivedotdev-0.19.0.tar.gz -
Subject digest:
3e0b35404e404c61ee43166de9aeb5c00bc562d891100718c02f8ff72842610d - Sigstore transparency entry: 1454286333
- Sigstore integration time:
-
Permalink:
primitivedotdev/sdks@100aa7921af37d164107a55e0a3ff0235d78259a -
Branch / Tag:
refs/heads/main - Owner: https://github.com/primitivedotdev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
self-hosted -
Publication workflow:
python-release.yml@100aa7921af37d164107a55e0a3ff0235d78259a -
Trigger Event:
push
-
Statement type:
File details
Details for the file primitivedotdev-0.19.0-py3-none-any.whl.
File metadata
- Download URL: primitivedotdev-0.19.0-py3-none-any.whl
- Upload date:
- Size: 217.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a6a20a95056912550e85563eceab546207832a508e7f518c6f29c783c17080cd
|
|
| MD5 |
671b7f0b8f7e27d61d2ea85feb20f184
|
|
| BLAKE2b-256 |
d2bbc0127249e7359cacaa39a5b5e041af6446b0bd93079358b6c7c9c405cdc2
|
Provenance
The following attestation bundles were made for primitivedotdev-0.19.0-py3-none-any.whl:
Publisher:
python-release.yml on primitivedotdev/sdks
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
primitivedotdev-0.19.0-py3-none-any.whl -
Subject digest:
a6a20a95056912550e85563eceab546207832a508e7f518c6f29c783c17080cd - Sigstore transparency entry: 1454286508
- Sigstore integration time:
-
Permalink:
primitivedotdev/sdks@100aa7921af37d164107a55e0a3ff0235d78259a -
Branch / Tag:
refs/heads/main - Owner: https://github.com/primitivedotdev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
self-hosted -
Publication workflow:
python-release.yml@100aa7921af37d164107a55e0a3ff0235d78259a -
Trigger Event:
push
-
Statement type: