Connectby Nevorai

Nevorai Connect: Send API

Lets a client's own website or app send one approved template from the client's own number, e.g. a love-report PDF right after payment (Sadhna Astro), a class link after sign-up (Landing Page OS), a receipt.

Send

POST https://connect.nevorai.com/api/v1/send
Authorization: Bearer nck_…
Content-Type: application/json

{
  "to": "919876543210",                  // country code + number; a bare 10-digit number is taken as Indian
  "template": "love_report",             // approved template name
  "language": "en",                      // only needed if the name exists in several languages
  "variables": ["Ravi", "Priya"],        // {{1}}, {{2}} … in the template text, in order
  "header": { "document": "https://…/report.pdf", "filename": "Love report.pdf" },
                                         // or {"image": "https://…"} / {"video": "https://…"} / {"text": "…"}
                                         // only if the template has a header; links must be public https
                                         // leave it out to send the photo/video/PDF saved with the template in Connect
  "buttons": ["ORDER123"],               // the {{1}} at the end of each link button, in order
  "ref": "order_123"                     // your own id; the same ref never sends twice
}

Answer (200):

{ "id": "7d1c…", "ref": "order_123", "to": "919876543210", "template": "love_report",
  "status": "sent", "error": null, "message_id": "wamid.HBg…", "created_at": "2026-09-27T…" }
HTTPcodemeaning
400bad_requestthe body is malformed (the message says which field)
400header_required / header_not_allowedthe header doesn't match the template
400wrong_variables / wrong_buttonsthe count doesn't match the template's blanks
400language_requiredthe template exists in several languages
401unauthorizedthe key is missing, wrong or revoked
403pausedthe business is paused
404template_not_foundthere is no approved template with that name on the number
409opted_outthe customer replied STOP
409number_not_connectedthere is no connected number
429rate_limitedmore than 120 requests a minute on one key
502meta_errorMeta refused the send (the message has Meta's reason). The send row is failed, and the same ref may be retried

Status

GET /api/v1/messages?id=<id> or ?ref=<your ref> (same header) returns the same shape with the latest status: sending → sent → delivered → read, or failed (with error).

Callback (optional): give the key a status address when making it. On every change Connect POSTs:

{ "event": "message.status", "id": "7d1c…", "ref": "order_123", "to": "919876543210",
  "template": "love_report", "status": "delivered", "error": null, "message_id": "wamid…", "at": "…" }

with the header X-Connect-Signature: sha256=<HMAC-SHA256 of the raw body, keyed by the status secret>. Check the signature, then answer 2xx within 5 s. There is one try per status; if you miss one, ask GET /api/v1/messages.

Follow-ups

A follow-up is a set of approved templates that send themselves, each at its own time: right after joining, N minutes later, on day N at a clock time, or N minutes before/after the person's own event time (a webinar, class, appointment, due date). The owner builds it in Connect → Follow-ups; the Website tab there shows a ready-to-copy request. Its API name (key, e.g. webinar-reminders) is what the website sends.

List what the business has, and what each needs per person:

GET https://connect.nevorai.com/api/v1/followups
Authorization: Bearer nck_…
{ "followups": [ { "followup": "webinar-reminders", "name": "Webinar reminders", "enabled": true, "messages": 4,
    "needs_event_at": true,
    "fields": [ { "key": "topic", "label": "Webinar name", "required": false, "default": "Meta Ads for beginners" },
                { "key": "link", "label": "Joining link", "required": true, "default": null } ] } ] }

Start one person (a sign-up, a payment, a new lead):

POST https://connect.nevorai.com/api/v1/followups/start

{
  "followup": "webinar-reminders",       // the API name
  "to": "919876543210",                  // country code + number; a bare 10-digit number is taken as Indian
  "name": "Ravi Kumar",                  // optional; fills "their name" blanks and names a new contact
  "event_at": "2026-10-05T19:00",        // India time, or with a zone (…T19:00:00+05:30). Needed when a message is timed around the event
  "fields": { "link": "https://…" },     // per-person details; a detail with a default may be left out
  "ref": "signup-1042"                   // your own id; the same ref again returns the first one
}

Answer (200): { "id", "followup", "to", "ref", "status": "active", "duplicate": false, "messages": [ { "template", "at", "status": "pending" | "sent" | "skipped", "note" } ] }. A message whose time has already passed (e.g. "1 hour before" for someone who joins 20 minutes before) comes back skipped with a note, and is never sent late.

Stop one person, e.g. once they have paid:

POST https://connect.nevorai.com/api/v1/followups/stop
{ "to": "919876543210", "followup": "fee-due" }   // leave out "followup" to stop all of theirs

Answer: { "to", "followup", "stopped": 1 } (how many places were ended; 0 is not an error).

HTTPcodemeaning
400bad_request / bad_event_atthe body is malformed (the message says which field)
400event_at_requireda message is timed around the event: send event_at
400fields_requireda detail has no default: send it in fields (the message names them)
404followup_not_foundno follow-up with that API name
409followup_off / followup_emptyit is switched off, or has no messages yet
409opted_outthe customer replied STOP

The key errors (401, 403 paused, 429) are the same as for sends. Every follow-up message appears in the client's inbox like any other send.

Contacts

Adds or updates one customer, e.g. from a sign-up form, so the business can tag them and send targeted broadcasts (Connect → Broadcasts → New → "Only people with these tags").

POST https://connect.nevorai.com/api/v1/contacts
Authorization: Bearer nck_…
Content-Type: application/json

{
  "phone": "919876543210",          // country code + number; a bare 10-digit number is taken as Indian
  "name": "Ravi Kumar",             // optional; a blank never wipes a saved name
  "tags": ["webinar-oct", "lead"],  // optional; ADDED to their tags (up to 20 per person, 30 letters each)
  "opted_in": true                  // optional; true only if they ticked "send me updates on WhatsApp"
}
{ "phone": "919876543210", "name": "Ravi Kumar", "tags": ["webinar-oct", "lead"], "opted_in": true, "created": true }

Stages

Tells Connect where a customer is on the way to buying: replied, qualified, booked, bought, not_interested, no_show. Connect keeps the stage on the chat, shows it in the inbox, and, for a customer who came from a Meta lead form, sends it back to the app that sent the lead (AdsPro) so Meta learns who really buys. Use it from a payment link, a CRM or Kaizen.

POST https://connect.nevorai.com/api/v1/contacts/stage
Authorization: Bearer nck_…
Content-Type: application/json

{
  "phone": "919876543210",
  "stage": "bought",
  "amount": 4999,                    // rupees; REQUIRED for "bought", ignored otherwise
  "reason": "Paid by UPI",           // optional, shown in the chat
  "confidence": 95                   // optional, 0-100
}
{ "stage": "bought", "changed": true }

Where outcomes go back (Connect button apps)

An app connected with the Connect button registers where lead outcomes should be sent, once, right after the code swap:

POST https://connect.nevorai.com/api/v1/connections/outcome-hook
Authorization: Bearer nck_…            // the connection's own key
{ "url": "https://adsproindia.com/api/public/webhooks/status", "key": "<a long random key Connect will send back>" }

The url must be https and on the app's own registered domain (its return address's origin). Connect then posts each outcome of a lead the app sent (lead_reference = the id it sent):

POST <url>
Authorization: Bearer <key>
Idempotency-Key: <outcome id>
{ "lead_reference": "…", "status": "qualified", "event_id": "<outcome id>", "source": "whatsapp" }
{ "lead_reference": "…", "status": "purchased", "amount": 4999, "currency": "INR", "event_id": "…", "source": "whatsapp" }

status is the app's word (AdsPro: contacted, qualified, not_qualified, booked, no_show, purchased). 2xx = taken; 400/404/422 = not accepted, not retried; 401/403 = the key or connection ended, not retried; anything else is retried (1 min, 5 min, 30 min, 2 h, 6 h, 24 h) and then dropped. Retries send the same event_id, so the receiver should treat a repeat as already done.

Plugins (no code)

For apps that can only send a webhook (Facebook lead ads through Pabbly Connect / Zapier / Make, Google Sheets or Forms, Razorpay, IndiaMART, Elementor, Tally, Typeform…), the owner adds a plugin instead of using a key: Connect → Settings → Plugins → pick the app. Each plugin has its own private link:

POST https://connect.nevorai.com/api/hooks/nch_…

Shared helper for Nevorai-built sites

Copy into the site as lib/connect.ts. Env: CONNECT_API_KEY, and CONNECT_STATUS_SECRET if you use callbacks. Server-only.

import { createHmac, timingSafeEqual } from "node:crypto";

const BASE = process.env.CONNECT_API_URL ?? "https://connect.nevorai.com";

export type ConnectSend = {
  to: string;
  template: string;
  language?: string;
  variables?: string[];
  header?: { document: string; filename?: string } | { image: string } | { video: string } | { text: string };
  buttons?: string[];
  ref?: string;
};

export type ConnectResult =
  | { ok: true; id: string; status: string; duplicate?: boolean }
  | { ok: false; code: string; message: string; retry: boolean };

/** Sends one approved template from the client's number. Never throws. */
export async function sendWhatsApp(msg: ConnectSend): Promise<ConnectResult> {
  const key = process.env.CONNECT_API_KEY;
  if (!key) return { ok: false, code: "not_configured", message: "CONNECT_API_KEY is not set", retry: false };
  try {
    const res = await fetch(`${BASE}/api/v1/send`, {
      method: "POST",
      headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
      body: JSON.stringify(msg),
      signal: AbortSignal.timeout(15_000),
    });
    const json = await res.json().catch(() => ({}));
    if (res.ok) return { ok: true, id: json.id, status: json.status, duplicate: json.duplicate };
    return {
      ok: false,
      code: json.error?.code ?? `http_${res.status}`,
      message: json.error?.message ?? `Connect answered ${res.status}`,
      retry: res.status === 429 || res.status >= 500,
    };
  } catch (e) {
    return { ok: false, code: "network", message: e instanceof Error ? e.message : "Connect unreachable", retry: true };
  }
}

/** For the status-callback route: true only if Connect signed this body. */
export function isFromConnect(rawBody: string, signature: string | null): boolean {
  const secret = process.env.CONNECT_STATUS_SECRET;
  if (!secret || !signature) return false;
  const want = Buffer.from(`sha256=${createHmac("sha256", secret).update(rawBody).digest("hex")}`);
  const got = Buffer.from(signature);
  return want.length === got.length && timingSafeEqual(want, got);
}

Always pass ref (the order or payment id). Then a retried payment webhook, or a double click, never messages the customer twice.