JavaScript sample

A minimal Node.js client for the BottleCRM API. Uses native fetch (Node 18+), no external dependencies.

A tiny client

const BASE = process.env.BOTTLECRM_URL.replace(/\/$/, "");
const TOKEN = process.env.BOTTLECRM_TOKEN;

async function api(path, init = {}) {
  const res = await fetch(`${BASE}${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${TOKEN}`,
      "Content-Type": "application/json",
      ...(init.headers ?? {}),
    },
  });
  if (!res.ok) {
    const body = await res.text();
    throw new Error(`${res.status} ${res.statusText}: ${body}`);
  }
  return res.status === 204 ? null : res.json();
}

export async function* listLeads(filters = {}) {
  const qs = new URLSearchParams(filters).toString();
  let url = `/api/leads/${qs ? `?${qs}` : ""}`;
  while (url) {
    const page = await api(url);
    yield* page.results;
    url = page.next ? new URL(page.next).pathname + new URL(page.next).search : null;
  }
}

export const createLead = (fields) =>
  api("/api/leads/", { method: "POST", body: JSON.stringify(fields) });

export const updateLead = (id, fields) =>
  api(`/api/leads/${id}/`, { method: "PATCH", body: JSON.stringify(fields) });

Example: handle an inbound webhook in Express

import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.BOTTLECRM_WEBHOOK_SECRET;

// Capture the raw body so we can verify the signature.
app.use(express.json({
  verify: (req, _res, buf) => { req.rawBody = buf; },
}));

app.post("/webhooks/bottlecrm", (req, res) => {
  const sig = req.header("X-BottleCRM-Signature") ?? "";
  const expected = "sha256=" + crypto
    .createHmac("sha256", SECRET)
    .update(req.rawBody)
    .digest("hex");

  if (
    sig.length !== expected.length ||
    !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))
  ) {
    return res.status(401).send("Invalid signature");
  }

  const event = req.body;
  switch (event.type) {
    case "lead.converted":
      // queue follow-up work, don't block the webhook
      enqueueOnboardingEmail(event.data.contact.email);
      break;
    case "invoice.paid":
      enqueueReceiptDelivery(event.data.invoice.id);
      break;
  }

  res.status(204).end();
});

app.listen(3000);

Example: build a custom dashboard widget

import { listLeads } from "./bottlecrm-client.js";

async function thisWeekByStatus() {
  const buckets = { new: 0, contacted: 0, qualified: 0, lost: 0, converted: 0 };
  const since = new Date(Date.now() - 7 * 24 * 3600 * 1000).toISOString();

  for await (const lead of listLeads({ created_at__gte: since })) {
    buckets[lead.status] = (buckets[lead.status] ?? 0) + 1;
  }
  return buckets;
}

console.log(await thisWeekByStatus());

Browser usage

In the browser, never embed a JWT in client code. Instead proxy through your own server, or use a session cookie that your origin sets after BottleCRM's OAuth flow.