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.cloudThe API is organised into three main route groups:
| Prefix | Purpose | Auth |
|---|---|---|
/api/v1/webhook | External integrations & sending | HMAC Signature |
/api/v1/associations | Association management | API Key + Secret |
/api/v1/redirect-links | Short-link redirects (e.g. for WhatsApp buttons) | API Key |
/api/v1/dashboard | Dashboard operations | Session (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
| Header | Description |
|---|---|
X-Webhook-Signature | HMAC-SHA256 hex digest of timestamp + "." + JSON body |
X-Webhook-Timestamp | Unix timestamp in milliseconds. Must be within 5 minutes of server time. |
Content-Type | application/json |
Signature Generation Steps
- Get the current timestamp in milliseconds:
Date.now() - Create the payload string:
timestamp + "." + JSON.stringify(body) - Generate HMAC-SHA256 hash using your Secret Key
- 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
| Header | Description |
|---|---|
x-api-key | Your association's API Key |
x-api-secret | Your association's Secret Key |
Content-Type | application/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"List Associations
API Key AuthRetrieve all associations. Super admin access only.
Endpoint
GET /api/v1/associationscURL 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"
}
]
}Create Association
API Key AuthCreate a new association. Returns the generated API Key and Secret Key.
Endpoint
POST /api/v1/associationsRequest Body
| Parameter | Type | Required | Description |
|---|---|---|---|
id | integer | Yes | Your external system's association ID (must be unique) |
name | string | Yes | Association name |
allowedDomains | string[] | Optional | Allowed 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 Association
API Key AuthRetrieve a single association by ID.
Endpoint
GET /api/v1/associations/:idExample 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"
}
}Update Association
API Key AuthUpdate an existing association.
Endpoint
PUT /api/v1/associations/:idRequest Body
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Optional | New name |
allowedDomains | string[] | Optional | Updated allowed domains |
isActive | boolean | Optional | Enable or disable association |
Example Request
{ "name": "Acme Corp Updated", "allowedDomains": ["acme.com", "acme.io", "acme.dev"], "isActive": true }Rotate Keys
API Key AuthRotate the API Key and/or Secret Key for an association. Old keys are immediately invalidated.
Endpoint
POST /api/v1/associations/:id/rotate-keysRequest Body
| Parameter | Type | Required | Description |
|---|---|---|---|
rotateApiKey | boolean | Optional | Rotate the API Key (default: false) |
rotateSecretKey | boolean | Optional | Rotate the Secret Key (default: false) |
Example Response
{
"success": true,
"data": { "apiKey": "ak_live_newxxxxxxxxxxxxxxxxxxxxxxx", "secretKey": "sk_live_newxxxxxxxxxxxxxxxxxxxxxxx" },
"message": "Keys rotated successfully"
}List Credentials
API Key AuthList all SMS provider credentials for an association.
Endpoint
GET /api/v1/associations/:associationId/credentialsExample 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" }
]
}Add Credential
API Key AuthAdd a new SMS provider credential to an association.
Endpoint
POST /api/v1/associations/:associationId/credentialsRequest Body
| Parameter | Type | Required | Description |
|---|---|---|---|
providerId | integer | Yes | Provider ID (use GET /api/v1/providers to list available providers) |
senderName | string | Yes | Sender name / ID shown to recipients |
credentials | object | Yes | Provider-specific credentials (see Providers List) |
apiUrl | string | Optional | Custom API URL override |
isDefault | boolean | Optional | Set 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"
}Update Credential
API Key AuthUpdate an existing credential's settings or provider credentials.
Endpoint
PUT /api/v1/associations/:associationId/credentials/:credentialIdRequest Body
| Parameter | Type | Required | Description |
|---|---|---|---|
senderName | string | Optional | Updated sender name |
credentials | object | Optional | Updated provider credentials |
apiUrl | string | Optional | Custom API URL override |
isDefault | boolean | Optional | Set as default |
Example Request
{ "senderName": "ACME_NEW", "credentials": { "username": "acme_new", "password": "newP@ss123" }, "isDefault": true }Delete Credential
API Key AuthRemove a provider credential.
Endpoint
DELETE /api/v1/associations/:associationId/credentials/:credentialIdExample Response
{ "success": true, "message": "Credential deleted successfully" }Redirect Links
Short links map https://send.jood.cloud/r/<code> to a target URL you provide. Use them for WhatsApp template button URLs: create a link per association (e.g. receipt URL), then pass the returned code as button_url when sending the template. Clicks are counted and the user is redirected to your saved URL. Public redirect is GET https://send.jood.cloud/r/<code> (no auth).
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/redirect-links | Create link. Body: associationId, targetUrl (required), name (optional). Returns code and shortUrl. |
| GET | /api/v1/redirect-links | List links. Query: associationId, page, limit. Response includes clickCount per link. |
| GET | /api/v1/redirect-links/:id | Get one link with click count. Scoped by association. |
| PUT | /api/v1/redirect-links/:id | Update targetUrl or name. |
| DELETE | /api/v1/redirect-links/:id | Delete link. Scoped by association. |
All redirect-links endpoints require X-API-Key. For association-scoped keys, associationId is inferred; for super-admin keys, pass associationId in body or query.
Create Example
curl -X POST "https://send.jood.cloud/api/v1/redirect-links" \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{ "associationId": 1, "targetUrl": "https://jood.sa/receipt/123", "name": "Receipt link" }'{ "success": true, "data": { "id": 1, "code": "a1b2c3d4", "shortUrl": "https://send.jood.cloud/r/a1b2c3d4", "targetUrl": "https://jood.sa/receipt/123", "name": "Receipt link", "createdAt": "2026-03-10T12:00:00.000Z" } }Provider Schemas
Session AuthRetrieve the configuration schema for all available SMS providers. Use this to build dynamic credential forms.
Endpoint
GET /api/v1/dashboard/providers/schemasExample 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"] }
]
}Send Single SMS (Webhook)
HMAC AuthQueue a single SMS message for delivery. The message is placed in a high-priority queue and processed asynchronously.
Endpoint
POST /api/v1/webhook/notificationRequest Body
| Parameter | Type | Required | Description |
|---|---|---|---|
associationId | integer | Yes | Your association ID |
recipient | string | Yes | Phone number in E.164 format |
message | string | Yes | Message content |
channel | string | Optional | "sms" (default) or "whatsapp" |
templateCode | string | Optional | Template code to use instead of raw message |
data | object | Optional | Template variable values |
scheduledAt | string | Optional | ISO 8601 date for scheduled delivery |
priority | integer | Optional | Queue 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!"}'Send Immediate (Dashboard)
Session AuthSend an SMS immediately from the dashboard. Unlike the webhook endpoint, this sends synchronously and returns the delivery result.
Endpoint
POST /api/v1/dashboard/send-smsRequest Body
| Parameter | Type | Required | Description |
|---|---|---|---|
associationId | integer | Yes | Association ID |
phoneNumber | string | Yes | Recipient phone number |
message | string | Yes | Message content |
credentialId | integer | Optional | Specific credential to use (default credential if omitted) |
Example Response
{
"success": true,
"data": { "logId": 5678, "messageId": "msg_xxxxxxxxxxxx", "success": true }
}Send with Template
HMAC AuthSend an SMS using a pre-defined template. Pass the templateCode and a data object with the template variables.
Endpoint
POST /api/v1/webhook/notificationExample 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.
Schedule SMS
Session AuthSchedule 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-smsRequest Body
| Parameter | Type | Required | Description |
|---|---|---|---|
associationId | integer | Yes | Association ID |
phoneNumber | string | Yes | Recipient phone number |
message | string | Yes | Message content |
scheduledAt | string | Yes | ISO 8601 delivery date (e.g. "2026-03-01T09:00:00Z") |
credentialId | integer | Optional | Specific 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.
View Queue
Session AuthList queued and scheduled messages with pagination.
Endpoint
GET /api/v1/dashboard/queue?page=1&limit=20Example 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
}
}Cancel Scheduled
Session AuthCancel a scheduled message before it is sent.
Endpoint
DELETE /api/v1/dashboard/queue/:idExample Response
{ "success": true, "message": "Scheduled message cancelled successfully" }Send Bulk (Webhook)
HMAC AuthSend an SMS to multiple recipients in a single request. Creates a campaign internally for tracking.
Endpoint
POST /api/v1/webhook/bulkRequest Body
| Parameter | Type | Required | Description |
|---|---|---|---|
associationId | integer | Yes | Association ID |
recipients | string[] | Yes | Array of phone numbers in E.164 format |
message | string | Yes | Message content |
channel | string | Optional | "sms" (default), "whatsapp", or "email" |
campaignName | string | Optional | Name for the campaign |
scheduledAt | string | Optional | ISO 8601 date for scheduled delivery |
data | object | Optional | Global template variables |
Example Response
{ "success": true, "data": { "campaignId": 1, "totalRecipients": 3, "status": "processing" }, "message": "Bulk notification campaign created" }Create Campaign
Session AuthCreate a bulk SMS campaign from the dashboard.
Endpoint
POST /api/v1/dashboard/campaignsRequest Body
| Parameter | Type | Required | Description |
|---|---|---|---|
associationId | integer | Yes | Association ID |
name | string | Yes | Campaign name |
message | string | Yes | Message content |
recipients | string[] | object[] | Yes | Array of phone numbers or recipient objects |
scheduledAt | string | Optional | ISO 8601 date for scheduled delivery |
credentialId | integer | Optional | Specific credential to use |
List Campaigns
Session AuthRetrieve a paginated list of campaigns.
Endpoint
GET /api/v1/dashboard/campaigns?page=1&limit=20Example 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
}
}Cancel Campaign
Session AuthCancel a pending or scheduled campaign. Already-sent messages cannot be recalled.
Endpoint
DELETE /api/v1/dashboard/campaigns/:idExample 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.
List Allocations
Session AuthList all country routing allocations.
Endpoint
GET /api/v1/dashboard/allocationsExample 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 }
]
}Create Allocation
Session AuthCreate a new country routing allocation to control which SMS provider is used for specific countries.
Endpoint
POST /api/v1/dashboard/allocationsRequest Body
| Parameter | Type | Required | Description |
|---|---|---|---|
associationId | integer | Yes | Association ID |
credentialId | integer | Yes | Credential ID to route to |
routingType | string | Yes | "include" (specific country) or "exclude" (all except) |
countryCode | string | Conditional | ISO 3166-1 alpha-2 code (for "include" type) |
countryName | string | Optional | Human-readable country name |
excludedCountries | string[] | Conditional | Country codes to exclude (for "exclude" type) |
priority | integer | Optional | Priority 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:
- Specific country match — An "include" rule matching the recipient's exact country code takes highest precedence.
- All-except rules — An "exclude" rule that does not exclude the recipient's country is evaluated next.
- Default provider — If no allocation matches, the association's default credential is used.
Within each level, the allocation with the higher priority number wins.
Update Allocation
Session AuthUpdate an existing routing allocation.
Endpoint
PUT /api/v1/dashboard/allocations/:idExample Request
{ "credentialId": 11, "priority": 15 }Delete Allocation
Session AuthRemove a country routing allocation.
Endpoint
DELETE /api/v1/dashboard/allocations/:idExample Response
{ "success": true, "message": "Allocation deleted successfully" }Get Logs
Session AuthRetrieve SMS delivery logs with filtering and pagination.
Endpoint
GET /api/v1/dashboard/logs?page=1&limit=20&status=success&associationId=1Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
limit | integer | 20 | Items per page |
status | string | all | Filter: "success", "failed", "pending" |
search | string | — | Search by phone number or message |
associationId | integer | — | Filter 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 Stats
Session AuthRetrieve aggregate SMS statistics.
Endpoint
GET /api/v1/dashboard/statsExample 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: signaturePHP
<?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.
| Status | Error | Description |
|---|---|---|
| 400 | Invalid request | Request body validation failed (missing fields, wrong types) |
| 401 | Missing signature | X-Webhook-Signature or X-Webhook-Timestamp header is missing |
| 401 | Invalid signature | HMAC verification failed (wrong secret or tampered payload) |
| 401 | Timestamp expired | Timestamp more than 5 minutes old |
| 401 | Invalid API key | Provided x-api-key / x-api-secret is invalid |
| 403 | IP not allowed | Request IP is not in the whitelist |
| 403 | Inactive association | Association has been deactivated |
| 429 | Rate limited | Too many requests — retry after the Retry-After header |
| 500 | Server error | Internal 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.
| Code | Name | Channel | Required Credentials |
|---|---|---|---|
yamamah | Yamamah | SMS | username, password |
shastra | Shastra | SMS | username, password |
qyadat | Qyadat | SMS | username, password |
unifonic | Unifonic | SMS | appSid |
4jawaly | 4Jawaly | SMS | appKey, appSecret |
infobip | Infobip | SMS | apiKey |
twilio | Twilio | SMS, WhatsApp | accountSid, 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.
Save WhatsApp Credentials
API Key AuthSave or update Meta WhatsApp Business API credentials for an association. If credentials already exist they will be overwritten.
Endpoint
POST /api/v1/whatsapp-credentials/:associationIdRequest Body
| Parameter | Type | Required | Description |
|---|---|---|---|
phoneNumberId | string | Yes | Meta phone number ID from WhatsApp Business Manager |
businessAccountId | string | Yes | WhatsApp Business Account ID (WABA) |
accessToken | string | Yes | Permanent access token from Meta |
displayPhoneNumber | string | Yes | Display phone number (e.g., +966920033773) |
verifiedName | string | Optional | Meta-verified business display name |
webhookVerifyToken | string | Optional | Token for Meta webhook verification callback |
Example Response
{ "success": true, "data": { "id": 1, "associationId": 123 } }Get WhatsApp Credentials
API Key AuthRetrieve WhatsApp credentials for an association. The accessToken is never included in the response for security.
Endpoint
GET /api/v1/whatsapp-credentials/:associationIdExample 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"
}
}Create WhatsApp Template
API Key AuthCreate a WhatsApp message template. Set submitToMeta: true to submit for Meta approval in the same call.
Endpoint
POST /api/v1/whatsapp-templatesRequest Body
| Parameter | Type | Required | Description |
|---|---|---|---|
associationId | integer | Yes | Association ID |
name | string | Yes | Template name (lowercase, underscores only) |
language | string | Yes | Language code (e.g., "en", "ar") |
category | string | Yes | MARKETING, UTILITY, or AUTHENTICATION |
bodyText | string | Yes | Message body with {{1}}, {{2}} variable placeholders |
headerType | string | Optional | NONE, TEXT, IMAGE, VIDEO, or DOCUMENT |
headerContent | string | Optional | Header text content |
headerMediaUrl | string | Optional | Header media URL (for IMAGE/VIDEO/DOCUMENT headers) |
footerText | string | Optional | Footer text |
buttons | array | Optional | Template buttons array |
variables | string[] | Optional | Variable names, e.g., ["name", "orderId"] |
sampleValues | object | Optional | Sample values for Meta approval review |
submitToMeta | boolean | Optional | Submit 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
}List WhatsApp Templates
API Key AuthList WhatsApp templates for an association, optionally filtered by status or category.
Endpoint
GET /api/v1/whatsapp-templates?associationId=123&status=APPROVED&category=UTILITYQuery Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
associationId | integer | Yes | Association ID |
status | string | Optional | Filter: PENDING, APPROVED, REJECTED, DELETED |
category | string | Optional | Filter: MARKETING, UTILITY, AUTHENTICATION |
Manage WhatsApp Template
API Key AuthGET /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 MetaGET 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)
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Optional | Template name |
language | string | Optional | Language code |
category | string | Optional | MARKETING, UTILITY, or AUTHENTICATION |
headerType | string | Optional | NONE, TEXT, IMAGE, VIDEO, or DOCUMENT |
headerContent | string | Optional | Header text content |
headerMediaUrl | string | Optional | Header media URL (for media headers) |
bodyText | string | Optional | Message body text |
footerText | string | Optional | Footer text |
buttons | array | Optional | Template buttons |
variables | string[] | Optional | Variable names |
sampleValues | object | Optional | Sample values for Meta |
isActive | boolean | Optional | Enable or disable template |
submitToMeta | boolean | Optional | Re-submit to Meta after update |
Submit & Sync Templates
API Key AuthPOST /api/v1/whatsapp-templates/:id/submit Submit a template to Meta for approvalPOST /api/v1/whatsapp-templates/:id/sync Sync a single template’s approval status from MetaPOST /api/v1/whatsapp-templates/sync-all Sync all templates from Meta for an associationSync-All Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
associationId | integer | Yes | Association 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"
}
}Send WhatsApp Template Message
API Key AuthSend an approved WhatsApp template message. The template must have APPROVED status from Meta.
Endpoint
POST /api/v1/whatsapp-send/templateRequest Body
| Parameter | Type | Required | Description |
|---|---|---|---|
associationId | integer | Yes | Association ID |
templateId | integer | Yes | Template ID (local database ID) |
recipient | string | Yes | Phone number with country code (e.g., +201097899908) |
parameters | object | Optional | Template 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 URLbutton_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": [...] }
}
}Send WhatsApp Text Message
API Key AuthSend 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/textRequest Body
| Parameter | Type | Required | Description |
|---|---|---|---|
associationId | integer | Yes | Association ID |
recipient | string | Yes | Phone number with country code |
message | string | Yes | Message text content |
Send WhatsApp Media Message
API Key AuthSend 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/mediaRequest Body
| Parameter | Type | Required | Description |
|---|---|---|---|
associationId | integer | Yes | Association ID |
recipient | string | Yes | Phone number with country code |
mediaType | string | Yes | image, document, video, or audio |
mediaUrl | string | Yes | Publicly accessible URL of the media file |
caption | string | Optional | Caption text (for image, video, document) |
filename | string | Optional | Filename (for document type) |
Send WhatsApp Bulk Messages
API Key AuthSend 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/bulkRequest Body
| Parameter | Type | Required | Description |
|---|---|---|---|
associationId | integer | Yes | Association ID |
templateId | integer | Yes | Approved template ID |
recipients | array | Yes | Array of {phone, parameters} objects. Include header_image for image headers and button_url for dynamic URL buttons in each recipient’s parameters. |
credentialId | integer | Optional | Specific 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/:jobIdStatus Response
{ "success": true, "data": { "jobId": "wa_bulk_...", "status": "processing", "sent": 25, "failed": 0, "total": 100, "startedAt": "2026-02-14T10:00:00.000Z" } }WhatsApp Messages & Stats
API Key AuthMessage History
GET /api/v1/whatsapp-messages?associationId=123&status=SENT&recipient=201097899908&limit=50&offset=0| Parameter | Type | Required | Description |
|---|---|---|---|
associationId | integer | Yes | Association ID |
status | string | Optional | Filter: SENT, DELIVERED, READ, FAILED |
recipient | string | Optional | Filter by recipient phone number (partial match) |
limit | integer | Optional | Max results (default: 50) |
offset | integer | Optional | Pagination offset (default: 0) |
Statistics
GET /api/v1/whatsapp-stats?associationId=123&days=30| Parameter | Type | Required | Description |
|---|---|---|---|
associationId | integer | Yes | Association ID |
days | integer | Optional | Number 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