WhatsApp + Instagram
REST API, v1.
Bearer auth. Cursor pagination. Idempotency keys. Signed webhooks. Free and Creator keys are read-only; paid Business and Developer plans can use write routes when their scopes, channels, and limits allow it.
Pick a runtime
const API_BASE = process.env.IR_API_URL ?? 'https://api.instantreply.co';
const headers = { 'Authorization': `Bearer ${process.env.IR_API_KEY}` };
// List active conversations
const res = await fetch(`${API_BASE}/v1/conversations?status=active`, { headers });
const { data, pagination } = await res.json();
for (const c of data) {
if (c.last_message_preview?.includes('refund')) {
await fetch(`${API_BASE}/v1/conversations/${c.id}`, {
method: 'PATCH', headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ tags: ['priority:refund'] }),
});
}
}v1 surface
Core routes, one auth header.
Live keys access the routes allowed by their scopes. Test keys cannot read tenant data or perform live operations; they only return synthetic responses for POST /messages, conversation replies, campaign sends, automation triggers, and comment replies. Every other test-key request returns 403. Plan access also applies: Free and Creator are read-only, while paid Business and Developer plans can use write routes within their scopes and limits.
- GET
/v1/conversationsList conversations, cursor pagination - GET
/v1/conversations/:idFetch one conversation - PATCH
/v1/conversations/:idUpdate status, assignee, tags - GET
/v1/conversations/:id/messagesList messages in a conversation - POST
/v1/conversations/:id/messagesSend an outbound reply - GET
/v1/messagesList sent and received messages with delivery status - POST
/v1/messagesSend a message by conversation or contact - GET
/v1/messages/:idFetch one message with delivery status - GET
/v1/ticketsList support tickets with filters and cursor pagination - GET
/v1/tickets/:idFetch one support ticket - PATCH
/v1/tickets/:idUpdate ticket status, priority, or assignee - GET
/v1/contactsList contacts - GET
/v1/contacts/:idFetch one contact - PATCH
/v1/contacts/:idUpdate name, email, lead stage - GET
/v1/channelsConnected platforms, status - GET
/v1/analytics/summaryDashboard analytics by date range - GET
/v1/usageTier, rate-limit headroom, request count - GET
/v1/keysList API keys for the workspace - POST
/v1/keysCreate a scoped live or test key - POST
/v1/keys/:id/rotateRotate an API key - DELETE
/v1/keys/:idRevoke an API key - GET
/v1/webhooksList registered webhook endpoints - POST
/v1/webhooksRegister a webhook, returns signing secret - DELETE
/v1/webhooks/:idRevoke a webhook endpoint - GET
/v1/webhooks/:id/deliveriesInspect recent webhook delivery attempts - POST
/v1/webhooks/sendTrigger a WhatsApp journey/send immediately - POST
/v1/webhooks/trigger-campaignCompatibility alias for /v1/webhooks/send - POST
/v1/webhooks/sign-payloadGenerate a test HMAC signature for a payload - GET
/v1/journeysList active WhatsApp journeys and required variables - GET
/v1/trigger/limitsRead journey quota, usage, and remaining capacity - POST
/v1/triggerEnroll a phone into a WhatsApp journey - GET
/v1/trigger/historyList recent journey trigger enrollments - POST
/v1/trigger/validateDry-run a journey trigger payload - POST
/v1/trigger/batchBatch enroll up to 100 recipients - DELETE
/v1/trigger/enrollmentsStop enrollments for a phone/journey - POST
/v1/eventsFire a semantic event mapped to a journey - GET
/v1/templatesList WhatsApp message templates - POST
/v1/templatesCreate and submit a template to Meta - GET
/v1/templates/:idFetch one template - DELETE
/v1/templates/:idDelete a template from Meta and the local registry - POST
/v1/templates/generate/validateValidate a draft objective before storing - POST
/v1/templates/:id/validateValidate category fit + UTILITY coaching (flagged phrases, cost gap) - POST
/v1/templates/:id/submitSubmit template to Meta for approval - GET
/v1/trigger/status/:idExplain a delivery outcome - what happened, fault (ours vs Meta), next step - GET
/v1/campaignsList broadcast campaigns - POST
/v1/campaignsCreate a campaign draft - GET
/v1/campaigns/:idFetch one campaign - PATCH
/v1/campaigns/:idUpdate a draft or scheduled campaign - POST
/v1/campaigns/:id/sendStart a draft campaign - GET
/v1/pipeline/leadsList pipeline leads - GET
/v1/pipeline/leads/:idFetch one pipeline lead - PATCH
/v1/pipeline/leads/:idUpdate pipeline lead fields - PATCH
/v1/pipeline/leads/:id/stageMove a lead to a new stage - GET
/v1/pipeline/stagesList pipeline stages - GET
/v1/automationsList automations - GET
/v1/automations/:idFetch one automation - POST
/v1/automations/:id/triggerManually trigger an automation - GET
/v1/commentsList tracked comments - GET
/v1/comments/:idFetch one comment - POST
/v1/comments/:id/replyReply to a tracked comment - GET
/v1/developer/capabilitiesScopes and features available to the current key - GET
/v1/developer/onboardingIntegration checklist - which steps are done - GET
/v1/developer/limitsRate limits and quotas for the current plan tier - GET
/v1/developer/troubleshooting/errors/:codePlain-English explanation of a Meta or platform error code
Design
Boring choices, on purpose.
- 01
Cursor pagination, always
Every list endpoint returns has_more and next_cursor. No skipped records under load, no off-by-one.
- 02
Idempotency-Key on POST
Pass a UUID and retry safely for 24 hours. Side effects fire exactly once.
- 03
X-Request-Id on every reply
Paste the ID in support and we trace the exact request, exact response, exact downstream call.
- 04
Errors with shape
Same JSON envelope on every failure. code, message, doc_url, request_id. No surprise nulls.
Errors
One envelope, every time.
No nested success flags. No mixed casing. No silent 200s on failure. If the call broke, the body tells you exactly which call, what it expected, and where the docs live.
// 422 Unprocessable Entity
{
"error": {
"code": "INVALID_PLATFORM",
"message": "Channel 'tiktok' is not yet supported on v1.",
"doc_url": "https://www.instantreply.co/api-docs#errors",
"request_id": "req_8f2a0b1c"
}
}Webhooks
Register once. Save the secret now.
POST /v1/webhooks with your endpoint URL and the events you want. The response returns a whsec_ signing secret exactly once — it is never stored in a readable form and never shown again. Copy it straight into your INSTANTREPLY_WEBHOOK_SECRET.
Lost it? There's no reveal endpoint — delete the webhook (DELETE /v1/webhooks/:id) and register a new one to get a fresh secret. Use it to verify X-InstantReply-Signature on every delivery.
// POST /v1/webhooks (scope: webhooks:write)
// Request
{
"url": "https://your-app.com/instantreply/webhook",
"events": ["*"],
"description": "Inbound events"
}
// 201 Created — the secret is returned ONCE, here only.
{
"id": "b1e7…",
"url": "https://your-app.com/instantreply/webhook",
"events": ["*"],
"secret": "whsec_9f2c…" // ← store this now; never shown again
}WhatsApp delivery
Why some messages don't send.
Some blocks we can catch up front. Some only WhatsApp can decide, after it has already accepted the send.
Blocked before sending
Template not yet approved by Meta, or the recipient opted out. The journey stops immediately with a reason — you're never charged for it.
Surfaced after sending (Meta-side)
Error 131049: Meta accepts the send, then throttles delivery based on the recipient's own engagement — no API can predict it in advance. We auto-pause a template after 5 throttled sends in an hour so you stop burning quota.
// message.delivery_failed webhook
{
"event": "message.delivery_failed",
"data": {
"template_name": "order_promo",
"error_code": "131049",
"error_reason": "WhatsApp limited this message to protect users \
from too many marketing messages. Switch to a Utility template or \
message people who've recently engaged.",
"throttled": true
}
}