Connect QuickSamvaad to your own systems: send order updates from your store, add leads from your website, or push incoming messages into your CRM. Owners, admins and the Developer role manage keys and webhooks in Settings → Developer. Tools such as Make or Pabbly Connect can use the same API keys and webhooks.
API keys
Create a key
In Settings → Developer, create a key, name it after the system that uses it, and tick only the scopes it needs.
Copy it once
The full key is shown only once. Store it in your server's secret manager. We keep only a fingerprint, so we can't show it again.
Send it as a bearer token
Add
Authorization: Bearer <key>to every request. Keys start withepk_live_in production.
| Scope | Allows |
|---|---|
| channels:read | Read your connected numbers |
| contacts:read / contacts:write | Read contacts / create, update and record consent |
| templates:read | List templates and their status |
| campaigns:read / campaigns:launch | Read a campaign / launch one |
| inbox:read / inbox:reply | Read message status / send messages |
- You can only grant scopes you hold yourself. A Developer can create read-only keys; sending needs someone with reply access.
- Each key allows 20 requests a second by default; over that you get
429with aRetry-Afterheader. - Optionally restrict a key to your servers' IP addresses, and give it an expiry date.
- Rotate a key to get a new one with the same settings; the old one keeps working for a grace period you choose (a day by default) so you can deploy without downtime. Revoke stops a key immediately.
- Team, billing and settings can't be reached with an API key, only by people signed in.
The public API
The base URL is https://api.quicksamvaad.com/public/v1. Every endpoint applies the same rules as the app: the 24-hour window, marketing consent, Meta's per-person cooldown and template approval. A machine-readable OpenAPI description is at /public/v1/openapi.json.
| Method and path | Scope | Does |
|---|---|---|
POST /messages | inbox:reply | Send a message inside the 24-hour window. Needs Idempotency-Key. |
POST /messages/template | inbox:reply | Send an approved template. Needs Idempotency-Key. |
GET /messages/{id} | inbox:read | Status, WhatsApp message id, error and pricing. |
GET /contacts, POST /contacts | contacts:read / write | List contacts; create or update one by phone number. |
GET /contacts/{id}, PATCH /contacts/{id} | contacts:read / write | Read or update one contact. |
POST /contacts/{id}/consents | contacts:write | Record an opt-in or opt-out in the consent history. |
GET /templates | templates:read | Filter by status, category, language or name. |
GET /campaigns/{id} | campaigns:read | A campaign and its progress. |
POST /campaigns/{id}/launch | campaigns:launch | Launch a prepared campaign. Needs Idempotency-Key. |
curl -X POST https://api.quicksamvaad.com/public/v1/messages/template \
-H "Authorization: Bearer $QUICKSAMVAAD_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"phone_number_id": "<your number id>", "to": "+919812345678",
"template": {"name": "order_update", "language": "en", "components": []}}'Errors use the standard problem format (RFC 7807) with a stable code field, such as window_closed, send_rejected or rate_limited, so your code can branch on it.
Idempotency: safe retries
Requests that send messages or launch campaigns require an Idempotency-Key header: a unique value, such as a UUID, that you generate once per intended action. If the network drops and you retry, use the same key.
| You send | You get |
|---|---|
| A new key | 202 Accepted: the message is queued. |
| The same key and the same body again | 200 OK with the original message. Nothing is sent twice. |
| The same key with a different body | 409 Conflict (idempotency_conflict). |
| No key | 400 (idempotency_key_required). |
Webhooks
Add an endpoint in Settings → Developer with an https URL and the events you want (or all of them). You get a signing secret once; keep it with your API secrets. Use Send test to queue a test event to that endpoint.
| Event | Sent when |
|---|---|
message.received | A customer sends you a message. Includes the message and the contact. |
message.status | An outgoing message is sent, delivered, read or fails. |
conversation.updated | A conversation's status, window or unread count changes. |
conversation.assigned | A conversation is assigned, claimed or handed off. |
channel.alert | A number is paused or has a connection problem. |
POST https://your-server.example/quicksamvaad
Content-Type: application/json
User-Agent: QuickSamvaad-Webhooks/1.0
X-Epik-Webhook-Id: 0193f1c2-... same on every retry: dedupe on it
X-Epik-Event: message.status
X-Epik-Delivery-Id: 0193f1c3-...
X-Epik-Delivery-Attempt: 1
X-Epik-Signature: t=1794787200,v1=5f1c...
{"id": "0193f1c2-...", "type": "message.status", "api_version": "2026-11-12",
"created_at": "2026-11-12T10:00:00Z", "workspace_id": "...", "data": {...}}Reply with any 2xx status within 10 seconds, then do the work in the background.
Verify the signature
Every delivery is signed so you can be sure it came from QuickSamvaad and wasn't changed. X-Epik-Signature holds a timestamp t and one or more v1 signatures: the HMAC-SHA256 of "<t>." + raw body, in hex, keyed with your endpoint's signing secret.
- Read the raw request body before any JSON parsing. Re-serialised JSON won't match.
- Reject the request if the timestamp is more than five minutes old.
- Compute the HMAC and compare it to each
v1value in constant time; accept if any matches.
import hashlib, hmac, time
def verify_signature(secret: str, header: str, raw_body: bytes, tolerance_s: int = 300) -> bool:
parts = [p.split("=", 1) for p in header.split(",") if "=" in p]
ts = next((v for k, v in parts if k == "t"), None)
sigs = [v for k, v in parts if k == "v1"]
if ts is None or not ts.isdigit() or not sigs or abs(time.time() - int(ts)) > tolerance_s:
return False
expected = hmac.new(secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, s) for s in sigs)const crypto = require("crypto");
// Use the raw request body, e.g. express.raw({ type: "application/json" }).
function verifySignature(secret, header, rawBody, toleranceS = 300) {
const parts = header.split(",").map((p) => p.split("="));
const ts = parts.find(([k]) => k === "t")?.[1];
const sigs = parts.filter(([k]) => k === "v1").map(([, v]) => v);
if (!ts || !/^\d+$/.test(ts) || !sigs.length) return false;
if (Math.abs(Date.now() / 1000 - Number(ts)) > toleranceS) return false;
const expected = crypto.createHmac("sha256", secret)
.update(Buffer.concat([Buffer.from(ts + "."), rawBody])).digest("hex");
return sigs.some((s) => s.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
}Retries, ordering and redelivery
- At least once. An event can arrive more than once. Store
X-Epik-Webhook-Idand ignore ids you've already processed. - No ordering guarantee. Events can arrive out of order. Use
created_at, and remember message statuses only move forward: sent, then delivered, then read. - Retries. A timeout, a
408,425,429or a5xxresponse is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours. Other4xxresponses and redirects are not retried. - Turned off after failures. After 50 failed attempts in a row the endpoint is turned off. Fix it, turn it on again, then redeliver from the delivery log.
- Delivery log. Every attempt is listed with its status, response time and error for 30 days. Redeliver sends an event again with the same webhook id, so your deduplication still works.
Common questions
- Is there a Zapier app?
- Not as a native app. Use Zapier, Make or Pabbly Connect with their generic webhook and HTTP steps together with an API key.
- Can I trigger a campaign from my system?
- Yes. Create and prepare the campaign in the app, then launch it with
POST /campaigns/{id}/launchand an Idempotency-Key. - Why was my send rejected with 409?
- A guard refused it: the window is closed (send a template), the contact has no marketing consent, is resting after Meta's limit, or the template isn't approved. The response's
codeand reasons say which.