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_hereKeys 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 contactscontacts:write— create / update contactsorders:read— list and read your ordersconversations:read— list and read your conversationsbroadcasts: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 only —
templateIdmust be one of your APPROVED templates. Free-form text isn't allowed for broadcasts. - Segment audience only —
segmentIdmust 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 period402 OVER_CAMPAIGN_CAP— estimated cost exceeds your per-campaign safety cap (passforce: trueto override)400 EMPTY_AUDIENCE— the segment has no reachable recipients400 TEMPLATE_INVALID— the template isn't approved or has an unfilled placeholder404 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/branch400 TEMPLATE_INVALID— the template isn't approved or has an unfilled placeholder502 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 sent401 INVALID_API_KEY— the key is wrong or revoked403 MISSING_SCOPE— the key lacks the required scope429 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 savedlead.qualified— a conversation is qualified as a hot sales leadorder.created— a customer places a new orderorder.paid— payment for an order is confirmedconversation.escalated— a chat is handed off to a humancampaign.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 nameX-AgentShift-Delivery— unique id for this delivery attemptX-AgentShift-Signature—sha256=<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.