Developers

REST API reference

Official Cloud API · self-deployed · zero markup: you pay Meta directly.

The Klaros API is a thin, official layer over the WhatsApp Cloud API running on your own deployment. Send messages and templates, read conversations and threads, and list approved templates, all with your data on your infrastructure and no per-message markup. See 12 API use cases with copy-paste recipes for order updates, OTP, helpdesk integration, and more.

Base URL: your deployment, e.g. https://<your-deployment>/api/v1

Auth: send any API key as a Bearer token: Authorization: Bearer uni_key_…. Machine-readable spec: openapi.json.

Two key types: Named developer keys (uni_key_…) are created in the Developers tab with scoped permissions (send, read, manage) and independent revocation. Workspace admin keys (uni_live_…) from Settings have full access. For production integrations, use named keys: give your backend send-only, your BI tool read-only, revoke one without breaking the other.

Quickstart: send your first message

Three steps to your first send

  1. Create a developer key. In the dashboard, go to Developers → Create key. Choose a name ("production-backend") and scopes (send for messaging, read for data, manage for mutations). Copy the key. It is shown once.
  2. Set your base URL. Your deployment URL, e.g. https://your-deployment.example.com.
  3. Send a template. Outside the 24h window, you need an approved template. Inside it, freeform text works too.
# Set your credentials
export BASE="https://your-deployment.example.com"
export KLAROS_API_KEY="uni_key_…"

# Send a template message
curl -X POST "$BASE/api/v1/messages" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to":"15551234567","templateName":"hello_world","templateLang":"en_US"}'

The response includes waMessageId and sentStatus. Check delivery via webhooks or the inbox.

On this page

Send a message

POST /messages

Send freeform text (only inside the 24-hour customer-service window) or an approved template (any time). Outside the window a text send returns 409 with needsTemplate: true. Add an optional from (a phoneNumberId from /numbers) to send from a specific line; omit it to use your default.

# Text (inside the 24h window)
curl -X POST "$BASE/api/v1/messages" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to":"15551234567","text":"Thanks for reaching out!"}'

# Approved template, sent from a specific line
curl -X POST "$BASE/api/v1/messages" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to":"15551234567","from":"123456789012345","template":{"name":"order_update","language":"en_US"}}'

List conversations

GET /conversations

Most-recent-first, with each conversation's 24h-window state. Query: limit (max 200).

curl "$BASE/api/v1/conversations?limit=50" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

Get a conversation thread

GET /conversations/{phone}/messages

Full thread, oldest → newest. phone is E.164 digits (no +).

curl "$BASE/api/v1/conversations/15551234567/messages" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

Templates (Meta lifecycle)

Full lifecycle management for your WABA's message templates: create, list, inspect, edit, and delete. Templates are submitted to Meta for approval. Status progresses through PENDINGAPPROVED or REJECTED. Editing an approved template resubmits it; the approved version keeps sending until the edit clears review.

GET /templates

List templates with approval status, components, and quality score. Filter by status, category, or name substring. Paginate with after cursor.

curl "$BASE/api/v1/templates?status=APPROVED&limit=50" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

GET /templates/:name

Get a template by exact name. Returns full components, quality score, and rejection reason. If the template exists in multiple languages, returns an array.

curl "$BASE/api/v1/templates/order_confirmation" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

POST /templates

Create and submit a template for Meta approval. Name must be lowercase alphanumeric with underscores. Variables in body text ({{1}}) require example values in the component's example field.

curl -X POST "$BASE/api/v1/templates" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "order_shipped",
    "category": "UTILITY",
    "language": "en_US",
    "components": [
      {
        "type": "BODY",
        "text": "Hi {{1}}, your order {{2}} has shipped. Track: {{3}}",
        "example": { "body_text": [["Ravi", "ORD-4821", "https://track.example/4821"]] }
      }
    ]
  }'

PATCH /templates/:id

Edit an existing template by its Meta numeric ID (from the id field in list/get responses). Only works on APPROVED or REJECTED templates. PENDING templates cannot be edited. Optionally change the category.

curl -X PATCH "$BASE/api/v1/templates/847291650384" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "components": [
      {
        "type": "BODY",
        "text": "Hi {{1}}, your order {{2}} shipped via {{3}}. Track: {{4}}",
        "example": { "body_text": [["Ravi", "ORD-4821", "BlueDart", "https://track.example/4821"]] }
      }
    ]
  }'

DELETE /templates/:name

Delete a template by name. This removes all language variants and is irreversible.

curl -X DELETE "$BASE/api/v1/templates/old_promo" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

Numbers (lines)

GET /numbers

Every WhatsApp line connected to your workspace: its phoneNumberId, friendly label, WABA, and which one is the sending default. Use a line's phoneNumberId as from when sending. One business, many numbers, one workspace.

curl "$BASE/api/v1/numbers" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

Account & connected number

GET /me

Your account, plan, and the connected WhatsApp number / WABA.

curl "$BASE/api/v1/me" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

Webhooks

Register an https endpoint and Klaros POSTs a signed event the moment something happens on your number. Perfect for n8n, Zapier, or your own backend. Events: message.received, message.status, message.optout. Failed deliveries retry automatically with backoff.

POST /webhooks

Register an endpoint. The signing secret is returned once. Store it to verify the X-Klaros-Signature header. Omit events to receive all.

curl -X POST "$BASE/api/v1/webhooks" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/klaros/webhook","events":["message.received","message.optout"]}'

# → 201 { "data": { "id": "…", "secret": "whsec_…", … } }  (the secret is shown only here)

# List / inspect deliveries / update / delete
curl "$BASE/api/v1/webhooks"                 -H "Authorization: Bearer $KLAROS_API_KEY"
curl "$BASE/api/v1/webhooks/$ID/deliveries"  -H "Authorization: Bearer $KLAROS_API_KEY"

# Update URL, events, or pause/resume (secret stays the same)
curl -X PATCH "$BASE/api/v1/webhooks/$ID" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://new.example.com/webhook","active":false}'

curl -X DELETE "$BASE/api/v1/webhooks/$ID"     -H "Authorization: Bearer $KLAROS_API_KEY"

Event payload & verifying the signature

Each delivery is a JSON body { id, type, created_at, data } with these headers: X-Klaros-Event, X-Klaros-Delivery, and X-Klaros-Signature: sha256=<hmac>. The HMAC is SHA-256 of the raw request body, keyed by your subscription secret. Verify it before trusting the event:

// Node.js: compute and compare
import crypto from 'node:crypto';

function verify(rawBody, header, secret) {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret)
    .update(rawBody).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}

// Example message.received body
{ "id": "…", "type": "message.received", "created_at": "2026-06-25T…Z",
  "data": { "from": "15551234567", "text": "Hi!", "wa_message_id": "wamid…" } }

Connect n8n, Zapier, or Make

There's no published Klaros app in any of these marketplaces yet. What's below uses each platform's own generic webhook/HTTP building blocks against the API and webhooks you already have. It works today with zero waiting on us; a native app would only save you the one-time setup below.

Trigger: Klaros → your workflow (inbound messages, status, opt-outs)

Every platform's "catch a webhook" building block accepts a plain HTTPS URL, so hand it the one your automation gives you, then register it as a Klaros webhook.

  • n8n: add a Webhook node, copy its Production URL, register it (POST /api/v1/webhooks). Verify X-Klaros-Signature in a downstream Code node using the snippet below before trusting the payload.
  • Zapier: create a Zap with trigger Webhooks by Zapier → Catch Hook, copy the custom webhook URL it gives you, register that. Signature verification needs a Code by Zapier step (Node.js) with the same snippet.
  • Make: add a Custom webhook trigger module, copy its URL, register that. Verify the signature in an inline HTTP > Set variable + crypto function, or skip verification if the scenario is low-stakes (logging, not money or state changes).

Same signature-check snippet as Webhooks above. Don't skip it for anything that writes data back anywhere.

Action: your workflow → Klaros (send a message, create an order, record a milestone)

Every REST v1 endpoint takes a Bearer $KLAROS_API_KEY. Point your platform's generic HTTP module at it:

  • n8n: an HTTP Request node, or import $BASE/api/v1/openapi.json directly (Import cURL / OpenAPI in newer n8n versions) to get every operation pre-filled with its parameters. Every operation carries a stable operationId (e.g. postCampaignsByIdLaunch) for tools that key off it.
  • Zapier: a Webhooks by Zapier → POST/PATCH/GET action, URL $BASE/api/v1/<resource>, header Authorization: Bearer $KLAROS_API_KEY.
  • Make: an HTTP → Make a request module, same URL/header shape. Make's "Create a custom app" flow can also import the OpenAPI spec above if you want typed fields instead of a raw JSON body.

The most useful single action for most automations is POST /milestones: one call from a Shopify/any e-commerce "order created" trigger turns into a WhatsApp confirmation with zero Klaros-side integration work per store.

Milestones: behavior-triggered WhatsApp

Your customers don't live on a calendar. They place an order, finish onboarding, abandon a cart. The right message is the one that answers that moment, not "day 3 of the drip". Milestones turn your own backend into the trigger: one HTTP call when something meaningful happens, and the rules you define in the dashboard send the contextual next-step template from your own number. No flow-builder ceremony, no per-message markup taxing every trigger: just your event, your template, your price from Meta.

POST /milestones

The whole integration is this call. Fire it from your order handler, your KYC callback, your cron, anywhere your code already knows the customer just did something. Idempotent per contact × milestone: call it on every occurrence, only the first one counts. Unknown phones auto-create a contact.

curl -X POST "$BASE/api/v1/milestones" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"phone":"919876543210","milestone":"order_placed","name":"Priya","meta":{"orderId":"ORD-1042"}}'

# → 200 { "data": { "contactId": "…", "milestone": "order_placed", "recorded": true } }
# "recorded": false on repeat calls: already achieved, nothing re-fires. Safe to spam.

milestone is your own vocabulary: 2–40 chars, lowercase letters, numbers, underscores (order_placed, kyc_completed, demo_booked). No pre-registration needed; a milestone exists the first time you record it.

Built-in: opted_in is fired by the platform itself the first time a contact opts in, from any source (keyword reply, button tap, imported consent). Define a rule on it to start a welcome journey with zero integration; a 30–60 min delay lands it after the instant welcome + cadence question. A later STOP→START never re-fires it.

GET /milestones?phone=…

A contact's achieved milestones, oldest first, for debugging your integration.

curl "$BASE/api/v1/milestones?phone=919876543210" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

# → { "data": [ { "milestone": "order_placed", "meta": { "orderId": "ORD-1042" },
#                "achievedAt": "2026-07-10T…Z" } ] }

Then the rule does the rest

In the dashboard under Engage → Behavior Sequences, define: "when order_placed → send template order_confirmation, after 5 minutes." The engine handles everything you'd otherwise have to build: one send per contact per rule (never a double message), consent and opt-out checked at send time (an opted-out contact is skipped and logged, never messaged), delivery outcomes visible in the activity feed. The send goes out on your own WABA at Meta's raw conversation price. Because there's no markup, a high-frequency trigger costs you exactly what it costs Meta.

Need an SDK for your language, or a custom event? Talk to us.

Developer keys

POST /keys

Create a named API key with scoped permissions. The raw key is returned once in the response. Store it securely.

curl -X POST "$BASE/api/v1/keys" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"production-backend","scopes":["send","read"]}'

Scopes: send = POST /messages, campaign launch. read = all GETs. manage = all other mutations. Default: all three.

Optional: expiresAt (ISO 8601) sets an automatic expiry for temporary integrations.

GET /keys

List all active keys for the workspace. Raw secrets are never returned after creation.

PATCH /keys/:id

Update a key's name or scopes. Scope changes take effect on the next request.

DELETE /keys/:id

Revoke a key immediately. Any in-flight request using this key will fail. Revocation is permanent.

Best practice: Create one key per integration. Name it after the system it powers ("analytics-dashboard", "crm-sync", "staging-test"). When you rotate, create the new key, update your config, then revoke the old one.

Full resource reference

The endpoints above cover the most common integration patterns. The full API includes 97 endpoints across these resource groups, all documented in the machine-readable OpenAPI 3.1 spec:

curl "$BASE/api/v1/openapi.json"   # no auth required
Resource Operations Scope
/contactslist, get, create, update, delete, bulk upsert, consentread / manage
/segmentslist, get, create, update, delete, members, previewread / manage
/campaignslist, get, create, update, delete, launch, schedule, pause, resume, analytics, recipients, dry-runread / manage
/sequenceslist, get, create, update, delete, enroll, unenroll, status, steps CRUDread / manage
/productslist, get, create, update, deleteread / manage
/conversationslist, thread, assign, resolve, reopen, notes, labelsread / manage
/labelslist, create, update, deleteread / manage
/canned-replieslist, create, update, deleteread / manage
/messagessend, get by ID, list with keyset paginationsend / read

Every mutation endpoint enforces scope. A key with only read + send can list resources and send messages but cannot create campaigns or modify contacts. See Developer keys for scope details.

Contacts

Create, query, and manage your contact list. Every contact is identified by phone (E.164 digits, no +). Tags, country, and consent status are filterable. Bulk upsert handles imports up to 1,000 rows at a time.

GET /contacts

List contacts with optional filters: search (name or phone substring), country, tag. Paginate with page and limit (default 50, max 200).

curl "$BASE/api/v1/contacts?search=priya&country=IN&limit=25" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

POST /contacts

Create a single contact. phone is required. Optional: name, country, tags (string array).

curl -X POST "$BASE/api/v1/contacts" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"phone":"919876543210","name":"Priya Sharma","country":"IN","tags":["vip","wholesale"]}'

POST /contacts/bulk

Upsert up to 1,000 contacts in one call. Existing phones are updated, new ones created. countryCode applies as a default for numbers without a country prefix.

curl -X POST "$BASE/api/v1/contacts/bulk" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"countryCode":"91","contacts":[{"phone":"9876543210","name":"Priya"},{"phone":"9123456789","name":"Ravi","tags":["retail"]}]}'

GET /contacts/:id

Get a single contact by ID, including tags, consent status, and last message timestamp.

curl "$BASE/api/v1/contacts/ct_abc123" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

PATCH /contacts/:id

Update a contact's name, country, or tags. Omitted fields are left unchanged.

curl -X PATCH "$BASE/api/v1/contacts/ct_abc123" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Priya S.","tags":["vip","gold"]}'

DELETE /contacts/:id

Delete a contact and its associated data. This is permanent.

curl -X DELETE "$BASE/api/v1/contacts/ct_abc123" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

POST /contacts/:id/consent

Record a consent change. status is opted_in or opted_out. Include source and evidenceText for the audit trail.

curl -X POST "$BASE/api/v1/contacts/ct_abc123/consent" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status":"opted_in","source":"website_form","evidenceText":"Checked opt-in box on checkout"}'

GET /contacts/:id/consent

Full consent audit trail for a contact: every opt-in, opt-out, source, and timestamp.

curl "$BASE/api/v1/contacts/ct_abc123/consent" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

Segments

Dynamic groups of contacts defined by filter rules. Use segments as campaign audiences or sequence enrollment sources. Filters match on tags, country, consent status, and activity.

GET /segments

List segments. Filter by status: active (default) or archived.

curl "$BASE/api/v1/segments?status=active" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

POST /segments

Create a segment. name is required. filters defines the matching rules. Optional description.

curl -X POST "$BASE/api/v1/segments" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Gold customers IN","description":"Opted-in Indian VIPs","filters":{"country":"IN","tags":["vip"],"consentStatus":"opted_in"}}'

GET /segments/:id

Get a segment's definition, filter rules, and current member count.

curl "$BASE/api/v1/segments/seg_xyz789" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

PATCH /segments/:id

Update a segment's name, description, or filters.

curl -X PATCH "$BASE/api/v1/segments/seg_xyz789" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"filters":{"country":"IN","tags":["vip","gold"]}}'

DELETE /segments/:id

Soft-archive a segment. Archived segments stop matching but remain visible for historical reference.

curl -X DELETE "$BASE/api/v1/segments/seg_xyz789" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

GET /segments/:id/contacts

Paginated list of contacts that currently match this segment's filters. page and limit (default 50, max 200).

curl "$BASE/api/v1/segments/seg_xyz789/contacts?page=1&limit=100" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

POST /segments/preview

Stateless preview: pass a filters object and get the matching contact count and sample rows without creating a segment.

curl -X POST "$BASE/api/v1/segments/preview" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"filters":{"country":"IN","tags":["wholesale"]}}'

Campaigns

Create, schedule, and manage broadcast campaigns. Campaigns target segments or contact lists and send templates or text messages. Lifecycle: draft, scheduled, active, paused, completed.

GET /campaigns

List campaigns with optional status filter. Paginate with page and limit.

curl "$BASE/api/v1/campaigns?status=active&limit=20" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

POST /campaigns

Create a campaign in draft state. campaignType: one_shot or recurring. messageKind: text or template. audienceSources defines the target segments or contact lists.

curl -X POST "$BASE/api/v1/campaigns" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"July promo","campaignType":"one_shot","messageKind":"template","templateName":"july_sale","audienceSources":[{"type":"segment","id":"seg_xyz789"}]}'

GET /campaigns/:id

Get campaign details, including status, audience size, and delivery progress.

curl "$BASE/api/v1/campaigns/cmp_abc123" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

PATCH /campaigns/:id

Update a campaign's name, audience, or message content. Only allowed while the campaign is in draft or paused state.

curl -X PATCH "$BASE/api/v1/campaigns/cmp_abc123" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"July promo v2","templateName":"july_sale_v2"}'

DELETE /campaigns/:id

Delete a draft campaign. Active or completed campaigns cannot be deleted.

curl -X DELETE "$BASE/api/v1/campaigns/cmp_abc123" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

POST /campaigns/:id/launch

Launch a draft campaign immediately. Consent is checked per-contact at send time.

curl -X POST "$BASE/api/v1/campaigns/cmp_abc123/launch" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

POST /campaigns/:id/schedule

Schedule a campaign for later. Pass scheduledAt (ISO 8601) for one-shot, or recurrence for recurring campaigns.

curl -X POST "$BASE/api/v1/campaigns/cmp_abc123/schedule" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"scheduledAt":"2026-08-01T09:00:00Z"}'

POST /campaigns/:id/pause

Pause an active campaign. Unsent messages are held until resumed.

curl -X POST "$BASE/api/v1/campaigns/cmp_abc123/pause" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

POST /campaigns/:id/resume

Resume a paused campaign. Sending picks up where it left off.

curl -X POST "$BASE/api/v1/campaigns/cmp_abc123/resume" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

GET /campaigns/:id/analytics

Delivery analytics: sent, delivered, read, and failed counts with rates.

curl "$BASE/api/v1/campaigns/cmp_abc123/analytics" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

GET /campaigns/:id/recipients

Per-contact delivery status for a campaign. Paginate with page and limit.

curl "$BASE/api/v1/campaigns/cmp_abc123/recipients?limit=100" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

POST /campaigns/:id/dry-run

Preview a campaign without sending. Returns the resolved audience, consent checks, and estimated cost.

curl -X POST "$BASE/api/v1/campaigns/cmp_abc123/dry-run" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

Sequences

Multi-step drip sequences that send a series of templates over time. Define steps with delays, enroll contacts or segments, and track progress through each step.

GET /sequences

List all sequences with their status and enrollment counts.

curl "$BASE/api/v1/sequences" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

POST /sequences

Create a sequence with an ordered steps array. Each step has kind, templateName, and delayDays (wait before sending).

curl -X POST "$BASE/api/v1/sequences" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Onboarding drip","steps":[{"kind":"template","templateName":"welcome_day1","delayDays":0},{"kind":"template","templateName":"tips_day3","delayDays":3},{"kind":"template","templateName":"checkin_day7","delayDays":7}]}'

GET /sequences/:id

Get a sequence's definition, steps, and enrollment summary.

curl "$BASE/api/v1/sequences/seq_abc123" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

PATCH /sequences/:id

Update a sequence's name or status (e.g. active, paused).

curl -X PATCH "$BASE/api/v1/sequences/seq_abc123" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status":"paused"}'

DELETE /sequences/:id

Delete a sequence. Active enrollments are terminated.

curl -X DELETE "$BASE/api/v1/sequences/seq_abc123" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

POST /sequences/:id/enroll

Enroll contacts into the sequence. Pass contactIds (max 500) or segmentId to enroll all members of a segment.

curl -X POST "$BASE/api/v1/sequences/seq_abc123/enroll" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"contactIds":["ct_001","ct_002","ct_003"]}'

POST /sequences/:id/unenroll

Remove contacts from the sequence. Pending steps are cancelled.

curl -X POST "$BASE/api/v1/sequences/seq_abc123/unenroll" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"contactIds":["ct_001"]}'

GET /sequences/:id/status

Enrollment summary: total enrolled, active, completed, and failed counts.

curl "$BASE/api/v1/sequences/seq_abc123/status" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

GET /sequences/:id/enrollments

Per-contact enrollment details: current step, next send time, and completion status.

curl "$BASE/api/v1/sequences/seq_abc123/enrollments" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

POST /sequences/:id/steps

Add a step to the sequence. Specify kind, templateName, delayDays, and optional position.

curl -X POST "$BASE/api/v1/sequences/seq_abc123/steps" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"kind":"template","templateName":"feedback_day14","delayDays":14}'

PUT /sequences/:id/steps/:stepId

Replace a step's configuration. The step keeps its position in the sequence.

curl -X PUT "$BASE/api/v1/sequences/seq_abc123/steps/stp_42" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"kind":"template","templateName":"feedback_v2","delayDays":14}'

DELETE /sequences/:id/steps/:stepId

Remove a step from the sequence. Contacts already past this step are unaffected.

curl -X DELETE "$BASE/api/v1/sequences/seq_abc123/steps/stp_42" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

Inbox operations

Manage conversations from your API: assign agents, resolve and reopen threads, add internal notes, and organize with labels. Conversations are identified by the contact's phone number (E.164 digits).

POST /conversations/:phone/assign

Assign a conversation to a team member. Pass assignee as an agentId, "me" (the authenticated user), or null to unassign.

curl -X POST "$BASE/api/v1/conversations/919876543210/assign" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"assignee":"agt_ravi42"}'

POST /conversations/:phone/resolve

Mark a conversation as resolved. The contact moves out of the active inbox.

curl -X POST "$BASE/api/v1/conversations/919876543210/resolve" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

POST /conversations/:phone/reopen

Reopen a resolved conversation. It returns to the active inbox.

curl -X POST "$BASE/api/v1/conversations/919876543210/reopen" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

POST /conversations/bulk-resolve

Resolve multiple conversations at once. Pass a phones array (max 200).

curl -X POST "$BASE/api/v1/conversations/bulk-resolve" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"phones":["919876543210","919123456789","918765432100"]}'

POST /conversations/:phone/notes

Add an internal note to a conversation. Visible to your team only, never sent to the contact. body max 4,000 characters.

curl -X POST "$BASE/api/v1/conversations/919876543210/notes" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"body":"Called back, prefers delivery on weekends only."}'

GET /conversations/:phone/labels

List labels attached to a conversation.

curl "$BASE/api/v1/conversations/919876543210/labels" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

POST /conversations/:phone/labels

Attach a label to a conversation by labelId.

curl -X POST "$BASE/api/v1/conversations/919876543210/labels" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"labelId":"lbl_urgent"}'

DELETE /conversations/:phone/labels/:labelId

Remove a label from a conversation.

curl -X DELETE "$BASE/api/v1/conversations/919876543210/labels/lbl_urgent" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

Labels CRUD

Manage workspace labels. Each label has a name and color.

# List all labels
curl "$BASE/api/v1/labels" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

# Create a label
curl -X POST "$BASE/api/v1/labels" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Urgent","color":"#ef4444"}'

# Update / Delete
curl -X PUT "$BASE/api/v1/labels/$ID"  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" -d '{"name":"Priority","color":"#f59e0b"}'
curl -X DELETE "$BASE/api/v1/labels/$ID" -H "Authorization: Bearer $KLAROS_API_KEY"

Canned replies CRUD

Pre-written reply templates for quick agent responses. Each has a shortcut, title, body, and optional category.

# List canned replies
curl "$BASE/api/v1/canned-replies" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

# Create a canned reply
curl -X POST "$BASE/api/v1/canned-replies" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"shortcut":"/hours","title":"Business hours","body":"We are available Mon-Sat 9AM-6PM IST.","category":"general"}'

# Update / Delete
curl -X PUT "$BASE/api/v1/canned-replies/$ID"  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" -d '{"body":"Mon-Sat 9AM-7PM IST."}'
curl -X DELETE "$BASE/api/v1/canned-replies/$ID" -H "Authorization: Bearer $KLAROS_API_KEY"

Products

Manage your product catalog for sharing in WhatsApp conversations. Products are linked to media files uploaded via your dashboard.

GET /products

List products. Pass activeOnly=false to include inactive products.

curl "$BASE/api/v1/products?activeOnly=true" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

POST /products

Create a product. name and mediaFileId are required.

curl -X POST "$BASE/api/v1/products" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"22K Gold Chain","mediaFileId":"mf_img_abc123"}'

GET /products/:id

Get a single product's details and media reference.

curl "$BASE/api/v1/products/prod_abc123" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

PATCH /products/:id

Update a product's name, media, or active status.

curl -X PATCH "$BASE/api/v1/products/prod_abc123" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"22K Gold Chain (20in)"}'

DELETE /products/:id

Delete a product from the catalog.

curl -X DELETE "$BASE/api/v1/products/prod_abc123" \
  -H "Authorization: Bearer $KLAROS_API_KEY"

What's new

July 2026
Named developer API keys: Create named, scoped (send / read / manage), independently revocable keys from the Developers tab. Give your backend send-only access; give your BI tool read-only. Revoke one without breaking the other. Docs ↑ Live
July 2026
Full v1 resource API (97 endpoints): Contacts, segments, campaigns, sequences, inbox operations, products, and developer key management. Every dashboard capability is now a REST resource. Live
July 2026
Milestones API + Behavior Sequences: POST /api/v1/milestones from your own backend when a customer acts; dashboard rules send the next-step template from your number. React to what customers do, not what day it is. Docs ↑ Live
June 2026
Multi-number per-line inbox: Send and receive from multiple WhatsApp numbers in one deployment. Each number gets its own inbox view with a line switcher. Live
June 2026
BYO-WABA self-deployed connect: Connect your own WhatsApp Business Account without electing Klaros as a Tech Provider. Health-check tooling and onboarding runbook included. Live
June 2026
Automation rules: Greeting, away, and keyword auto-replies configurable from the dashboard. Live
June 2026
Team attribution & assignment: Assign conversations to team members, see who replied, filter by assignee. Live
June 2026
Live cost dashboard: See true Meta conversation costs in your WABA's billing currency. Per-campaign cost and cost-per-reply in broadcast list. Live

Reliability

The details that decide whether you can put this in production, not just a demo.

Idempotency

Send an Idempotency-Key header on POST /messages (any unique string, e.g. a UUID). If the same key arrives again with the same body, the original response is replayed with "replayed": true and nothing is sent twice. The same key with a different body returns 409 idempotency_key_reused. Keys are scoped to your workspace and expire after 24 hours. Retries after a timeout are safe.

curl -X POST "$BASE/api/v1/messages" \
  -H "Authorization: Bearer $KLAROS_API_KEY" \
  -H "Idempotency-Key: 8f2a1c9e-…" \
  -H "Content-Type: application/json" \
  -d '{"to":"15551234567","text":"Your order shipped"}'

Rate limits

Every response carries the current window; a 429 tells you exactly when to retry:

X-RateLimit-Limit: 120       # requests per minute, per workspace
X-RateLimit-Remaining: 118
X-RateLimit-Reset: 47        # seconds until the window resets
Retry-After: 47              # sent only on a 429

Pace yourself off X-RateLimit-Remaining rather than waiting for the wall. Need a higher limit? Ask us.

Error codes

Branch on the stable error field, never the human message. These are a contract:

consent_blocked403: recipient opted out, or marketing to a not-yet-opted-in contact outside the 24h window. code gives the reason.
needsTemplate409: the 24h window is closed; send an approved template.
idempotency_key_reused409: same key, different body.
rate_limited429: see Retry-After.
forbidden_line403: your key may not send from that number.
number_not_found404: that from number is not on your workspace.
provider_error502: Meta failed the send; transient, safe to retry.

The full list is in the OpenAPI spec under x-error-codes.