Skip to main content

Official Python SDK for the Tabi WhatsApp business messaging API

Project description

tabi-sdk (Python)

Official Python client for the Tabi WhatsApp Business API. Matches the JavaScript and PHP SDKs where the REST surface is the same.

PyPI License: MIT

Install

pip install tabi-sdk

From this repo (editable):

cd packages/tabi-sdk-python
pip install -e .

Quick start

import os

from tabi_sdk import TabiClient

client = TabiClient(
    api_key=os.environ["TABI_API_KEY"],
    base_url="https://api.tabi.africa/api/v1",
)

client.messages.send(
    "your-channel-id",
    {
        "to": "2347000000000",
        "content": "Hello",
    },
)

Create an API key under Developer → API keys. Use the channel ID from Channels (URL or detail view). to is international digits only, no +.

Prefer loading the key from the environment (see below); do not commit keys or paste them into shared chat logs.

Configuration

import os
from tabi_sdk import TabiClient

client = TabiClient(
    api_key=os.environ["TABI_API_KEY"],
    base_url=os.environ.get("TABI_BASE_URL", "https://api.tabi.africa/api/v1"),
    timeout=30.0,
)

Client layout

Attribute REST area
auth, workspaces Auth, workspaces, members
channels, messages, conversations, contacts, quick_replies, notifications Lines, sends, inbox
automation_templates, automation_installs, campaigns Automations, broadcasts
api_keys, webhooks, integrations Keys, webhooks, integrations
files, analytics Media, metrics

Request parameters, webhook events, and the full API surface

Use this README for copy-paste examples, the send body table below, webhook event names, and the Resources section (everything exposed on TabiClient in this SDK).

When you need the complete contract for an endpoint—every optional field, enum, query string, and response type—open the Tabi Dashboard → Developer → API reference (OpenAPI) and treat that as the source of truth.

messages.send — JSON body for POST /channels/{channelId}/send

Field Required Description
to yes Recipient phone (digits; international, no leading + in JSON).
content yes Text or caption, max 4096 chars.
messageType no text (default), image, video, audio, document.
mediaUrl for media Required when messageType is not text (public URL or base64 data URI).
messageClass no transactional (default for API), conversational_reply, triggered_followup, broadcast.
contactName no Display name when creating a new contact.
channelId no Optional; if set, must match the channelId in the URL path.

Stickers, polls, location, contacts, reactions, etc. use other endpoints (see Messages under Resources); those have their own bodies in OpenAPI.

Webhook events (for client.webhooks.create)

Subscribe with a list of event names. Use * to receive all. Common values:

Event Meaning
message.inbound New message received on a channel.
message.status Outbound message status changed (e.g. delivered, read, failed).
conversation.created New conversation started.

Delivery body shape: { "event": "<name>", "data": { ... }, "timestamp": "<ISO8601>" }. Verify signatures using your subscription secret (see API reference).

OTP over WhatsApp

Hosted OTP on the Tabi API (recommended)

The API can generate the code, store a hash in Redis, send WhatsApp, and verify submissions—same auth as other channel routes (messages:send scope, etc.):

client.channels.send_otp("channel-uuid", {"phone": "+2347000000000"})
client.channels.verify_otp("channel-uuid", {"phone": "+2347000000000", "code": "123456"})

REST: POST /channels/{channelId}/otp/send, POST /channels/{channelId}/otp/verify. Your customer’s site calls your backend; your backend calls Tabi with the workspace API key.

WhatsApp / Meta policy (what Tabi covers vs. what you control)

On the Tabi side, OTP traffic goes through the same WhatsApp Business connection as your other sends: we queue transactional delivery, enforce API rate limits on send/verify, and keep codes off your logs—so typical integration mistakes (blasting numbers, hammering verify, treating OTP like a broadcast channel) are blocked before they become a Meta policy incident.

Templates and WABA rules still live in Meta. You manage your WhatsApp Business Account and any message templates Meta requires for your region or use case inside Meta Business Manager. Tabi does not replace that—but you are not expected to hand-craft low-level WhatsApp protocol details in your app; you call the API, and we route delivery appropriately.

What we ask of you and your customers: use OTP only for real sign-in / verification flows, only for people who should get that message (e.g. they just requested a code on your site), and do not abuse the API (no cold outreach, no marketing disguised as OTP, no trying to bypass limits). Follow Meta’s WhatsApp documentation for opt-in and template rules that apply to your WABA.

No platform can promise Meta will never flag an account; if something is unclear, check Meta’s current policies and your BSP’s guidance.

DIY flow (optional helpers)

If you do not use the hosted OTP routes, you can still send a custom body with POST /channels/{channelId}/send and manage storage yourself. Helpers: generate_numeric_otp, normalize_phone_digits, build_otp_message.

import hashlib
import hmac
import os

import redis  # or your own store

from tabi_sdk import TabiClient, TabiError, build_otp_message, generate_numeric_otp, normalize_phone_digits

r = redis.Redis.from_url(os.environ["REDIS_URL"])
client = TabiClient(api_key=os.environ["TABI_API_KEY"])
channel_id = os.environ["TABI_WHATSAPP_CHANNEL_ID"]


def send_login_otp(raw_phone: str) -> None:
    phone = normalize_phone_digits(raw_phone)
    code = generate_numeric_otp(6, secure=True)
    key = f"otp:login:{phone}"
    payload = hashlib.sha256(f"{code}:{phone}".encode()).hexdigest()
    r.setex(key, 600, payload)

    body = build_otp_message(code=code, brand_name="MyApp", expiry_minutes=10)
    try:
        client.messages.send(
            channel_id,
            {"to": phone, "content": body, "messageClass": "transactional"},
        )
    except TabiError as e:
        raise RuntimeError(f"send failed: {e.status}") from e


def verify_login_otp(raw_phone: str, user_entered: str) -> bool:
    phone = normalize_phone_digits(raw_phone)
    key = f"otp:login:{phone}"
    stored = r.get(key)
    if not stored:
        return False
    expected = stored.decode()
    candidate = hashlib.sha256(f"{user_entered.strip()}:{phone}".encode()).hexdigest()
    if not hmac.compare_digest(candidate, expected):
        return False
    r.delete(key)
    return True

Operational notes:

  • Do not log OTPs or bodies that contain them. Use generic errors to users (“Invalid or expired code”).
  • Cap send and verify attempts per phone (and per IP on web). On HTTP 429 from the API, back off; do not retry in a tight loop.
  • Use messageClass: "transactional" for OTP-style traffic when using messages.send.

Resources

Method names are snake_case. JSON bodies use the same keys as the HTTP API (e.g. refreshToken).

Auth

client.auth.login(os.environ["TABI_USER_EMAIL"], os.environ["TABI_USER_PASSWORD"])
client.auth.register({...})
client.auth.refresh("refresh_token_from_api")
client.auth.me()
client.auth.logout()
client.auth.invite_preview("invite_token")

Channels

client.channels.list()
client.channels.get("channel-id")
client.channels.create({"name": "Support", "provider": "go-whatsapp"})
client.channels.connect("channel-id")
client.channels.disconnect("channel-id")
client.channels.status("channel-id")
client.channels.update("channel-id", {"name": "Renamed"})
client.channels.reconnect("channel-id")
client.channels.delete("channel-id")
client.channels.send_otp("channel-id", {"phone": "+2347000000000"})
client.channels.verify_otp("channel-id", {"phone": "+2347000000000", "code": "123456"})

Messages

client.messages.send(
    "channel-id",
    {"to": "2347000000000", "content": "Hello", "messageClass": "transactional"},
)
client.messages.send(
    "channel-id",
    {
        "to": "2347000000000",
        "content": "See image",
        "messageType": "image",
        "mediaUrl": "https://cdn.example.com/a.png",
    },
)
client.messages.get("message-id")
client.messages.list_by_conversation("conversation-id", {"page": 1, "limit": 50})
client.messages.reply("conversation-id", {"content": "Reply text"})
client.messages.send_sticker("channel-id", {...})
client.messages.send_contact("channel-id", {...})
client.messages.send_location("channel-id", {"to": "234...", "latitude": 6.5, "longitude": 3.3})
client.messages.send_poll("channel-id", {...})
client.messages.react("channel-id", "message-id", {"emoji": "👍"})
client.messages.mark_read("channel-id", "message-id")
client.messages.revoke("channel-id", "message-id")
client.messages.edit("channel-id", "message-id", {"content": "Edited"})
client.messages.download_media("channel-id", "message-id")

Contacts

client.contacts.list({"page": 1, "search": "John"})
client.contacts.get("contact-id")
client.contacts.create({"phone": "2347000000000", "firstName": "Jane"})
client.contacts.update("contact-id", {"firstName": "Janet"})
client.contacts.delete("contact-id")
client.contacts.import_contacts({"contacts": [...]})
client.contacts.get_tags("contact-id")
client.contacts.add_tag("contact-id", "vip")
client.contacts.remove_tag("contact-id", "vip")
client.contacts.opt_in("contact-id")
client.contacts.opt_out("contact-id")

Conversations

client.conversations.list({"status": "open", "page": 1})
client.conversations.get("conversation-id")
client.conversations.update("conversation-id", {"assignedTo": "member-uuid"})
client.conversations.resolve("conversation-id")
client.conversations.reopen("conversation-id")
client.conversations.mark_read("conversation-id")

Webhooks

client.webhooks.create({...})
client.webhooks.list()
client.webhooks.get("id")
client.webhooks.update("id", {...})
client.webhooks.delete("id")
client.webhooks.ping("id")
client.webhooks.rotate_secret("id")
client.webhooks.delivery_logs({"page": 1})
client.webhooks.start_test_capture()
client.webhooks.stop_test_capture()
client.webhooks.test_capture_status()

API keys

Creating keys needs a user JWT, not a workspace API key.

client.api_keys.create(
    {"name": "Production", "scopes": ["messages:send", "channels:read"]}
)
client.api_keys.list()
client.api_keys.revoke("key-id")
client.api_keys.delete("key-id")

Files

client.files.list()
client.files.get("file-id")
client.files.get_url("file-id")
client.files.delete("file-id")

Campaigns

client.campaigns.create({...})
client.campaigns.list({"page": 1})
client.campaigns.get("id")
client.campaigns.update("id", {...})
client.campaigns.delete("id")
client.campaigns.schedule("id")
client.campaigns.pause("id")
client.campaigns.resume("id")
client.campaigns.cancel("id")

Automation templates

client.automation_templates.list()
client.automation_templates.get("template-id")

Automation installs

client.automation_installs.install({...})
client.automation_installs.list()
client.automation_installs.get("id")
client.automation_installs.update("id", {...})
client.automation_installs.enable("id")
client.automation_installs.disable("id")
client.automation_installs.uninstall("id")

Quick replies

client.quick_replies.list()
client.quick_replies.create({"shortcut": "/hi", "body": "Hello!"})
client.quick_replies.update("id", {"body": "..."})
client.quick_replies.delete("id")

Analytics

client.analytics.dashboard({"from": "2026-01-01", "to": "2026-01-31"})
client.analytics.channels({...})
client.analytics.conversations({...})

Notifications

client.notifications.list({"page": 1})
client.notifications.mark_read("id")
client.notifications.mark_all_read()
client.notifications.unread_count()

Integrations

client.integrations.list_providers()
client.integrations.create({...})
client.integrations.list()
client.integrations.get("id")
client.integrations.update("id", {...})
client.integrations.delete("id")
client.integrations.test("id")

Workspaces

client.workspaces.list()
client.workspaces.get("workspace-id")
client.workspaces.create({"name": "Team"})
client.workspaces.update("workspace-id", {"name": "Renamed"})
client.workspaces.list_members("workspace-id")
client.workspaces.invite_member("workspace-id", {"email": "member@example.com", "roleSlug": "admin"})

Error handling

Failures raise TabiError with status, body, and a message.

import os

from tabi_sdk import TabiClient, TabiError

client = TabiClient(api_key=os.environ["TABI_API_KEY"], base_url="https://api.tabi.africa/api/v1")

try:
    client.messages.send("channel-id", {"to": "234...", "content": "Hi"})
except TabiError as e:
    print(e.status, e.body)
Status Typical cause
400 Invalid payload
401 Bad or expired credential
403 Missing scope
404 Not found
429 Rate limited
500 Server error

Requirements

  • Python >= 3.10
  • requests >= 2.28

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

tabi_sdk-0.3.2.tar.gz (11.6 kB view details)

Uploaded Source

Built Distribution

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

tabi_sdk-0.3.2-py3-none-any.whl (19.7 kB view details)

Uploaded Python 3

File details

Details for the file tabi_sdk-0.3.2.tar.gz.

File metadata

  • Download URL: tabi_sdk-0.3.2.tar.gz
  • Upload date:
  • Size: 11.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.2

File hashes

Hashes for tabi_sdk-0.3.2.tar.gz
Algorithm Hash digest
SHA256 5f1ada4fe1e422a3a36b936aa0012e9ecb9041e27601639ee8315199dfd09fae
MD5 573b1066d9914ac1765c1a8762a945dd
BLAKE2b-256 0351fc4a299f676f5c874e5553726fb55d2d843b86ef364139cef9f46da0b56f

See more details on using hashes here.

File details

Details for the file tabi_sdk-0.3.2-py3-none-any.whl.

File metadata

  • Download URL: tabi_sdk-0.3.2-py3-none-any.whl
  • Upload date:
  • Size: 19.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.2

File hashes

Hashes for tabi_sdk-0.3.2-py3-none-any.whl
Algorithm Hash digest
SHA256 8deabc038ee2b96c0fe3c9bf63e832522241180e0115f4cabca1e5b53075ecfd
MD5 0646e22bd1815ad4f1403c07d8c8bd2e
BLAKE2b-256 9682016038ddb08bea8300eb4e4446f1579c63e1b120a6131ba7f44a84a8c409

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