Skip to content
Pre-production preview — not yet live.
Connectby IngeniousPro

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

Create keys in the app under Messages → API and webhooks, and send one as Authorization: Bearer cnk_…. A key is shown once; Connect keeps only a hash of it. Revoke it at any time.

Scoped to messaging

A key reaches sending, delivery, opt-outs and one-time codes — nothing about your account or its settings, and never another account’s data.

Idempotent sends

Give every send an 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

Register an HTTPS endpoint and choose its events. Each call is signed, and a failed one is retried for about fifteen hours.

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.

Endpoints available to an API key
Call
GET/messaging/my-services/{service}/templatesYour registered templates and their slots
GET/messaging/my-services/{service}/sendersYour registered sender IDs
POST/messaging/my-services/{service}/compose/previewCheck a send: recipients, skips, parts
POST/messaging/my-services/{service}/composeSend now, or schedule
GET/messaging/my-services/{service}/sendsYour sends, newest first
GET/messaging/my-sends/{send}One send and its delivery
POST/messaging/my-suppressionsReport an opt-out
POST/otp/sendSend a one-time code
POST/otp/verifyCheck 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
Send a templated SMS
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.

What your endpoint receives
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": "…" }
    ]
  }
}
Check the signature (Node.js)
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 + "." + body with 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.

A refused request
{
  "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.