API docs · Guide

Use the API with your WhatsApp number

Already have WhatsApp through Twilio or Meta's Cloud API? Forward each customer message to CallChatSyn and send back the reply. The business's FAQs answer questions, and customers can book from a numbered list of open times. You keep your own number and provider; CallChatSyn needs no WhatsApp setup of its own.

1. Get a key

Try everything first with the public demo key ccs_demo_public (a demo business; bookings are dry runs). For a real business, create a key in Dashboard → Developers; that also claims a free founder spot (100 API calls a day). One key acts for one business.

2. The reply logic (same for both providers)

Questions go to /answer. When the customer wants to book, list /slots as a numbered menu; when they reply with a number, /bookings books it with their WhatsApp number.

const CCS = "https://callchatsyn.com/api/v1";
const headers = { Authorization: `Bearer ${process.env.CCS_KEY}`, "Content-Type": "application/json" };
const offered = new Map(); // phone -> open times we listed (use your DB in production)

async function reply(phone, text) {
  // Customer picked a number from the list we sent: book it.
  const pick = offered.get(phone)?.[Number(text.trim()) - 1];
  if (pick) {
    offered.delete(phone);
    const r = await fetch(`${CCS}/bookings`, {
      method: "POST",
      headers,
      body: JSON.stringify({ start: pick.start, service: pick.service, phone }),
    }).then((res) => res.json());
    if (r.booked) return `You're booked for ${r.label}.`;
    if (r.demo) return `(Demo) ${pick.label} is open, but demo keys don't book.`;
    return r.error?.message ?? "Sorry, that didn't work. Please try again.";
  }

  // Anything else: let CallChatSyn answer from the business's FAQs.
  const a = await fetch(`${CCS}/answer`, { method: "POST", headers, body: JSON.stringify({ message: text }) }).then((res) => res.json());
  if (a.intent !== "appointment") return a.reply ?? "Sorry, something went wrong.";

  // Booking request: list the next open times as a numbered menu.
  const s = await fetch(`${CCS}/slots`, { headers }).then((res) => res.json());
  if (!s.slots?.length) return "Sorry, nothing is open this week.";
  const service = s.services?.[0] ?? "Appointment";
  offered.set(phone, s.slots.map((slot) => ({ ...slot, service })));
  return `${a.reply}\n` + s.slots.map((slot, i) => `${i + 1}) ${slot.label}`).join("\n") + "\nReply with a number.";
}

3a. Twilio

// WhatsApp (via Twilio) -> CallChatSyn: answers FAQs, lists times, books.
// npm i express twilio   ·   env: CCS_KEY, TWILIO_AUTH_TOKEN
const express = require("express");
const twilio = require("twilio");

// ...reply() from above...

const app = express();
app.set("trust proxy", true); // behind a proxy/load balancer, check Twilio's signature against the public https URL
app.use(express.urlencoded({ extended: false }));
// twilio.webhook() rejects requests that aren't signed by Twilio.
app.post("/whatsapp", twilio.webhook({ validate: process.env.SKIP_TWILIO_SIGNATURE !== "1" }), async (req, res) => {
  const phone = String(req.body.From).replace(/^whatsapp:/, ""); // "+6591234567"
  const twiml = new twilio.twiml.MessagingResponse();
  twiml.message(await reply(phone, String(req.body.Body ?? "")));
  res.type("text/xml").send(twiml.toString());
});
app.listen(3000);

In Twilio, set your WhatsApp sender's (or the WhatsApp Sandbox's) When a message comes in webhook to https://your-server/whatsapp (HTTP POST). twilio.webhook() rejects requests that aren't signed by Twilio; set SKIP_TWILIO_SIGNATURE=1 only for local testing. Keep app.set("trust proxy", true) if your server runs behind a proxy or load balancer (most hosts): Twilio signs the public https URL, and without it every real message is rejected with 403.

3b. Meta WhatsApp Cloud API

// WhatsApp Cloud API (Meta) -> CallChatSyn. Same reply() as the Twilio example.
// npm i express   ·   env: CCS_KEY, WA_TOKEN, WA_APP_SECRET, WA_VERIFY_TOKEN
const express = require("express");
const crypto = require("node:crypto");
const GRAPH = process.env.GRAPH_BASE ?? "https://graph.facebook.com/v24.0"; // use Meta's current Graph API version

// ...reply() from above...

const app = express();
app.use(express.json({ verify: (req, _res, buf) => (req.rawBody = buf) }));

// Meta's one-time webhook check when you save the callback URL.
app.get("/whatsapp", (req, res) => {
  const ok = req.query["hub.mode"] === "subscribe" && req.query["hub.verify_token"] === process.env.WA_VERIFY_TOKEN;
  res.status(ok ? 200 : 403).send(ok ? req.query["hub.challenge"] : "");
});

app.post("/whatsapp", async (req, res) => {
  // Only accept requests signed with your app secret.
  const expected = "sha256=" + crypto.createHmac("sha256", process.env.WA_APP_SECRET).update(req.rawBody).digest("hex");
  const given = String(req.get("x-hub-signature-256") ?? "");
  if (given.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected))) return res.sendStatus(401);
  res.sendStatus(200); // answer Meta right away, reply below

  const value = req.body.entry?.[0]?.changes?.[0]?.value;
  const msg = value?.messages?.[0];
  if (msg?.type !== "text") return;
  const text = await reply(`+${msg.from}`, msg.text.body);
  await fetch(`${GRAPH}/${value.metadata.phone_number_id}/messages`, {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.WA_TOKEN}`, "Content-Type": "application/json" },
    body: JSON.stringify({ messaging_product: "whatsapp", to: msg.from, type: "text", text: { body: text } }),
  });
});
app.listen(3000);

In your Meta app → WhatsApp → Configuration, set the callback URL to https://your-server/whatsapp, the verify token to your WA_VERIFY_TOKEN, and subscribe to the messages field. Requests are checked against your app secret (X-Hub-Signature-256).

Download the full examples

Both were run against the live API with the demo key before publishing. The Twilio example was also tested end to end through the Twilio WhatsApp Sandbox from a real phone.

Good to know