Launch offer: 40% off your first year·Ends soonClaim now →
Pulseby CloudFigma
How it worksSolutionsAgenciesPricingContact
Sign inGet started
  1. Pulse
  2. Developers

Send WhatsApp messages from your own software

By the Pulse team at CloudFigma· Updated 10 October 2026

In short

One HTTPS request sends a WhatsApp message from your business’s own number: a POST to /api/v1/messages with your Pulse API key. Send an approved template at any time, or a free plain-text reply within 24 hours of the customer’s last message. Webhooks tell your server about deliveries, reads and replies. Pulse adds nothing per message; Meta bills your own card.

On this page

Before you startSend a template messageReply with plain textRetry safelyCheck a messageWebhooksRequest a paymentErrorsLimits

Before you start

Every request goes to https://pulse.cloudfigma.com over HTTPS, with your API key. Messages go out from your business’s own WhatsApp number, the one Pulse set up for you.

  • Get a key. The account owner opens Settings › Developers in Pulse and creates one. It starts cfn_live_ and is shown once, so copy it then. Keep it on your server, never in a web page or an app.
  • Send it with every request, in either header below.
  • Send JSON with Content-Type: application/json. Answers are JSON too. Phone numbers go in international form, with the country code: +919876543210.
Either header
Authorization: Bearer cfn_live_YOUR_KEY

X-API-Key: cfn_live_YOUR_KEY

The endpoints

POST /api/v1/messages
Send an approved template, or a plain-text reply inside the 24-hour window.
GET /api/v1/messages/{id}
Where a message has got to: sent, delivered, read or failed.
POST /api/v1/payment-requests
Ask a customer for money on WhatsApp, through your own Razorpay.
GET /api/v1/payment-requests/{id}
Where a payment request has got to: opened, paid, expired or cancelled.

Every error has the same shape: error is a fixed code for your program to check, and message says what to fix, for a person. Errors lists every code.

An error
{
  "error": "template_not_found",
  "message": "No approved template named \"order_update\" in en. Check the Templates page. It may still be pending Meta's approval."
}

Send a template message

A template is a message Meta approved in advance. It can go at any time, even to a customer who has never written to you. Make templates in Pulse, under Templates; only approved ones can be sent.

The address
POST https://pulse.cloudfigma.com/api/v1/messages

What to send

to
Required. The customer’s number with its country code, such as +919876543210.
template
Required. The template’s name, such as order_update.
language
The template’s language code, such as en_US or hi. Left out, en.
variables
The template’s blanks, {{1}}, {{2}} and so on, in order, as a list of strings. Send numbers as strings too. None may be blank.
header
For a template with an image, video or document at the top: { "link": "https://…" }, a public https address WhatsApp can download, or { "id": "…" }, a WhatsApp media id. Add filename for a document. Left out, the template’s own file goes.
client_ref
Your own reference, such as an order number. It comes back with the message and in its webhooks.
cURL
curl -X POST https://pulse.cloudfigma.com/api/v1/messages \
  -H "Authorization: Bearer cfn_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1001" \
  -d '{
    "to": "+919876543210",
    "template": "order_update",
    "variables": ["Asha","1001"],
    "client_ref": "order-1001"
  }'

It answers 202: the message is stored and queued for WhatsApp, not yet delivered.

202 Accepted
{
  "id": "cm2msg0002",
  "status": "queued",
  "to": "+919876543210",
  "category": "utility",
  "charged": "₹0.00",
  "balance_after": null,
  "client_ref": "order-1001"
}
  • id is Pulse’s id for the message. Keep it to check the message and to match its webhooks.
  • category is the one Meta approved the template as (marketing, utility or authentication), never one the request chooses.
  • charged is always ₹0.00 and balance_after is null: Pulse adds nothing per message. Meta bills your own card at its rate.

A first test

Meta adds a sample template, hello_world in en_US, to most new WhatsApp accounts. If it is on your Templates page, it is the quickest first send: it has no blanks.

Request body
{
  "to": "+919876543210",
  "template": "hello_world",
  "language": "en_US"
}

Link buttons

Some templates have a button whose link is Pulse’s own: pulse.cloudfigma.com/l/… counts taps, and pulse.cloudfigma.com/p/… opens a payment link. For these, put the whole link in variables, at the button’s place, after the message’s own blanks. Pulse puts a short code of its own on the button. A link that cannot go is refused with 400 invalid_link, and nothing is sent.

Request body, for a button that counts taps
{
  "to": "+919876543210",
  "template": "diwali_offer",
  "variables": [
    "Priya",
    "https://yourshop.com/diwali"
  ]
}

Reply with plain text

In the 24 hours after a customer last wrote to you, you can answer with your own words instead of a template. WhatsApp charges nothing for these replies. Send text in place of template:

cURL
curl -X POST https://pulse.cloudfigma.com/api/v1/messages \
  -H "Authorization: Bearer cfn_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ticket-5521-reply-1" \
  -d '{
    "to": "+919876543210",
    "text": "Yes, your order left this morning. It arrives tomorrow.",
    "client_ref": "ticket-5521"
  }'
202 Accepted
{
  "id": "cm2msg0003",
  "status": "sent",
  "to": "+919876543210",
  "category": "service",
  "charged": "₹0.00",
  "balance_after": null,
  "client_ref": "ticket-5521"
}
  • It goes to WhatsApp straight away, so status is already sent. Your team sees it in the Pulse Inbox, in the chat, as the business’s reply. category is service and charged is ₹0.00.
  • text is 1 to 4,096 characters, and not only spaces. Otherwise: 400 invalid_text.
  • Send text or template, never both. variables, language and header belong to templates; beside text they are refused with 400 invalid_request.
  • Outside the window, or to someone who has never written to you: 409 window_closed. Nothing is sent. Send an approved template instead.
  • If WhatsApp refuses the message: 502 send_failed, with the message’s id. It is kept as failed, as it would be in the Inbox.

Retry safely

A timeout does not tell you whether the message went. Send an Idempotency-Key header, unique to each message (your order id, or a UUID), and a retry is safe:

  • Same key, same body: 200 with the first message’s id and "idempotent_replay": true. Nothing is sent again.
  • Same key, different body: 422 idempotency_conflict. Use a new key for each different message.
  • A plain-text reply still being sent with that key: 409 idempotency_in_progress. Ask again in a moment.
  • A request that was refused does not use up its key, so the same request can go once the reason is gone.

Every request example on this page sends one, and payment requests take the same header.

Check a message

A 202 means accepted, not delivered. To see where a message has got to, ask with its id and your key:

Request
GET https://pulse.cloudfigma.com/api/v1/messages/{id}
200 OK
{
  "id": "cm2msg0002",
  "status": "read",
  "to": "+919876543210",
  "category": "utility",
  "client_ref": "order-1001",
  "error_code": null,
  "error_detail": null,
  "refunded": false,
  "created_at": "2026-10-09T10:00:00.000Z",
  "delivered_at": "2026-10-09T10:00:05.000Z",
  "read_at": "2026-10-09T10:02:41.000Z",
  "links": []
}
  • status moves queued → sent → delivered → read, or ends at failed.
  • error_code and error_detail say why a message failed: WhatsApp’s error code, and what it means in words. Meta does not bill a message it could not deliver.
  • refunded says whether a charge for a failed message was given back. Pulse charges nothing per message, so it stays false.
  • created_at, delivered_at and read_at are ISO 8601 times in UTC; the last two stay null until it happens.
  • links lists a tap-counting button’s page, its clicks, and the first and last tap. Empty for a message without one.
  • An id that is not one of your account’s messages: 404 not_found.

Webhooks tell you the same as it happens, without asking.

Webhooks

Instead of asking, your server can be told. The account owner adds your https URL in Pulse under Settings › Developers, chooses its events, and copies its signing secret (whsec_…), which is shown once. Pulse then posts each event to the URL as JSON as it happens.

The events

message.received
A customer wrote to you. Once per message, and never from a number you have blocked.
message.status
A message sent through Pulse (the API, a campaign, the Inbox) was sent, delivered, read or failed. Meta can report these out of order; use the time in the event.
campaign.completed
A campaign has nobody left to send to. Deliveries and reads after that arrive as message.status.
contact.opted_out
A customer opted out of marketing: a STOP reply, the panel, an import or the API. Not repeated for a second STOP.
contact.opted_in
A customer who had opted out opted in again.
link.clicked
A customer tapped a tracked link (a button that goes through /l/…) in a message you sent. Every tap, with the count so far; a chat app drawing the link’s preview is not a tap.

What arrives

Every request has the same envelope: id (this delivery, the same on every retry), event, version (1), created_at (when it happened) and data. Numbers are written +919876543210, times are ISO 8601 in UTC, and a field the send API returns has the same name here: id is Pulse’s message id, client_ref is yours.

Headers
POST /pulse-webhook HTTP/1.1
Content-Type: application/json
User-Agent: CloudFigma-Pulse-Webhooks/1
X-Pulse-Event: message.received
X-Pulse-Delivery: cm2dlv0001
X-Pulse-Timestamp: 1760004000
X-Pulse-Signature: sha256=3b1f6c…(64 hex characters)
message.received
{
  "id": "cm2dlv0001",
  "event": "message.received",
  "version": 1,
  "created_at": "2026-10-09T10:00:00.000Z",
  "data": {
    "message": {
      "id": "cm2msg0001",
      "wa_message_id": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAEhgg",
      "from": "+919876543210",
      "type": "text",
      "text": "Is my order out for delivery?",
      "media": null,
      "reply_to_wa_message_id": null,
      "received_at": "2026-10-09T10:00:00.000Z"
    },
    "conversation_id": "cm2conv0001"
  }
}

Examples. Version 1 may gain fields; none is renamed or removed without a version 2.

Check the signature

X-Pulse-Signature is sha256= and the hex HMAC-SHA256 of X-Pulse-Timestamp, a dot and the raw request body, keyed with your webhook’s whole signing secret, whsec_ included. Compute it over the bytes you received, before parsing the JSON; compare in constant time; and refuse a timestamp more than 5 minutes from your clock, so a captured request cannot be replayed.

Check the signature (Node.js)
const crypto = require("crypto")
const express = require("express")
const app = express()

// Your webhook's secret (whsec_…), shown once.
const SECRET = process.env.PULSE_WEBHOOK_SECRET

// express.raw keeps the exact bytes it was sent.
app.post(
  "/pulse-webhook",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const ts = req.get("X-Pulse-Timestamp") || ""
    const sig = req.get("X-Pulse-Signature") || ""
    const expected = "sha256=" + crypto
      .createHmac("sha256", SECRET)
      .update(ts + "." + req.body)
      .digest("hex")

    const fresh =
      Math.abs(Date.now() / 1000 - Number(ts)) <= 300
    const valid = sig.length === expected.length &&
      crypto.timingSafeEqual(
        Buffer.from(sig), Buffer.from(expected))
    if (!fresh || !valid) return res.sendStatus(401)

    const event = JSON.parse(req.body)
    // event.id repeats on a retry: skip it if seen.
    console.log(event.event, event.data)

    res.sendStatus(200) // within 10 s; slow work after
  },
)

app.listen(3000)

Answering and retries

  • Answer with any 2xx within 10 seconds, then do slow work. Anything else is a failure, including no answer and a redirect: Pulse never follows one.
  • A failed event is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours: 6 attempts in all. Then it is marked failed in the URL’s delivery log in Pulse, where Retry now sends it again.
  • Delivery is at least once and not in order: skip an id you have handled, and order by the event’s own time.
  • After 20 failures in a row, Pulse switches the URL off and emails the account owner. Events while it is off are not kept.
  • URLs must be https and on the public internet. Pulse checks the address it connects to on every attempt.
  • Send test event, in Settings, posts a ping signed like any other, with { "test": true, "endpoint": { "id", "url" } } as its data.

Request a payment

With the business’s own Razorpay connected in Pulse (Settings › Payments), one request asks a customer for money on WhatsApp. Pulse makes a Razorpay payment link for exactly the amount, in the business’s own Razorpay account, and sends it: as a free reply inside the customer’s 24-hour window, or else as an approved template with Pulse’s Pay now button. The money goes straight to the business’s Razorpay.

Request
POST https://pulse.cloudfigma.com/api/v1/payment-requests
Authorization: Bearer cfn_live_YOUR_KEY
Content-Type: application/json
Idempotency-Key: order-10482

{
  "to": "+919876543210",
  "amount": "800.00",
  "purpose": "Cleaning, 7 Oct",
  "due_date": "2026-10-16",
  "client_ref": "order-10482"
}
  • amount is rupees with at most two decimals, ₹1.00 or more; or send amount_paise as a whole number. purpose is required. due_date is YYYY-MM-DD; left out, a week from today.
  • template names an approved template with a Pay now button, for outside the window; left out, the first one the account has. language and client_ref work as they do for messages.
  • It answers 201 with the request: its id, status (requested, opened, paid, expired or cancelled), amount, payment_link, sent_as (text or template) and message_id.
  • GET /api/v1/payment-requests/{id} says where it has got to. Once paid, payment holds Razorpay’s payment id, the amount and the method. Only Razorpay can mark a request paid.
  • Asked again with the same Idempotency-Key, it answers 200 with the same request and "idempotent_replay": true, and makes no second link.

Errors

Every error answers { "error": …, "message": … }. Check error in your code; show or log message, which says what to fix. Nothing is sent when a request is refused.

Every error the API answers, by HTTP status
HTTPerrorWhat it meansWhat to do
400invalid_recipientto is not a phone number in international form.Send a plus, the country code and the number: "+919876543210".
400missing_templateNeither template nor text was sent.Name an approved template, or send text to reply inside the 24-hour window.
400invalid_templatetemplate is not a string.Send the template’s name as text, such as "order_update".
400invalid_languagelanguage is not a language code.Send the template’s language as text, such as "en" or "en_US".
400invalid_variablesvariables is not a list of strings.Send every value as a string, numbers too: ["Asha", "1042"].
400variable_countThe number of values does not match the template’s blanks.Send one value per blank, in order. The message says how many it takes.
400empty_variableA value is blank, and WhatsApp refuses blank values.Fill every value.
400invalid_headerheader is not exactly one of link (https) or id, or filename is not text.Send one of link or id, and filename as a string.
400missing_header_mediaThe template shows an image, video or document at the top, and has no file to send.Send header, or add a default file to the template in Pulse.
400unsupported_componentThe template has a part Pulse cannot send yet, such as a location header.Use a template without that part.
400invalid_client_refclient_ref is not a string.Send it as text, such as "order-1042".
400invalid_linkA link for one of Pulse’s link buttons cannot go.The message says why. Send the whole https link.
400invalid_texttext is not a string, is blank, or is over 4,096 characters.Send the reply as 1 to 4,096 characters.
400invalid_amountamount is not rupees with at most two decimals, or amount_paise is not a whole number.Send "800.50", or 80050 as amount_paise.
400invalid_requestA field is missing or does not belong: text beside a template’s fields, a payment request without purpose, a due_date that is not YYYY-MM-DD, or an amount under ₹1.00.The message names the field. Fix it and send again.
401missing_api_keyNo API key was sent.Send Authorization: Bearer cfn_live_… or X-API-Key: cfn_live_….
401invalid_keyThe key is not one Pulse knows (403 when the key may not send).Copy the whole key again, or ask the account owner for a new one.
401revokedThe key was revoked.Ask the account owner for a new key.
403tenant_suspendedSending is paused on the Pulse account, for example while its renewal is unpaid.The account owner settles it or contacts support. Nothing was sent.
404template_not_foundNo approved template has that name in that language.Check Templates in Pulse: it may still be waiting for Meta’s approval.
404not_foundNo such message or payment request on this account, or no such address.Check the id and the path.
409waba_not_activeThe WhatsApp account is not active, or needs reconnecting.Nothing was sent. CloudFigma is told; the account owner can contact support.
409no_active_numberThe account has no active WhatsApp number.Finish connecting a number in Pulse, or contact support.
409marketing_pausedWhatsApp lowered the number’s quality rating, so marketing templates are paused.Utility and authentication templates still go.
409recipient_opted_outThe customer replied STOP, so marketing templates cannot go to them.Utility and authentication templates still go.
409recipient_blockedThe business blocked this customer in Pulse.Unblock them on their customer page in Pulse first.
409window_closedThe customer has not written in the last 24 hours (or never has), so a plain-text reply cannot go.Send an approved template instead.
409idempotency_in_progressA plain-text reply with this Idempotency-Key is still being sent.Ask again in a moment, or use a new key for a new message.
409gateway_not_connectedThe business has not connected its Razorpay in Pulse.The account owner connects it in Settings › Payments.
409no_payment_templateThe customer is outside the 24-hour window, and there is no approved template with Pulse’s Pay now button to use.Create one in Templates, or name one in template.
409request_closedA retried payment request (same Idempotency-Key) is no longer waiting for payment, or was never sent.Read it with its id; use a new key for a new request.
413payload_too_largeThe request body is over 5 MB.Send less. A file goes in header.link, not in the body.
415unsupported_media_typeThe body is not JSON.Send JSON with Content-Type: application/json.
422idempotency_conflictThis Idempotency-Key was already used with a different body.Use a new key for each different message.
429rate_limitedOver 6,000 requests in a minute on this key.Wait retry_after_seconds, then try again.
500internal_errorSomething failed on Pulse’s side.Try again in a minute. If it repeats, send support the request_id.
502send_failedWhatsApp refused a plain-text reply (the answer carries its id), or a payment request could not be sent.The message has the reason. A refused reply is kept, as failed.
502gateway_errorRazorpay would not make the payment link.The message has Razorpay’s reason. Check the Razorpay account.

A 500 never carries the fault itself. It carries a request_id, also sent as the x-request-id header: if a retry a minute later fails too, send it to support@cloudfigma.com and we can find what happened.

500 Internal Server Error
{
  "error": "internal_error",
  "message": "Something went wrong on our side. Try again in a minute; if it keeps happening, send us this request_id.",
  "request_id": "5b0e7c2a-91d4-4f3e-8a61-2f6c0d9e4b17"
}

Limits

  • 6,000 requests a minute per API key, across every endpoint. Past it, 429 rate_limited: wait retry_after_seconds, then try again.
  • Request bodies up to 5 MB. A file for a template goes as a link in header, not in the body.
  • A plain-text reply up to 4,096 characters.
  • Template messages wait in a queue for your number and go to WhatsApp in turn, so a burst is sent rather than refused.
  • Webhooks: answer within 10 seconds.
429 Too Many Requests
{
  "error": "rate_limited",
  "message": "Too many requests, the limit is 6000 per minute. Retry in 42s.",
  "retry_after_seconds": 42
}

Questions about the API: support@cloudfigma.com.

Your booking is confirmed ✅See you tomorrow at 4 pm!Thanks for the lovely review ⭐Your order is on its way 🚚

Start talking to your customers where they already are.

We set everything up. You send your first message in about a week.

Get startedChat with us on WhatsApp

₹9,999 a year · setup included · no card to start

Pulse by CloudFigma

The WhatsApp Business API, set up for you. Built and supported in Bengaluru.

CloudFigma Private Limited5, Kothnur Main Rd, J. P. Nagar, Bengaluru 560078+91 63614 13305Sales: info@cloudfigma.comSupport: support@cloudfigma.com
5.0 on Google · 250+ reviews of CloudFigma

Product

  • How it works
  • Solutions
  • For agencies
  • Pricing
  • Questions
  • Get the app
  • Developer API
  • Sign in

Industries

  • Clinics and doctors
  • Salons and spas
  • Shops and stores
  • Restaurants and cafés
  • Gyms and studios
  • Coaching and tuition
  • Cleaning and home services
  • Pharmacies
  • Real estate
  • Schools and preschools

Company

  • About Pulse
  • Contact us
  • CloudFigma: hosting and websites
  • Privacy policy
  • Terms of service
  • Refunds and cancellation

Guides

  • WhatsApp Business API pricing in India, October 2026
  • What is the WhatsApp Business API?
  • WhatsApp Business app vs WhatsApp Business API
  • How to get the WhatsApp Business API in India
  • WhatsApp template categories: marketing, utility and authentication
  • WhatsApp marketing message limits: why some messages are not delivered
  • Click-to-WhatsApp ads: the free 72-hour window explained
  • WhatsApp for Shopify and WooCommerce stores in India
  • Arabic WhatsApp templates for UAE businesses

Compare

  • Best providers in India
  • Pulse vs AiSensy
  • Pulse vs Interakt
  • Pulse vs WATI
  • Pulse vs Gallabox
  • Pulse vs DoubleTick
  • Pulse vs Gupshup

WhatsApp Business API in

  • All cities
  • Bengaluru
  • Mumbai
  • Delhi NCR
  • Hyderabad
  • Chennai
  • Pune
  • UAE

© 2026 CloudFigma Private Limited, Bengaluru.

Pulse runs on the WhatsApp Business Platform. Not affiliated with or endorsed by Meta. WhatsApp is a trademark of Meta Platforms, Inc.

Claim 40% offWhatsApp us