Dakiya API
Connect your CRM, website or billing software to WhatsApp: send approved templates, start broadcasts, keep contacts in sync, and receive customer replies and read receipts as they happen. All requests and responses are JSON over HTTPS.
Quick start
- In the app open Developers and create an API key. It is shown once; store it on your server, never in a browser or mobile app.
- Send your first template:
curl -X POST "https://dakiya.smartaccounts.in/api/v1/messages/template" \
-H "Authorization: Bearer dk_your_key" \
-H "Content-Type: application/json" \
-d '{
"to": "919876543210",
"template": "order_update",
"language": "en",
"body": ["Meera", "#4521"]
}'{ "ok": true, "message_id": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAERgS...", "to": "919876543210", "status": "sent", "charged": 0.135, "balance": 1842.615 }charged is what was taken from the prepaid message balance and balance is what is left. Both are absent on accounts that are not charged per message.
- Add a webhook address under Developers, Webhooks to hear when it is delivered, read or answered.
Authentication
Base address: https://dakiya.smartaccounts.in/api/v1. If your server cannot reach it, the same API also answers at https://dakiya.smartaccounts.in/api.php/v1.
Send the key in every request: Authorization: Bearer dk_... (or the header X-API-Key). A key belongs to one workspace and can only see that workspace's data. Delete a key under Developers to revoke it at once.
Phone numbers are digits with country code and no plus sign in responses (919876543210). In requests, +91 98765 43210 and 10-digit numbers for the workspace's default country are accepted too. Times are ISO 8601 in UTC.
Errors
A failed call returns a matching HTTP status and this shape. Use code in your program and show message to people.
{ "ok": false, "error": { "code": "outside_24h_window", "message": "The customer has not written in the last 24 hours, so WhatsApp only allows an approved template." } }| Status | Code | Meaning |
|---|---|---|
| 400 | bad_json | The body is not valid JSON. |
| 401 | unauthorized | Missing, wrong or deleted key. |
| 402 | insufficient_balance | The prepaid message balance does not cover this message. Add money under Plan and billing; nothing was sent. |
| 402 | account_expired, account_suspended, account_pending, quota_exceeded, contact_limit | The plan does not allow this right now. Renew or upgrade under Plan and billing. |
| 403 | plan_without_api | The workspace's plan has no API access. |
| 404 | not_found, template_not_found, tag_not_found, unknown_route | The thing you named does not exist in this workspace. |
| 409 | outside_24h_window, unsubscribed, whatsapp_not_connected, not_cancellable | The request is valid but not allowed in the current state. |
| 422 | invalid_phone, missing_variable, missing_template, invalid_url, country_not_priced, ... | Something in the request is missing or malformed. The message says what. |
| 429 | rate_limited | Too many calls this minute. Wait for the seconds in Retry-After. |
| 502 | whatsapp_rejected | WhatsApp refused the message. The message carries WhatsApp's reason. |
Message charges
Each message is charged from the workspace's prepaid balance when WhatsApp accepts it: template messages by their category (marketing, utility, authentication), free-form messages as service replies. If WhatsApp later reports the message as failed, the charge is returned and you receive a message.failed webhook. GET /me returns the current balance and prices. A broadcast pauses when the balance runs out and continues by itself after a top-up.
Rate limits
Each key may make a set number of calls per minute, depending on the plan. Every response carries X-RateLimit-Limit and X-RateLimit-Remaining. To message many people at once, use one broadcast call instead of thousands of single sends: it is one API call and is paced for you.
Safe retries
Networks fail. To retry a POST without risking a duplicate WhatsApp message, send a unique Idempotency-Key header (for example your own order or invoice id). Repeating the call with the same key within 24 hours returns the first answer and sends nothing new; the reply then carries Idempotent-Replay: true.
Sending
POST/messages/template
Sends one approved template message. Works at any time; this is how a conversation is started. The contact is created if it does not exist.
| Field | Type | Notes |
|---|---|---|
to | string, required | Recipient's WhatsApp number. |
template | string, required | Template name, as listed by GET /templates. |
language | string | For example en, hi, gu. Needed only when the template exists in several languages. |
body | array or object | Values for the message variables, in order: ["Meera","#4521"], or by name: {"1":"Meera","2":"#4521"}. |
header | array or object | Value for a text header's variable, if it has one. |
header_media_url | string | Public https link to the image, video or PDF, for templates with a media header. header_media_name sets the file name of a document. |
buttons | object | Values for dynamic buttons, by button position: {"0":"track/4521"} for a link ending or a coupon code. |
name, email, tags, fields | Optional contact details to save at the same time. | |
ignore_unsubscribed | boolean | Send even if the contact opted out. Only for messages they still expect, such as an order or payment update. |
POST/messages/text
Sends free text. WhatsApp allows this only within 24 hours of the customer's last message; otherwise the call fails with outside_24h_window.
curl -X POST "https://dakiya.smartaccounts.in/api/v1/messages/text" -H "Authorization: Bearer dk_your_key" -H "Content-Type: application/json" \
-d '{"to":"919876543210","text":"Your order is packed and leaves today."}'Two more calls follow the same 24-hour rule:
- POST
/messages/mediawithto,type(image,video,audioordocument),url(public https link), and optionalcaptionandfilename. - POST
/messages/buttonswithto,textandbuttons: one to three short labels, for example["Yes","No"]. The label the customer taps arrives as a normal incoming message.
GET/messages/{id}
Returns the latest known status of a message you sent: sent, delivered, read or failed (with error). Prefer webhooks over polling this.
POST/broadcasts
Sends one template to many people in a single call. The service queues the messages, paces them within WhatsApp's limits, skips people who unsubscribed, and reports progress.
curl -X POST "https://dakiya.smartaccounts.in/api/v1/broadcasts" -H "Authorization: Bearer dk_your_key" -H "Content-Type: application/json" \
-d '{
"name": "Payment reminders, October",
"template": "payment_reminder",
"body": ["{name|Customer}", "{invoice_no}", "{amount}", "15 Oct"],
"to": { "contacts": [
{ "phone": "919876543210", "name": "Meera Shah", "fields": { "invoice_no": "INV-101", "amount": "4,500" } },
{ "phone": "919812345678", "name": "Rohan Patel", "fields": { "invoice_no": "INV-102", "amount": "12,000" } }
] },
"schedule_at": "2026-10-20T10:30:00+05:30"
}'tois one of:{"contacts":[{phone, name, email, fields}]}or{"phones":[...]}(up to 10,000 per call),{"tags":["VIP"]}, or{"all":true}.- Template values may contain placeholders filled per contact:
{name},{first_name},{phone},{email}, or any custom field such as{invoice_no}. Add a fallback after a bar:{first_name|there}. A contact whose value is empty and has no fallback is skipped, not sent a broken message. - Leave out
schedule_atto start immediately.
{ "ok": true, "broadcast_id": 42, "status": "scheduled", "recipients": 2, "invalid_numbers": 0, "scheduled_at": "2026-10-20T05:00:00+00:00" }GET/broadcasts/{id} returns the status and counts (recipients, waiting, sent, delivered, read, replied, failed, skipped). GET/broadcasts lists the latest 50. POST/broadcasts/{id}/cancel stops one that is scheduled, running or paused.
Data
POST/contacts
Creates a contact or updates the one with the same number.
curl -X POST "https://dakiya.smartaccounts.in/api/v1/contacts" -H "Authorization: Bearer dk_your_key" -H "Content-Type: application/json" \
-d '{"phone":"919876543210","name":"Meera Shah","email":"meera@example.com",
"tags":["Lead","Gandhinagar"],"fields":{"city":"Gandhinagar","plan":"Gold"},"subscribed":true}'Existing names and emails are kept unless you pass "overwrite": true. Tags are added; use remove_tags to take some away. New field names appear in the app once you add a custom field with the same name.
- GET
/contacts?search=&tag=&subscribed=&page=&limit=lists contacts, newest first (up to 200 per page). - GET
/contacts/{phone}returns one contact. - DELETE
/contacts/{phone}removes it.
GET/templates
Lists the workspace's templates with their status and the variables each one needs, so your software can build the right form.
GET/conversations/{phone}/messages
Returns the latest messages exchanged with one customer (up to 200, oldest first) and whether the 24-hour window is open. Incoming files have a media_download address that needs the same API key.
GET/me
Returns the workspace, the connected WhatsApp number and its quality, the plan, whether sending is currently allowed, this month's usage, and the prepaid balance with the price per message type. Useful as a connection test.
Webhooks
A webhook is an https address on your server that we call when something happens. Add addresses under Developers, Webhooks, or through the API:
curl -X POST "https://dakiya.smartaccounts.in/api/v1/webhooks" -H "Authorization: Bearer dk_your_key" -H "Content-Type: application/json" \
-d '{"url":"https://crm.example.com/hooks/whatsapp","events":["message.received","message.read","message.failed"]}'The answer includes a secret used to sign every delivery. Leave out events to receive everything. GET/webhooks lists addresses; DELETE/webhooks/{id} removes one.
Each delivery is a POST with a JSON body like this:
{
"id": "evt_5f2a9c0e7b1d4a66c3e8f0a1",
"event": "message.received",
"created_at": "2026-10-09T06:12:44+00:00",
"workspace_id": 7,
"data": {
"message_id": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAEhgU...",
"from": "919876543210",
"name": "Meera Shah",
"type": "text",
"text": "Is this available in blue?",
"media": null,
"reply_to_broadcast_id": 42,
"timestamp": "2026-10-09T06:12:43+00:00"
}
}- Answer with any
2xxstatus within 6 seconds. Do slow work after answering. - If your server is down or answers with an error, we retry after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours, then stop. Deliveries and their results are listed under Developers.
- The same event can occasionally arrive twice. Use
idto ignore repeats. - Events can arrive out of order (a
readbefore adelivered). Usetimestamp.
Verify the signature
Every delivery carries X-Dakiya-Signature: sha256=..., the HMAC-SHA256 of the raw request body using the address's secret. Check it before trusting the content.
<?php // PHP
$body = file_get_contents("php://input");
$sign = "sha256=" . hash_hmac("sha256", $body, "whsec_your_secret");
if (!hash_equals($sign, $_SERVER["HTTP_X_DAKIYA_SIGNATURE"] ?? "")) { http_response_code(401); exit; }
$event = json_decode($body, true);
http_response_code(200);// Node.js (Express): use the raw body, not the parsed one
const crypto = require("crypto");
app.post("/hooks/whatsapp", express.raw({ type: "application/json" }), (req, res) => {
const sign = "sha256=" + crypto.createHmac("sha256", "whsec_your_secret").update(req.body).digest("hex");
const got = req.get("X-Dakiya-Signature") || "";
if (sign.length !== got.length || !crypto.timingSafeEqual(Buffer.from(sign), Buffer.from(got))) return res.sendStatus(401);
const event = JSON.parse(req.body);
res.sendStatus(200);
});
Event reference
| Event | When | Fields in data |
|---|---|---|
message.received | A customer sent a message or tapped a button. | message_id, from, name, type, text, media (id, mime_type, filename, download), reply_to_broadcast_id, timestamp |
message.deliveredmessage.readmessage.failed | A message you sent (single or broadcast) changed status. | message_id, to, status, timestamp, broadcast_id, error, error_code |
contact.unsubscribedcontact.subscribed | A contact sent a stop or start word. | phone, name, keyword |
template.status | WhatsApp approved, rejected, paused or disabled a template. | name, language, status, reason |
broadcast.completed | A broadcast finished sending. | broadcast_id, name, recipients, sent, failed, skipped |
Status events for a large broadcast can be many thousands. Subscribe to them only if your CRM stores per-message status; otherwise use broadcast.completed and GET /broadcasts/{id}.