SendyStack
API documentation

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.

Quick send
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

Start here

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.

Reference

Endpoints

POST/messagesscope: messages:send

Send 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"
}
Examples

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.body

Java

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

message.sentmessage.deliveredmessage.readmessage.failedmessage.replycontact.opted_outtemplate.status_changed
// 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.

400invalid_bodyThe JSON body is malformed or missing required fields.
401unauthorizedThe API key is missing, invalid, expired, or revoked.
403forbiddenThe key lacks the required scope, the message violates policy, or quota is exhausted.
409idempotency_conflictThe same idempotency key was reused with a different body.
429rate_limitedThe workspace or phone number is sending faster than its allowed tier.
500server_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.