tabi_sdk (Dart / Flutter)

Tabi is a WhatsApp Business messaging platform. This package is the official Dart / Flutter client: send messages, manage channels, webhooks, campaigns, and automations using the same JSON bodies as the PHP and Python SDKs.

pub package License: MIT API docs

Links: Tabi · HTTP / OpenAPI reference · Repository · Issue tracker

Search keywords: WhatsApp API, WhatsApp Business API, messaging, Tabi, Flutter SDK, Dart HTTP client.

Table of contents


Install

pub.dev (after the package is published):

dependencies:
  tabi_sdk: ^0.1.0

Git (always available):

dependencies:
  tabi_sdk:
    git:
      url: https://github.com/Tabi-messaging/tabi-sdk-dart.git
      ref: main

Then:

dart pub get
# or, in a Flutter app:
flutter pub get

Quick start

Send a text message from server-side or trusted code using a workspace or channel API key:

import 'package:tabi_sdk/tabi_sdk.dart';

Future<void> main() async {
  final client = TabiClient(
    'tk_your_api_key',
    baseUrl: 'https://api.tabi.africa/api/v1',
  );

  await client.messages().send('your-channel-id', {
    'to': '2348012345678',
    'content': 'Hello from Dart!',
  });
}
  • to: digits only, international format, no + in the JSON value (same as other SDKs).
  • channel-id: from the dashboard (Channels → open a channel → ID in the URL or detail view).
  • API key: create under Developer → API keys. Load from environment or a secret store; do not embed keys in mobile or browser apps.

Configuration

import 'package:tabi_sdk/tabi_sdk.dart';

// Resolve `apiKey` from your app: e.g. compile-time defines, env, or a secrets service.
final client = TabiClient(
  apiKey,
  baseUrl: 'https://api.tabi.africa/api/v1',
  connectTimeout: const Duration(seconds: 30),
  receiveTimeout: const Duration(seconds: 30),
);

Passing the key safely

  • CLI / tests: dart run --dart-define=TABI_API_KEY=tk_... then const String.fromEnvironment('TABI_API_KEY').
  • Flutter: flutter_dotenv, --dart-define, or native secure storage—not hard-coded strings in source control.
  • Server / desktop: dart:io Platform.environment['TABI_API_KEY'] where appropriate.

Default base URL if you omit the second argument: https://api.tabi.africa/api/v1.


Where credentials come from

What Where to get it
API key or user JWT Dashboard → DeveloperAPI keys, or login flow for a JWT
Base URL Usually https://api.tabi.africa/api/v1 (default in the client)
Channel ID Channels → open a channel → copy the ID from the URL or screen

Some operations (for example creating API keys) require a user JWT, not a channel key. The method descriptions below mention that where it matters.


How the client is organised

TabiClient exposes resource groups. Each group maps to an area of the REST API (same idea as PHP ->messages() and Python client.messages).

Group on TabiClient REST areas
auth(), workspaces() Auth, workspaces, members, invites
channels(), messages(), conversations(), contacts(), quickReplies(), notifications() Lines, sends, inbox, people, shortcuts
automationTemplates(), automationInstalls(), campaigns() Template catalog, installed flows, broadcasts
apiKeys(), webhooks(), integrations() API keys (JWT for create), webhooks, third-party links
files(), analytics() Uploads, metrics

Method names in Dart are camelCase. JSON keys in maps are exactly as in the HTTP API (camelCase), e.g. refreshToken, assignedTo.


Request bodies and the full API

Use this README for copy-paste examples, the messages().send table below, webhook event names, and the Resources section.

For every optional field, enum, query parameter, and response shape, use the Dashboard → Developer → API reference (OpenAPI) as the source of truth.


Send message body

Maps to POST /channels/{channelId}/send (messages().send in this SDK).

Field Required Description
to yes Recipient phone: digits only, international, no leading + in JSON.
content yes Text or caption, up to 4096 characters.
messageType no text (default), image, video, audio, document.
mediaUrl for media Public URL or base64 data URI when messageType is not text.
messageClass no e.g. transactional (typical for API), conversational_reply, triggered_followup, broadcast.
contactName no Display name when creating a new contact.
channelId no If set, must match the channel in the URL.

Stickers, polls, location, contacts, reactions, etc. use other methods on messages() (see Resources); see OpenAPI for their bodies.


Webhook events

When creating a webhook subscription, pass a list of event names. Use * for all. Common values:

Event Meaning
message.inbound New inbound message on a channel.
message.status Outbound status update (delivered, read, failed, …).
conversation.created New conversation.

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


OTP over WhatsApp

The API can generate the code, store a hash, send WhatsApp, and verify—use the same credential as for messages().send (for example workspace API key with messages:send).

await client.channels().sendOtp('channel-uuid', {
  'phone': '+2347000000000',
});

await client.channels().verifyOtp('channel-uuid', {
  'phone': '+2347000000000',
  'code': '123456',
});

REST: POST /channels/{channelId}/otp/send, POST /channels/{channelId}/otp/verify.

Security: call these only from your backend. Never put the Tabi API key in a mobile or web client; the customer app talks to your server, and your server calls Tabi.

Compliance

  • OTP uses the same WhatsApp Business channel and rate limits as other sends.
  • Follow Meta / WhatsApp policies for templates, opt-in, and WABA setup (Meta docs).
  • Use OTP only for real verification flows (e.g. sign-in), not for cold outreach or marketing.

Custom OTP (without hosted routes)

Generate codes, hash them, store them in your database or Redis, then send the text with messages().send and messageClass appropriate for transactional traffic. The Python SDK documents a full DIY pattern with helpers; in Dart you implement storage yourself and use the same JSON bodies as in OpenAPI.


Resources (all methods)

Auth

Login, register, tokens, session, invite preview.

await client.auth().login('user@example.com', 'password');
await client.auth().register({...});
await client.auth().refresh('refresh_token_from_api');
await client.auth().me();
await client.auth().logout();
await client.auth().invitePreview('invite_token');

Channels

await client.channels().list();
await client.channels().get('channel-id');
await client.channels().create({'name': 'Support', 'provider': 'messaging'});
await client.channels().connect('channel-id');
await client.channels().connect('channel-id', {'optional': 'body'});
await client.channels().disconnect('channel-id');
await client.channels().status('channel-id');
await client.channels().update('channel-id', {'name': 'Renamed'});
await client.channels().reconnect('channel-id');
await client.channels().delete('channel-id');

await client.channels().sendOtp('channel-id', {'phone': '+2347000000000'});
await client.channels().verifyOtp('channel-id', {
  'phone': '+2347000000000',
  'code': '123456',
});

update typically needs a user JWT (not only a channel key)—see OpenAPI.

Messages

await client.messages().send('channel-id', {
  'to': '2347000000000',
  'content': 'Hello',
  'messageClass': 'transactional',
});

await client.messages().send('channel-id', {
  'to': '2347000000000',
  'content': 'See image',
  'messageType': 'image',
  'mediaUrl': 'https://cdn.example.com/a.png',
});

await client.messages().get('message-id');
await client.messages().listByConversation('conversation-id', {'page': 1, 'limit': 50});
await client.messages().reply('conversation-id', {'content': 'Reply text'});

await client.messages().sendSticker('channel-id', {...});
await client.messages().sendContact('channel-id', {...});
await client.messages().sendLocation('channel-id', {
  'to': '234...',
  'latitude': 6.5,
  'longitude': 3.3,
});
await client.messages().sendPoll('channel-id', {...});
await client.messages().react('channel-id', 'message-id', {'emoji': '👍'});
await client.messages().markRead('channel-id', 'message-id');
await client.messages().revoke('channel-id', 'message-id');
await client.messages().edit('channel-id', 'message-id', {'content': 'Edited'});
await client.messages().downloadMedia('channel-id', 'message-id');

Contacts

await client.contacts().list({'page': 1, 'search': 'John'});
await client.contacts().get('contact-id');
await client.contacts().create({'phone': '2347000000000', 'firstName': 'Jane'});
await client.contacts().update('contact-id', {'firstName': 'Janet'});
await client.contacts().delete('contact-id');
await client.contacts().import({'contacts': []});
await client.contacts().getTags('contact-id');
await client.contacts().addTag('contact-id', 'vip');
await client.contacts().removeTag('contact-id', 'vip');
await client.contacts().optIn('contact-id');
await client.contacts().optOut('contact-id');

Conversations

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

Webhooks

await client.webhooks().create({...});
await client.webhooks().list();
await client.webhooks().get('id');
await client.webhooks().update('id', {...});
await client.webhooks().delete('id');
await client.webhooks().ping('id');
await client.webhooks().rotateSecret('id');
await client.webhooks().deliveryLogs({'page': 1});
await client.webhooks().startTestCapture({...});
await client.webhooks().stopTestCapture({...});
await client.webhooks().testCaptureStatus({...});

API keys

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

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

Files

await client.files().list();
await client.files().get('file-id');
await client.files().getUrl('file-id');
await client.files().delete('file-id');

Campaigns

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

Automation templates

await client.automationTemplates().list();
await client.automationTemplates().get('template-id');

Automation installs

await client.automationInstalls().install({...});
await client.automationInstalls().list();
await client.automationInstalls().get('id');
await client.automationInstalls().update('id', {...});
await client.automationInstalls().enable('id');
await client.automationInstalls().disable('id');
await client.automationInstalls().uninstall('id');

Quick replies

await client.quickReplies().list();
await client.quickReplies().create({'shortcut': '/hi', 'body': 'Hello!'});
await client.quickReplies().update('id', {'body': '...'});
await client.quickReplies().delete('id');

Analytics

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

Notifications

await client.notifications().list({'page': 1});
await client.notifications().markRead('id');
await client.notifications().markAllRead();
await client.notifications().unreadCount();

Integrations

await client.integrations().listProviders();
await client.integrations().create({...});
await client.integrations().list();
await client.integrations().get('id');
await client.integrations().update('id', {...});
await client.integrations().delete('id');
await client.integrations().test('id');

Workspaces

await client.workspaces().list();
await client.workspaces().get('workspace-id');
await client.workspaces().create({'name': 'Team'});
await client.workspaces().update('workspace-id', {'name': 'Renamed'});
await client.workspaces().listMembers('workspace-id');
await client.workspaces().inviteMember('workspace-id', {
  'email': 'member@example.com',
  'roleSlug': 'admin',
});

Error handling

Failed calls throw TabiException with message, statusCode, and optional body (decoded JSON when the API returns JSON).

import 'package:tabi_sdk/tabi_sdk.dart';

try {
  await client.messages().send('channel-id', {'to': '234...', 'content': 'Hi'});
} on TabiException catch (e) {
  print('${e.statusCode} ${e.message}');
  print(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

On 429, back off; do not retry in a tight loop.


Return values

Successful responses are decoded JSON (Map, List, or scalar) after the API envelope is unwrapped. Exact shapes match the OpenAPI schemas for each endpoint.


Requirements

  • Dart SDK >= 3.0.0 (Flutter apps use the same environment constraint in pubspec.yaml).
  • Dependency: dio (HTTP client).
  • Platforms: Android, iOS, Linux, macOS, Web, and Windows (pure Dart + dio; no dart:io in lib/).

API documentation (dartdoc)

After you add the dependency, browse generated docs on pub.dev:

pub.dev/documentation/tabi_sdk

That page lists TabiClient, resource classes, and methods. For request/response field details, use the OpenAPI reference in the Tabi dashboard.


Support

  • Bugs and feature requests: GitHub issues
  • Product / API questions: tabi.africa and in-dashboard Developer → API reference


License

MIT — see LICENSE.

Libraries

tabi_sdk
Official Dart / Flutter SDK for the Tabi WhatsApp Business API.