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.
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
Tabi does not provide /send-otp or /verify-otp. You generate the code, store a hash (or ciphertext) with a TTL, verify input with constant-time comparison, and send the plaintext with POST /channels/{channelId}/send. Tabi carries the message; you own crypto, storage, rate limits, and compliance.
Helpers (optional): 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. Follow Meta / local rules for opt-in and templates.
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")
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
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 tabi_sdk-0.2.0.tar.gz.
File metadata
- Download URL: tabi_sdk-0.2.0.tar.gz
- Upload date:
- Size: 9.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
88b7ab18382d8f7e86e351a0a007541ad5836162e2f4eecc99ff0ff6578beea7
|
|
| MD5 |
253842758281136c9675bc2f40419f85
|
|
| BLAKE2b-256 |
39d61cd8321cc2ef61a7bf5ec42a9fb891b36950c7a3b2640050db311d1b5783
|
File details
Details for the file tabi_sdk-0.2.0-py3-none-any.whl.
File metadata
- Download URL: tabi_sdk-0.2.0-py3-none-any.whl
- Upload date:
- Size: 17.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2664cad33081c26565467c70b71cc97f055aaaeb1185c6bfb05e84c5142deed5
|
|
| MD5 |
aeaff6dc34fc62151a360d8914a7f221
|
|
| BLAKE2b-256 |
310e23898139e29e2ea9d24a5744ca1d92766e0734141a3067f2d6a0dad522a3
|