Developers
The messaging your team uses, from your own code.
A plain HTTPS API over the same templates, senders, contacts and delivery reports as the Connect app — with idempotent sends and signed webhooks. API access is switched on for an account on request.
The basics
Four things to know before the first request.
API keys
Authorization: Bearer cnk_…. A key is shown once; Connect keeps only a hash of it. Revoke it at any time.Scoped to messaging
Idempotent sends
Idempotency-Key of your own, such as your order number. A retry with the same key returns the first send instead of messaging everybody again.Signed webhooks
Base URL: https://api.connect.erptools.in/api/v1. Every response is JSON in one envelope — success, data or error, and a request_id to quote when you ask us about a call.
Endpoints
What a key can do.
Paths are relative to the base URL. {service} is the id of your messaging service, shown in the example on the API and webhooks page of the app.
Building an integration? Ask us for the request and response details of the calls you need.
| Call | For |
|---|---|
GET/messaging/my-services/{service}/templatesYour registered templates and their slots | Your registered templates and their slots |
GET/messaging/my-services/{service}/sendersYour registered sender IDs | Your registered sender IDs |
POST/messaging/my-services/{service}/compose/previewCheck a send: recipients, skips, parts | Check a send: recipients, skips, parts |
POST/messaging/my-services/{service}/composeSend now, or schedule | Send now, or schedule |
GET/messaging/my-services/{service}/sendsYour sends, newest first | Your sends, newest first |
GET/messaging/my-sends/{send}One send and its delivery | One send and its delivery |
POST/messaging/my-suppressionsReport an opt-out | Report an opt-out |
POST/otp/sendSend a one-time code | Send a one-time code |
POST/otp/verifyCheck a one-time code | Check a one-time code |
Sending
A template, a sender, numbers, and what fills each slot.
Each slot is filled either once for the whole send (a form value) or from each recipient’s contact record, with a value to use when a contact has no such detail. Add a schedule to send later, or daily, weekly or monthly.
- Numbers are checked and duplicates sent once
- Opt-outs on your list are skipped
- Preview first with the same body to see who will receive it
curl -X POST https://api.connect.erptools.in/api/v1/messaging/my-services/{service}/compose \
-H "Authorization: Bearer cnk_…" \
-H "Idempotency-Key: order-10042" \
-H "Content-Type: application/json" \
-d '{
"template_public_id": "…",
"sender_id": "ACMEST",
"numbers": ["98XXXXXXXX", "97XXXXXXXX"],
"variables": [
{ "source": "contact", "field": "first_name", "value": "Customer" },
{ "source": "form", "value": "10042" }
]
}'Webhooks
Hear about every change, and prove it came from Connect.
Choose which events an endpoint receives: send.updated as a send progresses, message.updated as messages reach their final status, and otp.updated for one-time codes. Message text is never included — you composed it.
POST https://your-system.example/webhooks/connect
Content-Type: application/json
X-Connect-Event: message.updated
X-Connect-Delivery: 5f0c…
X-Connect-Signature: t=1758268800,v1=9b1e…
{
"id": "5f0c…",
"event": "message.updated",
"created_at": "2026-09-19T10:40:00+00:00",
"data": {
"send_id": "c2a7…",
"messages": [
{ "mobile": "98XXXXXXXX", "status": "delivered", "error": null, "at": "…" }
]
}
}import crypto from "node:crypto";
// rawBody: the request body exactly as received, before any JSON parsing.
export function isFromConnect(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(
signatureHeader.split(",").map((part) => part.split("=")),
);
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
return (
fresh &&
expected.length === parts.v1?.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
);
}- The signature is HMAC-SHA256 of
t + "." + bodywith your signing secret. - Refuse a call whose signature does not match, or whose timestamp is more than five minutes old.
- Answer with any 2xx. Anything else is retried after 1 minute, 5, 30, 2 hours and 12 hours.
- Endpoints must be public HTTPS addresses; redirects are not followed.
Errors
Errors that say what to fix.
A refused call has a stable code to branch on, a message written for a person, and details naming each field that needs attention.
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Check the schedule.",
"details": [{ "field": "schedule.run_at", "code": "PAST", "message": "Choose a time that has not passed." }]
},
"meta": { "request_id": "…" }
}Ready to integrate?
Log in to create an API key, or tell us about your integration and we will switch API access on for your account.