Merchant API · v1

Send order confirmations from your own backend.

Your server creates an order. PULSE sends your customer a WhatsApp message with Confirm and Cancel buttons from your own WhatsApp Business number, then tells your server what the customer chose.

Access is by invitation. To get keys, write to pulsse.studio@gmail.com

Overview

The base URL for every request is https://go.pulseautomation.app/api/v1. Requests and responses are JSON. All timestamps are ISO 8601 in UTC, for example 2026-10-09T10:02:41.000Z.

You create an orderOne POST /orders from your backend.
Customer gets WhatsAppFrom your own number, with Confirm and Cancel buttons.
Customer tapsSeconds or hours later. No app, no login.
You find outPULSE calls your webhook, or you read the status.

Quickstart

Use a pk_test_ key first. It sends no real message, so you can try everything safely. Create an order:

curl -X POST https://go.pulseautomation.app/api/v1/orders \
  -H "Authorization: Bearer pk_test_YOUR_KEY" \
  -H "Idempotency-Key: ORD-10452" \
  -H "Content-Type: application/json" \
  -d '{
    "external_order_id": "ORD-10452",
    "customer": { "name": "Ahmed Ali", "phone": "+201000000001" },
    "items": [ { "title": "Black Party Pants", "quantity": 1 } ],
    "total": 450,
    "currency": "EGP",
    "delivery_address": "12 Street, October City, Giza"
  }'

The answer:

{
  "id": "ord_8f3k2Qx1",
  "external_order_id": "ORD-10452",
  "status": "sent",
  "created_at": "2026-10-09T10:00:00.000Z"
}

A phone number ending in 0001 makes the sandbox confirm the order a few seconds later. See Sandbox for the other outcomes.

Authentication

Send your secret key in the Authorization header of every request:

Authorization: Bearer pk_live_xxxxxxxxxxxxxxxxxxxxxxxx
  • You get two keys: pk_test_ (sandbox) and pk_live_ (real messages). Orders made with one are invisible to the other.
  • Keep keys on your server. Never put them in a mobile app, a web page or a public repository.
  • We store only a one-way hash of your key, so we cannot show it again. If you lose it, we issue a new one.
  • Rotation: we issue a new key and the old one keeps working for 24 hours so you can switch without downtime.
  • A missing, wrong, revoked or suspended key returns 401 unauthorized.

Every response has an X-Request-Id header. Quote it when you contact us about a request.

Create an order

POST/orders

Creates the order and sends the WhatsApp confirmation. The Idempotency-Key header is required: 1 to 200 printable characters, no spaces. Sending the same request again with the same key never messages the customer twice. You get the original answer back, with the header Idempotent-Replayed: true. Reusing a key with a different body returns 409 idempotency_key_reuse.

FieldRules
external_order_idYour own order number. Required, up to 100 characters, unique per order.
customer.nameRequired, up to 100 characters.
customer.phoneRequired. International format with + and country code, for example +201001234567. Spaces and dashes are accepted.
itemsRequired, 1 to 50 entries. Each has a title (up to 200 characters) and an integer quantity from 1 to 999.
totalRequired, a number of 0 or more.
currencyRequired, 3 uppercase letters, for example EGP.
delivery_addressOptional, up to 300 characters.
languageOptional. Only "en" is supported in v1.

The response is 201 with the order. If WhatsApp refuses the message immediately you still get 201, with "status": "failed" and a failure_reason. A replay of an earlier request returns 200.

Get an order

GET/orders/{id}
GET/orders?external_order_id=ORD-10452
{
  "id": "ord_8f3k2Qx1",
  "external_order_id": "ORD-10452",
  "status": "confirmed",
  "created_at": "2026-10-09T10:00:00.000Z",
  "timeline": [
    { "event": "created",   "at": "2026-10-09T10:00:00.000Z" },
    { "event": "sent",      "at": "2026-10-09T10:00:02.000Z" },
    { "event": "delivered", "at": "2026-10-09T10:00:05.000Z" },
    { "event": "confirmed", "at": "2026-10-09T10:02:41.000Z" }
  ]
}

Status values

StatusMeaning
sendingCreated, being handed to WhatsApp.
sent · delivered · readMessage progress.
confirmedThe customer tapped Confirm Order. Final.
cancelledThe customer tapped Cancel Order. Final.
failedThe message could not be delivered. Final. failure_reason is one of invalid_phone, not_on_whatsapp, sender_error (a problem with your WhatsApp account) or send_failed.
no_replyNo answer within your timeout (24 hours by default). Final. PULSE changes nothing on your side; your system decides what to do.
withdrawnYou withdrew the order. Final.

Withdraw an order

POST/orders/{id}/cancel

Works only while the status is sending or sent, meaning nobody has answered yet. The order becomes withdrawn. It cannot recall a message that was already delivered; it only stops PULSE acting on a late tap. No webhook is sent for a withdrawal you made yourself. Once the order is final you get 409 invalid_state.

Usage

GET/usage
{
  "period_start": "2026-10-01T00:00:00.000Z",
  "period_end":   "2026-11-01T00:00:00.000Z",
  "used": 412,
  "cap": 1500,
  "remaining": 1088,
  "paid_until": "2026-11-08T10:57:33.272Z"
}

The month is the calendar month in UTC. Only messages actually handed to WhatsApp count; sandbox orders never do. At the limit, new live orders return 402 quota_exceeded until the 1st of the next month.

Webhooks

Give us a public https address and PULSE calls it the moment an order reaches an outcome. You get one call per final outcome:

order.confirmedorder.cancelledorder.failedorder.no_reply

{
  "event": "order.confirmed",
  "event_id": "evt_Qx1mP9kLw2",
  "id": "ord_8f3k2Qx1",
  "external_order_id": "ORD-10452",
  "status": "confirmed",
  "at": "2026-10-09T10:02:41.000Z"
}

order.failed also carries a failure_reason.

Verify every call

Anyone can send a request to your address, so check that it really came from PULSE. Each call has two headers:

X-Pulse-Timestamp: 1791545734
X-Pulse-Signature: sha256=3f1c...

The signature is the hex HMAC-SHA256 of the text <timestamp>.<raw request body>, using your webhook secret as the key. Three rules matter:

  • Use the raw body exactly as received, before any JSON parsing. Parsing and re-serialising changes the bytes and the check fails.
  • Compare in constant time (the functions below do).
  • Reject old calls. If the timestamp is more than 5 minutes away from your clock, refuse it. This stops replays.
import crypto from "node:crypto";
import express from "express";

const app = express();

// express.raw keeps the body as untouched bytes
app.post("/pulse-webhook", express.raw({ type: "application/json" }), (req, res) => {
  const ts = req.header("X-Pulse-Timestamp");
  const sig = req.header("X-Pulse-Signature") || "";
  const rawBody = req.body.toString("utf8");

  if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(400);

  const expected =
    "sha256=" +
    crypto.createHmac("sha256", process.env.PULSE_WEBHOOK_SECRET).update(`${ts}.${rawBody}`).digest("hex");
  const ok = sig.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  if (!ok) return res.sendStatus(401);

  const event = JSON.parse(rawBody);
  // 1. ignore it if you already handled event.event_id
  // 2. update your order from event.external_order_id and event.status
  res.sendStatus(200); // answer quickly
});

Delivery and retries

  • Answer with any 2xx within 10 seconds. We do not follow redirects.
  • Otherwise we retry after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours (7 attempts in total), then stop. The order itself is never lost; read it with GET /orders/{id}.
  • The same event can arrive more than once. Handle each event_id only once.
  • Your address must be public https. Private, local and plain http addresses are refused.

Polling instead of a webhook

A webhook is optional. If you cannot host a public address, leave it unset and read the status yourself with GET /orders/{id}. Check each open order every minute or two, and stop once it reaches a final status. Staying under 10 requests per second keeps you inside the rate limit. A webhook is faster and needs far fewer calls, so use one when you can, and keep an occasional poll as a safety net.

Errors

Errors have one shape. Quote the request_id if you contact us.

{
  "error": { "code": "invalid_phone", "message": "Phone must be in international format, e.g. +201001234567." },
  "request_id": "req_a1b2c3d4e5f6a7b8"
}
HTTP · codeMeaning and what to do
400 invalid_requestA field is missing or wrong. The message names it.
400 invalid_phoneUse international format with + and country code.
400 unsupported_languageOnly "en" is supported.
401 unauthorizedMissing, wrong, expired or revoked key, or the account is suspended.
402 payment_requiredYour subscription is not active. Contact PULSE to renew. Sandbox keys still work.
402 quota_exceededMonthly message limit reached. It resets on the 1st (UTC).
404 not_foundUnknown order.
405 method_not_allowedWrong HTTP method for that address.
409 duplicate_orderThat external_order_id already exists. Read it with GET /orders.
409 idempotency_key_reuseThe same key was used with a different request.
409 sender_not_readyThe live key was used before your WhatsApp number is connected and approved.
409 invalid_stateThe order can no longer be withdrawn.
413 payload_too_largeThe body is over 32 KB.
429 rate_limitedMore than 10 requests per second (bursts of 20). Wait for Retry-After.
500 server_errorOur side. Retry with the same Idempotency-Key; it is safe.

Sandbox

With a pk_test_ key no WhatsApp message is sent and nothing counts against your limit. About 5 seconds after you create an order, it plays out an outcome chosen by the last four digits of the phone number:

Phone ends inOutcome
0001confirmed
0002cancelled
0003failed (not_on_whatsapp)
0004no_reply
anything elsedelivered, with no final outcome

For example +201000000001 confirms. Webhooks fire exactly as in production, so you can test your signature check and your retry handling end to end.

Going live

Live messages go out from your own WhatsApp Business number, so your customers see your brand. Meta bills you directly for the messages.

  1. Create a WhatsApp Business Account in Meta Business Manager, add a phone number and a payment method. The number must not be in use in the normal WhatsApp app.
  2. In Meta Business Manager, add PULSE as a partner on that account. We send you the Business ID to use.
  3. Send us your WhatsApp Business Account ID. We connect it, create the English confirmation template in your account and subscribe to its replies.
  4. Meta approves the template, usually within minutes and sometimes up to a day. We tell you when it is ready.
  5. We give you your pk_live_ key and set your webhook address and secret.

Until step 4 is finished, the live key returns 409 sender_not_ready. The test key works from the start.

Rules & limits

  • Send messages only for orders your customer actually placed. No marketing and no bulk sends.
  • Send only to customers who agreed to receive order updates on WhatsApp.
  • Rate limit: 10 requests per second, with bursts of 20.
  • Each account has a monthly message limit; read it from GET /usage.
  • Order data is kept for 365 days, then deleted.
  • Language: English only in v1.
Questions or a problem? Write to pulsse.studio@gmail.com and include the request_id from the response.