Pushrail Docs
Open app
SDKs · Python

Python SDK, Ingestion

Single and batch event emission with the pushrail Python SDK, plus typed errors.

Ingestion

The Python SDK exposes ingestion on client.events. Two methods cover the entire ingestion surface: emit for one event, emit_batch for up to 100. Both accept either dicts or Pydantic models and both return typed result models.

Single events

from pushrail import Pushrail

client = Pushrail(api_key=os.environ["PUSHRAIL_API_KEY"])

result = client.events.emit({
    "eventType": "subscription.renewed",
    "source": "billing-service",
    "customerExternalId": "cust_42",
    "payload": {"subscriptionId": "sub_abc", "amountCents": 2900},
    # Optional:
    # "occurredAt": "2026-05-16T12:00:00Z",  # defaults to datetime.now(utc).isoformat()
    # "idempotencyKey": "billing:renew:sub_abc:202605",  # defaults to UUIDv7
    # "correlationId": request.headers.get("x-correlation-id"),
    # "metadata": {"source_region": "us-east-1"},
})

The SDK accepts both the wire-format camelCase keys (eventType, customerExternalId) and snake_case (event_type, customer_external_id) via Pydantic field aliases. Mix freely; the wire serialization is always camelCase. Prefer the typed model for code that touches events repeatedly:

from pushrail import EmitEvent

event = EmitEvent(
    event_type="subscription.renewed",
    source="billing-service",
    customer_external_id="cust_42",
    payload={"subscriptionId": "sub_abc", "amountCents": 2900},
)
result = client.events.emit(event)

The model gives you IDE completion and catches missing fields before they reach the wire.

Batch emission

result = client.events.emit_batch([
    {"eventType": "order.completed", "source": "checkout", "customerExternalId": "c1", "payload": {...}},
    {"eventType": "order.completed", "source": "checkout", "customerExternalId": "c2", "payload": {...}},
])

print(result.accepted, result.rejected)
for item in result.events:
    if item.status == "rejected":
        print("rejected", item.index, item.error)

The server caps a batch at 100 events; if you have more, slice into chunks of 100 on the caller side. See Batch ingest for partial-failure semantics.

Idempotency

Every emit call gets a UUIDv7 idempotency_key when you don't supply one. The generated value is on the returned EmitResult.idempotency_key so you can log it. For deterministic dedup, supply a stable key:

client.events.emit({
    "eventType": "order.completed",
    "source": "checkout",
    "customerExternalId": row.customer_id,
    "idempotencyKey": f"order.completed:{row.id}",
    "payload": {"orderId": row.id},
})

See Idempotency for the dedup-window semantics.

Error types

The SDK raises a typed hierarchy rooted at PushrailError:

from pushrail import (
    Pushrail,
    PushrailError,
    PushrailValidationError,  # 400/422, permanent
    PushrailAuthError,        # 401/403, permanent
    PushrailRateLimitError,   # 429, transient, retried automatically
    PushrailServerError,      # 5xx, transient, retried automatically
    PushrailNetworkError,     # I/O fail, transient, retried automatically
)

try:
    client.events.emit(input)
except PushrailValidationError as err:
    # Fix the payload; do not retry.
    logger.warning("validation failed: %s", err)
except PushrailAuthError:
    # Rotate or fix the key.
    raise
except PushrailError:
    # Generic catch, includes retried-and-exhausted transient failures.
    raise

Each exception carries status_code, response_body, and request_id. Surface request_id in your logs. Pushrail support can look up a delivery by request id in seconds.

The Python SDK uses synchronous HTTP under the hood. For async codebases, wrap calls in a thread executor; a native async client is on the roadmap.