Build WhatsApp workflows from any stack
Send approved templates, trigger transactional messages, retry safely with idempotency keys, and listen to delivery events through the SendyStack REST API.
curl -X POST https://api.sendystack.com/v1/messages \
-H "Authorization: Bearer wac_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: msg_$(date +%s)" \
-d '{
"to": "+254712345678",
"type": "template",
"template": {
"name": "hello_world",
"language": "en_US",
"bodyParams": ["Dominic"]
}
}'Base URL
https://api.sendystack.com/v1
Auth
Authorization: Bearer YOUR_API_KEY
Content type
application/json
Idempotency
Idempotency-Key on every POST
Current public send endpoint
POST /messages
Authentication and request rules
Generate keys from Dashboard > API Keys. Store them as secrets on your server and never expose them in browser JavaScript.
Bearer keys
Every request needs an API key in the Authorization header.
Scopes
Use least-privilege keys. Message sends require messages:send.
Safe retries
Send a unique Idempotency-Key for every POST request.
Server side only
Do not place live keys in mobile apps, frontends, or public repos.
Endpoints
/messagesscope: messages:sendSend a WhatsApp message
Send an approved template or a free-form text message when the contact is inside an active customer-service window.
Template request
// json
{
"to": "+254712345678",
"type": "template",
"template": {
"name": "hello_world",
"language": "en_US",
"bodyParams": ["Dominic"]
}
}Accepted response
// json
{
"status": "sent",
"message_id": "msg_01J...",
"meta_message_id": "wamid.HBg...",
"tenant_id": "tenant_123",
"api_key_id": "key_123"
}Send messages from Python, Node.js, PHP and more
All examples use the same REST endpoint, so you can use any HTTP client that supports headers and JSON.
curl
// bash
curl -X POST https://api.sendystack.com/v1/messages \
-H "Authorization: Bearer wac_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: msg_$(date +%s)" \
-d '{
"to": "+254712345678",
"type": "template",
"template": {
"name": "hello_world",
"language": "en_US",
"bodyParams": ["Dominic"]
}
}'Python
// python
import os
import uuid
import requests
api_key = os.environ["SENDYSTACK_API_KEY"]
response = requests.post(
"https://api.sendystack.com/v1/messages",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"to": "+254712345678",
"type": "template",
"template": {
"name": "hello_world",
"language": "en_US",
"bodyParams": ["Dominic"],
},
},
timeout=20,
)
response.raise_for_status()
print(response.json())Node.js
// ts
const response = await fetch("https://api.sendystack.com/v1/messages", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SENDYSTACK_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
to: "+254712345678",
type: "template",
template: {
name: "hello_world",
language: "en_US",
bodyParams: ["Dominic"],
},
}),
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());PHP
// php
<?php
$apiKey = getenv("SENDYSTACK_API_KEY");
$payload = [
"to" => "+254712345678",
"type" => "template",
"template" => [
"name" => "hello_world",
"language" => "en_US",
"bodyParams" => ["Dominic"],
],
];
$ch = curl_init("https://api.sendystack.com/v1/messages");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . $apiKey,
"Content-Type: application/json",
"Idempotency-Key: " . bin2hex(random_bytes(16)),
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status >= 400) {
throw new Exception($body);
}
echo $body;Go
// go
package main
import (
"bytes"
"crypto/rand"
"encoding/hex"
"fmt"
"net/http"
"os"
)
func main() {
body := []byte(`{
"to":"+254712345678",
"type":"template",
"template":{"name":"hello_world","language":"en_US","bodyParams":["Dominic"]}
}`)
key := make([]byte, 16)
_, _ = rand.Read(key)
req, _ := http.NewRequest("POST", "https://api.sendystack.com/v1/messages", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("SENDYSTACK_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", hex.EncodeToString(key))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
fmt.Println(res.Status)
}Ruby
// ruby
require "json"
require "net/http"
require "securerandom"
uri = URI("https://api.sendystack.com/v1/messages")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch("SENDYSTACK_API_KEY")}"
request["Content-Type"] = "application/json"
request["Idempotency-Key"] = SecureRandom.uuid
request.body = {
to: "+254712345678",
type: "template",
template: {
name: "hello_world",
language: "en_US",
bodyParams: ["Dominic"]
}
}.to_json
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }
raise response.body if response.code.to_i >= 400
puts response.bodyJava
// java
var client = java.net.http.HttpClient.newHttpClient();
var request = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create("https://api.sendystack.com/v1/messages"))
.header("Authorization", "Bearer " + System.getenv("SENDYSTACK_API_KEY"))
.header("Content-Type", "application/json")
.header("Idempotency-Key", java.util.UUID.randomUUID().toString())
.POST(java.net.http.HttpRequest.BodyPublishers.ofString("""
{
"to": "+254712345678",
"type": "template",
"template": {
"name": "hello_world",
"language": "en_US",
"bodyParams": ["Dominic"]
}
}
"""))
.build();
var response = client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());C#
// csharp
using System.Net.Http.Headers;
using System.Text;
using var client = new HttpClient();
using var request = new HttpRequestMessage(HttpMethod.Post, "https://api.sendystack.com/v1/messages");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("SENDYSTACK_API_KEY"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
request.Content = new StringContent("""
{
"to": "+254712345678",
"type": "template",
"template": {
"name": "hello_world",
"language": "en_US",
"bodyParams": ["Dominic"]
}
}
""", Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());Webhooks
Configure a HTTPS webhook URL in the dashboard to receive message lifecycle and customer reply events. Verify signatures before trusting a webhook payload.
// json
{
"id": "evt_01J...",
"type": "message.delivered",
"created_at": "2026-05-28T10:15:00Z",
"data": {
"message_id": "msg_01J...",
"to": "+254712345678",
"status": "delivered"
}
}Errors and retries
Treat `429` and most `5xx` responses as retryable with exponential backoff. Keep the same idempotency key when retrying the same send.
invalid_bodyThe JSON body is malformed or missing required fields.unauthorizedThe API key is missing, invalid, expired, or revoked.forbiddenThe key lacks the required scope, the message violates policy, or quota is exhausted.idempotency_conflictThe same idempotency key was reused with a different body.rate_limitedThe workspace or phone number is sending faster than its allowed tier.server_errorA SendyStack or upstream provider error occurred.Use templates first
Meta requires approved templates outside the 24-hour service window.
Validate JSON
Send compact, valid JSON and keep phone numbers in E.164 format.
Track statuses
Store message_id and meta_message_id so webhooks can reconcile delivery.