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

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 (risk reduction)

Tabi implements delivery and OTP storage; you and your customers remain responsible for Meta’s rules for the WhatsApp Business Account. In practice:

  • Consent: Only message people who agreed to WhatsApp contact for that purpose (e.g. checked a box on the site, or an existing transactional relationship—follow Meta’s opt-in guidance for your region).
  • Purpose: Use OTP / login flows for authentication or transactional use cases; do not use them as a cover for cold marketing or spam.
  • Templates: Where Meta requires pre-approved message templates (categories differ by use case and region), register and use them; for many authentication flows Meta expects Authentication-style templates—confirm in Meta’s WhatsApp documentation for your WABA.
  • Quality: High block/report rates hurt your phone number quality rating. Rate-limit sends and verification attempts; avoid blasting users.
  • Accuracy: Message content should match what the user opted into; do not mislead.

Nothing in software alone “guarantees” approval; when in doubt, review Meta’s current Commerce and Business 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.0.tar.gz (10.7 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.0-py3-none-any.whl (18.7 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: tabi_sdk-0.3.0.tar.gz
  • Upload date:
  • Size: 10.7 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.0.tar.gz
Algorithm Hash digest
SHA256 77f7c8e6c826b47dfda81db09f79a7fdd1bc258e67b59099ab5f573d87be9c9b
MD5 9a2a0644111257df316e48e51501072f
BLAKE2b-256 26fd666e35875c5d1e5e2bccba49410447e81860c62f2703e08f5f4f7891811a

See more details on using hashes here.

File details

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

File metadata

  • Download URL: tabi_sdk-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 18.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.0-py3-none-any.whl
Algorithm Hash digest
SHA256 460937b52c38e7e80fccb5db99ce22df1c0f93173c2720d41b5899eb4487ba67
MD5 8418e46397ec0509638fb8266f41966d
BLAKE2b-256 2c84f91d6dc9ecafd2e4df238d3a67ac425ad13e1955d66887d8ba3cd0c198c5

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