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
- whatsapp-twilio.js (
npm i express twilio) - whatsapp-meta.js (
npm i express)
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
- WhatsApp only allows free-form replies within 24 hours of the customer's last message, which covers replying to them. Messages you start yourself need an approved template.
- The examples keep the offered times in memory; use your database when you run more than one server.
- If the business connected Cal.com and its event type requires an email, a phone-only booking returns a 400 that says so. Ask the customer for their email and send it as
email, or have the business switch Cal.com's booking confirmation to phone. - Bookings return an
id; cancel withDELETE /api/v1/bookings/{id}. Full reference: API docs.