Public API · v1

Messages, Contacts & Campaigns

The endpoints an external CRM needs: send an approved WhatsApp template on your number, schedule a full campaign to thousands of contacts, and keep a contact's attributes in sync — all authenticated with your business's own API key.

Base URL https://api.getsetsales.in

Authentication

Every request carries your business's API key as a bearer token. Get or rotate it from Settings → API Key in the app — the raw key is shown exactly once at generation time; only its hash is kept on our side, so if it's lost, rotate rather than retrieve.

Header on every request
Authorization: Bearer gss_live_xxxxxxxxxxxxxxxxxxxx

An x-api-key: gss_live_… header is accepted as an equivalent — same key, either spelling.

Rotating invalidates the old key immediately — there's no overlap window, so redeploy the new key everywhere it's used before rotating in production.

POST Send a template message

Sends an approved WhatsApp template to one recipient, on behalf of your business's connected number.

POST/public/v1/messages
FieldTypeRequiredNotes
to string required Recipient's number with country code, digits only — e.g. 919999999999, no leading +.
templateName string required Must already be approved on the sending number's WABA (check the Templates page).
languageCode string optional Defaults to en. Must match the template's approved language exactly.
parameters string[] optional Body variable values in order — fills {{1}}, {{2}}, etc. Omit entirely for a template with no variables.
headerImageUrl string optional Required for templates approved with an IMAGE header — Meta rejects the send without it. A public https URL to a JPG or PNG; Meta fetches it itself, so it cannot be behind auth.
phoneNumberId string optional Which connected number to send from. Defaults to the business's first number if you only have one.
Request — cURL
# templateName must already be Meta-approved
curl -X POST \
  https://api.getsetsales.in/public/v1/messages \
  -H "Authorization: Bearer gss_live_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "919999999999",
    "templateName": "order_confirmation",
    "languageCode": "en",
    "parameters": ["Raksha", "#1234"],
    "headerImageUrl": "https://cdn.yoursite.com/banner.jpg"
  }'
Request — Node
await fetch("https://api.getsetsales.in/public/v1/messages", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    to: "919999999999",
    templateName: "order_confirmation",
    languageCode: "en",
    parameters: ["Raksha", "#1234"],
  }),
});
Hi Raksha, your order #1234 is confirmed.
✓✓ delivered
Response — 200 OK
{
  "messageId": "wamid.HBgMOTE5...",
  "status": "sent"
}
Blocked below the ₹100 wallet floor — same limit the in-app composer enforces. The actual debit still happens once Meta's delivery webhook confirms the message went out, not at send time, but the send itself won't go through on an empty wallet.

POST Update contact attributes

Creates the contact if the phone number is new, or merges into the existing one. Attributes merge shallowly — keys you send overwrite, everything else already on the contact is left alone.

POST/public/v1/contacts
FieldTypeRequiredNotes
phone string required Identity key for the contact. Normalised the same way on both sides, so formatting (spaces, +) doesn't matter.
name string optional Only overwrites if you send a non-empty value — omitting it never blanks an existing name.
attributes object optional Flat string→string map. Merged, not replaced — sending {"plan":"pro"} updates just that key and leaves every other stored attribute untouched.
tags string[] optional Added to the contact's existing tags (union) — this endpoint never removes a tag.
Request — cURL
# updates plan + csat, leaves other attributes as-is
curl -X POST \
  https://api.getsetsales.in/public/v1/contacts \
  -H "Authorization: Bearer gss_live_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "919999999999",
    "attributes": {
      "plan": "pro",
      "csat_score": "9"
    },
    "tags": ["upgraded"]
  }'
Request — Node
await fetch("https://api.getsetsales.in/public/v1/contacts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    phone: "919999999999",
    attributes: { plan: "pro", csat_score: "9" },
    tags: ["upgraded"],
  }),
});
Response — 200 OK
{
  "contact": {
    "phone": "919999999999",
    "name": "Raksha Verma",
    "attributes": { "plan": "pro", "csat_score": "9" },
    "tags": ["upgraded"],
    "engagement": "replied",
    "source": "api",
    "createdAt": "2026-07-02T09:14:00.000Z",
    "updatedAt": "2026-08-09T11:02:00.000Z"
  }
}
Contacts created here are tagged source: "api", so you can always tell them apart from ones added manually or via CSV import on the Contacts page.

Reading contacts back: the same route also accepts GET /public/v1/contacts?search=&tag=&limit=&cursor= for paginated lookups against the same contact book.

POST Schedule a campaign

Creates and schedules a template campaign to up to 10,000 contacts in one call — the exact same campaign the in-app wizard builds. Batching, pacing, opt-out filtering, duplicate handling and billing all behave identically, and the campaign appears on the Campaigns page with live delivered/read/reply stats.

POST/public/v1/campaigns
FieldTypeRequiredNotes
campaignName string required Short prefix for the campaign id — letters, numbers, - and _ only (e.g. aug-promo).
templateName string required Must already be approved on the sending number's WABA.
contacts object[] required Up to 10,000 of { phone, name?, params? }. phone is digits with country code; params fills the template's {{1}}…{{N}} for that contact.
parameters string[] optional Fixed body params used wherever a contact has no params of its own.
templateCategory string optional UTILITY (default) or MARKETING — used for the cost estimate and billing category.
phoneNumberId string optional Which connected number sends the campaign. Defaults to the business's first number.
headerImageUrl string optional Required for templates approved with an IMAGE header. Public https JPG or PNG, used as the header for every message in the campaign.
scheduleAt string optional IST send time, "YYYY-MM-DD HH:mm" (ISO with offset also accepted). Omit to send as soon as possible (~2 minutes out). Must be at least a minute in the future.
Request — cURL
# omit scheduleAt to send immediately
curl -X POST \
  https://api.getsetsales.in/public/v1/campaigns \
  -H "Authorization: Bearer gss_live_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "campaignName": "aug-promo",
    "templateName": "wa_update_n",
    "templateCategory": "UTILITY",
    "headerImageUrl": "https://cdn.yoursite.com/banner.jpg",
    "scheduleAt": "2026-09-10 18:30",
    "contacts": [
      { "phone": "919999999999", "name": "Raksha", "params": ["Raksha", "AIMA"] },
      { "phone": "919888888888", "name": "Ravi" }
    ],
    "parameters": ["there", "AIMA"]
  }'
Request — Node
await fetch("https://api.getsetsales.in/public/v1/campaigns", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    campaignName: "aug-promo",
    templateName: "wa_update_n",
    scheduleAt: "2026-09-10 18:30",
    contacts: leads.map((l) => ({ phone: l.phone, name: l.name, params: [l.name, l.program] })),
  }),
});
Response — 200 OK
{
  "campaignId": "aug-promo_ab12cd",
  "campaignName": "aug-promo_ab12cd",
  "totalContacts": 2,
  "scheduledFor": "2026-09-10 18:30 IST",
  "estimatedCostInr": 0.3
}
Wallet is checked up front — the request is rejected with 402 if the balance can't cover the whole campaign's platform fee, so a campaign never starts that it can't finish. Delivery itself is still billed per message on Meta's delivery receipt, same as wizard campaigns.
One number's daily limit still applies — Meta's own 24-hour messaging tier (1K/10K/100K unique customers) binds regardless of campaign size. Check the sending number's tier on the Businesses page before large sends.

Errors

Every error response shares one envelope — an HTTP status and a message string:

{ "error": "<description>" }
StatusMeaning
401Missing or invalid Authorization header — check the key is current (not rotated out).
400A required field is missing, or (on /messages) no connected number matches the given phoneNumberId.
403The business isn't approved yet — sends are blocked until an admin approves the account.
402Wallet balance is at or below the ₹100 minimum — or, on /campaigns, can't cover the campaign's estimated cost. Top up first.
404The business behind the API key no longer exists.
502Meta rejected the send — the message field carries Meta's own rejection reason (e.g. template mismatch, recipient opted out).
500Something failed on our side. Safe to retry.