JavaScript sample¶
What this does¶
The same flow as the Python sample, in JavaScript: obtain an access token,
list the org's leads, and create one, using only the built-in fetch, no npm dependencies. It's
written for Node (tested for compatibility with Node 24), but the fetch calls themselves would
run unmodified in any modern browser's console against a CORS-enabled backend.
As with the Python sample, "obtains a token" means reading a personal access token (bcrm_pat_…)
from the environment rather than driving an interactive sign-in. See
Tokens and API keys for what a PAT
authenticates as, and the Python sample for how to mint one
locally.
The script¶
#!/usr/bin/env node
// List an org's leads and create one, using a BottleCRM personal access token.
//
// Usage:
// BCRM_BASE_URL=http://localhost:8000 BCRM_TOKEN=bcrm_pat_... node leads-sample.mjs
//
// Requires Node 18+ for the built-in `fetch`. The flag history, precisely:
// --experimental-fetch was a Node 17.5 flag; Node 18 made fetch available
// with no flag at all (still labeled experimental, with a startup warning);
// Node 21 dropped that warning and made fetch stable. Tested for Node 24.
// No npm dependencies.
//
// See docs/api/tokens-and-api-keys.md for how to mint BCRM_TOKEN, and
// docs/api/leads.md for the shape of the /api/leads/ responses this reads.
const BASE_URL = (process.env.BCRM_BASE_URL || "http://localhost:8000").replace(/\/$/, "");
const TOKEN = process.env.BCRM_TOKEN;
if (!TOKEN) {
console.error(
"Set BCRM_TOKEN to a bcrm_pat_... personal access token " +
"(see docs/api/tokens-and-api-keys.md)."
);
process.exit(1);
}
const headers = {
Authorization: `Bearer ${TOKEN}`,
Accept: "application/json",
"Content-Type": "application/json",
};
// GET /api/leads/, the response splits leads into open/closed sections
// rather than one flat list; see docs/api/leads.md#list-leads.
async function listLeads() {
const res = await fetch(`${BASE_URL}/api/leads/`, { headers });
const data = await res.json();
if (!res.ok) {
throw new Error(`List failed (${res.status}): ${JSON.stringify(data)}`);
}
const openSection = data.open_leads;
const leads = openSection.open_leads;
console.log(
`${leads.length} open lead(s) on this page (of ${openSection.leads_count} total open):`
);
for (const lead of leads) {
const name = `${lead.first_name ?? ""} ${lead.last_name ?? ""}`.trim();
console.log(
` - ${name || "(no name)"} <${lead.email ?? "no email"}> [${lead.status}]`
);
}
return data;
}
// POST /api/leads/. A 200 with {"error": false, ...} is success; the
// response does not echo the created record, so we look it up afterward.
async function createLead() {
const email = `sample.lead+${Date.now()}@example.com`;
const res = await fetch(`${BASE_URL}/api/leads/`, {
method: "POST",
headers,
body: JSON.stringify({
first_name: "Sample",
last_name: "Lead",
email,
status: "assigned",
source: "other",
}),
});
const body = await res.json();
if (!res.ok || body.error) {
throw new Error(`Create failed (${res.status}): ${JSON.stringify(body)}`);
}
console.log(`Create response: ${JSON.stringify(body)}`);
return email;
}
// GET /api/leads/?email=...; `email` is a case-insensitive contains filter
// (docs/api/conventions.md#filtering-and-search), fine here since the address
// we just created is unique in this org.
async function findByEmail(email) {
const url = new URL(`${BASE_URL}/api/leads/`);
url.searchParams.set("email", email);
const res = await fetch(url, { headers });
const data = await res.json();
if (!res.ok) {
throw new Error(`Lookup failed (${res.status}): ${JSON.stringify(data)}`);
}
const matches = data.open_leads.open_leads;
if (matches.length) {
console.log(`Found the new lead: id=${matches[0].id}`);
} else {
console.log("New lead not on this page (increase limit/offset to find it).");
}
}
async function main() {
console.log("Listing existing leads...");
await listLeads();
console.log("\nCreating a lead...");
const email = await createLead();
console.log("\nConfirming it was created...");
await findByEmail(email);
}
main().catch((err) => {
console.error(err.message);
process.exit(1);
});
Running it¶
Save the script as leads-sample.mjs. It uses no import/export and no top-level await (the
await calls are all inside async function main(), invoked with .catch() rather than awaited
at the top level), so it runs the same way whether Node treats the file as CommonJS or an ES
module, the .mjs extension just removes any ambiguity from a package.json in the same
directory. You need a reachable
BottleCRM backend and a personal access token, minted as described in the
Python sample:
A note on verification: like the Python sample, this script was checked by static means,
every URL and field against Leads and Conventions, and
the whole file with node --check, rather than executed against a live database, to avoid
writing a row into a shared instance this documentation didn't provision. The example output is
illustrative, not a captured run, and is identical in shape to the
Python sample's since both call the same endpoints:
Listing existing leads...
3 open lead(s) on this page (of 3 total open):
- Jordan Blake <jordan.blake@example.com> [assigned]
- Priya Shah <priya.shah@example.com> [in process]
- (no name) <no email> [assigned]
Creating a lead...
Create response: {"error":false,"message":"Lead Created Successfully"}
Confirming it was created...
Found the new lead: id=3f2a1c9e-....
A validation failure comes back as a 400 and the script throws with the error body BottleCRM
returned, per Errors: