Skip to main content

Python SDK (converger)

converger is the server-side Python SDK: REST clients built on httpx (sync and async), a Protocol v1 WebSocket client with resume built on websockets, Pydantic v2 models and webhook signature verification. Source: packages/client-python. What all SDKs share is described in Server-side SDKs.

pip install converger

Python 3.10 or later. Dependencies: httpx, pydantic 2, websockets 13 or later.

To build an AI agent or bot that answers every conversation of a channel (WhatsApp, the web widget, ...) over one socket, use the Python agent SDK (converger-agent) instead.

Clients​

from converger import Converger, AsyncConverger

cvg = Converger("https://chat.example.com", api_key="cvg_live_...", channel_secret="...")
ArgumentUsed for
api_keycvg.tenant_api (x-api-key)
channel_secretcvg.client_api.generate_token() (Authorization: Bearer <secret>)
timeout, headers, transport, http_clientpassed to httpx

Both are server-side credentials; never ship them to a browser or app. AsyncConverger has the same methods as coroutines and adds socket().

Client API (cvg.client_api)​

MethodEndpointReturns
generate_token(user_id=None, scope=None)POST /api/v1/converger/tokens/generateTokenResponse
refresh_token(token)POST /api/v1/converger/tokens/refreshTokenResponse
create_conversation(token)POST /api/v1/converger/conversationsConversationToken (conversation_id, conversation-bound token, stream_url)
get_conversation(token, id, watermark=None)GET /api/v1/converger/conversations/:idConversationToken
close_conversation(token, id), reopen_conversation(token, id)POST .../close, .../reopenConversationStatus
post_activity(token, id, text=, type=, from_id=, attachments=, reply_to_id=, channel_data=, idempotency_key=)POST .../activitiesResourceResponse (id)
list_activities(token, id, watermark=None, limit=None)GET .../activitiesClientActivityPage (activities, watermark, has_more)

Tenant API (cvg.tenant_api)​

MethodEndpointReturns
list_conversations(limit=, cursor=, status=, channel_id=, external_id=)GET /api/v1/conversationsPage[Conversation] (data, next_cursor, has_more)
get_conversation(id), close_conversation(id), reopen_conversation(id)/api/v1/conversations/:id[...]Conversation
post_activity(id, text=, type=, sender=, attachments=, metadata=, reply_to_id=, idempotency_key=)POST .../activitiesActivity (with seq)
list_activities(id, watermark=None, limit=None)GET .../activitiesActivityPage (data, watermark, has_more)

watermark on both page models is int | str | None: the integer seq from v3.0 servers, an opaque string from older ones (migration). Pass it back unchanged as watermark=.

Models keep unknown fields (model_extra), so fields the server adds later are not lost.

Errors​

Every non-2xx response raises converger.ApiError with status, code, message, details, request_id, retry_after and the raw body (normalization rules).

from converger import ApiError

try:
cvg.client_api.post_activity(token, conversation_id, text="late")
except ApiError as err:
if err.code == "conversation_closed":
...

WebSocket (Protocol v1)​

import asyncio
from converger import AsyncConverger

async def main():
async with AsyncConverger("https://chat.example.com", channel_secret="...") as cvg:
token = await cvg.client_api.generate_token(user_id="user-42")
conv = await cvg.client_api.create_conversation(token.token)

async with cvg.socket(conv.token, conversation_id=conv.conversation_id) as sock:
ack = await sock.send_text("Where is my order?") # Ack(client_id, id, seq, duplicate)
async for frame in sock: # dicts, persistent frames in seq order, once
print(frame["type"], frame.get("data"))

asyncio.run(main())
ConvergerSocket
cvg.socket(token, conversation_id=None, watermark=0, **options)token is a string or a (sync or async) callable that returns a fresh token on every connect
await sock.send_text(text, client_id=None, metadata=None, reply_to=None)waits for the ack; raises ProtocolError (code, retryable, retry_after_ms) when refused
await sock.send_message(type, data, ...)any message type
await sock.recv() / async for frame in sockthe next frame for the application; raises ConnectionClosed (code, reason) once the socket ended for good
await sock.typing(True), await sock.read(watermark=None), await sock.refresh(token)typing, read and auth frames
sock.watermark, sock.conversation_id, sock.welcomeresume position, conversation, last welcome data
optionsreconnect=True, max_retries=None, backoff_initial=0.5, backoff_max=30, open_timeout=10, ack_timeout=30, headers

Store sock.watermark to resume a later connection exactly where this one stopped. converger.protocol.Session is the transport-independent resume state machine if you bring your own transport.

Webhook signature verification​

from converger import WebhookSignatureError, verify_signature, is_valid_signature

verify_signature(headers, raw_body, secret) # raises WebhookSignatureError(reason)
verify_signature(headers, raw_body, [new_secret, old_secret]) # while rotating
is_valid_signature(headers, raw_body, secret, tolerance=300) # bool

headers is any mapping or list of pairs (Starlette, Werkzeug, httpx and plain dicts work; names are matched case-insensitively). Pass the raw body bytes. reason is one of missing_signature, no_secret, malformed_signature, timestamp_out_of_tolerance, invalid_signature. See verifying the signature.

FastAPI​

from fastapi import FastAPI, HTTPException, Request
from converger import WebhookEvent, WebhookSignatureError, verify_signature

app = FastAPI()

@app.post("/converger/webhook")
async def webhook(request: Request):
body = await request.body()
try:
verify_signature(request.headers, body, CONVERGER_WEBHOOK_SECRET)
except WebhookSignatureError as exc:
raise HTTPException(status_code=401, detail=exc.reason)
event = WebhookEvent.model_validate_json(body)
# deduplicate on request.headers["x-converger-delivery-id"]
return {"ok": True}

Flask​

from flask import Flask, abort, request
from converger import WebhookEvent, WebhookSignatureError, verify_signature

app = Flask(__name__)

@app.post("/converger/webhook")
def webhook():
body = request.get_data()
try:
verify_signature(request.headers, body, CONVERGER_WEBHOOK_SECRET)
except WebhookSignatureError:
abort(401)
event = WebhookEvent.model_validate_json(body)
return {"ok": True}

Development​

cd packages/client-python
pip install -e '.[test]'
pytest # unit, transport and conformance tests

The end-to-end test is described in Server-side SDKs. Releases are tags client-python/vX.Y.Z.