kredoo. Developers
Kredoo WhatsApp API

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.

https://kredoo.in/api

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

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

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

  3. 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":""}
  4. Watch the receipts arrive

    Within seconds your endpoint receives message.status events for that wamid — sent, then delivered, then read. When the customer replies, a message.received event lands the same way.

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

ScopeGrants access to
messagesSending and reading messages (/messages/*, /whatsapp/send-template)
templatesTemplate create, edit, sync, delete (/whatsapp/templates*)
flowsAutomation flows (/whatsapp/flows*)
leadsContacts / CRM records (/leads*)
usageBilling reads (/usage, /wallet) — every key has this
*Everything, including key management and webhook config
GET/developer/keys

List your keys (masked) with scopes and last-used times.

POST/developer/keys

Mint a key: {"label":"production","scopes":["messages","templates","usage"]}. Needs a full-access key or a login session — a scoped key cannot mint keys.

DELETE/developer/keys/{id}

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:

StatusMeaningWhat to do
400Malformed request — a missing field, an invalid phoneFix the request; the message names the field
401Missing, invalid or revoked API keyCheck the key; ask your admin for a new one
402Prepaid wallet is emptyTop up; sends resume immediately
403Key lacks the scope for this endpointThe message names the scope you need
404No such resourceCheck the id
409Outside the 24-hour window (free-form sends)Send an approved template instead
429Rate limit hit — 300 requests/min per workspaceBack off and retry after the Retry-After header
502Meta 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 409 rather than silently failing.
  • Delivery is what counts. Meta will report sent and then fail a message. Only a delivered or read receipt means it landed — watch your message.status events, 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

POST/whatsapp/send-template

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

POST/messages/send
{ "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

POST/messages/send-media

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.

POST/developer/webhook

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.

POST/developer/webhook/test

Fires a signed sample event at your URL so you can finish your handler before anything real flows.

DELETE/developer/webhook

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_at and 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.

GET/whatsapp/templates

Your templates with status: APPROVED, PENDING, or REJECTED with the reason.

POST/whatsapp/templates

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:

POST/uploads/template-header

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.

POST/whatsapp/templates/sync

Pull live statuses from Meta. Approval is asynchronous — sync (or poll GET /whatsapp/templates) rather than assuming.

PUT/whatsapp/templates/{id}
DELETE/whatsapp/templates/{id}

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

GET/messages

Recent conversations across your inbox, newest first.

GET/messages/thread?key=p_9876543210

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:

POST/leads
{
  "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.

GET/leads

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.

GET/usage?month=2026-09
{
  "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 }
  }
}
GET/wallet

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.

POST/partner/orgs
{
  "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.

GET/partner/orgs

All your child workspaces, with connection status and this month's charge each.

GET/partner/usage?month=2026-09

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.