DocsAPI & Webhooks

API & Webhooks

Pull your contacts and orders over REST, and receive signed real-time events in your own systems.

The AgentShift API lets your own systems read your data and react to events in real time. There are two halves: a REST API (you call us) and outbound webhooks (we call you).

Base URL: https://api.agentshift.in/api/v1

Authentication

Every request needs a tenant API key. Create one in Settings → API & Webhooks — pick the scopes it needs, and copy the key (it starts with sk_live_ and is shown only once).

Send it on every request as a Bearer token (or the X-API-Key header):

Authorization: Bearer sk_live_your_key_here

Keys are stored hashed — we can never show a key again after creation. Lost it? Revoke it and create a new one.

Scopes

A key grants only the scopes you tick when creating it:

  • contacts:read — list and read your contacts
  • contacts:write — create / update contacts
  • orders:read — list and read your orders
  • conversations:read — list and read your conversations
  • broadcasts:write — launch template broadcasts (spends broadcast quota)
  • messages:write — send a single WhatsApp message (text or template)

Calling an endpoint without its scope returns 403 with code MISSING_SCOPE.

Rate limits

Up to 120 requests per minute per key. Responses include the standard RateLimit-* headers; exceeding the limit returns 429 with code RATE_LIMITED.

Endpoints

GET /v1/me

Returns the tenant and scopes for the key. Handy for verifying a key works.

curl https://api.agentshift.in/api/v1/me \
  -H "Authorization: Bearer sk_live_your_key_here"
{
  "success": true,
  "data": {
    "tenantId": "6a6044514dea8adab14c54fa",
    "scopes": ["contacts:read", "orders:read"]
  }
}

GET /v1/contacts

Paginated list of your contacts. Requires contacts:read. Query params: page (default 1), limit (default 50, max 100).

curl "https://api.agentshift.in/api/v1/contacts?page=1&limit=50" \
  -H "Authorization: Bearer sk_live_your_key_here"
{
  "success": true,
  "data": [
    {
      "phone": "+919000000000",
      "name": "Asha",
      "leadStatus": "hot",
      "leadScore": 100,
      "totalOrders": 3,
      "totalRevenue": 149700,
      "lastInteractionAt": "2026-08-11T17:21:09.744Z",
      "createdAt": "2026-07-02T09:14:00.000Z"
    }
  ],
  "page": 1,
  "limit": 50,
  "total": 214
}

GET /v1/orders

Paginated list of your orders. Requires orders:read. Same pagination as contacts. Amounts are in paise (₹499.00 = 49900).

{
  "success": true,
  "data": [
    {
      "orderNumber": "ORD-1042",
      "status": "confirmed",
      "paymentStatus": "paid",
      "paymentMethod": "online",
      "total": 49900,
      "customerPhone": "+919000000000",
      "customerName": "Asha",
      "branchId": "6a6044514dea8adab14c54ff",
      "createdAt": "2026-08-11T17:20:00.000Z"
    }
  ],
  "page": 1,
  "limit": 50,
  "total": 87
}

GET /v1/contacts/:id  ·  GET /v1/orders/:id

Fetch a single contact or order by its id (requires the matching :read scope). Returns 404 if it doesn't belong to your tenant.

POST /v1/contacts

Create or update a contact by phone (upsert) — push contacts in from your CRM or a web form. Requires contacts:write. Uses the same dedup logic as inbound WhatsApp, so a number never forks.

curl -X POST https://api.agentshift.in/api/v1/contacts \
  -H "Authorization: Bearer sk_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"phone":"+919000000000","name":"Asha"}'

Body: phone (required), name (optional). Returns 201 with the saved contact.

GET /v1/conversations

Paginated list of your WhatsApp conversations — metadata only (customer, status, handled-by, last message + time), not full message history. Requires conversations:read.

Broadcasts (send)

Launch a WhatsApp template broadcast to a saved segment, programmatically. Requires broadcasts:write. This spends your broadcast quota and Meta bills per message — treat it accordingly.

Two guardrails are enforced (Meta rules + safety):

  • Approved template onlytemplateId must be one of your APPROVED templates. Free-form text isn't allowed for broadcasts.
  • Segment audience onlysegmentId must be a saved segment. You can't blast an arbitrary phone list.

POST /v1/broadcasts

curl -X POST https://api.agentshift.in/api/v1/broadcasts \
  -H "Authorization: Bearer sk_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "templateId": "<approved_template_id>",
    "templateParams": ["20%"],
    "segmentId": "<segment_id>"
  }'
{
  "success": true,
  "data": { "campaignId": "6a7c...", "recipients": 512, "status": "queued" }
}

Body: templateId (required), segmentId (required), templateParams (fills the template's {{1}}… vars), branchId (optional), name (optional),force (optional — override the per-campaign spend cap). Returns 202 with a campaignId; the send fans out asynchronously.

Blocked launches return a clear code so you can handle them:

  • 402 QUOTA_EXCEEDED — not enough broadcast quota left this period
  • 402 OVER_CAMPAIGN_CAP — estimated cost exceeds your per-campaign safety cap (pass force: true to override)
  • 400 EMPTY_AUDIENCE — the segment has no reachable recipients
  • 400 TEMPLATE_INVALID — the template isn't approved or has an unfilled placeholder
  • 404 SEGMENT_NOT_FOUND — no such segment

GET /v1/broadcasts/:id

Track a broadcast's progress — status and send stats (total, sent, failed). You'll also get the campaign.completed webhook when it finishes.

Messages (send one)

Send a single WhatsApp message to one number. Requires messages:write. Two modes, matching Meta's rules:

  • text — free-form text. Meta only delivers this inside the 24-hour customer-service window (the customer messaged you first). Outside it, the send is rejected — use a template instead.
  • template — an APPROVED template, which reaches anyone anytime. Meta bills per template message.

POST /v1/messages — text

curl -X POST https://api.agentshift.in/api/v1/messages \
  -H "Authorization: Bearer sk_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+919000000000",
    "type": "text",
    "text": "Thanks for your message — how can we help?"
  }'
{
  "success": true,
  "data": { "messageId": "wamid.HBg...", "status": "sent" }
}

POST /v1/messages — template

curl -X POST https://api.agentshift.in/api/v1/messages \
  -H "Authorization: Bearer sk_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+919000000000",
    "type": "template",
    "templateId": "<approved_template_id>",
    "templateParams": ["Asha"]
  }'

Body: to (required), type (text | template), text (for text), templateId + templateParams (for template), branchId (optional — which connected number to send from). Returns 202 with a messageId.

Failure codes:

  • 400 WHATSAPP_NOT_CONNECTED — no WhatsApp number is connected for this tenant/branch
  • 400 TEMPLATE_INVALID — the template isn't approved or has an unfilled placeholder
  • 502 SEND_FAILED — Meta rejected the send (often: text outside the 24h window)

Errors

Every error is JSON: { "success": false, "error": "…", "code": "…" }.

  • 401 NO_API_KEY — no key was sent
  • 401 INVALID_API_KEY — the key is wrong or revoked
  • 403 MISSING_SCOPE — the key lacks the required scope
  • 429 RATE_LIMITED — too many requests

Webhooks

Register an HTTPS endpoint in Settings → API & Webhooks → Outbound Webhooks, pick the events you care about, and we'll POST a signed JSON payload to it in real time. You can scope an endpoint to one branch, or leave it tenant-wide (all branches).

Events

  • contact.created — a new contact is saved
  • lead.qualified — a conversation is qualified as a hot sales lead
  • order.created — a customer places a new order
  • order.paid — payment for an order is confirmed
  • conversation.escalated — a chat is handed off to a human
  • campaign.completed — a broadcast campaign finishes sending

Payload

Every delivery is a POST with this envelope — data holds the event-specific fields:

{
  "event": "order.paid",
  "deliveryId": "6a7b5637290a8865020d6921",
  "tenantId": "6a6044514dea8adab14c54fa",
  "branchId": null,
  "createdAt": "2026-08-11T17:21:09.744Z",
  "data": {
    "orderId": "6a7b...",
    "orderNumber": "ORD-1042",
    "amount": 49900,
    "currency": "INR",
    "customerPhone": "+919000000000",
    "customerName": "Asha",
    "paymentId": "pay_...",
    "status": "confirmed"
  }
}

Each request also carries these headers:

  • X-AgentShift-Event — the event name
  • X-AgentShift-Delivery — unique id for this delivery attempt
  • X-AgentShift-Signaturesha256=<hmac> (see below)

Verifying the signature

Compute an HMAC-SHA256 of the raw request body using your endpoint's signing secret (shown once when you create the endpoint), and compare it to the X-AgentShift-Signature header. Always use a constant-time comparison.

import crypto from 'crypto'
import express from 'express'

const app = express()
const SIGNING_SECRET = process.env.AGENTSHIFT_WEBHOOK_SECRET  // shown once when you create the endpoint

// Capture the RAW body — you must verify against the exact bytes we sent.
app.use(express.json({ verify: (req, _res, buf) => { (req as any).rawBody = buf } }))

app.post('/webhooks/agentshift', (req, res) => {
  const signature = req.header('X-AgentShift-Signature') || ''
  const expected  = 'sha256=' + crypto
    .createHmac('sha256', SIGNING_SECRET)
    .update((req as any).rawBody)
    .digest('hex')

  const a = Buffer.from(signature)
  const b = Buffer.from(expected)
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.status(401).send('bad signature')
  }

  // Verified — handle the event.
  const { event, data } = req.body
  console.log('AgentShift event:', event, data)
  res.sendStatus(200)   // respond 2xx quickly; we retry non-2xx
})

Retries & reliability

  • Respond 2xx quickly (do heavy work async) — any non-2xx or timeout is retried.
  • Up to 5 attempts with exponential backoff.
  • An endpoint that fails 15 times in a row auto-disables — re-enable it from Settings once fixed.
  • Every attempt is logged (event, status, response) under the endpoint in Settings, so you can debug deliveries.

Need help?

Stuck integrating? Contact support — include the deliveryId or a request timestamp and we can trace it in our logs.