Skip to main content

Official Python SDK for the Omnifox.io REST API v1 — omnichannel messaging, CRM and automation. 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 — 77 calls covering every public method of every resource — so the shapes below are the ones the server actually accepts.

PyPI Python License: MIT

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:`

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 — the X-Request-Id header, 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

omnifox-0.2.0.tar.gz (28.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

omnifox-0.2.0-py3-none-any.whl (41.9 kB view details)

Uploaded Python 3

File details

Details for the file omnifox-0.2.0.tar.gz.

File metadata

  • Download URL: omnifox-0.2.0.tar.gz
  • Upload date:
  • Size: 28.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for omnifox-0.2.0.tar.gz
Algorithm Hash digest
SHA256 65fc4585a9c3a083dda3dddb88c65282e0794177b43a8f22b62d6d02bc8d1157
MD5 542dfdf9732c01181ef77af597580216
BLAKE2b-256 4a8aaaa43e827bb19bf07980d0a22be06bee06368ad8f735e04983497478ffa8

See more details on using hashes here.

File details

Details for the file omnifox-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: omnifox-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 41.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for omnifox-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 675c490fa164fa215c6a48ef8821a34d43fa789277370f380bf04ee63f5be51f
MD5 b5594e89ab877a7b33c347337b3b71ff
BLAKE2b-256 ae87c843dcacdebda09dfc4d0efe3a5f74fd4c092c8342882ea2ade57a067b5f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page