Is B Tools safe?
B Tools sends the logged-in WhatsApp user's name, phone number, email and country as a sales lead to two undisclosed third-party CRMs.
When you're signed in to WhatsApp, B Tools reads your name, phone number, email, country, and an alternate phone number, then posts them as a new sales lead to two separate CRM backends: TeleCRM and a CRM on Joyz.ai's platform. The same identity fields are also attached to every analytics event the extension sends to its vendor. One of the CRM endpoints is called using a static admin-level credential that is shipped in the extension's code and identical for every install. None of this data collection is mentioned in the extension's store listing, which describes only local WhatsApp message-template features.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
B Tools posts the logged-in WhatsApp user's identity to two undisclosed CRMs
Code analysis shows B Tools reads the logged-in WhatsApp account's name and phone number, plus an email, country and alternate phone from its own signup, then posts it as a sales lead to two undisclosed CRMs using hardcoded tokens.
You open the B Tools popup on web.whatsapp.com while logged in to WhatsApp and complete the extension's own account setup screen.
Entering an email or a second phone number during that setup populates the extension's own account record.
The extension reads your WhatsApp name and phone number plus your account email, country and alternate phone, then posts them as a new sales lead to two undisclosed CRM backends.
The same fields are also attached as user properties on every analytics event the extension sends to its vendor's Amplitude pipeline.
| Field | Value | Why it matters | |
|---|---|---|---|
Your name | Priya Sharma | Sent to two CRM backends as the lead's contact name, so anyone with CRM access can see who you are. | |
Your account phone number | +919876543210 | Sent as the lead record's phone number field to both CRM backends. | |
Your email address | priya.sharma@example.com | Sent to both CRMs as your contact email, and attached to every analytics event. | |
Your country | India | Sent as context on the lead record; also used to pick a default dial code. | |
Your logged-in WhatsApp number | +919812345678 | Your actual logged-in WhatsApp number is sent too, labelled as an alternate phone. |
Building and posting the sales lead to both CRMs
class UniversalUserPropertiesService {
// The DI-injected argument is accepted but never used
createLead() {
const self = this;
return (async function* () {
// NOTE: 'alternatePhone' below is actually the logged-in WhatsApp
// account's own phone number (extAuth.currentUser), not a second
// number the user entered.
const waAccountPhoneNumber = self.extAuth.currentUser.phoneNumber;
const waAccountName = self.extAuth.currentUser.name;
const signupEmail = self.extAuth.getUserDetails()?.email;
const signupCountryInfo = self.extAuth.getUserDetails()?.countryInfo;
const signupPhoneNumber = self.extAuth.getUserDetails()?.number;
return self.leadService.createLead({
name: waAccountName,
phoneNumber: signupPhoneNumber,
email: signupEmail,
countryInfo: signupCountryInfo,
alternatePhone: waAccountPhoneNumber,
});
})();
}
}const JOYZ_ENTERPRISE_ID = "67585858ed44a90bb3662ee1";
class JoyzLeadService {
async createLead(lead) {
const fields = [
{ fieldId: "contact_name", type: "TEXT", valueText: lead.name },
{
fieldId: "contact_phone",
type: "PHONE",
valuePhone: {
countryInfo: lead.countryInfo || {
name: "India",
code: "IN",
dial_code: "91",
},
phoneNumber: lead.phoneNumber,
},
},
{ fieldId: "contact_status", type: "DROPDOWN", valueDropdown: "New lead" },
{ fieldId: "contact_source", type: "DROPDOWN", valueDropdown: "WACA" },
];
if (lead.email) {
fields.push({ fieldId: "contact_email", type: "EMAIL", valueEmail: lead.email });
}
if (lead.alternatePhone) {
fields.push({ fieldId: "1_TEXT", type: "TEXT", valueText: lead.alternatePhone });
}
const entity = {
id: "",
eId: JOYZ_ENTERPRISE_ID,
entityType: "CONTACT",
fields,
parentEntityId: "",
parentEntityType: null,
activities: [],
createdBy: "",
lastSeenTimestamp: 0,
creationTimestamp: Date.now(),
modificationTimestamp: Date.now(),
};
try {
const res = await fetch(
`https://6vokqgdnu4.execute-api.ap-south-1.amazonaws.com/enterprise/${JOYZ_ENTERPRISE_ID}/entities`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
// Hardcoded admin-scoped credential, identical for every install
Authorization: "<redacted>",
},
body: JSON.stringify({ entity }),
},
);
if (!res.ok) {
const err = await res.json();
throw new Error(`HTTP ${res.status}: ${err.message || "Unknown error"}`);
}
return await res.json();
} catch (err) {
console.error("Error creating contact:", err);
}
}
}// Hardcoded TeleCRM lead-intake endpoint, same enterprise ID for every install
const TELECRM_ROOT_URL = "https://us-central1-waplugin-34798.cloudfunctions.net",
TELECRM_LEAD_ROUTE =
"https://next-api.telecrm.in/enterprise/60a51c74589084cee99c1903/autoupdatelead",class BackgroundApi {
static createLead(leadPayload) {
const self = this;
return (async function* () {
return self.httpPost(`${TELECRM_LEAD_ROUTE}`, leadPayload, {
// Hardcoded bearer token, identical for every install
Authorization: "Bearer <redacted>",
});
})();
}
}case Messages.API_CREATE_LEAD:
return (
BackgroundApi
.createLead(message.data.properties)
.then((result) => sendResponse(result))
.catch((err) => {
console.error("Error in API_CREATE_LEAD:", err);
sendResponse(null);
}),
true
);- next-api.telecrm.in
TeleCRM sales-CRM SaaS. Receives the lead record (name, phone, email, country) via a hardcoded bearer token, tagged with source WACA.
- 6vokqgdnu4.execute-api.ap-south-1.amazonaws.com
AWS API Gateway backing a second CRM on the Joyz.ai platform. Receives the same identity fields via a hardcoded admin-scoped token.
- us-central1-waplugin-34798.cloudfunctions.net
The vendor's own Amplitude analytics relay. Every logged event also carries your name, phone number, email and country as user properties.
Static analysis finding. This behaviour was identified by reading the shipped extension code and has not yet been reproduced in a live run. The trigger conditions and the exact data sent are read from the code, not from an observed capture.