Everything WhatsApp, behind one key.
Send template and free-form messages, get every inbound message and delivery receipt pushed to your server in real time, submit templates to Meta for approval, and read your bill — all from your own software. Kredoo is an approved Meta Tech Provider; the hard parts (the 24-hour rule, opt-outs, retries, delivery reconciliation) are handled for you.
It is a single JSON API. Every request carries your key in one header; every response is JSON. There are no SDKs to install and nothing to whitelist — if your stack can make an HTTPS request, it can use Kredoo.
What you need before you start
- A Kredoo workspace — sign up takes a minute.
- A WhatsApp number connected to it. Two ways:
- Through Kredoo — Meta's Embedded Signup popup inside your workspace. About two minutes, no Meta knowledge needed. Works with a fresh number or one already on the WhatsApp Business app.
- Bring your own — if you already run a WhatsApp Business API account (a WABA with
its own Meta app), connect its credentials and point your app's webhook at
https://kredoo.in/api/wa/webhook.
- An API key — minted in your workspace (see Authentication).
- An HTTPS endpoint on your server to receive events. Optional, but without it you cannot see incoming messages in real time.
- At least one approved template, because the first message to any customer who hasn't written to you in 24 hours must be a template. See ground rules.
Quickstart — first message in five steps
-
Get your key
Your Kredoo admin mints one for you with the scopes you need. Keys are shown once and stored hashed — keep it server-side, never in a browser or app.
-
Point your webhook at us and test it
# register your endpoint (the signing secret comes back once — store it) curl -X POST https://kredoo.in/api/developer/webhook \ -H "X-API-Key: krd_live_..." -H "Content-Type: application/json" \ -d '{"url":"https://yourapp.com/kredoo/events"}' # fire a signed sample event at it curl -X POST https://kredoo.in/api/developer/webhook/test \ -H "X-API-Key: krd_live_..."
When
{"delivered":true}comes back, your pipe works. -
Send a template
curl -X POST https://kredoo.in/api/whatsapp/send-template \ -H "X-API-Key: krd_live_..." -H "Content-Type: application/json" \ -d '{"phone":"+919876543210","template":"order_update","values":["Ramesh","ORD-1042"]}' # → {"sent":true,"via":"native","wamid":"wamid.HBg...","error":""}
-
Watch the receipts arrive
Within seconds your endpoint receives
message.statusevents for thatwamid—sent, thendelivered, thenread. When the customer replies, amessage.receivedevent lands the same way. -
Check your bill
curl https://kredoo.in/api/usage \ -H "X-API-Key: krd_live_..."Messages, Meta's cost, and Kredoo's platform charge — the same numbers your invoice is built from.
Authentication & scopes
Every request carries your key in the X-API-Key header:
X-API-Key: krd_live_ebfc60a1…
Keys are scoped — each one names exactly what it may touch. A key minted for sending cannot rewrite your flows or mint further keys. Keys can be revoked at any moment and the plaintext is only ever shown at creation.
| Scope | Grants access to |
|---|---|
messages | Sending and reading messages (/messages/*, /whatsapp/send-template) |
templates | Template create, edit, sync, delete (/whatsapp/templates*) |
flows | Automation flows (/whatsapp/flows*) |
leads | Contacts / CRM records (/leads*) |
usage | Billing reads (/usage, /wallet) — every key has this |
* | Everything, including key management and webhook config |
List your keys (masked) with scopes and last-used times.
Mint a key: {"label":"production","scopes":["messages","templates","usage"]}.
Needs a full-access key or a login session — a scoped key cannot mint keys.
Revoke instantly.
Errors & rate limits
Errors are JSON with a human-readable message and the name of whoever refused the request:
{"error":"…","source":"kredoo"}.
source is "kredoo" when one of our own rules stopped the request before
it left us, and "meta" when WhatsApp refused it. The two need completely different
responses, so the message text is prefixed to match — "Kredoo: …" or
"Meta: …" — and an app that simply displays error shows the
right one without any extra work. The status code tells you what kind of problem it is:
| Status | Meaning | What to do |
|---|---|---|
| 400 | Malformed request — a missing field, an invalid phone | Fix the request; the message names the field |
| 401 | Missing, invalid or revoked API key | Check the key; ask your admin for a new one |
| 402 | Prepaid wallet is empty | Top up; sends resume immediately |
| 403 | Key lacks the scope for this endpoint | The message names the scope you need |
| 404 | No such resource | Check the id |
| 409 | Outside the 24-hour window (free-form sends) | Send an approved template instead |
| 429 | Rate limit hit — 300 requests/min per workspace | Back off and retry after the Retry-After header |
| 502 | Meta rejected the request (e.g. a template submission) | The message carries Meta's reason |
Design for retries. Treat 429 and network
timeouts as retryable with backoff. Everything 4xx except 429 is your request's
problem — retrying it unchanged will fail again.
WhatsApp ground rules (read once, save hours)
- The 24-hour window. Free-form text and images only reach a customer who has
messaged you in the last 24 hours. Outside that, the only legal message is an
approved template. Kredoo enforces this — a free-form send outside the window
returns
409rather than silently failing. - Delivery is what counts. Meta will report
sentand then fail a message. Only adeliveredorreadreceipt means it landed — watch yourmessage.statusevents, not the send response. - Opt-outs are automatic. When a customer sends STOP, Kredoo records it and refuses further template sends to them. You don't build this; you inherit it.
- Meta prices by category. Marketing costs several times more than utility or authentication per message, and Meta re-categorises templates by their content — naming a promotional message "utility" does not make it cheap.
- Quality is a budget. Blocks and reports lower your number's quality rating and messaging limits. Send things people want.
Sending template messages
Sends one approved template to one contact. This is the workhorse: first contact, OTPs, order updates, reminders.
{
"phone": "+919876543210",
"template": "order_update", // approved template name (or pass template_id)
"values": ["Ramesh", "ORD-1042", "tomorrow 6pm"], // fills {{1}}, {{2}}, {{3}}
"lead_id": "…" // optional: pin it to a CRM contact
}
Response: {"sent":true,"via":"native","wamid":"wamid.HBg…","error":""}.
Keep the wamid — every later delivery receipt for this message references it.
Omit values and Kredoo auto-fills: {{1}} becomes the contact's name and
the rest come from the contact's stored fields. Values are sanitised for you (Meta rejects
newlines and runs of spaces inside parameters).
Opted-out numbers are refused here with a clear error — you cannot accidentally template-message someone who said STOP.
Sending free-form text
{ "phone": "+919876543210", "content": "Your order is packed and ships tonight!" }
Response: {"ok":true,"via":"native"}. The message is queued and
goes out within seconds; its receipts arrive as webhook events.
Only works inside the customer's 24-hour window — otherwise you get
409 Outside the 24-hour window. Send an approved template instead.
Sending images
Multipart form: phone, optional caption (up to 1024 chars), and the
image as file (JPEG/PNG, max 5 MB — Meta's limit). Same 24-hour window rule as text.
curl -X POST https://kredoo.in/api/messages/send-media \ -H "X-API-Key: krd_live_..." \ -F "phone=+919876543210" -F "caption=Here's the catalogue" \ -F "file=@catalogue.jpg"
Receiving events — your webhook
Register once, then every inbound customer message and every delivery receipt is POSTed to your URL as JSON, usually within a second or two of Meta delivering it to us.
Body: {"url":"https://yourapp.com/kredoo/events"}. The response carries your
signing secret — shown once. Saving a new URL rotates the secret. HTTPS URLs on
public hosts only.
Fires a signed sample event at your URL so you can finish your handler before anything real flows.
Stop forwarding.
Event: a customer wrote to you
{
"id": "e0a1…", // unique — use it to de-duplicate
"type": "message.received",
"created_at": "2026-09-02T09:14:33+00:00",
"wamid": "wamid.HBg…",
"phone": "919876543210", // sender, digits only
"msg_type": "text",
"text": "Do you have this in blue?",
"data": { … } // the untouched Meta Cloud API message object
}
Event: a delivery receipt
{
"type": "message.status",
"wamid": "wamid.HBg…", // matches the wamid your send returned
"status": "delivered", // sent | delivered | read | failed
"phone": "919876543210",
"error_code": "131049", // only on failed
"data": { … }
}
The data field is the raw Meta Cloud API object, untouched — if you have existing
Cloud API webhook code, it parses these directly. The convenience fields on top mean simple
integrations never need to.
Delivery guarantees
- Respond fast with 2xx. Anything else, or a timeout, counts as a failure. Do your processing after acknowledging.
- We retry. Exponential backoff, up to 8 attempts over ~6 hours — a deploy or an outage on your side doesn't lose events.
- De-duplicate by
id. Retries mean the same event can arrive twice. Processing must be idempotent. - Order is not guaranteed under retries — trust
created_atand the status values, not arrival order.
Verifying signatures
Every event carries a signature header so you can prove it came from Kredoo:
X-Kredoo-Signature: t=1756800000,v1=6f8a2c…
Recompute HMAC-SHA256(secret, "<t>.<raw request body>") and compare with
v1 using a constant-time comparison. Reject if it differs, or if t is
more than five minutes old (replay protection).
Node.js
const crypto = require("crypto"); function verify(rawBody, header, secret) { const parts = Object.fromEntries(header.split(",").map(p => p.split("="))); if (Math.abs(Date.now()/1000 - Number(parts.t)) > 300) return false; const expected = crypto.createHmac("sha256", secret) .update(parts.t + "." + rawBody).digest("hex"); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1)); }
PHP
function verify(string $rawBody, string $header, string $secret): bool {
parse_str(str_replace(',', '&', $header), $p);
if (abs(time() - (int)($p['t'] ?? 0)) > 300) return false;
$expected = hash_hmac('sha256', $p['t'] . '.' . $rawBody, $secret);
return hash_equals($expected, $p['v1'] ?? '');
}
Sign the raw body. Verify against the exact bytes you received, before any JSON parsing or re-encoding — a re-serialised body will not match.
Templates
Templates are pre-approved message formats — the only thing you may send outside the 24-hour window. You create them through Kredoo; Meta approves them, usually within minutes to a few hours.
Your templates with status: APPROVED, PENDING, or
REJECTED with the reason.
Submit to Meta:
{
"name": "order_update", // lowercase_with_underscores
"language": "en",
"category": "UTILITY", // MARKETING | UTILITY | AUTHENTICATION
"components": [
{ "type": "BODY",
"text": "Hi {{1}}, your order {{2}} is on its way — expected {{3}}." }
],
"sample_values": ["Ramesh", "ORD-1042", "tomorrow 6pm"]
}
Comes back PENDING; a 502 means Meta refused the submission and the
error carries Meta's reason.
Media headers
A template whose header is an image, video or document needs a sample, because Meta reviews the media, not just the text.
The easy way — send the file with the template. If your own site already
took the upload from your user, post it straight through as multipart/form-data: the
file in a file field and the template JSON in a payload field. One call,
no public URL, nothing to upload first.
# the whole submission, in one request curl -X POST https://kredoo.in/api/whatsapp/templates \ -H "X-API-Key: $KREDOO_KEY" \ -F "file=@/tmp/demo.mp4" \ -F 'payload={"name":"loyalty_intro","language":"en","category":"MARKETING", "components":[{"type":"HEADER","format":"VIDEO"}, {"type":"BODY","text":"Hi {{1}}, watch how it works."}], "sample_values":["Ramesh"]}'
We register the sample with Meta, keep the media for sending, and submit the template — so your user presses Submit for approval once and it appears in Kredoo and in Meta.
Or send a link, if the file already lives somewhere public. Same single call,
with header_url instead of an attachment:
{
"name": "loyalty_intro",
"language": "en",
"category": "MARKETING",
"components": [
{ "type": "HEADER", "format": "VIDEO" },
{ "type": "BODY", "text": "Hi {{1}}, watch how it works." }
],
"sample_values": ["Ramesh"],
"header_url": "https://yourcdn.com/demo.mp4" // that is all
}
The link must be reachable from the public internet and inside Meta's size limits (below). It is read once, at submission; you can take it down afterwards.
The manual way, if you would rather register the media ahead of time and reuse it across several templates — upload it first:
multipart/form-data with a single field named file. Meta's own limits
apply: JPEG or PNG up to 5 MB, MP4 or 3GP up to 16 MB, PDF up to 100 MB.
{
"url": "https://kredoo.in/uploads/...", // public URL, yours to display
"media_id": "1234567890", // WhatsApp media id, reused on every send
"handle": "4::aW1hZ2Uv...", // the review sample Meta needs
"kind": "video"
}
Then create the template with a HEADER component and pass all three
values back at the top level:
{
"name": "loyalty_intro",
"language": "en",
"category": "MARKETING",
"components": [
{ "type": "HEADER", "format": "VIDEO" },
{ "type": "BODY", "text": "Hi {{1}}, watch how it works." }
],
"sample_values": ["Ramesh"],
"header_handle": "4::aW1hZ2Uv...", // review sample
"header_media_id": "1234567890", // what actually rides on each send
"header_url": "https://kredoo.in/uploads/..."
}
Send all three, or the header silently disappears.
header_handle is what Meta reviews, but header_media_id is what gets
attached to each outgoing message. Submit only the handle and the template is approved and looks
perfect, while every message goes out with no media at all and nothing reports an error.
You never pass media at send time — the stored media is attached automatically on every send of
that template. On PUT, omit these fields to keep the existing sample, or send new ones
to replace it.
Pull live statuses from Meta. Approval is asynchronous — sync (or poll
GET /whatsapp/templates) rather than assuming.
Meta allows one edit per 24 hours on an approved template, and every edit goes back through review. Plan template changes; don't iterate them live.
Conversations
Recent conversations across your inbox, newest first.
One conversation's full history. key is p_ plus the last 10 digits of
the phone, or a contact id.
These are for history and backfill — live traffic should come from your webhook, not polling.
Contacts
Every conversation is pinned to a contact (a lead) in the workspace CRM. Push your own records in so names, stages and custom fields ride along with the chat:
{
"name": "Ramesh Kumar",
"stage": "new",
"source": "your-platform",
"phone": "+919876543210", // E.164; validated
"custom_data": { "city": "Hyderabad" }
}
name, stage and source are required. Creating a lead can
fire your workspace's new-lead automations — a welcome template, an AI agent — with no further code.
Usage & wallet
Billing is transparent by design: the endpoint you read is the same data your invoice is built from. Template sends carry a platform fee as a percentage over Meta's per-message rate; free-form sends carry a small flat fee; your exact rates are in the response. Meta's own message charges bill to your Meta account directly — Kredoo's fee is separate and itemised.
{
"data": {
"month": "2026-09", "currency": "INR",
"breakdown": [ { "msg_type": "template", "category": "UTILITY",
"source": "api", "messages": 4120,
"meta_cost": 473.80, "charge": 71.07 }, … ],
"totals": { "messages": 9384, "meta_cost": 812.40, "platform_charge": 421.15 }
}
}
Prepaid workspaces run on a wallet: each send debits it, and at ₹0 sends stop everywhere with
402 until it's topped up. A WhatsApp alert warns the workspace owner as the balance
runs low. Check the balance here anytime; recent credits are listed alongside.
Partner workspaces — WhatsApp for your customers
Building a platform that serves many businesses? Provision a child workspace per business — each with its own WhatsApp number, its own scoped key, and its own data — and one wallet on your account funds them all. Your customers never leave your product.
{
"name": "Ramesh Textiles",
"admin_email": "ramesh@example.com" // optional: a login for the Meta popup step
}
// → the child's org_id, its scoped API key (shown once), and login details
Requires a full-access key. Then the business connects its number — Meta's Embedded Signup popup via the login you were handed back, or its own WABA credentials — and your platform drives that workspace with the child key exactly as described on this page.
All your child workspaces, with connection status and this month's charge each.
The rollup: one line per workspace, one grand total, your wallet balance. Your whole invoice on one screen.
Wallet inheritance: children have no wallets of their own — every send anywhere under your account debits your wallet. One top-up, one balance, one bill.