WhatsApp API Documentation
Send messages, manage templates, and access contacts from any application.
Quick Start
1. Get an API Key
Log into the platform at social.slick.company, go to Settings, and create an API key under "WhatsApp API Keys".
2. Authenticate
Include your API key in every request:
# Header (recommended)
X-API-Key: slick_your_api_key_here
# Or query parameter
?api_key=slick_your_api_key_here
3. Set Business ID
If your API key is scoped to a business, it's automatic. Otherwise pass business_id in the request body, query string, or X-Business-ID header.
Available businesses: warpoint, notebook
Base URL
https://social.slick.company/api/v1/whatsapp
Template Lifecycle
WhatsApp requires message templates to be approved by Meta before use. The lifecycle is:
- Create a template via API → status:
draft - Submit to Meta for review → status:
submitted - Wait 24-48h. Platform auto-polls every 30 min → status:
approvedorrejected - Send messages using the approved template
Free-form text messages (without templates) can only be sent within a 24-hour window after the customer messages you first.
Endpoints
Send a WhatsApp message to one recipient using a template or free-form text.
| Parameter | Type | Required | Description |
|---|---|---|---|
business_id | string | Yes | Business slug, e.g. "notebook" |
to | string | Yes | Phone number in international format, e.g. "+96512345678" |
template_name | string | * | Meta-approved template name. Use this OR template_id. |
template_id | string | * | Template ID from this platform (name resolved automatically) |
language | string | No | Template language code. Default: "ar" |
parameters | object | No | Template variables: {"body": ["value1", "value2"]} |
text | string | No | Free-form text (24h window only). Overrides template fields. |
Example
curl -X POST https://social.slick.company/api/v1/whatsapp/send \
-H "X-API-Key: slick_your_key" \
-H "Content-Type: application/json" \
-d '{
"business_id": "notebook",
"to": "+96512345678",
"template_name": "hello_world",
"language": "en"
}'
Response
{
"ok": true,
"result": {
"messages": [{"id": "wamid.HBgLOTY1MTIzNDU2NzgVAgA..."}]
}
}
Send a template message to multiple recipients. Provide a list of phone numbers OR a segment name to pull contacts from the database. Runs in the background.
| Parameter | Type | Required | Description |
|---|---|---|---|
business_id | string | Yes | Business slug |
template_name | string | Yes | Approved template name |
language | string | No | Default: "ar" |
recipients | string[] | No | Phone numbers array. If empty, uses segment. |
segment | string | No | Contact segment: "customers", "vip", "all" |
Example — send to a segment from the database
curl -X POST https://social.slick.company/api/v1/whatsapp/send-bulk \
-H "X-API-Key: slick_your_key" \
-H "Content-Type: application/json" \
-d '{
"business_id": "notebook",
"template_name": "promo_sale",
"segment": "customers"
}'
Example — send to specific numbers
curl -X POST https://social.slick.company/api/v1/whatsapp/send-bulk \
-H "X-API-Key: slick_your_key" \
-H "Content-Type: application/json" \
-d '{
"business_id": "warpoint",
"template_name": "tournament_invite",
"language": "ar",
"recipients": ["+96565965089", "+96599887766", "+96555443322"]
}'
Response
{
"ok": true,
"campaign_id": "api-1a2b3c4d",
"recipients": 1103,
"status": "sending",
"message": "Sending to 1103 recipients in background"
}
Get all WhatsApp message templates. Filter by status to get only approved ones ready for sending.
| Query Param | Description |
|---|---|
business_id | Filter by business slug |
status | draft, submitted, approved, or rejected |
curl "https://social.slick.company/api/v1/whatsapp/templates?business_id=notebook&status=approved" \
-H "X-API-Key: slick_your_key"
Response
{
"templates": [
{
"id": "tpl-abc123",
"name": "hello_world",
"category": "MARKETING",
"language": "en",
"body_text": "Hello! Welcome to Notebook.",
"status": "approved",
"meta_template_id": "123456789"
}
],
"count": 1
}
Create a new template as a draft. Use {{1}}, {{2}} for variables in the body.
| Parameter | Type | Required | Description |
|---|---|---|---|
business_id | string | Yes | Business slug |
name | string | Yes | Template name (lowercase, underscores, no spaces) |
body_text | string | Yes | Message body (max 1024 chars) |
category | string | No | MARKETING (default), UTILITY, AUTHENTICATION |
language | string | No | Default: "ar" |
header_type | string | No | "text", "image", "video", or empty |
footer_text | string | No | Footer (max 60 chars) |
curl -X POST https://social.slick.company/api/v1/whatsapp/templates \
-H "X-API-Key: slick_your_key" \
-H "Content-Type: application/json" \
-d '{
"business_id": "notebook",
"name": "order_update",
"body_text": "Hi {{1}}, your order #{{2}} is on its way! Track: {{3}}",
"category": "UTILITY",
"language": "en"
}'
Submit a draft template to Meta for review. Approval typically takes 24-48 hours. The platform polls every 30 minutes and updates status automatically.
curl -X POST https://social.slick.company/api/v1/whatsapp/templates/tpl-abc123/submit \
-H "X-API-Key: slick_your_key"
Response
{"ok": true, "status": "submitted", "meta_template_id": "123456789"}
Retrieve contacts from the database. Contacts are synced daily from business databases and can be added via API.
| Query Param | Description |
|---|---|
business_id | Filter by business |
segment | Filter by segment (e.g. "customers", "vip") |
curl "https://social.slick.company/api/v1/whatsapp/contacts?business_id=warpoint" \
-H "X-API-Key: slick_your_key"
Response
{
"contacts": [
{"id": "c-xxx", "phone": "+96565965089", "name": "Ahmed", "segment": "customers", "opted_in": true}
],
"count": 1030
}
Fire-and-forget endpoint for sending a template with variables. Ideal for OTPs, order confirmations, booking reminders. Language is auto-detected from the template database. For AUTHENTICATION templates (like OTP), the copy-code button parameter is automatically included.
| Parameter | Type | Required | Description |
|---|---|---|---|
to | string | Yes | Phone number in international format |
template_name | string | Yes | Approved template name (e.g. "send_otp") |
variables | string[] | No | Values for {{1}}, {{2}}, etc. |
language | string | No | Auto-detected from DB if omitted |
Example — Send OTP
curl -X POST https://social.slick.company/api/v1/whatsapp/trigger \
-H "X-API-Key: slick_your_key" \
-H "Content-Type: application/json" \
-d '{
"to": "+96566477000",
"template_name": "send_otp",
"variables": ["847291"]
}'
Example — Booking Reminder
curl -X POST https://social.slick.company/api/v1/whatsapp/trigger \
-H "X-API-Key: slick_your_key" \
-H "Content-Type: application/json" \
-d '{
"to": "+96599887766",
"template_name": "system_booking_reminder_new",
"variables": ["Ahmed", "VR Arena", "Tomorrow 6 PM"]
}'
Example — Discount Notification
curl -X POST https://social.slick.company/api/v1/whatsapp/trigger \
-H "X-API-Key: slick_your_key" \
-H "Content-Type: application/json" \
-d '{
"to": "+96566477000",
"template_name": "discount_reminder",
"variables": ["Fahad", "15", "2026-05-01", "66477000", "Fahad", "15", "May 1st 2026", "66477000"]
}'
Response
{"ok": true, "result": {"messages": [{"id": "wamid.HBgL..."}]}}
How to use for OTP in your app
// Node.js example - send OTP from your backend
const otp = Math.floor(100000 + Math.random() * 900000); // 6-digit OTP
await fetch('https://social.slick.company/api/v1/whatsapp/trigger', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'slick_your_warpoint_key'
},
body: JSON.stringify({
to: '+96566477000',
template_name: 'send_otp',
variables: [String(otp)]
})
});
| Parameter | Type | Required | Description |
|---|---|---|---|
business_id | string | Yes | Business slug |
phone | string | Yes | International format |
name | string | No | Contact name |
segment | string | No | e.g. "customers", "leads", "vip" |
curl -X POST https://social.slick.company/api/v1/whatsapp/contacts \
-H "X-API-Key: slick_your_key" \
-H "Content-Type: application/json" \
-d '{"business_id": "warpoint", "phone": "+96599887766", "name": "Fahad", "segment": "vip"}'
Ads API
Read spend and campaigns across Meta (Facebook & Instagram), Google Ads, TikTok and Snapchat for one business, and pause, resume or re-budget its campaigns. Use the same business-scoped X-API-Key as the WhatsApp API.
Base URL: https://social.slick.company/api/v1/ads
- All money is in KWD: spend, revenue and budgets, in and out. Each ad account's own currency is detected when you select it. Accounts that report in KWD pass through unchanged; USD accounts are converted at the platform's operator rate (currently 0.3075).
revenueis what the ad platform itself reports from its pixel or conversion tracking, not a confirmed sale.revenue_sourcenames the reporter; show it next to the number.conversionsis always a whole number. Google's modelled fractional conversions are rounded.
Connecting an ad account
Each platform is connected once per business with the platform's own login (OAuth), then you choose which ad account to use. One login often reaches several ad accounts, so the account is never guessed.
POST /connect/{platform}with yourcallback_url→ returnsdata.auth_url.- Send the user to
auth_url. They sign in to the platform and approve access. - After sign-in the browser returns to your
callback_urlwithcodeandstate(for every platform, including TikTok). POST /connect/{platform}/callbackwith thatcodeandstate. If the login reaches exactly one ad account it is selected automatically; otherwise the response has"needs_account_selection": trueand the list ofaccounts.POST /connect/{platform}/accountwith the chosenaccount_id. (GET /connect/{platform}/accountslists them again at any time.)
Nothing to register. The ad platforms always return the user to Slick, and Slick forwards the browser to your callback_url. Any http(s) URL you control works.
{platform} is one of facebook, google, tiktok, snapchat.
| Field | Type | Required | Description |
|---|---|---|---|
callback_url | string | Yes | Your page that receives ?code=…&state=… after sign-in (Slick forwards to it) |
Response
{"success": true, "data": {"auth_url": "https://www.facebook.com/v21.0/dialog/oauth?..."}}
| Field | Type | Required | Description |
|---|---|---|---|
code | string | Yes | From the redirect to your callback_url |
state | string | Yes | From the redirect; must match the one Slick issued |
Response
{
"success": true,
"platform": "google",
"account_id": "",
"account_name": "",
"needs_account_selection": true,
"accounts": [
{"account_id": "5182527639", "account_name": "Warpoint", "currency": "USD", "timezone": "Asia/Kuwait"},
{"account_id": "1344141276", "account_name": "Another business", "currency": "USD", "timezone": "Asia/Kuwait"}
]
}
{"success": true, "data": {"accounts": [ ...same shape as above... ], "selected_account_id": "5182527639"}}
| Field | Type | Required | Description |
|---|---|---|---|
account_id | string | Yes | One of the listed accounts. Meta ids may be sent with or without act_. |
Response
{"success": true, "data": {"account_id": "5182527639", "account_name": "Warpoint", "currency": "USD"}}
Returns 404 if the connected login can't reach that account.
{"success": true}
{
"platforms": [
{"platform": "google", "is_connected": true, "account_id": "5182527639", "account_name": "Warpoint",
"currency": "USD", "needs_account_selection": false, "last_synced": "2026-10-03 09:12:44"},
{"platform": "snapchat", "is_connected": false}
]
}
A platform that is connected but still needs_account_selection returns no spend or campaigns until an account is chosen.
| Query | Required | Description |
|---|---|---|
start_date, end_date | Yes | YYYY-MM-DD, inclusive, in each ad account's own time zone. Any length of range works in one call; Slick pages through each platform and splits long ranges where a platform requires it. An invalid or reversed range returns 400. |
platforms | No | Comma-separated subset, e.g. google,tiktok. Default: all connected. |
Response
{
"data": [
{"platform": "google", "date": "2026-10-02", "campaign_id": "12345678901",
"spend": 5.286, "clicks": 382, "impressions": 9411, "conversions": 0,
"revenue": 0, "revenue_source": "reported by Google Ads"}
]
}
Snapchat: at account level Snapchat reports spend only, so its rows have clicks and impressions of 0.
Errors per platform: if a platform can't be read (expired login, API error), the others are still returned and the response adds an errors object, for example "errors": {"tiktok": "tiktok: Access token is expired"}. The field is absent when everything succeeded, so a platform with no rows and no entry in errors genuinely had no spend.
{
"campaigns": [
{"platform_campaign_id": "12345678901", "platform": "google",
"name": "Warpoint VR Arena — Kuwait Gamers & Activities", "status": "active",
"objective": "SEARCH", "daily_budget": 4.92, "lifetime_budget": 0, "currency": "KWD"}
]
}
Meta, Google and TikTok, every campaign (all pages). status is active or paused. A platform that fails is listed in an errors object, as for /spend.
Body: {"platform": "google"}
Body: {"platform": "google"}
Body: {"platform": "tiktok", "daily_budget": 5}. The budget is in KWD and converted to the ad account's currency. Not available for Snapchat yet.
Errors
Failures return an HTTP error status with {"success": false, "error": "..."}. Calls to each platform are lightly throttled per business; a burst of requests waits briefly instead of failing.
Integration Examples
Node.js
const API_KEY = 'slick_your_key_here';
const BASE = 'https://social.slick.company/api/v1/whatsapp';
async function sendWhatsApp(to, templateName, bizId = 'notebook') {
const res = await fetch(`${BASE}/send`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-API-Key': API_KEY },
body: JSON.stringify({ business_id: bizId, to, template_name: templateName, language: 'ar' }),
});
return res.json();
}
// Usage
const result = await sendWhatsApp('+96512345678', 'order_confirmation');
Python
import requests
API_KEY = 'slick_your_key_here'
BASE = 'https://social.slick.company/api/v1/whatsapp'
def send_whatsapp(to, template_name, biz_id='notebook'):
return requests.post(f'{BASE}/send',
headers={'X-API-Key': API_KEY, 'Content-Type': 'application/json'},
json={'business_id': biz_id, 'to': to, 'template_name': template_name, 'language': 'ar'}
).json()
result = send_whatsapp('+96512345678', 'order_confirmation')
PHP
$apiKey = 'slick_your_key_here';
$base = 'https://social.slick.company/api/v1/whatsapp';
$ch = curl_init("$base/send");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', "X-API-Key: $apiKey"],
CURLOPT_POSTFIELDS => json_encode([
'business_id' => 'notebook', 'to' => '+96512345678',
'template_name' => 'order_confirmation', 'language' => 'ar',
]),
CURLOPT_RETURNTRANSFER => true,
]);
$result = json_decode(curl_exec($ch), true);
Go
package main
import (
"bytes"
"encoding/json"
"net/http"
)
func sendWhatsApp(to, template, bizID string) (map[string]interface{}, error) {
body, _ := json.Marshal(map[string]string{
"business_id": bizID, "to": to, "template_name": template, "language": "ar",
})
req, _ := http.NewRequest("POST", "https://social.slick.company/api/v1/whatsapp/send", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-API-Key", "slick_your_key_here")
resp, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer resp.Body.Close()
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
return result, nil
}
Error Codes
| HTTP | Meaning | Common Cause |
|---|---|---|
401 | Unauthorized | Missing or invalid API key |
400 | Bad Request | Missing required fields |
404 | Not Found | Template or endpoint not found |
500 | Server Error | Meta API failure |
All errors include an "error" field. If WhatsApp is not configured: {"error":"WhatsApp not configured","status":"not_configured"}
Rate Limits & Notes
- Bulk sending is throttled to ~10 messages/second to stay within Meta's limits
- Template names must be lowercase with underscores only (Meta requirement)
- Free-form text messages require the customer to have messaged you in the last 24 hours
- Contacts are synced daily from business databases (WARPOINT customers, Notebook users)
- Template polling runs every 30 minutes — after submitting, status updates automatically