SaarFlow API
Let your own software send WhatsApp on your own number — your billing system, your booking site, your school dashboard — without it ever holding a Meta token.
What this is
SaarFlow already holds your WhatsApp connection: the number, the approved templates, the webhooks, the inbox your team answers from. This API lets another system of yours ask SaarFlow to send one of those approved templates, and read back what happened to each message.
It is send-only, deliberately. Replies from customers arrive in the SaarFlow inbox where a person can answer them; nothing is pushed back to your software. That keeps one record of every conversation instead of two halves that disagree.
Because then your software needs a copy of the token that can send as your business, create and delete your templates, and change where your webhooks point. One copy, on our server, is easier to protect than two — and messages sent this way still appear in your inbox thread, so whoever answers the reply can see what was sent.
Get a key
In SaarFlow: Settings → API keys → Create key. The key is shown once, starts with
sf_live_, and only its hash is stored — we cannot show it to you again, and a copy of our database
cannot send messages as you. Revoke it from the same screen at any time.
Keep it on your server. A key in browser JavaScript is a key anyone can read.
Authentication
Every request carries the key in a header. Base URL:
# Base URL https://saarflow.com/api/v1/ext # Every request X-API-Key: sf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Content-Type: application/json
List your templates
curl -s https://saarflow.com/api/v1/ext/templates \
-H "X-API-Key: $SAARFLOW_API_KEY"
{
"data": [
{
"name": "fee_pending_reminder",
"language": "hi",
"category": "UTILITY",
"variables": 0,
"preview": "प्रिय अभिभावक / Dear Parents, …"
}
]
}
variables is how many {{n}} the body contains. Send exactly that many values, in
order. Only APPROVED templates are listed — a draft or rejected one is not sendable.
Which number it sends from
{ "phoneNumber": "917581010627", "displayName": "Mahaveer Jain National School" }
Send to one person
curl -s -X POST https://saarflow.com/api/v1/ext/messages/template \ -H "X-API-Key: $SAARFLOW_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "to": "9685171608", "template": "fee_pending_reminder", "reference": "STU-1042" }'
| Field | Meaning | |
|---|---|---|
to | required | Any shape: 9685171608, 919685171608, +91 96851 71608. |
template | required | Its name from /templates. |
language | optional | Only needed if you have the same template name in two languages. |
variables | optional | Array of strings, in {{1}}, {{2}}… order. |
reference | optional | Yours — a student id, an invoice number. It comes back in the report. |
{ "id": "9b88915c-…", "status": "QUEUED",
"message": "Queued. Poll /ext/broadcasts/{id} for the delivery report." }
202 Accepted: the reply is immediate and the sending happens in the background. Keep the
id — it is how you read the report.
Send to a list
curl -s -X POST https://saarflow.com/api/v1/ext/messages/bulk \ -H "X-API-Key: $SAARFLOW_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "template": "payment_reminder", "name": "October defaulters", "recipients": [ { "to": "9685171608", "reference": "STU-1042", "variables": ["Rahul", "2"] }, { "to": "9826000000", "reference": "STU-1043", "variables": ["Priya", "1"] } ], "ratePerSecond": 10, "dailyCap": 900 }'
{ "id": "9b88915c-…", "status": "QUEUED", "total": 2,
"unusableNumbers": 0, "ratePerSecond": 10, "dailyCap": 900 }
| Field | Meaning | |
|---|---|---|
template | required | Must be APPROVED. |
recipients | required | Array of { to, variables?, reference? }. Maximum 20,000. |
name | optional | What to call this send in SaarFlow. |
ratePerSecond | optional | Default 10, maximum 40. |
dailyCap | optional | Default 900. It never raises WhatsApp's own limit, only keeps you below it. |
Read the delivery report
{
"id": "9b88915c-…", "name": "October defaulters", "status": "SENDING",
"total": 2333,
"counts": { "pending": 1433, "sent": 900, "skipped": 0, "failed": 0,
"delivered": 812, "read": 466, "undelivered": 88 },
"pausedReason": null, "resumeAfter": null,
"recipients": [
{ "to": "919685171608", "reference": "STU-1042", "state": "READ", "sentAt": "…" },
{ "to": "91900000", "reference": "STU-1044", "state": "FAILED",
"error": "Not a usable phone number" }
]
}
Leave ?recipients=true off for the counts alone — lighter when you are polling.
state | Means |
|---|---|
PENDING | Queued, not sent yet |
SENT | WhatsApp accepted it |
DELIVERED | It reached the phone |
READ | They opened it |
FAILED | Could not be sent — error says why |
SKIPPED | Duplicate in the list, or the number opted out |
Poll every 10–15 seconds while a send is running. There is no webhook back to you by design.
Recent sends
Stop a send
Pacing and the daily limit
WhatsApp limits how many people a number may start a conversation with in 24 hours — commonly 1,000 for a new number, rising as it keeps a good quality rating. A list of 2,333 is therefore not one send.
A broadcast counts everything that number has already sent in the last 24 hours, and when the allowance is gone it stops by itself:
{ "status": "PAUSED_DAILY_CAP",
"pausedReason": "The number's daily allowance (900) is used up. Sending resumes automatically.",
"resumeAfter": "2026-10-04T18:30:00.000Z" }
It continues the next morning with nobody pressing anything. Show pausedReason to your user and a
three-day send explains itself. The same happens if WhatsApp starts rate-limiting the number.
What it does for you
- The same number twice in one list is sent once. A parent with two children is not told
twice; the duplicate comes back as
SKIPPED. - A number that cannot be a phone number is reported as
FAILEDin the report rather than failing your whole request. - Anyone who opted out is never sent to, whoever asks.
- Every message lands in that person's inbox thread, so your team sees what was sent before they answer the reply.
Errors
| Code | Meaning |
|---|---|
401 | The key is missing, wrong or revoked |
402 | The SaarFlow account's plan has lapsed — the reply carries who to call |
400 | Missing template or recipients, or the template is not approved |
404 | No template by that name on this account |
Every error is { "error": "…", "message": "…" }, and message is written to be shown
to a person as it is.
Help
Stuck, or want an endpoint that is not here? Write to support@techsaar.com or message +91 96851 71608 — you will reach the people who built it.
You do not need this. SaarFlow's own Campaigns screen sends the same messages to your contacts, your ad leads, a CRM stage or an uploaded list — with the same pacing and the same reports. See what SaarFlow costs.