@@ -8,7 +8,7 @@ The official Python client library for the [Posthook](https://posthook.io) API -
88pip install posthook-python
99```
1010
11- ** Requirements:** Python 3.9+. Only dependency is [ httpx] ( https://www.python-httpx.org/ ) .
11+ ** Requirements:** Python 3.9+. Dependencies: [ httpx] ( https://www.python-httpx.org/ ) and [ websockets ] ( https://websockets.readthedocs.io /) .
1212
1313## Quick Start
1414
@@ -70,7 +70,7 @@ hook = client.hooks.schedule(
7070
7171### Absolute UTC time (` post_at ` )
7272
73- Schedule at an exact UTC time. Accepts ` datetime ` objects or ISO 8601 strings:
73+ Schedule at an exact UTC time. Accepts ` datetime ` objects or RFC 3339 strings:
7474
7575``` python
7676from datetime import datetime, timedelta, timezone
@@ -82,7 +82,7 @@ hook = client.hooks.schedule(
8282 data = {" userId" : " 123" },
8383)
8484
85- # Using an ISO string
85+ # Using an RFC 3339 string
8686hook = client.hooks.schedule(
8787 path = " /webhooks/send-reminder" ,
8888 post_at = " 2026-06-15T10:00:00Z" ,
@@ -186,7 +186,7 @@ async for hook in client.hooks.list_all(status="failed"):
186186
187187### Delete a hook
188188
189- Idempotent -- returns ` None ` on both success and 404 (already delivered or gone ):
189+ To cancel a pending hook, delete it before delivery. Idempotent -- returns ` None ` on both 200 (deleted) and 404 (already deleted ):
190190
191191``` python
192192client.hooks.delete(" hook-uuid" )
@@ -330,9 +330,175 @@ delivery = client.signatures.parse_delivery(
330330)
331331```
332332
333+ ## WebSocket Listener
334+
335+ The WebSocket listener receives hooks in real time without running an HTTP server. Use ` AsyncPosthook ` and call ` hooks.listen() ` with an async handler:
336+
337+ ``` python
338+ import asyncio
339+ import posthook
340+
341+ async def main ():
342+ async with posthook.AsyncPosthook(" pk_..." ) as client:
343+ async def on_hook (delivery ):
344+ print (f " Received: { delivery.hook_id} -> { delivery.path} " )
345+ print (f " Data: { delivery.data} " )
346+ return posthook.Result.ack()
347+
348+ listener = await client.hooks.listen(
349+ on_hook,
350+ on_connected = lambda info : print (f " Connected: { info.project_name} " ),
351+ )
352+ await listener.wait() # Blocks until closed
353+
354+ asyncio.run(main())
355+ ```
356+
357+ ### Result types
358+
359+ Your handler must return a ` Result ` :
360+
361+ | Factory | Effect |
362+ | ---------| --------|
363+ | ` Result.ack() ` | Processing complete — hook is marked as delivered immediately |
364+ | ` Result.nack(error?) ` | Reject — triggers retry according to project settings |
365+ | ` Result.accept(timeout) ` | Async — you have ` timeout ` seconds to call back via HTTP (see below) |
366+
367+ ``` python
368+ return posthook.Result.ack()
369+ return posthook.Result.nack(" processing failed" )
370+ return posthook.Result.accept(timeout = 120 )
371+ ```
372+
373+ ### Async processing with ` accept `
374+
375+ Use ` accept ` when your handler needs more time than the 10-second ack window.
376+ After returning ` accept ` , POST to the callback URLs on the delivery to report
377+ the outcome:
378+
379+ ``` python
380+ async def on_hook (delivery ):
381+ # Kick off background work, save the callback URLs
382+ await queue.enqueue(" process" , {
383+ " data" : delivery.data,
384+ " ack_url" : delivery.ack_url,
385+ " nack_url" : delivery.nack_url,
386+ })
387+ return posthook.Result.accept(timeout = 300 ) # 5 minutes to call back
388+
389+ # Later, in the background worker:
390+ await posthook.async_ack(job[" ack_url" ])
391+ # or on failure:
392+ await posthook.async_nack(job[" nack_url" ], {" error" : " failed" })
393+ ```
394+
395+ If neither URL is called before the deadline, the hook is retried.
396+
397+ ### Concurrency
398+
399+ By default, handlers run with unlimited concurrency (matching HTTP delivery behavior). Set ` max_concurrency ` to limit parallel handlers — deliveries that arrive while at capacity are nacked immediately so the server can retry them:
400+
401+ ``` python
402+ listener = await client.hooks.listen(on_hook, max_concurrency = 10 )
403+ ```
404+
405+ ### Lifecycle callbacks
406+
407+ ``` python
408+ listener = await client.hooks.listen(
409+ on_hook,
410+ on_connected = lambda info : print (f " Connected: { info.connection_id} " ),
411+ on_disconnected = lambda err : print (f " Disconnected: { err} " ),
412+ on_reconnecting = lambda attempt : print (f " Reconnecting (attempt { attempt} ) " ),
413+ )
414+ ```
415+
416+ ### Stream API
417+
418+ For manual control over ack/nack, use ` hooks.stream() ` which returns an async iterator:
419+
420+ ``` python
421+ async with posthook.AsyncPosthook(" pk_..." ) as client:
422+ async with await client.hooks.stream() as stream:
423+ async for delivery in stream:
424+ print (delivery.hook_id, delivery.data)
425+
426+ if should_process(delivery):
427+ await stream.ack(delivery.hook_id)
428+ else :
429+ await stream.nack(delivery.hook_id, " not ready" )
430+ ```
431+
432+ ### HTTP fallback
433+
434+ If your project has a domain configured, hooks are delivered via HTTP when no
435+ WebSocket listener is connected. You can run both an HTTP endpoint and a
436+ WebSocket listener — the server uses WebSocket when available and falls back to
437+ HTTP automatically. Since both paths use the same ` Result ` type, you can share
438+ your handler logic:
439+
440+ ``` python
441+ async def process_hook (delivery ):
442+ await process_order(delivery.data)
443+ return posthook.Result.ack()
444+
445+ # HTTP delivery (ASGI endpoint)
446+ app = signatures.asgi_handler(process_hook)
447+
448+ # WebSocket delivery (runs alongside)
449+ listener = await client.hooks.listen(process_hook)
450+ ```
451+
452+ ### WebSocket delivery metadata
453+
454+ Deliveries received via WebSocket include a ` ws ` field with attempt info:
455+
456+ ``` python
457+ async def on_hook (delivery ):
458+ if delivery.ws:
459+ print (f " Attempt { delivery.ws.attempt} / { delivery.ws.max_attempts} " )
460+ if delivery.ws.forward_request:
461+ print (f " Original body: { delivery.ws.forward_request.body} " )
462+ return posthook.Result.ack()
463+ ```
464+
465+ ## ASGI/WSGI Handlers
466+
467+ For quick integration without a full web framework, ` SignaturesService ` provides handler wrappers:
468+
469+ ### ASGI
470+
471+ ``` python
472+ import posthook
473+
474+ signatures = posthook.create_signatures(" ph_sk_..." )
475+
476+ async def on_hook (delivery ):
477+ print (delivery.data)
478+ return posthook.Result.ack()
479+
480+ # Mount as an ASGI endpoint (e.g. with uvicorn)
481+ app = signatures.asgi_handler(on_hook)
482+ ```
483+
484+ ### WSGI
485+
486+ ``` python
487+ import posthook
488+
489+ signatures = posthook.create_signatures(" ph_sk_..." )
490+
491+ def on_hook (delivery ):
492+ print (delivery.data)
493+ return posthook.Result.ack()
494+
495+ # Mount as a WSGI endpoint (e.g. with gunicorn)
496+ app = signatures.wsgi_handler(on_hook)
497+ ```
498+
333499## Async Hooks
334500
335- When [ async hooks] ( https://posthook.io/docs /essentials/async-hooks ) are enabled, ` parse_delivery() ` populates ` ack_url ` and ` nack_url ` on the delivery object. Return 202 from your handler and call back when processing completes.
501+ When [ async hooks] ( https://docs. posthook.io/essentials/async-hooks ) are enabled, ` parse_delivery() ` populates ` ack_url ` and ` nack_url ` on the delivery object. Return 202 from your handler and call back when processing completes.
336502
337503### FastAPI
338504
@@ -551,7 +717,16 @@ client = posthook.Posthook("pk_...", http_client=http_client)
551717
552718When you provide a custom client, the SDK does ** not** close it on ` client.close() ` -- you are responsible for its lifecycle.
553719
720+ ## Resources
721+
722+ - [ Documentation] ( https://docs.posthook.io ) — guides, concepts, and patterns
723+ - [ API Reference] ( https://docs.posthook.io/api-reference/introduction ) — endpoint specs and examples
724+ - [ Quickstart] ( https://docs.posthook.io/quickstart ) — get started in under 2 minutes
725+ - [ Pricing] ( https://posthook.io/pricing ) — free tier included
726+ - [ Status] ( https://status.posthook.io ) — uptime and incident history
727+
554728## Requirements
555729
556730- Python 3.9+
557731- [ httpx] ( https://www.python-httpx.org/ ) >= 0.25.0
732+ - [ websockets] ( https://websockets.readthedocs.io/ ) >= 12.0 (for WebSocket listener/stream)
0 commit comments