GlueWABA API Docs v1
Developer Portal

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.

Base URL 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.

1

Get an API key

Log in → Developer Portal → Generate Key. Copy your wl_live_ key.

2

Send a text message

Run the curl command below. Replace the phone number and key.

3

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

POST /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

GET /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

GET /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

GET /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

POST /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

PATCH /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

GET /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

GET /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

GET /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

POST /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

GET /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

GET /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

GET /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

POST /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

DELETE /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

POST /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

POST /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

GET /api/v1/partner/tenants
GET /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

POST /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

GET /api/v1/partner/rates
POST /api/v1/partner/rates
DELETE /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

POST /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

GET /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

DELETE /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

POST /api/v1/partner/tenants/{id}/webhooks
DELETE /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