Jood
Admin

Introduction

Welcome to the Jood Notifications API. This API allows you to send SMS messages programmatically to your customers worldwide through multiple SMS providers.

High Performance

Send thousands of messages per second with our queue-based architecture.

Secure

HMAC signature verification and IP whitelisting for maximum security.

Multi-Provider

Automatic failover between multiple SMS providers for reliability.

Base URL

All API requests should be made to the following base URL:

https://send.jood.cloud

The API is organised into three main route groups:

PrefixPurposeAuth
/api/v1/webhookExternal integrations & sendingHMAC Signature
/api/v1/associationsAssociation managementAPI Key + Secret
/api/v1/redirect-linksShort-link redirects (e.g. for WhatsApp buttons)API Key
/api/v1/dashboardDashboard operationsSession (cookie)

Response Format

All API responses follow a consistent JSON structure:

Success Response

{
  "success": true,
  "data": { ... },
  "message": "Operation completed successfully"
}

Error Response

{
  "success": false,
  "error": "Descriptive error message"
}

Webhook Authentication (HMAC)

All /api/v1/webhook requests must be signed using HMAC-SHA256. The associationId is included in the request body, and the secret key used for signing is provided by the administrator when your association is created.

Keep your credentials secure

Never expose your Secret Key in client-side code or public repositories.

Required Headers

HeaderDescription
X-Webhook-SignatureHMAC-SHA256 hex digest of timestamp + "." + JSON body
X-Webhook-TimestampUnix timestamp in milliseconds. Must be within 5 minutes of server time.
Content-Typeapplication/json

Signature Generation Steps

  1. Get the current timestamp in milliseconds: Date.now()
  2. Create the payload string: timestamp + "." + JSON.stringify(body)
  3. Generate HMAC-SHA256 hash using your Secret Key
  4. Convert to hexadecimal string

Note: The associationId is always included in the JSON body — not as a header. The server looks up the association's secret key from the body's associationId and uses it to verify the signature.

API Key Authentication

The /api/v1/associations endpoints use API Key + Secret authentication. These credentials are created when an association is first set up and can be rotated.

Required Headers

HeaderDescription
x-api-keyYour association's API Key
x-api-secretYour association's Secret Key
Content-Typeapplication/json

Example cURL

curl -X GET "https://send.jood.cloud/api/v1/associations" \
  -H "x-api-key: your-api-key" \
  -H "x-api-secret: your-secret-key" \
  -H "Content-Type: application/json"
GET

List Associations

API Key Auth

Retrieve all associations. Super admin access only.

Endpoint

GET /api/v1/associations

cURL Example

curl -X GET "https://send.jood.cloud/api/v1/associations" \
  -H "x-api-key: your-api-key" \
  -H "x-api-secret: your-secret-key"

Example Response

{
  "success": true,
  "data": [
    {
      "id": 1,
      "name": "Acme Corp",
      "allowedDomains": ["acme.com", "acme.io"],
      "isActive": true,
      "createdAt": "2026-01-15T10:30:00.000Z"
    }
  ]
}
POST

Create Association

API Key Auth

Create a new association. Returns the generated API Key and Secret Key.

Endpoint

POST /api/v1/associations

Request Body

ParameterTypeRequiredDescription
idintegerYesYour external system's association ID (must be unique)
namestringYesAssociation name
allowedDomainsstring[]OptionalAllowed domains for embed widget

Example Request

{
  "id": 123,
  "name": "Acme Corp"
}

Example Response

{
  "success": true,
  "data": {
    "association": {
      "id": 123,
      "name": "Acme Corp",
      "apiKey": "jn_pub_xxxxxxxxxxxxxxxxxxxxxxxx",
      "secretKey": "jn_sec_xxxxxxxxxxxxxxxxxxxxxxxx",
      "allowedDomains": []
    },
    "adminApiKey": "jn_admin_xxxxxxxxxxxxxxxxxxxxxxxx"
  },
  "message": "Store the secretKey and adminApiKey securely - they will not be shown again!"
}

Important: The apiKey and secretKey are only returned once at creation time. Store them securely.

GET

Get Association

API Key Auth

Retrieve a single association by ID.

Endpoint

GET /api/v1/associations/:id

Example Response

{
  "success": true,
  "data": {
    "id": 1, "name": "Acme Corp", "allowedDomains": ["acme.com"],
    "isActive": true, "createdAt": "2026-01-15T10:30:00.000Z", "updatedAt": "2026-02-01T14:00:00.000Z"
  }
}
PUT

Update Association

API Key Auth

Update an existing association.

Endpoint

PUT /api/v1/associations/:id

Request Body

ParameterTypeRequiredDescription
namestringOptionalNew name
allowedDomainsstring[]OptionalUpdated allowed domains
isActivebooleanOptionalEnable or disable association

Example Request

{ "name": "Acme Corp Updated", "allowedDomains": ["acme.com", "acme.io", "acme.dev"], "isActive": true }
POST

Rotate Keys

API Key Auth

Rotate the API Key and/or Secret Key for an association. Old keys are immediately invalidated.

Endpoint

POST /api/v1/associations/:id/rotate-keys

Request Body

ParameterTypeRequiredDescription
rotateApiKeybooleanOptionalRotate the API Key (default: false)
rotateSecretKeybooleanOptionalRotate the Secret Key (default: false)

Example Response

{
  "success": true,
  "data": { "apiKey": "ak_live_newxxxxxxxxxxxxxxxxxxxxxxx", "secretKey": "sk_live_newxxxxxxxxxxxxxxxxxxxxxxx" },
  "message": "Keys rotated successfully"
}
GET

List Credentials

API Key Auth

List all SMS provider credentials for an association.

Endpoint

GET /api/v1/associations/:associationId/credentials

Example Response

{
  "success": true,
  "data": [
    { "id": 10, "providerId": 1, "senderName": "ACME", "isDefault": true, "apiUrl": null, "provider": { "code": "yamamah", "name": "Yamamah", "channel": "sms" }, "createdAt": "2026-01-15T10:30:00.000Z" },
    { "id": 11, "providerId": 4, "senderName": "ACME", "isDefault": false, "apiUrl": null, "provider": { "code": "unifonic", "name": "Unifonic", "channel": "sms" }, "createdAt": "2026-01-16T08:00:00.000Z" }
  ]
}
POST

Add Credential

API Key Auth

Add a new SMS provider credential to an association.

Endpoint

POST /api/v1/associations/:associationId/credentials

Request Body

ParameterTypeRequiredDescription
providerIdintegerYesProvider ID (use GET /api/v1/providers to list available providers)
senderNamestringYesSender name / ID shown to recipients
credentialsobjectYesProvider-specific credentials (see Providers List)
apiUrlstringOptionalCustom API URL override
isDefaultbooleanOptionalSet as default credential (default: false)

Example Request

{
  "providerId": 1,
  "senderName": "ACME",
  "credentials": { "username": "acme_user", "password": "s3cretP@ss" },
  "isDefault": true
}

Example Response

{
  "success": true,
  "data": { "id": 12, "providerId": 1, "provider": { "code": "yamamah", "name": "Yamamah" }, "senderName": "ACME", "isDefault": true },
  "message": "Credential created successfully"
}
PUT

Update Credential

API Key Auth

Update an existing credential's settings or provider credentials.

Endpoint

PUT /api/v1/associations/:associationId/credentials/:credentialId

Request Body

ParameterTypeRequiredDescription
senderNamestringOptionalUpdated sender name
credentialsobjectOptionalUpdated provider credentials
apiUrlstringOptionalCustom API URL override
isDefaultbooleanOptionalSet as default

Example Request

{ "senderName": "ACME_NEW", "credentials": { "username": "acme_new", "password": "newP@ss123" }, "isDefault": true }
DELETE

Delete Credential

API Key Auth

Remove a provider credential.

Endpoint

DELETE /api/v1/associations/:associationId/credentials/:credentialId

Example Response

{ "success": true, "message": "Credential deleted successfully" }
GET

Provider Schemas

Session Auth

Retrieve the configuration schema for all available SMS providers. Use this to build dynamic credential forms.

Endpoint

GET /api/v1/dashboard/providers/schemas

Example Response

{
  "success": true,
  "data": [
    { "id": "yamamah", "name": "Yamamah", "channel": "sms", "fields": ["username", "password"] },
    { "id": "shastra", "name": "Shastra", "channel": "sms", "fields": ["username", "password"] },
    { "id": "qyadat", "name": "Qyadat", "channel": "sms", "fields": ["username", "password"] },
    { "id": "unifonic", "name": "Unifonic", "channel": "sms", "fields": ["appSid"] },
    { "id": "4jawaly", "name": "4Jawaly", "channel": "sms", "fields": ["appKey", "appSecret"] },
    { "id": "infobip", "name": "Infobip", "channel": "sms", "fields": ["apiKey"] },
    { "id": "twilio", "name": "Twilio", "channel": "sms", "fields": ["accountSid", "authToken"] }
  ]
}
POST

Send Single SMS (Webhook)

HMAC Auth

Queue a single SMS message for delivery. The message is placed in a high-priority queue and processed asynchronously.

Endpoint

POST /api/v1/webhook/notification

Request Body

ParameterTypeRequiredDescription
associationIdintegerYesYour association ID
recipientstringYesPhone number in E.164 format
messagestringYesMessage content
channelstringOptional"sms" (default) or "whatsapp"
templateCodestringOptionalTemplate code to use instead of raw message
dataobjectOptionalTemplate variable values
scheduledAtstringOptionalISO 8601 date for scheduled delivery
priorityintegerOptionalQueue priority (higher = sooner)

Example Request

{
  "associationId": 1,
  "recipient": "+966500000000",
  "message": "Your verification code is 123456"
}

Example Response

{
  "success": true,
  "data": { "queueId": 123, "jobId": "job_xyz789", "status": "queued" },
  "message": "Notification queued for sending"
}

cURL Example

curl -X POST "https://send.jood.cloud/api/v1/webhook/notification" \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Timestamp: 1707600000000" \
  -H "X-Webhook-Signature: a1b2c3d4e5f6..." \
  -d '{"associationId":1,"recipient":"+966500000000","message":"Hello!"}'
POST

Send Immediate (Dashboard)

Session Auth

Send an SMS immediately from the dashboard. Unlike the webhook endpoint, this sends synchronously and returns the delivery result.

Endpoint

POST /api/v1/dashboard/send-sms

Request Body

ParameterTypeRequiredDescription
associationIdintegerYesAssociation ID
phoneNumberstringYesRecipient phone number
messagestringYesMessage content
credentialIdintegerOptionalSpecific credential to use (default credential if omitted)

Example Response

{
  "success": true,
  "data": { "logId": 5678, "messageId": "msg_xxxxxxxxxxxx", "success": true }
}
POST

Send with Template

HMAC Auth

Send an SMS using a pre-defined template. Pass the templateCode and a data object with the template variables.

Endpoint

POST /api/v1/webhook/notification

Example Request

{
  "associationId": 1,
  "recipient": "+966500000000",
  "templateCode": "otp_verify",
  "data": { "code": "482901", "expiry": "5" }
}

Note: When templateCode is provided, the message field is ignored. The template's content is used with variables from the data object.

POST

Schedule SMS

Session Auth

Schedule an SMS for future delivery. You can also schedule via the webhook by including scheduledAt in the notification body.

Endpoint (Dashboard)

POST /api/v1/dashboard/schedule-sms

Request Body

ParameterTypeRequiredDescription
associationIdintegerYesAssociation ID
phoneNumberstringYesRecipient phone number
messagestringYesMessage content
scheduledAtstringYesISO 8601 delivery date (e.g. "2026-03-01T09:00:00Z")
credentialIdintegerOptionalSpecific credential to use

Example Response

{ "success": true, "data": { "queueId": 456, "jobId": "job_sch_789", "status": "queued" }, "message": "Notification queued for sending" }

Webhook scheduling: Add "scheduledAt": "2026-03-01T09:00:00Z" to the POST /api/v1/webhook/notification body to schedule via HMAC-authenticated webhook.

GET

View Queue

Session Auth

List queued and scheduled messages with pagination.

Endpoint

GET /api/v1/dashboard/queue?page=1&limit=20

Example Response

{
  "success": true,
  "data": {
    "items": [
      { "id": "q_sch_456", "associationId": 1, "recipient": "+966500000000", "message": "Your appointment is tomorrow...", "status": "scheduled", "scheduledAt": "2026-03-01T09:00:00.000Z", "createdAt": "2026-02-11T10:00:00.000Z" }
    ],
    "total": 5, "page": 1, "limit": 20
  }
}
DELETE

Cancel Scheduled

Session Auth

Cancel a scheduled message before it is sent.

Endpoint

DELETE /api/v1/dashboard/queue/:id

Example Response

{ "success": true, "message": "Scheduled message cancelled successfully" }
POST

Send Bulk (Webhook)

HMAC Auth

Send an SMS to multiple recipients in a single request. Creates a campaign internally for tracking.

Endpoint

POST /api/v1/webhook/bulk

Request Body

ParameterTypeRequiredDescription
associationIdintegerYesAssociation ID
recipientsstring[]YesArray of phone numbers in E.164 format
messagestringYesMessage content
channelstringOptional"sms" (default), "whatsapp", or "email"
campaignNamestringOptionalName for the campaign
scheduledAtstringOptionalISO 8601 date for scheduled delivery
dataobjectOptionalGlobal template variables

Example Response

{ "success": true, "data": { "campaignId": 1, "totalRecipients": 3, "status": "processing" }, "message": "Bulk notification campaign created" }
POST

Create Campaign

Session Auth

Create a bulk SMS campaign from the dashboard.

Endpoint

POST /api/v1/dashboard/campaigns

Request Body

ParameterTypeRequiredDescription
associationIdintegerYesAssociation ID
namestringYesCampaign name
messagestringYesMessage content
recipientsstring[] | object[]YesArray of phone numbers or recipient objects
scheduledAtstringOptionalISO 8601 date for scheduled delivery
credentialIdintegerOptionalSpecific credential to use
GET

List Campaigns

Session Auth

Retrieve a paginated list of campaigns.

Endpoint

GET /api/v1/dashboard/campaigns?page=1&limit=20

Example Response

{
  "success": true,
  "data": {
    "items": [
      { "id": 1, "name": "Welcome Campaign", "status": "completed", "totalRecipients": 500, "sent": 498, "failed": 2, "createdAt": "2026-02-01T10:00:00.000Z" }
    ],
    "total": 12, "page": 1, "limit": 20
  }
}
DELETE

Cancel Campaign

Session Auth

Cancel a pending or scheduled campaign. Already-sent messages cannot be recalled.

Endpoint

DELETE /api/v1/dashboard/campaigns/:id

Example Response

{ "success": true, "message": "Campaign cancelled successfully" }

Personalization

Use {{variable}} placeholders in your message to personalize content per recipient. When using bulk endpoints, pass each recipient as an object with individual variables.

Recipient Data Format

{
  "associationId": 1,
  "message": "Hello {{name}}, your code is {{code}}. Expires in {{expiry}} minutes.",
  "recipients": [
    { "phone": "+966500000001", "variables": { "name": "Ahmed", "code": "123456", "expiry": "5" } },
    { "phone": "+966500000002", "variables": { "name": "Sara", "code": "654321", "expiry": "10" } }
  ],
  "campaignName": "Verification Codes"
}

Note: If a variable placeholder has no matching key, it will be left as-is. Variable names are case-sensitive.

GET

List Allocations

Session Auth

List all country routing allocations.

Endpoint

GET /api/v1/dashboard/allocations

Example Response

{
  "success": true,
  "data": [
    { "id": 1, "associationId": 1, "credentialId": 10, "routingType": "include", "countryCode": "SA", "countryName": "Saudi Arabia", "priority": 10 },
    { "id": 2, "associationId": 1, "credentialId": 11, "routingType": "exclude", "excludedCountries": ["SA", "AE"], "priority": 5 }
  ]
}
POST

Create Allocation

Session Auth

Create a new country routing allocation to control which SMS provider is used for specific countries.

Endpoint

POST /api/v1/dashboard/allocations

Request Body

ParameterTypeRequiredDescription
associationIdintegerYesAssociation ID
credentialIdintegerYesCredential ID to route to
routingTypestringYes"include" (specific country) or "exclude" (all except)
countryCodestringConditionalISO 3166-1 alpha-2 code (for "include" type)
countryNamestringOptionalHuman-readable country name
excludedCountriesstring[]ConditionalCountry codes to exclude (for "exclude" type)
priorityintegerOptionalPriority number (higher wins, default: 0)

Example — Route Saudi Arabia to Yamamah

{
  "associationId": 1, "credentialId": 10, "routingType": "include",
  "countryCode": "SA", "countryName": "Saudi Arabia", "priority": 10
}

Priority System

The routing engine evaluates allocations in this order:

  1. Specific country match — An "include" rule matching the recipient's exact country code takes highest precedence.
  2. All-except rules — An "exclude" rule that does not exclude the recipient's country is evaluated next.
  3. Default provider — If no allocation matches, the association's default credential is used.

Within each level, the allocation with the higher priority number wins.

PUT

Update Allocation

Session Auth

Update an existing routing allocation.

Endpoint

PUT /api/v1/dashboard/allocations/:id

Example Request

{ "credentialId": 11, "priority": 15 }
DELETE

Delete Allocation

Session Auth

Remove a country routing allocation.

Endpoint

DELETE /api/v1/dashboard/allocations/:id

Example Response

{ "success": true, "message": "Allocation deleted successfully" }
GET

Get Logs

Session Auth

Retrieve SMS delivery logs with filtering and pagination.

Endpoint

GET /api/v1/dashboard/logs?page=1&limit=20&status=success&associationId=1

Query Parameters

ParameterTypeDefaultDescription
pageinteger1Page number
limitinteger20Items per page
statusstringallFilter: "success", "failed", "pending"
searchstringSearch by phone number or message
associationIdintegerFilter by association

Example Response

{
  "success": true,
  "data": {
    "items": [
      { "id": 5678, "associationId": 1, "recipient": "+966500000000", "message": "Hello!", "status": "success", "provider": "yamamah", "messageId": "msg_xxxxxxxxxxxx", "sentAt": "2026-02-11T09:00:00.000Z" }
    ],
    "total": 150, "page": 1, "limit": 20
  }
}
GET

Get Stats

Session Auth

Retrieve aggregate SMS statistics.

Endpoint

GET /api/v1/dashboard/stats

Example Response

{
  "success": true,
  "data": {
    "totalSent": 15420, "totalFailed": 83, "totalPending": 12, "successRate": 99.46,
    "todaySent": 342, "todayFailed": 2,
    "byProvider": {
      "yamamah": { "sent": 8200, "failed": 30 },
      "unifonic": { "sent": 5100, "failed": 40 },
      "twilio": { "sent": 2120, "failed": 13 }
    }
  }
}

HMAC Signing

All webhook requests must be signed using HMAC-SHA256. Below are complete signing examples.

JavaScript / Node.js

const crypto = require('crypto');

function signRequest(body, secretKey) {
  const timestamp = Date.now().toString();
  const payload = timestamp + '.' + JSON.stringify(body);
  const signature = crypto
    .createHmac('sha256', secretKey)
    .update(payload)
    .digest('hex');
  return { timestamp, signature };
}

const body = { associationId: 1, recipient: '+966500000000', message: 'Hello!' };
const { timestamp, signature } = signRequest(body, 'your-secret-key');
// Headers: X-Webhook-Timestamp: timestamp, X-Webhook-Signature: signature

PHP

<?php
function signRequest($body, $secretKey) {
    $timestamp = round(microtime(true) * 1000);
    $payload = $timestamp . '.' . json_encode($body);
    $signature = hash_hmac('sha256', $payload, $secretKey);
    return ['timestamp' => $timestamp, 'signature' => $signature];
}

$body = ['associationId' => 1, 'recipient' => '+966500000000', 'message' => 'Hello!'];
$sign = signRequest($body, 'your-secret-key');
$headers = [
    'Content-Type: application/json',
    'X-Webhook-Timestamp: ' . $sign['timestamp'],
    'X-Webhook-Signature: ' . $sign['signature']
];

Python

import hmac, hashlib, json, time

def sign_request(body, secret_key):
    timestamp = str(int(time.time() * 1000))
    payload = timestamp + '.' + json.dumps(body, separators=(',', ':'))
    signature = hmac.new(
        secret_key.encode(), payload.encode(), hashlib.sha256
    ).hexdigest()
    return timestamp, signature

body = {'associationId': 1, 'recipient': '+966500000000', 'message': 'Hello!'}
timestamp, signature = sign_request(body, 'your-secret-key')
headers = {
    'Content-Type': 'application/json',
    'X-Webhook-Timestamp': timestamp,
    'X-Webhook-Signature': signature
}

IP Whitelisting

For enhanced security, your API access can be restricted to specific IP addresses. Contact your administrator to configure the allowed IPs for your integration.

What to Provide

  • Your server's public IP address(es)
  • CIDR ranges if using multiple IPs (e.g., 10.0.0.0/24)
  • Cloud provider NAT gateway IPs if applicable

Note: When IP whitelisting is enabled, requests from non-whitelisted IPs will receive a 403 Forbidden response.

Error Codes

The API uses standard HTTP status codes and returns detailed error messages.

StatusErrorDescription
400Invalid requestRequest body validation failed (missing fields, wrong types)
401Missing signatureX-Webhook-Signature or X-Webhook-Timestamp header is missing
401Invalid signatureHMAC verification failed (wrong secret or tampered payload)
401Timestamp expiredTimestamp more than 5 minutes old
401Invalid API keyProvided x-api-key / x-api-secret is invalid
403IP not allowedRequest IP is not in the whitelist
403Inactive associationAssociation has been deactivated
429Rate limitedToo many requests — retry after the Retry-After header
500Server errorInternal server error — retry later

Error Response Format

{ "success": false, "error": "Invalid signature: HMAC verification failed" }

Rate Limits

To ensure fair usage and system stability, API requests are rate limited.

Single SMS Endpoint

100

requests per minute per IP

Bulk SMS Endpoint

10

requests per minute per IP

Need higher limits? Contact your administrator. When rate limited, the response includes a Retry-After header.

Providers List

Supported SMS providers and their configuration.

CodeNameChannelRequired Credentials
yamamahYamamahSMSusername, password
shastraShastraSMSusername, password
qyadatQyadatSMSusername, password
unifonicUnifonicSMSappSid
4jawaly4JawalySMSappKey, appSecret
infobipInfobipSMSapiKey
twilioTwilioSMS, WhatsAppaccountSid, authToken

WhatsApp Integration

Send WhatsApp messages through the Meta WhatsApp Business API. Supports template messages, text messages (within the 24-hour customer service window), media messages, and bulk sending.

Templates

Create and manage Meta-approved message templates

Text Messages

Send free-form text within 24h customer service window

Media

Images, documents, video, and audio

Bulk Send

Send templates to thousands of recipients

Authentication: All WhatsApp endpoints use API Key authentication via the X-API-Key header — the same key used for association and credential management.

POST

Save WhatsApp Credentials

API Key Auth

Save or update Meta WhatsApp Business API credentials for an association. If credentials already exist they will be overwritten.

Endpoint

POST /api/v1/whatsapp-credentials/:associationId

Request Body

ParameterTypeRequiredDescription
phoneNumberIdstringYesMeta phone number ID from WhatsApp Business Manager
businessAccountIdstringYesWhatsApp Business Account ID (WABA)
accessTokenstringYesPermanent access token from Meta
displayPhoneNumberstringYesDisplay phone number (e.g., +966920033773)
verifiedNamestringOptionalMeta-verified business display name
webhookVerifyTokenstringOptionalToken for Meta webhook verification callback

Example Response

{ "success": true, "data": { "id": 1, "associationId": 123 } }
GET

Get WhatsApp Credentials

API Key Auth

Retrieve WhatsApp credentials for an association. The accessToken is never included in the response for security.

Endpoint

GET /api/v1/whatsapp-credentials/:associationId

Example Response

{
  "success": true,
  "data": {
    "id": 1,
    "associationId": 123,
    "phoneNumberId": "960908243776022",
    "businessAccountId": "755633283894125",
    "displayPhoneNumber": "+966920033773",
    "verifiedName": "My Business",
    "isActive": true,
    "createdAt": "2026-01-15T10:00:00.000Z",
    "updatedAt": "2026-01-15T10:00:00.000Z"
  }
}
POST

Create WhatsApp Template

API Key Auth

Create a WhatsApp message template. Set submitToMeta: true to submit for Meta approval in the same call.

Endpoint

POST /api/v1/whatsapp-templates

Request Body

ParameterTypeRequiredDescription
associationIdintegerYesAssociation ID
namestringYesTemplate name (lowercase, underscores only)
languagestringYesLanguage code (e.g., "en", "ar")
categorystringYesMARKETING, UTILITY, or AUTHENTICATION
bodyTextstringYesMessage body with {{1}}, {{2}} variable placeholders
headerTypestringOptionalNONE, TEXT, IMAGE, VIDEO, or DOCUMENT
headerContentstringOptionalHeader text content
headerMediaUrlstringOptionalHeader media URL (for IMAGE/VIDEO/DOCUMENT headers)
footerTextstringOptionalFooter text
buttonsarrayOptionalTemplate buttons array
variablesstring[]OptionalVariable names, e.g., ["name", "orderId"]
sampleValuesobjectOptionalSample values for Meta approval review
submitToMetabooleanOptionalSubmit to Meta for approval immediately (default: false)

Example Request

{
  "associationId": 123,
  "name": "order_confirmation",
  "language": "en",
  "category": "UTILITY",
  "bodyText": "Hello {{1}}, your order {{2}} is confirmed.",
  "footerText": "Thank you for shopping with us",
  "variables": ["name", "orderId"],
  "submitToMeta": true
}
GET

List WhatsApp Templates

API Key Auth

List WhatsApp templates for an association, optionally filtered by status or category.

Endpoint

GET /api/v1/whatsapp-templates?associationId=123&status=APPROVED&category=UTILITY

Query Parameters

ParameterTypeRequiredDescription
associationIdintegerYesAssociation ID
statusstringOptionalFilter: PENDING, APPROVED, REJECTED, DELETED
categorystringOptionalFilter: MARKETING, UTILITY, AUTHENTICATION

Manage WhatsApp Template

API Key Auth
GET /api/v1/whatsapp-templates/:id Get a single template by ID. Includes a sendInfo object with required parameters and an example payload.
PUT /api/v1/whatsapp-templates/:id Update template fields (bodyText, footerText, variables, headerMediaUrl, sampleValues, etc.)
DELETE /api/v1/whatsapp-templates/:id?deleteFromMeta=true Delete template; add deleteFromMeta=true to also remove from Meta

GET Response — sendInfo

When you fetch a template via GET /api/v1/whatsapp-templates/:id, the response includes a sendInfo object that tells you exactly what parameters to pass when sending:

{
  "sendInfo": {
    "requiredParams": [
      { "key": "var1", "type": "text", "description": "Body variable {{1}}" },
      { "key": "header_image", "type": "url", "description": "IMAGE header URL (optional, defaults to template's saved media)" },
      { "key": "button_url", "type": "text", "description": "Dynamic value for URL button \"إيصال التبرع\" (pattern: https://send.jood.cloud/r/{{1}})" }
    ],
    "headerType": "IMAGE",
    "defaultHeaderMedia": "https://send.jood.cloud/media/donation_received_temp.png",
    "urlButtons": [
      { "text": "إيصال التبرع", "urlPattern": "https://send.jood.cloud/r/{{1}}", "isDynamic": true }
    ],
    "examplePayload": {
      "associationId": 1,
      "templateId": 22,
      "recipient": "+966500000000",
      "parameters": { "var1": "value", "header_image": "https://example.com/file.jpg", "button_url": "value" }
    }
  }
}

PUT Request Body (all fields optional)

ParameterTypeRequiredDescription
namestringOptionalTemplate name
languagestringOptionalLanguage code
categorystringOptionalMARKETING, UTILITY, or AUTHENTICATION
headerTypestringOptionalNONE, TEXT, IMAGE, VIDEO, or DOCUMENT
headerContentstringOptionalHeader text content
headerMediaUrlstringOptionalHeader media URL (for media headers)
bodyTextstringOptionalMessage body text
footerTextstringOptionalFooter text
buttonsarrayOptionalTemplate buttons
variablesstring[]OptionalVariable names
sampleValuesobjectOptionalSample values for Meta
isActivebooleanOptionalEnable or disable template
submitToMetabooleanOptionalRe-submit to Meta after update
POST

Submit & Sync Templates

API Key Auth
POST /api/v1/whatsapp-templates/:id/submit Submit a template to Meta for approval
POST /api/v1/whatsapp-templates/:id/sync Sync a single template’s approval status from Meta
POST /api/v1/whatsapp-templates/sync-all Sync all templates from Meta for an association

Sync-All Request Body

ParameterTypeRequiredDescription
associationIdintegerYesAssociation ID

Sync-All Response

{
  "success": true,
  "data": {
    "created": 3,
    "updated": 5,
    "deleted": 1,
    "errors": [],
    "message": "Sync complete: 3 created, 5 updated, 1 marked as deleted"
  }
}
POST

Send WhatsApp Template Message

API Key Auth

Send an approved WhatsApp template message. The template must have APPROVED status from Meta.

Endpoint

POST /api/v1/whatsapp-send/template

Request Body

ParameterTypeRequiredDescription
associationIdintegerYesAssociation ID
templateIdintegerYesTemplate ID (local database ID)
recipientstringYesPhone number with country code (e.g., +201097899908)
parametersobjectOptionalTemplate variable values. Body variables: {"name": "Ahmed"}. For templates with media headers, include header_image, header_video, or header_document with a public URL to override the default media. For URL buttons with dynamic suffix, include button_url.

Special Parameters (inside parameters)

Header media (for templates with IMAGE, VIDEO, or DOCUMENT headers):

  • header_image — public URL to an image (JPEG/PNG)
  • header_video — public URL to a video (MP4)
  • header_document — public URL to a document (PDF)

If omitted, the template’s default header media is used.


URL button (for templates with dynamic URL buttons containing {{1}}):

  • button_url — the dynamic suffix value that replaces {{1}} in the button URL
  • button_url_0, button_url_1 — use indexed keys if the template has multiple URL buttons

Required if the template has a URL button with a dynamic placeholder. You can use a short code from the Redirect Links API: create a link with your target URL (e.g. per-association receipt), then pass the returned code as button_url so the template button https://send.jood.cloud/r/{{1}} becomes a tracked redirect to your URL.

cURL Example

curl -X POST "https://send.jood.cloud/api/v1/whatsapp-send/template" \
  -H "X-API-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "associationId": 1,
    "templateId": 22,
    "recipient": "+201097899908",
    "parameters": {
      "var1": "جمعية الوفاء",
      "header_image": "https://send.jood.cloud/media/donation_received_temp.png",
      "button_url": "https://jood.sa/receipt/123"
    }
  }'

Success Response

{
  "success": true,
  "data": {
    "success": true,
    "messageId": 42,
    "metaMessageId": "wamid.HBgMMjAxMDk3ODk5OTA4...",
    "providerResponse": { "messaging_product": "whatsapp", "contacts": [...], "messages": [...] }
  }
}
POST

Send WhatsApp Text Message

API Key Auth

Send a free-form text message. Only works within the 24-hour customer service window — the customer must have messaged your number first.

Endpoint

POST /api/v1/whatsapp-send/text

Request Body

ParameterTypeRequiredDescription
associationIdintegerYesAssociation ID
recipientstringYesPhone number with country code
messagestringYesMessage text content
POST

Send WhatsApp Media Message

API Key Auth

Send a media message (image, document, video, or audio). Requires the 24-hour customer service window for non-template messages.

Endpoint

POST /api/v1/whatsapp-send/media

Request Body

ParameterTypeRequiredDescription
associationIdintegerYesAssociation ID
recipientstringYesPhone number with country code
mediaTypestringYesimage, document, video, or audio
mediaUrlstringYesPublicly accessible URL of the media file
captionstringOptionalCaption text (for image, video, document)
filenamestringOptionalFilename (for document type)
POST

Send WhatsApp Bulk Messages

API Key Auth

Send an approved template to multiple recipients with per-recipient variables. Batches larger than 50 are processed asynchronously and return a jobId for status polling.

Endpoint

POST /api/v1/whatsapp-send/bulk

Request Body

ParameterTypeRequiredDescription
associationIdintegerYesAssociation ID
templateIdintegerYesApproved template ID
recipientsarrayYesArray of {phone, parameters} objects. Include header_image for image headers and button_url for dynamic URL buttons in each recipient’s parameters.
credentialIdintegerOptionalSpecific credential ID (optional, uses default)

Example Request

{
  "associationId": 1,
  "templateId": 14,
  "recipients": [
    { "phone": "+201097899908", "parameters": { "var1": "جمعية الوفاء", "header_image": "https://example.com/image.jpg", "button_url": "https://jood.sa/receipt/001" } },
    { "phone": "+966500000002", "parameters": { "var1": "متجر سبات", "header_image": "https://example.com/image.jpg", "button_url": "https://jood.sa/receipt/002" } }
  ]
}

Poll Bulk Job Status

GET /api/v1/whatsapp-send/bulk/status/:jobId

Status Response

{ "success": true, "data": { "jobId": "wa_bulk_...", "status": "processing", "sent": 25, "failed": 0, "total": 100, "startedAt": "2026-02-14T10:00:00.000Z" } }
GET

WhatsApp Messages & Stats

API Key Auth

Message History

GET /api/v1/whatsapp-messages?associationId=123&status=SENT&recipient=201097899908&limit=50&offset=0
ParameterTypeRequiredDescription
associationIdintegerYesAssociation ID
statusstringOptionalFilter: SENT, DELIVERED, READ, FAILED
recipientstringOptionalFilter by recipient phone number (partial match)
limitintegerOptionalMax results (default: 50)
offsetintegerOptionalPagination offset (default: 0)

Statistics

GET /api/v1/whatsapp-stats?associationId=123&days=30
ParameterTypeRequiredDescription
associationIdintegerYesAssociation ID
daysintegerOptionalNumber of days to look back (default: 30)

Stats Response

{
  "success": true,
  "data": {
    "total": 1250,
    "sent": 1100,
    "delivered": 980,
    "read": 750,
    "failed": 150,
    "templates": 8,
    "period": 30
  }
}

Code Examples

Full end-to-end examples for sending an SMS via the webhook endpoint in various languages.

const crypto = require('crypto');
const https = require('https');

const SECRET_KEY = 'your-secret-key';
const body = {
  associationId: 1,
  recipient: '+966500000000',
  message: 'Your verification code is 482901'
};

const timestamp = Date.now().toString();
const payload = timestamp + '.' + JSON.stringify(body);
const signature = crypto.createHmac('sha256', SECRET_KEY).update(payload).digest('hex');

const data = JSON.stringify(body);
const options = {
  hostname: 'send.jood.cloud',
  path: '/api/v1/webhook/notification',
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Content-Length': Buffer.byteLength(data),
    'X-Webhook-Timestamp': timestamp,
    'X-Webhook-Signature': signature
  }
};

const req = https.request(options, (res) => {
  let body = '';
  res.on('data', (chunk) => body += chunk);
  res.on('end', () => console.log(JSON.parse(body)));
});
req.write(data);
req.end();
<?php
$secretKey = 'your-secret-key';
$body = [
    'associationId' => 1,
    'recipient' => '+966500000000',
    'message' => 'Your verification code is 482901'
];

$timestamp = round(microtime(true) * 1000);
$jsonBody = json_encode($body);
$payload = $timestamp . '.' . $jsonBody;
$signature = hash_hmac('sha256', $payload, $secretKey);

$ch = curl_init('https://send.jood.cloud/api/v1/webhook/notification');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $jsonBody,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'X-Webhook-Timestamp: ' . $timestamp,
        'X-Webhook-Signature: ' . $signature,
    ],
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;
import hmac, hashlib, json, time, requests

SECRET_KEY = 'your-secret-key'
body = {
    'associationId': 1,
    'recipient': '+966500000000',
    'message': 'Your verification code is 482901'
}

timestamp = str(int(time.time() * 1000))
json_body = json.dumps(body, separators=(',', ':'))
payload = timestamp + '.' + json_body
signature = hmac.new(SECRET_KEY.encode(), payload.encode(), hashlib.sha256).hexdigest()

response = requests.post(
    'https://send.jood.cloud/api/v1/webhook/notification',
    json=body,
    headers={
        'X-Webhook-Timestamp': timestamp,
        'X-Webhook-Signature': signature,
    }
)
print(response.json())
using System;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        var secretKey = "your-secret-key";
        var body = new { associationId = 1, recipient = "+966500000000", message = "Your verification code is 482901" };
        var jsonBody = JsonSerializer.Serialize(body);
        var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString();
        var payload = timestamp + "." + jsonBody;

        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey));
        var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
        var signature = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();

        using var client = new HttpClient();
        var request = new HttpRequestMessage(HttpMethod.Post, "https://send.jood.cloud/api/v1/webhook/notification");
        request.Content = new StringContent(jsonBody, Encoding.UTF8, "application/json");
        request.Headers.Add("X-Webhook-Timestamp", timestamp);
        request.Headers.Add("X-Webhook-Signature", signature);

        var response = await client.SendAsync(request);
        Console.WriteLine(await response.Content.ReadAsStringAsync());
    }
}
# Generate signature first (example using bash):
TIMESTAMP=$(date +%s%3N)
BODY='{"associationId":1,"recipient":"+966500000000","message":"Hello!"}'
SECRET="your-secret-key"
PAYLOAD="${TIMESTAMP}.${BODY}"
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')

curl -X POST "https://send.jood.cloud/api/v1/webhook/notification" \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Timestamp: $TIMESTAMP" \
  -H "X-Webhook-Signature: $SIGNATURE" \
  -d "$BODY"

Jood Notifications API — Developed and powered by Jood Team