Introduction
The GlueWABA REST API lets you build WhatsApp messaging into any application. Send text messages, use approved templates, manage your contacts, and receive real-time events via webhooks — all with a simple HTTP interface.
https://api.whatslink.abdul25.dev
Send Messages
Text and template messages to any WhatsApp number.
Manage Contacts
Create, search, and segment your contact database.
Receive Events
Webhooks for inbound messages and delivery status.
Secure by Default
HMAC-SHA256 signatures on every webhook delivery.
Quick Start
Send your first message in under 5 minutes.
Get an API key
Log in → Developer Portal → Generate Key. Copy your wl_live_ key.
Send a text message
Run the curl command below. Replace the phone number and key.
Check the response
A 200 response with a wamid message_id means it was queued.
curl -X POST https://api.whatslink.abdul25.dev/api/v1/messages \
-H "X-API-Key: wl_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "255700000000",
"type": "text",
"body": "Hello, thanks for your order!"
}'
Authentication
Every request must include your API key in the X-API-Key header.
curl https://api.whatslink.abdul25.dev/api/v1/developer/contacts \
-H "X-API-Key: wl_live_YOUR_KEY"
wl_live_ keys
Production. Sends real messages and deducts credits.
wl_test_ keys
Sandbox. Requests are validated but messages are not delivered and credits are not deducted.
Send Message
/api/v1/messages
Sends a WhatsApp message to a single recipient. Use type: text for
freeform replies within an open 24-hour window, type: template to
initiate a conversation with a pre-approved template, or type: media
to send an image, video, audio, or document within an open window.
Request body
| Field | Type | Description |
|---|---|---|
to required |
string | Recipient phone in E.164 format without the '+'. Example: 255700000000 |
type required |
string | One of: "text", "template", or "media". |
body |
string | Message text. Required when type is "text". Max 4096 chars. |
template |
object | Template payload. Required when type is "template". Contains name, language, and components. |
media_type |
string | Required when type is "media". One of: "image", "video", "audio", "document". |
media_url |
string | Publicly reachable HTTPS URL of the media file. Required when type
is "media", unless media_id is given instead. |
media_id |
string | A media ID previously uploaded to Meta. Use this instead of
media_url to avoid re-uploading the same file on repeat sends. |
caption |
string | Optional caption. Only applies to image, video, and document types. |
Text message
curl -X POST https://api.whatslink.abdul25.dev/api/v1/messages \
-H "X-API-Key: wl_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "255700000000",
"type": "text",
"body": "Hello, thanks for your order!"
}'
<?php
$client = new \GuzzleHttp\Client();
$response = $client->post('https://api.whatslink.abdul25.dev/api/v1/messages', [
'headers' => ['X-API-Key' => 'wl_live_YOUR_KEY'],
'json' => [
'to' => '255700000000',
'type' => 'text',
'body' => 'Hello, thanks for your order!',
],
]);
$data = json_decode($response->getBody(), true);
const axios = require('axios');
const { data } = await axios.post(
'https://api.whatslink.abdul25.dev/api/v1/messages',
{ to: '255700000000', type: 'text', body: 'Hello, thanks for your order!' },
{ headers: { 'X-API-Key': 'wl_live_YOUR_KEY' } }
);
Template message
curl -X POST https://api.whatslink.abdul25.dev/api/v1/messages \
-H "X-API-Key: wl_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "255700000000",
"type": "template",
"template": {
"name": "order_confirmation",
"language": "en_US",
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "Amani" },
{ "type": "text", "text": "ORD-9821" }
]
}
]
}
}'
<?php
$response = $client->post('https://api.whatslink.abdul25.dev/api/v1/messages', [
'headers' => ['X-API-Key' => 'wl_live_YOUR_KEY'],
'json' => [
'to' => '255700000000',
'type' => 'template',
'template' => [
'name' => 'order_confirmation',
'language' => 'en_US',
'components' => [[
'type' => 'body',
'parameters' => [
['type' => 'text', 'text' => 'Amani'],
['type' => 'text', 'text' => 'ORD-9821'],
],
]],
],
],
]);
await axios.post(
'https://api.whatslink.abdul25.dev/api/v1/messages',
{
to: '255700000000',
type: 'template',
template: {
name: 'order_confirmation',
language: 'en_US',
components: [{
type: 'body',
parameters: [
{ type: 'text', text: 'Amani' },
{ type: 'text', text: 'ORD-9821' },
],
}],
},
},
{ headers: { 'X-API-Key': 'wl_live_YOUR_KEY' } }
);
Media message
curl -X POST https://api.whatslink.abdul25.dev/api/v1/messages \
-H "X-API-Key: wl_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "255700000000",
"type": "media",
"media_type": "image",
"media_url": "https://your-app.com/files/receipt.jpg",
"caption": "Your receipt is attached"
}'
<?php
$response = $client->post('https://api.whatslink.abdul25.dev/api/v1/messages', [
'headers' => ['X-API-Key' => 'wl_live_YOUR_KEY'],
'json' => [
'to' => '255700000000',
'type' => 'media',
'media_type' => 'image',
'media_url' => 'https://your-app.com/files/receipt.jpg',
'caption' => 'Your receipt is attached',
],
]);
await axios.post(
'https://api.whatslink.abdul25.dev/api/v1/messages',
{
to: '255700000000',
type: 'media',
media_type: 'image',
media_url: 'https://your-app.com/files/receipt.jpg',
caption: 'Your receipt is attached',
},
{ headers: { 'X-API-Key': 'wl_live_YOUR_KEY' } }
);
16MB max file size (Meta's limit). Supported
media_type/MIME pairs mirror WhatsApp's own: images (JPEG/PNG), video (MP4/3GPP),
audio (AAC/MP3/OGG), and documents (PDF, Word).
Response
{
"status": true,
"message": "Message queued.",
"data": { "message_id": "wamid.HBgLMjU1NzAwMDAwMDAwFQIAERgSM..." }
}
List Messages
/api/v1/developer/messages
Returns the message history for a single contact, most recent first.
Query parameters
| Param | Type | Description |
|---|---|---|
contact_id required |
string | Contact UUID whose message history to return. |
page |
integer | Page number. Default: 1. |
limit |
integer | Results per page. Default: 20. |
curl "https://api.whatslink.abdul25.dev/api/v1/developer/messages?contact_id=CONTACT_ID&page=1&limit=20" \
-H "X-API-Key: wl_live_YOUR_KEY"
List Contacts
/api/v1/developer/contacts
Returns a cursor-paginated list of all contacts in your tenant, ordered by ID.
Query parameters
| Param | Type | Description |
|---|---|---|
after |
string | Cursor from a previous response's pagination.next_cursor.
Omit for the first page. |
limit |
integer | Results per page. Max 200. Default: 50. |
curl "https://api.whatslink.abdul25.dev/api/v1/developer/contacts?limit=50" \
-H "X-API-Key: wl_live_YOUR_KEY"
$response = $client->get('https://api.whatslink.abdul25.dev/api/v1/developer/contacts', [
'headers' => ['X-API-Key' => 'wl_live_YOUR_KEY'],
'query' => ['limit' => 50],
]);
const { data } = await axios.get(
'https://api.whatslink.abdul25.dev/api/v1/developer/contacts',
{ params: { limit: 50 }, headers: { 'X-API-Key': 'wl_live_YOUR_KEY' } }
);
Response
{
"status": true,
"data": [{ "id": "a1b2...", "name": "Amani Juma", "wa_id": "255700000000", "tags": ["vip"] }],
"pagination": { "has_more": true, "next_cursor": "a1b2c3..." }
}
Get Contact
/api/v1/developer/contacts/{id}
Retrieves a single contact by ID.
curl "https://api.whatslink.abdul25.dev/api/v1/developer/contacts/CONTACT_ID" \
-H "X-API-Key: wl_live_YOUR_KEY"
$response = $client->get("https://api.whatslink.abdul25.dev/api/v1/developer/contacts/{$contactId}", [
'headers' => ['X-API-Key' => 'wl_live_YOUR_KEY'],
]);
const { data } = await axios.get(
`https://api.whatslink.abdul25.dev/api/v1/developer/contacts/${contactId}`,
{ headers: { 'X-API-Key': 'wl_live_YOUR_KEY' } }
);
Create Contact
/api/v1/developer/contacts
Creates a contact. If a contact with the same wa_id already
exists, the existing record is returned — this endpoint is idempotent.
Request body
| Field | Type | Description |
|---|---|---|
name required |
string | Contact's display name. |
wa_id required |
string | WhatsApp phone in E.164 without '+'. Example: 255700000000. |
email |
string | Optional email address. |
tags |
string[] | Optional array of tag strings for segmentation. |
curl -X POST https://api.whatslink.abdul25.dev/api/v1/developer/contacts \
-H "X-API-Key: wl_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Amani Juma",
"wa_id": "255700000000",
"email": "amani@example.com",
"tags": ["vip", "dar-es-salaam"]
}'
$response = $client->post('https://api.whatslink.abdul25.dev/api/v1/developer/contacts', [
'headers' => ['X-API-Key' => 'wl_live_YOUR_KEY'],
'json' => [
'name' => 'Amani Juma',
'wa_id' => '255700000000',
'email' => 'amani@example.com',
'tags' => ['vip', 'dar-es-salaam'],
],
]);
const { data } = await axios.post(
'https://api.whatslink.abdul25.dev/api/v1/developer/contacts',
{
name: 'Amani Juma',
wa_id: '255700000000',
email: 'amani@example.com',
tags: ['vip', 'dar-es-salaam'],
},
{ headers: { 'X-API-Key': 'wl_live_YOUR_KEY' } }
);
Update Contact
/api/v1/developer/contacts/{id}
Partial update — send only the fields you want to change.
curl -X PATCH https://api.whatslink.abdul25.dev/api/v1/developer/contacts/CONTACT_ID \
-H "X-API-Key: wl_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Amani Updated",
"tags": ["vip", "new-tag"]
}'
List Templates
/api/v1/templates
Returns a paginated list of your approved/pending/rejected message templates.
curl "https://api.whatslink.abdul25.dev/api/v1/templates?page=1&limit=20" \
-H "X-API-Key: wl_live_YOUR_KEY"
Get Template
/api/v1/templates/{id}
Get a single template by its ID.
curl "https://api.whatslink.abdul25.dev/api/v1/templates/TEMPLATE_ID" \
-H "X-API-Key: wl_live_YOUR_KEY"
List Broadcasts
/api/v1/broadcasts
Returns a paginated list of your broadcast campaigns.
curl "https://api.whatslink.abdul25.dev/api/v1/broadcasts?page=1&limit=20" \
-H "X-API-Key: wl_live_YOUR_KEY"
Launch Broadcast
/api/v1/broadcasts
Queues a template broadcast to a set of contacts for background delivery.
curl -X POST https://api.whatslink.abdul25.dev/api/v1/broadcasts \
-H "X-API-Key: wl_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"whatsapp_number_id": "NUMBER_ID",
"template_name": "order_confirmation",
"category": "marketing",
"contact_ids": ["CONTACT_ID_1", "CONTACT_ID_2"],
"components": []
}'
Get Broadcast
/api/v1/broadcasts/{id}
Retrieves a broadcast record with current send counts.
curl "https://api.whatslink.abdul25.dev/api/v1/broadcasts/BROADCAST_ID" \
-H "X-API-Key: wl_live_YOUR_KEY"
Broadcast Messages
/api/v1/broadcasts/{id}/messages
Returns the paginated list of messages queued or sent for this broadcast.
curl "https://api.whatslink.abdul25.dev/api/v1/broadcasts/BROADCAST_ID/messages?page=1&limit=20" \
-H "X-API-Key: wl_live_YOUR_KEY"
List Webhooks
/api/v1/developer/webhooks
Returns all registered webhook endpoints for your tenant.
curl "https://api.whatslink.abdul25.dev/api/v1/developer/webhooks" \
-H "X-API-Key: wl_live_YOUR_KEY"
Register Webhook
/api/v1/developer/webhooks
Register an HTTPS endpoint to receive real-time event notifications. GlueWABA delivers a signed POST request for every event within milliseconds of it occurring.
Request body
curl -X POST https://api.whatslink.abdul25.dev/api/v1/developer/webhooks \
-H "X-API-Key: wl_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/whatsapp",
"events": ["*"]
}'
Save your webhook secret
The response includes a secret that is shown only once.
Store it securely — you need it to verify incoming requests.
Remove Webhook
/api/v1/developer/webhooks/{id}
Removes a registered webhook endpoint.
curl -X DELETE "https://api.whatslink.abdul25.dev/api/v1/developer/webhooks/WEBHOOK_ID" \
-H "X-API-Key: wl_live_YOUR_KEY"
Test Ping
/api/v1/developer/webhooks/{id}/test
Sends a synthetic ping event to your endpoint for verification.
curl -X POST "https://api.whatslink.abdul25.dev/api/v1/developer/webhooks/WEBHOOK_ID/test" \
-H "X-API-Key: wl_live_YOUR_KEY"
Signature Verification
Every webhook delivery includes an X-WhatsLink-Signature header.
Always verify this signature before processing the event — it protects against spoofed requests.
The value is sha256= followed by the HMAC-SHA256 hex digest of the raw
request body, signed with your webhook secret. Use a constant-time comparison
(e.g. hash_equals / timingSafeEqual) to prevent timing attacks.
<?php
function verifyWhatsLinkSignature(string $rawBody, string $secret, string $header): bool {
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, $header);
}
// In your webhook handler:
$rawBody = file_get_contents('php://input');
$secret = getenv('WHATSLINK_WEBHOOK_SECRET');
$sigHeader = $_SERVER['HTTP_X_WHATSLINK_SIGNATURE'] ?? '';
if (!verifyWhatsLinkSignature($rawBody, $secret, $sigHeader)) {
http_response_code(401);
exit;
}
$event = json_decode($rawBody, true);
// Handle $event...
const crypto = require('crypto');
function verifyWhatsLinkSignature(rawBody, secret, header) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(header)
);
}
// Express.js example:
app.post('/webhooks/whatsapp', express.raw({ type: '*/*' }), (req, res) => {
const sig = req.headers['x-whatslink-signature'];
if (!verifyWhatsLinkSignature(req.body, process.env.WL_WEBHOOK_SECRET, sig)) {
return res.status(401).send('Unauthorized');
}
const event = JSON.parse(req.body);
// Handle event...
res.status(200).send('OK');
});
Webhook Events
message.received
Fired when an inbound WhatsApp message arrives from a contact.
{
"event": "message.received",
"timestamp": "2026-05-24T10:30:00Z",
"data": {
"message_id": "wamid.HBgLMjU1NzAwMDAwMDAwFQIAERgSM...",
"from": "255700000000",
"type": "text",
"text": "Hello! I need help with my order.",
"received_at": "2026-05-24T10:30:00Z"
}
}
message.status_update
Fired when a message delivery status changes: sent → delivered → read, or
failed.
{
"event": "message.status_update",
"timestamp": "2026-05-24T10:31:05Z",
"data": {
"message_id": "wamid.HBgLMjU1NzAwMDAwMDAwFQIAERgSM...",
"to": "255700000000",
"status": "delivered",
"error_code": null,
"updated_at": "2026-05-24T10:31:05Z"
}
}
whatsapp_number.connected
Fired when an admin completes a pending WhatsApp number connection (see Connect WhatsApp Number for platform-partner accounts — every tenant, partner-provisioned or not, can subscribe to this event).
{
"event": "whatsapp_number.connected",
"timestamp": "2026-07-19T10:31:05Z",
"data": {
"id": "a1b2c3...",
"display_phone_number": "+255700000000",
"waba_name": "Acme Traders"
}
}
whatsapp_number.connection_failed
Fired when a pending connection request is rejected instead of completed.
{
"event": "whatsapp_number.connection_failed",
"timestamp": "2026-07-19T10:31:05Z",
"data": {
"id": "a1b2c3...",
"reason": "Could not verify ownership of this WABA."
}
}
Platform Partner API
Built for platforms that want to offer WhatsApp to their own customers — a bulk SMS provider, a CRM, a marketplace — without sending anyone through GlueWABA's own dashboard. A partner provisions a fully headless sub-tenant per customer, gets back an API key immediately, and everything downstream (sending messages, contacts, templates, broadcasts) uses the existing Developer API documented above with that key — nothing new to learn there.
Partner access is granted, not self-serve
Every endpoint below requires your own account to be flagged as a platform partner. Contact migambile25@gmail.com to discuss becoming one — a reseller relationship, not a feature you can switch on yourself.
Two billing modes are available per sub-tenant, chosen when you provision it: direct (the sub-tenant carries its own prepaid balance, same as any normal tenant) or pooled (usage is billed to your own wallet instead — fund it via Fund Sub-Tenant). You can also set your own resale price per message category via Rate Overrides — the margin between that and the platform's base rate is yours.
Provision Sub-Tenant
/api/v1/partner/tenants
Creates a new headless sub-tenant and immediately issues its API key. There is no dashboard signup, no email verification, and no password — the returned key is the only credential your customer's integration will ever need.
Request body
| Field | Type | Description |
|---|---|---|
business_name required |
string | Name of the end customer's business. Min 3 characters. |
billing_mode |
string | One of: "direct" (default — sub-tenant pays for its own usage) or "pooled" (billed to your own wallet). |
curl -X POST https://api.whatslink.abdul25.dev/api/v1/partner/tenants \
-H "X-API-Key: wl_live_YOUR_PARTNER_KEY" \
-H "Content-Type: application/json" \
-d '{
"business_name": "Amani Traders",
"billing_mode": "direct"
}'
Response
{
"status": true,
"data": {
"tenant": {
"id": "a1b2c3...",
"name": "Amani Traders",
"partner_id": "your-partner-tenant-id",
"billing_owner_id": null,
"status": "active"
},
"api_key": "wl_live_...",
"api_key_id": "d4e5f6..."
}
}
Save the API key
api_key is shown only once, exactly like keys generated from the
Developer Portal. There is no dashboard to retrieve it later — store it in your
own system against this sub-tenant.
List / Get Sub-Tenants
/api/v1/partner/tenants
/api/v1/partner/tenants/{id}
Returns only sub-tenants you provisioned — never another partner's.
curl https://api.whatslink.abdul25.dev/api/v1/partner/tenants \
-H "X-API-Key: wl_live_YOUR_PARTNER_KEY"
Fund Sub-Tenant
/api/v1/partner/tenants/{id}/credits
Tops up your own wallet — only meaningful for a sub-tenant provisioned
with billing_mode: "pooled", since that's the wallet its usage
actually draws from. Rejected with a 400 for a "direct" sub-tenant; fund that
one's own balance the normal way instead (Billing in the dashboard, or ask your
customer to do so directly).
curl -X POST https://api.whatslink.abdul25.dev/api/v1/partner/tenants/SUBTENANT_ID/credits \
-H "X-API-Key: wl_live_YOUR_PARTNER_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 50000,
"currency": "TZS"
}'
Rate Overrides
/api/v1/partner/rates
/api/v1/partner/rates
/api/v1/partner/rates/{id}
Sets what your sub-tenants pay per message category/country — the resale price you control. It's floored at the platform's own base rate for that category/country, so you can never accidentally price below your own cost; the difference between your rate and the base rate is your margin.
curl -X POST https://api.whatslink.abdul25.dev/api/v1/partner/rates \
-H "X-API-Key: wl_live_YOUR_PARTNER_KEY" \
-H "Content-Type: application/json" \
-d '{
"category": "marketing",
"country_code": "TZ",
"cost_tsh": 200
}'
Connect WhatsApp Number
/api/v1/partner/tenants/{id}/whatsapp-numbers
Submits a connection request on behalf of a sub-tenant — the WABA ID and phone
number ID only, never an access token. A GlueWABA admin completes
the connection manually, the same trust boundary every tenant goes through today.
Since a partner-provisioned sub-tenant has no dashboard to watch for this,
subscribe to whatsapp_number.connected /
whatsapp_number.connection_failed (see Webhook
Events) via Register Sub-Tenant Webhook, or
poll this same endpoint with GET.
curl -X POST https://api.whatslink.abdul25.dev/api/v1/partner/tenants/SUBTENANT_ID/whatsapp-numbers \
-H "X-API-Key: wl_live_YOUR_PARTNER_KEY" \
-H "Content-Type: application/json" \
-d '{
"waba_id": "123456789012345",
"phone_number_id": "987654321098765"
}'
Response
{
"status": true,
"message": "Connection request submitted. An admin will review it shortly.",
"data": { "id": "a1b2c3...", "status": "pending_admin_review" }
}
List / poll connection status
/api/v1/partner/tenants/{id}/whatsapp-numbers
curl https://api.whatslink.abdul25.dev/api/v1/partner/tenants/SUBTENANT_ID/whatsapp-numbers \
-H "X-API-Key: wl_live_YOUR_PARTNER_KEY"
Disconnect Number
/api/v1/partner/tenants/{id}/whatsapp-numbers/{numberId}
Unlike connecting, disconnecting needs no admin step — it's immediate and self-serve.
curl -X DELETE https://api.whatslink.abdul25.dev/api/v1/partner/tenants/SUBTENANT_ID/whatsapp-numbers/NUMBER_ID \
-H "X-API-Key: wl_live_YOUR_PARTNER_KEY"
Register Sub-Tenant Webhook
/api/v1/partner/tenants/{id}/webhooks
/api/v1/partner/tenants/{id}/webhooks/{webhookId}
Registers a webhook for a sub-tenant on its behalf — since it has no dashboard of its own to do this itself. Same event types, signature scheme, and delivery behavior as Register Webhook; most partners register the same receiver URL for every sub-tenant they provision.
curl -X POST https://api.whatslink.abdul25.dev/api/v1/partner/tenants/SUBTENANT_ID/webhooks \
-H "X-API-Key: wl_live_YOUR_PARTNER_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-platform.com/webhooks/whatslink",
"events": ["*"]
}'
Error Reference
All error responses follow the same envelope: status: false, a
human-readable error string, and a machine-readable code.
| Status | Code | Meaning & fix |
|---|---|---|
400 |
bad_request | Malformed JSON body or unsupported content type. Check your request encoding. |
401 |
unauthorized | Missing or invalid API key. Check the X-API-Key header. |
403 |
forbidden | Your key does not have permission for this resource or action. |
404 |
not_found | The requested resource ID does not exist in your tenant. |
422 |
validation_error | One or more fields failed validation. Check the details object for per-field messages. |
429 |
rate_limit_exceeded | You have exceeded your per-minute request limit. Wait for the Retry-After header value (in seconds) before retrying. |
500 |
server_error | An unexpected error on our side. If it persists, contact migambile25@gmail.com. |
Rate Limits
Rate limits are applied per API key per minute. Every response includes the following headers:
| Header | Description |
|---|---|
X-RateLimit-Limit |
Maximum requests allowed per minute for your key tier. |
X-RateLimit-Remaining |
Requests remaining in the current minute window. |
Retry-After |
Seconds to wait before retrying (returned on 429 only). |
When you receive a 429 response, wait for Retry-After seconds
before retrying. Do not use exponential backoff on rate limit errors — the window
resets at the exact second indicated.
Sandbox
Use a wl_test_ key to test your integration without sending real messages or consuming
credits.
Validation: All field validation runs exactly as in production. Invalid payloads still return 422.
Message delivery: Messages are accepted and return a synthetic wamid, but nothing is sent to WhatsApp.
Credits: No credits are deducted for test-key requests.
Webhooks: The "Test Ping" button in the Developer Portal works with test keys.
Rate limits: Rate limits are enforced identically in sandbox and production.
Questions? Email migambile25@gmail.com