Add contacts, send approved templates and enrol people in sequences from your own systems, and get signed events back when things happen.
The public API and webhooks are plan features, and need the Owner or Admin role to set up. If API keys says the API isn't switched on for your organisation, write to support@i-connect.world.
API keys
- Create keys in Settings → API keys. Give each system its own key, with only the scopes it needs, an optional list of IP addresses it may call from, and an optional expiry date.
- The key is shown once. i-Connect stores only a hash of it. If you lose it, rotate the key.
| Scope | Lets the key |
|---|---|
contacts.read | Read contacts |
contacts.write | Create and update contacts (and record consent) |
messages.send.template | Send template messages |
messages.read | Read message status |
sequences.enroll | Enrol contacts in sequences |
forms.submit | Submit forms |
bookings.read | Read lists, calendars, free slots and bookings |
bookings.write | Make and cancel bookings |
lists.write | Change lists and calendars |
orders.read | Read products and orders |
orders.write | Place orders and mark them paid, shipped, delivered or cancelled |
products.write | Add and change products and stock |
Basics
- Base URL
https://api.i-connect.world/public/v1. JSON in and out. Phone numbers in international format, e.g.+919876543210. - Authenticate every call with a key from API keys:
Authorization: Bearer ic_live_…. Keys work only here (not on the dashboard API), only for their scopes and, if you set one, only from their IP list. - Limits: 20 requests a second per key, bursts of 50. Over that you get
429withRetry-After: 1. - Errors are
application/problem+jsonwith a stablecodeto branch on:
{ "type": "https://i-connect.world/problems/template-not-approved", "title": "The template isn't approved (PENDING)",
"status": 409, "code": "TEMPLATE_NOT_APPROVED", "requestId": "…" } Idempotency. Send Idempotency-Key: <your unique id> (required on POST /messages).
Repeating a key with the same body returns the first response with Idempotent-Replayed: true; with a
different body you get 422; while the first is still running, 409. Keys are kept 24 hours; a
request that failed frees its key.
Endpoints
POST /contacts
Scope: contacts.write (Create and update contacts (and record consent))
Creates or updates a contact by phone. Consent is recorded only with the exact wording the person agreed to.
curl https://api.i-connect.world/public/v1/contacts \
-H "Authorization: Bearer $IC_KEY" -H "Content-Type: application/json" \
-d '{"phone":"+919876543210","name":"Asha Rao","tags":["website"],
"attributes":{"city":"Pune"},
"consent":{"purpose":"MARKETING","text":"Yes, send me offers on WhatsApp","ip":"203.0.113.7"}}' Returns contactId, created, consentOutcome and any skippedFields.
GET /contacts/{phone}
Scope: contacts.read (Read contacts)
One contact, with tags, attributes and consent state.
POST /contacts/{phone}/tags
Scope: contacts.write (Create and update contacts (and record consent))
{ "tags": ["hot-lead", "2bhk"] } POST /messages
Scope: messages.send.template (Send template messages)
Queues an approved template to one person (answer 202). The dashboard's checks apply: consent and opt-outs, quiet hours and the frequency cap. Leave out channelId if you have one number.
curl https://api.i-connect.world/public/v1/messages \
-H "Authorization: Bearer $IC_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: order-10234-packed" \
-d '{"to":"+919876543210",
"template":{"name":"order_status_update","language":"en","variables":{"1":"Asha","2":"#10234"}}}' Returns { "outboxId", "status", "contactId" }.
GET /messages/{outboxId}
Scope: messages.read (Read message status)
Where the message is: status, a skipReason if it wasn't sent, WhatsApp's id and delivered / read times.
POST /sequences/{id}/enroll
Scope: sequences.enroll (Enrol contacts in sequences)
{ "phone": "+919876543210" } Returns outcome (enrolled, or why not) and enrollmentId.
POST /forms/{id}/submissions
Scope: forms.submit (Submit forms)
Submits a live form server-side, with the same validation and consent rules as the hosted page (answer 201).
{ "phone": "+919876543210", "values": { "name": "Asha Rao", "budget": "60-80L" }, "consent": true } Webhooks
- Add an endpoint in Settings → Webhooks: your server's HTTPS URL (HTTPS only; private and internal addresses are refused) and the events you want.
- Each endpoint has a signing secret, shown once. Rotate signing secret gives you a new one.
- Settings → Webhooks also keeps a delivery log for each endpoint.
| Event | Sent when |
|---|---|
message.received | A customer sends a message |
message.status | A message is delivered, read or fails |
contact.created | A contact is created |
contact.opted_out | A contact opts out |
form.submitted | A form is submitted |
flow.completed | A WhatsApp Flow is completed |
campaign.completed | A broadcast finishes |
conversation.assigned | A conversation is assigned |
sequence.step | A sequence reaches a “Call webhook” step |
booking.created | A slot is booked |
booking.cancelled | A booking is cancelled |
order.placed | An order is placed |
order.paid | An order is marked paid |
order.status_changed | An order is packed, shipped, delivered or cancelled |
Verifying webhook signatures
Each delivery is a POST with a JSON body { "id", "type", "createdAt", "orgId", "data" } and
these headers: X-iConnect-Event, X-iConnect-Event-Id (the same on retries: use it to ignore
duplicates), X-iConnect-Delivery and
X-iConnect-Signature: t=1760166000,v1=5f1c…e9 v1 is the hex HMAC-SHA256 of t + "." + raw body with the endpoint's signing secret. Check it on the raw bytes, before parsing:
// Node.js (Express: app.post(path, express.raw({ type: "application/json" }), handler))
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (!parts.t || !parts.v1 || age > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
Answer 2xx within 5 seconds and do the work afterwards. Anything else is retried after 1 minute, 5
minutes, 30 minutes, 2 hours and 6 hours; after 10 failures in a row the endpoint is marked failing and the owner is
told.
Lists, calendars and bookings
The lists and calendars your booking Flows read (see Bookings and calendars). Keep them in step with your own booking system through these calls: Flows read them live, show only free slots, and a slot is never booked past its places.
curl -X PUT https://api.i-connect.world/public/v1/lists/doctors \
-H "Authorization: Bearer $IC_KEY" -H "Content-Type: application/json" \
-d '{"name":"Doctors","items":[{"id":"dr_mehta","title":"Dr. Mehta (General)"},
{"id":"dr_rao","title":"Dr. Rao (ENT)"}]}' A calendar, for PUT /calendars/appointments:
{ "name": "Appointments", "timezone": "Asia/Kolkata", "daysAhead": 14, "minNoticeMinutes": 60,
"itemListKey": "doctors", "itemDays": { "dr_rao": ["MONDAY", "WEDNESDAY", "FRIDAY"] },
"closedDates": ["2026-11-12"], "bookedTag": "appt-booked", "cancelledTag": "appt-cancelled",
"contactFields": { "date": "appointment_date", "time": "appointment_slot", "item": "doctor" },
"slots": [ { "id": "morning", "title": "Morning (9–12)", "start": "09:00",
"days": ["MONDAY","TUESDAY","WEDNESDAY","THURSDAY","FRIDAY","SATURDAY"], "capacity": 6 } ] } PUT /lists/{key}
Scope: lists.write (Change lists and calendars)
Creates or replaces a list. Names are at most 30 characters (WhatsApp's limit); ids are yours, or made from the name. Turning an item off keeps it on old bookings but stops offering it.
PUT /lists/{key}/items/{itemId}
Scope: lists.write (Change lists and calendars)
Adds or changes one item: { "title": "Dr. Rao (ENT)", "active": false }. DELETE on the same path removes it.
PUT /calendars/{key}
Scope: lists.write (Change lists and calendars)
Creates or replaces a calendar: time slots with their places, days ahead, minimum notice, closed dates, places per item of a list (with the days each item can be booked), the tags added on booking and cancellation, and the contact fields every booking fills.
GET /calendars/{key}/availability?from=&to=&item=
Scope: bookings.read (Read lists, calendars, free slots and bookings)
Places per day and slot: capacity, booked and free. item is required when the calendar counts places per item. Up to 90 days per call.
POST /bookings
Scope: bookings.write (Make and cancel bookings)
Books a slot made outside WhatsApp (phone, walk-in, your own app) so Flows stop offering it. A full or closed slot answers 409 SLOT_UNAVAILABLE. Send an Idempotency-Key to retry safely. A new phone number becomes a contact.
GET /bookings?calendar=&from=&to=&status=&phone=
Scope: bookings.read (Read lists, calendars, free slots and bookings)
Bookings in date order (up to 500), with the item and slot names as booked, where each was made (FLOW, API, DASHBOARD) and your externalRef. GET /bookings/{id} returns one.
POST /bookings/{id}/cancel
Scope: bookings.write (Make and cancel bookings)
Frees the place: { "reason": "Patient called" }. Cancelling twice is harmless.
A booking, for POST /bookings:
{ "calendar": "appointments", "item": "dr_mehta", "date": "2026-11-03", "slot": "morning",
"phone": "+919876543210", "name": "Asha Rao", "externalRef": "PMS-10234", "note": "First visit" }
The webhook events booking.created and booking.cancelled carry the contact and a
booking object: calendar, item, date, slot, start time, source, name, phone and your
externalRef.
Dynamic Flows: data requests
A WhatsApp Flow screen set to Load from your system asks the flow's webhook endpoint for its data while
the person fills the flow in, for example the free slots for the service they chose. The request is signed like every
webhook (X-iConnect-Event: flow.data_request) and is not retried: the person is waiting.
{ "type": "flow.data_request", "requestId": "…", "test": false,
"flow": { "id": "…", "name": "Book a visit" }, "screen": "SLOT",
"answers": { "service": "repair" },
"contact": { "id": "…", "phone": "+919876543210", "name": "Asha Rao" },
"flowToken": "ft_…" }
Answer 2xx within 5 seconds with every key the screen uses: options as a list of id and
title (titles up to 30 characters; up to 200 options for a dropdown, 20 for radio buttons and checkboxes),
texts as strings. Other keys are ignored. Or answer with an error to show that message on the previous
screen instead.
{ "data": { "intro": "Times for tomorrow",
"slots": [ { "id": "t10", "title": "10:00" }, { "id": "t11", "title": "11:00" } ] } }
{ "error": "No slots left today" }
If your server is slow, fails or answers with the wrong shape, the person sees “Sorry, we couldn't load this right
now” and the flow builder shows why. Test lookup in the builder sends a request with
"test": true. The first screen of a flow can't load data: ask something first.
Still stuck? Write to support@i-connect.world with your organisation's name and what you see on screen.