FastAPI's BackgroundTasks runs work after the response, but it is in-process and does not provide durable delivery or automatic retries. Use it only when that loss model is acceptable; use a durable queue for critical password-reset, verification, or billing mail.
Pass an idempotency key derived from the triggering business event. For a password reset, reuse the persisted reset-request ID; for an order confirmation, reuse the order ID. A UUID minted per inbound request can change when a client or upstream retry re-enters the route, which can enqueue a duplicate.
Inside an async def background task, use httpx (or another async HTTP client) to call PulsePigeon's REST API so synchronous network I/O does not block the event loop. A normal def background task may use the synchronous requests library because Starlette runs that task in its thread pool, but thread-pool capacity is still finite.
The published pulsepigeon Python package provides synchronous and asynchronous clients. Install version 1.0.0 from PyPI with pip install pulsepigeon for the project-key surface; your worker remains responsible for bounded retries. Consult the SDK method reference and developer API reference before wiring the application request.
Record the accepted send response's id, not the API key or message content, in your application trace. Use that value to query tenant-scoped logs or events and to correlate signed delivery-status webhook payloads, whose message_id is the same identifier.