Official Python SDK for the Omnifox.io REST API v1 — inbox, CRM, boards, calendar, catalog, quotes and reports. Sync + async, verified end-to-end against a live workspace.
Project description
Omnifox — Official Python SDK
The official Python SDK for the Omnifox.io REST API v1 — an omnichannel customer messaging platform (WhatsApp, Webchat, Email, SMS, Telegram, and more).
Sync and async clients, pydantic models, retries with backoff, and cursor + page pagination. Every method in this release has been executed against a live Omnifox workspace — 154 calls covering every public method of every resource — so the shapes below are the ones the server actually accepts.
Since 0.3.0 the SDK covers the whole platform, not just the inbox: CRM (companies, deals, pipelines, activities), Boards, the Calendar, the product catalog and quotes, and the analytics reports.
Why this exists: at the time of writing, respond.io ships only a TypeScript SDK. Omnifox ships both Python and Node so your data team can build pipelines, your AI engineers can wire up agents, and your back-office automations don't have to reinvent the wheel.
Install
pip install omnifox
Requirements: Python 3.9+, httpx>=0.27, pydantic>=2.0.
Quickstart — sync
from omnifox import Omnifox
client = Omnifox("omf_live_...", workspace_id=1)
# Get a contact by id, email, phone, or external id
contact = client.contacts.get("email:ada@example.com")
# Upsert (create-or-update by email/phone/external_id)
contact = client.contacts.upsert(
email="ada@example.com",
first_name="Ada",
last_name="Lovelace",
)
# Send a message
client.messages.send(
contact="id:42",
channel_id="whatsapp_main",
text="Hello from the Omnifox SDK!",
idempotency_key="welcome-ada-2026-05-07",
)
# Iterate every contact (cursor pagination — server-friendly)
for c in client.contacts.list(pagination="cursor"):
print(c.id, c.display_name)
client.close() # or use `with Omnifox(...) as client:`
Beyond the inbox
# CRM
pipeline = client.pipelines.create(name="Ventas 2026")
stage = client.pipelines.create_stage(pipeline.id, name="Ganado", is_won=True)
deal = client.deals.create(title="Implementación ACME", pipeline_id=pipeline.id, amount=12_000)
client.deals.move_to_stage(deal.id, stage_id=stage.id, position=0)
client.deals.close(deal.id, result="won", close_reason_note="Firmado")
client.crm_activities.create("deal", deal.id, title="Kickoff", type="meeting")
# Boards
board = client.boards.create(name="Onboarding", template="blank")
columns = client.boards.columns(board.id)
item = client.board_items.create(board.id, title="Firmar contrato")
client.board_items.set_cell_value(item.id, columns[0]["id"], "Working on it")
# Calendar — a taken slot answers 409
appt = client.appointments.book(calendar_id=1, starts_at="2026-08-01T15:00:00Z", contact_id=42)
client.appointments.cancel(appt.id, reason="El cliente reagendó")
# Catalog + quotes
product = client.products.create(name="Plan Crece", unit_price=49)
quote = client.quotes.create(contact_id=42, items=[{"product_id": product.id, "quantity": 12}])
client.quotes.send(quote.id, email=True, whatsapp=True)
pdf_bytes = client.quotes.pdf(quote.id) # raw bytes, not JSON
# Reports
client.reports.overview(preset="30d")
client.reports.crm_funnel(preset="this_month")
Closing a deal needs the pipeline to own a stage flagged is_won / is_lost; without one the server refuses with 422. Board items are addressed flat at /boards/items/{id} — only creation goes through the board.
Workspaces
Tokens issued from the API keys screen are pinned to one workspace and the server resolves it from the token, so workspace_id is optional for them.
Pass it anyway if you can: four endpoints validate workspace_id as a required body field whatever the token is scoped to — users.update_status, teams.create, channels.connect and broadcasts.create. With workspace_id= set on the client the SDK fills it in; without it those four raise a clear ValueError before any HTTP happens, instead of returning an opaque 422.
Quickstart — async
import asyncio
from omnifox import AsyncOmnifox
async def main() -> None:
async with AsyncOmnifox("omf_live_...", workspace_id=1) as client:
async for contact in client.contacts.list(pagination="cursor"):
print(contact.id, contact.email)
await client.messages.send(
contact="phone:+593987654321",
whatsapp_template={"name": "welcome_v2", "language": "es"},
)
asyncio.run(main())
Flexible identifiers
Anywhere a resource takes an id, you may pass any of these forms — the SDK URL-encodes the value portion automatically:
| Form | Means |
|---|---|
42 |
numeric id |
"id:42" |
numeric id (explicit prefix, equivalent to 42) |
"email:foo@bar.com" |
look up by email |
"phone:+593987654321" |
look up by phone (E.164) |
"external:crm-12345" |
look up by your external CRM id |
client.contacts.get(42)
client.contacts.get("email:ada@example.com")
client.contacts.get("phone:+593987654321")
client.contacts.get("external:crm-12345")
Resources at a glance
Each line is one example call against a resource. All write methods accept an optional idempotency_key= keyword argument.
# Contacts ---------------------------------------------------------------
client.contacts.list(pagination="cursor")
client.contacts.get("email:ada@example.com")
client.contacts.create(first_name="Ada", email="ada@example.com")
client.contacts.update("id:42", first_name="Augusta")
client.contacts.upsert(email="ada@example.com", first_name="Ada")
client.contacts.merge(primary_id=42, duplicate_ids=[43, 44])
client.contacts.search("Ada")
client.contacts.attach_tag("id:42", tag_id=7)
client.contacts.attach_tags_by_name("id:42", names=["VIP", "newsletter"])
client.contacts.open_conversation("id:42")
client.contacts.close_conversation("id:42")
client.contacts.assign_conversation("id:42", user_id=99)
client.contacts.set_conversation_status("id:42", status="resolved")
client.contacts.comment("id:42", text="internal note")
# Conversations ----------------------------------------------------------
client.conversations.list(status="open")
client.conversations.get(123)
client.conversations.create(contact_id=42, channel_id=7)
client.conversations.assign(123, user_id=99)
client.conversations.close(123)
client.conversations.reopen(123)
client.conversations.add_note(123, text="follow up tomorrow")
# Messages ---------------------------------------------------------------
client.messages.send(contact="id:42", channel_id=7, text="Hi!")
client.messages.send(conversation_id=123, text="Reply in thread")
client.messages.send(contact="phone:+593...", whatsapp_template={"name": "welcome", "language": "es"})
client.messages.list(123, pagination="cursor")
client.messages.get(123, message_id=999)
# Channels ---------------------------------------------------------------
client.channels.list()
client.channels.get(7)
client.channels.connect(type="whatsapp", name="Sales WA", config={...})
client.channels.disconnect(7)
client.channels.health(7)
client.channels.templates(7)
# Broadcasts -------------------------------------------------------------
client.broadcasts.list()
client.broadcasts.create(name="Spring sale", channel_id=7, segment_filters={...})
client.broadcasts.cancel(99)
# Custom fields ----------------------------------------------------------
client.custom_fields.list()
client.custom_fields.create(name="LTV", type="number")
client.custom_fields.update(5, description="Lifetime value in USD")
client.custom_fields.delete(5)
# Tags -------------------------------------------------------------------
client.tags.list()
client.tags.create(name="VIP", color="#fbbf24")
client.tags.update(7, name="VIP+")
client.tags.delete(7)
# Snippets (canned responses) -------------------------------------------
client.snippets.list()
client.snippets.create(name="Greeting", shortcut="/hi", content_text="Hello {{first_name}}!")
client.snippets.update(3, content_text="Hi {{first_name}}!")
client.snippets.delete(3)
# Webhooks ---------------------------------------------------------------
client.webhooks.list()
client.webhooks.create(name="To Zapier", url="https://hooks.zapier.com/...", events=["message.created"])
client.webhooks.rotate_secret(5)
client.webhooks.delete(5)
# Workflows --------------------------------------------------------------
client.workflows.list()
client.workflows.activate(11)
client.workflows.deactivate(11)
client.workflows.trigger(11, contact_id=42, payload={"reason": "manual"})
# Users / Agents ---------------------------------------------------------
client.users.list() # alias: client.agents.list()
client.users.get(99)
client.users.update_status(99, status="online") # online | away | busy | offline
# Workspaces -------------------------------------------------------------
client.workspaces.list()
client.workspaces.create(name="Acme")
client.workspaces.update(1, timezone="America/Guayaquil")
# Teams ------------------------------------------------------------------
client.teams.list()
client.teams.create(name="Tier 1 Support", assignment_strategy="round_robin")
client.teams.add_member(2, user_id=99, role="lead")
client.teams.remove_member(2, user_id=99)
Every resource above is also available on AsyncOmnifox with the exact same API — write await in front of the call (and async for for list()).
Error handling
All non-2xx responses raise a subclass of OmnifoxApiError:
from omnifox import (
OmnifoxApiError,
OmnifoxRateLimitError,
OmnifoxValidationError,
)
try:
client.contacts.create(first_name="x")
except OmnifoxValidationError as e:
# 422 — request payload was rejected
print(e.field_errors) # {"email": ["required"], ...}
except OmnifoxRateLimitError as e:
# 429 — SDK already retried `max_retries` times before giving up
print("retry after", e.retry_after_sec, "seconds")
except OmnifoxApiError as e:
# any other 4xx/5xx
print(e.http_status, e.code, e.message, e.request_id)
Every exception carries:
http_status— the integer HTTP status code.code— the API error code (e.g."NOT_FOUND","VALIDATION").message— human-readable message.details— free-form details dict from the API.request_id— theX-Request-Idheader, if present. Include this when you contact support@omnifox.io.
Idempotency
Every write method accepts idempotency_key=. If the network drops mid-request and you retry with the same key within 24 hours, the API returns the original result without duplicating side effects.
client.messages.send(
contact="id:42",
text="One-time discount code: ABC123",
idempotency_key=f"discount-{order_id}",
)
Retries
The SDK retries on 429 (honouring Retry-After), 5xx, and connection errors using exponential backoff with full jitter. Tune via:
client = Omnifox(
api_key="...",
max_retries=5, # default 3
backoff_base=0.5, # initial backoff in seconds
backoff_max=30.0, # cap
timeout=60.0, # per-request timeout
)
Pagination
Two modes, controlled by the pagination= argument:
pagination="page"(default) — Laravel-style page numbers. Best for small/medium result sets.pagination="cursor"— server-sent opaque cursors. Use this for large exports — it's faster and more stable under concurrent writes.
Both modes return an iterator; the SDK fetches subsequent pages lazily.
# Stop after 1000 contacts:
for c in client.contacts.list(pagination="cursor", max_items=1000):
...
Comparison vs respond.io
| Feature | Omnifox Python SDK | respond.io |
|---|---|---|
| Python SDK | Yes — this package | No (only TypeScript) |
| Node/TypeScript SDK | Yes (@omnifox/sdk) |
Yes |
| Sync and async client | Yes | n/a |
| Pydantic models | v2, full coverage | n/a |
| Cursor pagination | First-class iterator | Manual |
| Idempotency keys | Per-method kwarg | Manual header |
| Open source | MIT | n/a |
If you're migrating from respond.io: most concepts map 1:1 (Contact, Conversation, Channel, Workflow, Broadcast). The main wins are typed responses and the sync/async parity.
Development
git clone https://github.com/omnifox/sdk-python
cd sdk-python
poetry install
poetry run pytest
poetry run mypy --strict omnifox
License
MIT — see LICENSE.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file omnifox-0.3.0.tar.gz.
File metadata
- Download URL: omnifox-0.3.0.tar.gz
- Upload date:
- Size: 37.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aebf7aed219c3f9fec6c6aed2e6e6c3f8fb661de222f7854a5e7ea8ee56b629d
|
|
| MD5 |
a0db1ce34cec7dfb5cb4942e6149aaca
|
|
| BLAKE2b-256 |
c0febbbc287074eeb0024ec429e0773efd50fafeea88f91c78b978ab0ea7d8a3
|
File details
Details for the file omnifox-0.3.0-py3-none-any.whl.
File metadata
- Download URL: omnifox-0.3.0-py3-none-any.whl
- Upload date:
- Size: 50.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d41ddc50481f70cd15340ab9e25e199d62ee535887e9c7e7b8bf0f36b54a419e
|
|
| MD5 |
4e82da7a15ffd40b56f8ee4badd3ee48
|
|
| BLAKE2b-256 |
e4172cb62a653ad2cae1e6681b8aca2b9ac8c0f447d2b89a07d50d7833374df1
|