0800 310 1010
← All articles
CRM & integrations · 9 min read

Connect Salesforce to NumberGroup: log calls, populate Activity History, attribute to Opportunities

Push NumberGroup call events into Salesforce — Task records on Contact / Lead / Opportunity, the Activity History feed, Lightning Voice considerations, and OAuth-flow receiver implementation.

Salesforce is the heavy CRM in this list. The integration is the same shape as the others — a webhook receiver that calls an API — but Salesforce’s auth model is OAuth-flavoured rather than a single token, and the object model is slightly different. This post walks both.

Foundational reading: NumberGroup call webhooks explained.

What you’ll have at the end

  • Every inbound call creates a Task record with
    Type = Call
    and
    Status = Completed
    .
  • The Task is associated with the matching Contact or Lead (by phone number).
  • If the Contact has open Opportunities, the Task is also attached to the most relevant one — so the Activity History on the Opportunity record reflects call activity.
  • Reps see the call in their Salesforce activity feed without leaving Salesforce.

Why not just use Salesforce Lightning Voice / Service Cloud Voice?

Honest answer: those products are excellent if you’re paying for them. They cost roughly £30–40/user/month on top of your Salesforce licence. For most SMEs, a webhook-driven activity log covers 90% of what they actually want from a CTI integration. The remaining 10% (screen-pop, click-to-dial inside Salesforce) is available via the OpenCTI framework — that’s a separate, deeper integration we’ll cover in a follow-up post if there’s demand. Ask us if you need it.

Step 1 — Create a Salesforce Connected App

  • Salesforce Setup → App Manager → New Connected App.
  • Name: “NumberGroup call logger”. Contact email: yours.
  • Tick Enable OAuth Settings. Callback URL: your worker URL (only used for initial token grant).
  • Scopes:
    api
    ,
    refresh_token, offline_access
    .
  • Tick Enable Client Credentials Flow (modern, no user redirect needed). Save.
  • Edit the app → Manage Consumer Details → copy Consumer Key and Consumer Secret.
  • Under Client Credentials Flow, assign an Integration User. That’s the user whose permissions the API calls run as. Give it Task / Contact / Lead / Opportunity read+write.

Step 2 — Token exchange

Your worker exchanges the consumer key/secret for an access token on demand. Cache the token (Workers KV, Redis) for ~50 minutes — tokens last an hour.

async function getToken(env) {
  const cached = await env.KV.get("sf_token");
  if (cached) return cached;
  const body = new URLSearchParams({
    grant_type: "client_credentials",
    client_id: env.SF_CLIENT_ID,
    client_secret: env.SF_CLIENT_SECRET,
  });
  const r = await fetch(env.SF_LOGIN_URL + "/services/oauth2/token", {
    method: "POST",
    headers: { "content-type": "application/x-www-form-urlencoded" },
    body,
  });
  const j = await r.json();
  await env.KV.put("sf_token", j.access_token, { expirationTtl: 3000 });
  // j.instance_url tells you which pod you're on, e.g. https://yourorg.my.salesforce.com
  await env.KV.put("sf_instance", j.instance_url, { expirationTtl: 3000 });
  return j.access_token;
}

SF_LOGIN_URL
is
https://login.salesforce.com
for production or
https://test.salesforce.com
for sandbox.

Step 3 — Find or create the WhoId / WhatId

Salesforce activities have two reference fields:

  • WhoId — the person. Contact or Lead.
  • WhatId — the related object. Account, Opportunity, Case, custom object.

For inbound calls the lookup order is: search Contacts by phone, then Leads by phone, then create a Lead if neither matches.

async function findWhoId(env, token, instance, phone) {
  const soql = encodeURIComponent(
    `FIND {${phone}} IN PHONE FIELDS RETURNING Contact(Id, AccountId), Lead(Id)`
  );
  const r = await fetch(`${instance}/services/data/v60.0/search/?q=${soql}`, {
    headers: { authorization: "Bearer " + token },
  });
  const j = await r.json();
  const records = j.searchRecords || [];
  const contact = records.find(x => x.attributes.type === "Contact");
  if (contact) return { whoId: contact.Id, accountId: contact.AccountId };
  const lead = records.find(x => x.attributes.type === "Lead");
  if (lead) return { whoId: lead.Id, accountId: null };
  // Create a Lead
  const cr = await fetch(`${instance}/services/data/v60.0/sobjects/Lead`, {
    method: "POST",
    headers: { authorization: "Bearer " + token, "content-type": "application/json" },
    body: JSON.stringify({
      LastName: "Unknown caller",
      Phone: phone,
      Company: "Unknown",
      LeadSource: "Inbound call",
    }),
  });
  const cj = await cr.json();
  return { whoId: cj.id, accountId: null };
}

Step 4 — Pick the WhatId (deal attribution)

If the caller is a Contact with an Account, pull open Opportunities on that Account, sort by close date ascending, and use the closest one as the WhatId. This isn’t perfect attribution but it’s the right default — the rep can re-parent the Task if needed.

Step 5 — Create the Task

async function createCallTask(env, token, instance, t) {
  return fetch(`${instance}/services/data/v60.0/sobjects/Task`, {
    method: "POST",
    headers: { authorization: "Bearer " + token, "content-type": "application/json" },
    body: JSON.stringify({
      Subject: `Inbound call to ${t.dialled}`,
      Type: "Call",
      Status: "Completed",
      Priority: "Normal",
      CallType: "Inbound",
      CallDurationInSeconds: t.duration,
      CallObject: t.callId,           // NumberGroup call ID — useful for dedupe
      ActivityDate: new Date().toISOString().slice(0, 10),
      WhoId: t.whoId,                 // Contact or Lead
      WhatId: t.whatId || null,       // Opportunity or Account
      Description: t.recordingUrl
        ? `Caller: ${t.caller}\nRecording: ${t.recordingUrl}`
        : `Caller: ${t.caller}`,
    }),
  });
}

Step 6 — Wire it into NumberGroup

  • Portal → Web Hook Manager → new webhook.
  • Name: Salesforce — log call as Task.
  • URL: your worker. Attach as End of call webhook on relevant DDIs.

Salesforce-specific gotchas

  • Sandbox first. Always wire this up in a sandbox before production. Test record creation, retention policies, and any flow triggers.
  • Validation rules. Many Salesforce orgs have validation rules that reject Tasks without certain custom fields populated. If your worker gets
    INVALID_FIELD_FOR_INSERT_UPDATE
    , your admin has rules you need to satisfy.
  • Phone-field SOSL search normalises numbers but not perfectly. If you’re missing matches, also try a SOQL search on
    MobilePhone
    ,
    HomePhone
    ,
    OtherPhone
    , and the Account’s
    Phone
    .
  • API limits. Per-day API call limits exist (15k for most Sales Cloud editions). If you do high call volume, batch via the Composite API.
  • Field-level security. The Integration User needs FLS on every field you write. Easy to miss on custom call fields.

Need help wiring this up?

Salesforce orgs are bespoke — every org has its own custom objects, flows, and validation rules. Ring us on 0800 310 1010 and we’ll either walk you through it or point you at a Salesforce admin who’s done it for other Members. We don’t charge for that conversation.

Move your phones to us. We'll save you money.

Memorable numbers, unlimited UK numbers, every smart feature and a UK team — all included with Membership at £149/mo. No contract.

See what Membership includes →Browse phone numbers →
Related reading