The Courier Python SDK provides typed access to the Courier REST API from any Python 3.9+ application. It includes synchronous and asynchronous clients, Pydantic response models, and TypedDict request params.
Available on
GitHub
and PyPI .
Installation
Requires Python 3.9+.
Quick Start
from courier import Courier
client = Courier()
response = client.send.message(
message = {
"to" : { "email" : "you@example.com" },
"content" : {
"title" : "Hello from Courier!" ,
"body" : "Your first notification, sent with the Python SDK." ,
},
},
)
print (response.request_id)
The client reads COURIER_API_KEY from your environment automatically. You can also pass it explicitly: Courier(api_key='your-key').
Authentication
Get your API key from Settings > API Keys in the Courier dashboard. Set it as an environment variable:
export COURIER_API_KEY = "your-api-key"
The SDK picks this up by default. To pass it explicitly:
client = Courier( api_key = "your-api-key" )
We recommend using a .env file with python-dotenv so your API key stays out of source control.
Sending Notifications
With a template
Design your notification in the template designer , then reference it by ID:
response = client.send.message(
message = {
"to" : { "user_id" : "user_123" },
"template" : "my-template-id" ,
"data" : { "orderNumber" : "10042" , "itemName" : "Courier Hoodie" },
},
)
With inline content
Skip templates and define content directly in code:
response = client.send.message(
message = {
"to" : { "email" : "jane@example.com" },
"content" : {
"title" : "Order shipped" ,
"body" : "Your order {{ orderNumber }} has shipped!" ,
},
"data" : { "orderNumber" : "10042" },
"routing" : {
"method" : "single" ,
"channels" : [ "email" ],
},
},
)
To multiple recipients
Send to a list of users in a single call:
response = client.send.message(
message = {
"to" : [
{ "user_id" : "user_1" },
{ "user_id" : "user_2" },
{ "email" : "guest@example.com" },
],
"template" : "welcome-template" ,
},
)
Async Usage
Basic async client
Import AsyncCourier instead of Courier and use await:
import asyncio
from courier import AsyncCourier
client = AsyncCourier()
async def main ():
response = await client.send.message(
message = {
"to" : { "email" : "you@example.com" },
"content" : {
"title" : "Hello from Courier!" ,
"body" : "Sent from the async Python client." ,
},
},
)
print (response.request_id)
asyncio.run(main())
The async client has the same API as the sync client; every method returns an awaitable.
Using aiohttp instead of httpx
For improved concurrency performance, you can swap the HTTP backend to aiohttp:
pip install trycourier[aiohttp]
from courier import AsyncCourier, DefaultAioHttpClient
async with AsyncCourier( http_client = DefaultAioHttpClient()) as client:
response = await client.send.message(
message = {
"to" : { "user_id" : "user_123" },
"template" : "my-template-id" ,
},
)
Available Resources
The SDK covers the full Courier API. Every method is typed and documented with docstrings.
Resource Namespace Description Send client.sendSend messages to one or more recipients Messages client.messagesRetrieve status, history, and content of sent messages Profiles client.profilesCreate, update, and retrieve user profiles Users client.usersManage preferences, tenants, and push tokens per user Auth client.authIssue JWT tokens for client-side SDK authentication Bulk client.bulkSend messages to large recipient lists via jobs Lists client.listsManage subscription lists and their subscribers Audiences client.audiencesDefine and query audience segments Tenants client.tenantsManage tenants for multi-tenant setups Automations client.automationsInvoke multi-step automation workflows Brands client.brandsManage brand settings (logos, colors, templates) Notifications client.notificationsList and inspect notification templates Translations client.translationsManage localized content
Common Operations
Checking Message Status
After sending, use the request_id to check delivery status or get the full event timeline:
message = client.messages.retrieve( "your-message-id" )
print (message.status) # e.g. 'DELIVERED'
print (message.delivered) # timestamp
print (message.channels) # which channels were used
history = client.messages.history( "your-message-id" )
for event in history.results:
print (event.type, event.event)
You can also list recent messages with optional filters:
recent = client.messages.list( status = "DELIVERED" )
Managing User Profiles
Profiles store recipient data (email, phone, custom fields) that Courier uses for delivery. You need a profile before you can send to a user_id.
# Create or merge into a profile
client.profiles.create( "user_123" , profile = {
"email" : "jane@example.com" ,
"phone_number" : "+15551234567" ,
"name" : "Jane Doe" ,
})
# Retrieve a profile
profile = client.profiles.retrieve( "user_123" )
print (profile.profile[ "email" ])
# Partial update (merge)
client.profiles.update( "user_123" , profile = { "name" : "Jane Smith" })
create merges with any existing profile. Use replace for a full overwrite (any fields not included will be removed).
Issuing JWT Tokens
If you use Courier’s client-side SDKs (React, JavaScript, mobile), your backend needs to issue JWT tokens for user authentication. The auth.issue_token method handles this:
result = client.auth.issue_token(
scope = "user_id:user_123 inbox:read:messages inbox:write:events read:preferences write:preferences" ,
expires_in = "2 days" ,
)
# Return result.token to your frontend
print (result.token)
The scope string controls what the token can access. Common scopes:
Scope Permission user_id:<id>Which user the token is for (required) inbox:read:messagesRead inbox messages inbox:write:eventsMark messages as read/archived read:preferencesRead notification preferences write:preferencesUpdate notification preferences write:user-tokensRegister push notification tokens
Bulk Sending
For large recipient lists, use the bulk API. It works in three steps: create a job, add users, then run it.
# 1. Create a bulk job
job = client.bulk.create_job(
message = { "event" : "welcome-notification" },
)
# 2. Add recipients
client.bulk.add_users(job.job_id, users = [
{ "profile" : { "email" : "alice@example.com" }, "data" : { "name" : "Alice" }},
{ "profile" : { "email" : "bob@example.com" }, "data" : { "name" : "Bob" }},
])
# 3. Run the job
client.bulk.run_job(job.job_id)
# Check status
status = client.bulk.retrieve_job(job.job_id)
print (status.status)
For email-based bulk jobs, include profile.email on each user. The to.email field alone is not sufficient for email provider routing.
Type Safety
Request parameters are TypedDicts, and responses are Pydantic models. This gives you autocomplete and inline docs in your editor.
response = client.send.message(
message = {
"to" : { "user_id" : "user_123" },
"template" : "my-template-id" ,
"data" : { "foo" : "bar" },
},
)
# Response is a Pydantic model
print (response.request_id)
print (response.to_dict())
print (response.to_json())
For VS Code type checking, set python.analysis.typeCheckingMode to basic in your settings.
Configuration
Error Handling
The SDK throws typed errors for API failures. All errors extend courier.APIError:
import courier
from courier import Courier
client = Courier()
try :
client.send.message(
message = {
"to" : { "user_id" : "user_123" },
"template" : "my-template-id" ,
},
)
except courier.APIConnectionError as e:
print ( "The server could not be reached" )
print (e.__cause__)
except courier.RateLimitError as e:
print ( "Rate limited; back off a bit." )
except courier.APIStatusError as e:
print ( f "API error: { e.status_code } " )
print (e.response)
Status Code Error Type 400 BadRequestError401 AuthenticationError403 PermissionDeniedError404 NotFoundError422 UnprocessableEntityError429 RateLimitError>=500 InternalServerErrorN/A APIConnectionError
Retries
The SDK automatically retries failed requests up to 2 times with exponential backoff. Retried errors include connection failures, 408, 409, 429, and 5xx responses.
# Disable retries
client = Courier( max_retries = 0 )
# Override per-request
client.with_options( max_retries = 5 ).send.message(
message = {
"to" : { "user_id" : "user_123" },
"template" : "my-template-id" ,
},
)
Timeouts
Requests time out after 60 seconds by default. Configure globally or per-request:
import httpx
from courier import Courier
# Simple timeout
client = Courier( timeout = 20.0 ) # 20 seconds
# Granular control
client = Courier(
timeout = httpx.Timeout( 60.0 , read = 5.0 , write = 10.0 , connect = 2.0 ),
)
# Override per-request
client.with_options( timeout = 5.0 ).send.message(
message = {
"to" : { "user_id" : "user_123" },
"template" : "my-template-id" ,
},
)
On timeout, an APITimeoutError is thrown. Timed-out requests are retried by default.
Logging
Enable debug logging with the COURIER_LOG environment variable:
export COURIER_LOG = info # or 'debug' for full request/response logging
The SDK uses Python’s standard logging module, so it integrates with any logging setup you already have.
Raw Response Access
Access HTTP headers or stream the response body:
from courier import Courier
client = Courier()
# Get headers + parsed data
response = client.send.with_raw_response.message(
message = {
"to" : { "user_id" : "user_123" },
"template" : "my-template-id" ,
},
)
print (response.headers.get( "x-request-id" ))
send_result = response.parse()
# Stream the response
with client.send.with_streaming_response.message(
message = {
"to" : { "user_id" : "user_123" },
"template" : "my-template-id" ,
},
) as response:
for line in response.iter_lines():
print (line)
Custom HTTP Client
Override the default httpx client for proxies, custom transports, or other advanced use cases:
import httpx
from courier import Courier, DefaultHttpxClient
client = Courier(
base_url = "http://my.test.server.example.com:8083" ,
http_client = DefaultHttpxClient(
proxy = "http://my.proxy.example.com" ,
transport = httpx.HTTPTransport( local_address = "0.0.0.0" ),
),
)
More Operations
The SDK covers the full Courier REST API . Here are a few more resources beyond what’s documented above:
Resource Method Use case User preferences client.users.preferences.retrieve(user_id)Fetch a user’s notification preferences for your preference center Cancel a message client.messages.cancel(message_id)Cancel a delayed or queued message before delivery (returns 409 if already delivered) Push tokens client.users.tokens.add_single(token, user_id=user_id)Register a device push token for iOS , Android , or React Native Automations client.automations.invoke.invoke_ad_hoc(automation=...)Run a multi-step workflow (delay, send, update profile) via Automations
API Reference Full REST API docs with request/response examples.
Send API Learn about the Send endpoint, routing, and message options.
Quickstart Send your first notification in under two minutes.
GitHub Source code, issues, and changelog.