{"id":1144,"date":"2026-05-21T09:00:00","date_gmt":"2026-05-21T14:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=1144"},"modified":"2026-03-07T03:34:56","modified_gmt":"2026-03-07T08:34:56","slug":"webhook-crm-integration","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/webhook-crm-integration\/","title":{"rendered":"CRM Integration with Deep Link Webhooks"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Your sales team wants to know which deep link a lead clicked before signing up. Your marketing team wants to see which campaigns drive qualified leads. Your product team wants to understand how referral links map to account creation. All of this data exists in your <a href=\"https:\/\/tolinku.com\/features\/webhooks\">Tolinku webhooks<\/a>, but it&#39;s not useful until it lands in your CRM.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This guide covers how to connect deep link webhook events to Salesforce, HubSpot, and Pipedrive. The pattern is the same for any CRM: receive the webhook, match the event to a contact or lead, and update the record with attribution data. If you&#39;re new to webhooks, start with the <a href=\"https:\/\/tolinku.com\/blog\/webhook-setup-guide\/\">webhook setup guide<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><img decoding=\"async\" src=\"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/platform-webhooks.png\" alt=\"Tolinku webhook configuration for event notifications\">\n<em>The webhooks page with create form, webhook list, and delivery log.<\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What Deep Link Data Belongs in Your CRM<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Not every webhook event needs to go into your CRM. A <code>link.clicked<\/code> event fires on every single deep link tap, which could be thousands per day. Your CRM doesn&#39;t need that volume, and your sales team won&#39;t look at raw click records.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The events that matter for CRM integration are:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong><code>install.tracked<\/code><\/strong>: A user installed your app through a deep link. This is a lead creation or lead enrichment event.<\/li>\n<li><strong><code>referral.created<\/code><\/strong>: A user signed up through a referral link. This creates a lead with referral attribution.<\/li>\n<li><strong><code>referral.completed<\/code><\/strong>: A referred user completed the target action (e.g., first purchase, subscription). This updates the lead&#39;s lifecycle stage.<\/li>\n<li><strong><code>deferred_link.claimed<\/code><\/strong>: A deferred deep link was resolved after install. This enriches the lead with the original campaign context.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Subscribe only to these events in your <a href=\"https:\/\/tolinku.com\/docs\/user-guide\/webhooks\/\">webhook configuration<\/a>. Filtering at the source keeps your pipeline lean.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Receiver Pattern<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Every CRM integration follows the same architecture:<\/p>\n\n\n\n<pre><code>Tolinku Webhook \u2192 Receiver (verify signature) \u2192 Match\/Create Contact \u2192 Update CRM\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Here&#39;s the shared receiver code. The CRM-specific logic plugs into the <code>processEvent<\/code> function.<\/p>\n\n\n\n<pre><code class=\"language-typescript\">import express from &#39;express&#39;;\nimport crypto from &#39;crypto&#39;;\n\nconst app = express();\napp.use(&#39;\/webhooks&#39;, express.raw({ type: &#39;application\/json&#39; }));\n\nconst WEBHOOK_SECRET = process.env.WEBHOOK_SECRET!; \/\/ whsec_...\n\napp.post(&#39;\/webhooks\/tolinku&#39;, async (req, res) =&gt; {\n  const signature = req.headers[&#39;x-webhook-signature&#39;] as string;\n  const expected = crypto\n    .createHmac(&#39;sha256&#39;, WEBHOOK_SECRET)\n    .update(req.body)\n    .digest(&#39;hex&#39;);\n\n  if (signature !== expected) {\n    return res.status(401).send(&#39;Invalid signature&#39;);\n  }\n\n  res.status(200).send(&#39;OK&#39;);\n\n  const event = JSON.parse(req.body.toString());\n  try {\n    await processEvent(event);\n  } catch (err) {\n    console.error(&#39;CRM update failed:&#39;, err);\n  }\n});\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">See the <a href=\"https:\/\/tolinku.com\/blog\/webhook-security-signing\/\">webhook security guide<\/a> for detailed signature verification patterns.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">HubSpot Integration<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/developers.hubspot.com\/docs\/api\/crm\/contacts\" rel=\"nofollow noopener\" target=\"_blank\">HubSpot&#39;s API<\/a> uses a straightforward REST interface for contact management.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Creating or Updating Contacts<\/h3>\n\n\n\n<pre><code class=\"language-typescript\">const HUBSPOT_TOKEN = process.env.HUBSPOT_ACCESS_TOKEN!;\n\nasync function processEvent(event: any) {\n  const { event: eventType, timestamp, data } = event;\n\n  switch (eventType) {\n    case &#39;install.tracked&#39;:\n      await upsertHubSpotContact(data, {\n        lifecycle_stage: &#39;lead&#39;,\n        first_touch_campaign: data.campaign || &#39;&#39;,\n        first_touch_platform: data.platform || &#39;&#39;,\n        install_date: timestamp,\n      });\n      break;\n\n    case &#39;referral.created&#39;:\n      await upsertHubSpotContact(data, {\n        lifecycle_stage: &#39;lead&#39;,\n        referral_source: data.referrer_token || &#39;&#39;,\n        referral_date: timestamp,\n      });\n      break;\n\n    case &#39;referral.completed&#39;:\n      await upsertHubSpotContact(data, {\n        lifecycle_stage: &#39;customer&#39;,\n        conversion_date: timestamp,\n      });\n      break;\n  }\n}\n\nasync function upsertHubSpotContact(\n  data: any,\n  properties: Record&lt;string, string&gt;\n) {\n  \/\/ Search for existing contact by email or create new\n  const response = await fetch(\n    &#39;https:\/\/api.hubapi.com\/crm\/v3\/objects\/contacts&#39;,\n    {\n      method: &#39;POST&#39;,\n      headers: {\n        &#39;Content-Type&#39;: &#39;application\/json&#39;,\n        &#39;Authorization&#39;: `Bearer ${HUBSPOT_TOKEN}`,\n      },\n      body: JSON.stringify({\n        properties: {\n          ...properties,\n          deep_link_token: data.token || &#39;&#39;,\n          deep_link_prefix: data.prefix || &#39;&#39;,\n        },\n      }),\n    }\n  );\n\n  if (response.status === 409) {\n    \/\/ Contact already exists; update instead\n    const existing = await response.json();\n    const contactId = existing.message.match(\/ID: (\\d+)\/)?.[1];\n    if (contactId) {\n      await fetch(\n        `https:\/\/api.hubapi.com\/crm\/v3\/objects\/contacts\/${contactId}`,\n        {\n          method: &#39;PATCH&#39;,\n          headers: {\n            &#39;Content-Type&#39;: &#39;application\/json&#39;,\n            &#39;Authorization&#39;: `Bearer ${HUBSPOT_TOKEN}`,\n          },\n          body: JSON.stringify({ properties }),\n        }\n      );\n    }\n  }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Custom Properties<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Before this works, you need to create custom contact properties in HubSpot:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Go to <strong>Settings &gt; Properties &gt; Contact Properties<\/strong><\/li>\n<li>Create these custom properties:<ul>\n<li><code>first_touch_campaign<\/code> (Single-line text)<\/li>\n<li><code>first_touch_platform<\/code> (Single-line text)<\/li>\n<li><code>install_date<\/code> (Date picker)<\/li>\n<li><code>referral_source<\/code> (Single-line text)<\/li>\n<li><code>referral_date<\/code> (Date picker)<\/li>\n<li><code>conversion_date<\/code> (Date picker)<\/li>\n<li><code>deep_link_token<\/code> (Single-line text)<\/li>\n<li><code>deep_link_prefix<\/code> (Single-line text)<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Group them under a &quot;Deep Link Attribution&quot; property group so your sales team can find them easily on the contact record.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Salesforce Integration<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/developer.salesforce.com\/docs\/atlas.en-us.api_rest.meta\/api_rest\/\" rel=\"nofollow noopener\" target=\"_blank\">Salesforce&#39;s REST API<\/a> requires OAuth authentication and uses a slightly different upsert pattern.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Authentication<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Salesforce uses an OAuth 2.0 flow. For server-to-server integration, use the <a href=\"https:\/\/help.salesforce.com\/s\/articleView?id=sf.remoteaccess_oauth_jwt_flow.htm\" rel=\"nofollow noopener\" target=\"_blank\">JWT Bearer flow<\/a> or a <a href=\"https:\/\/help.salesforce.com\/s\/articleView?id=sf.connected_app_client_credentials_setup.htm\" rel=\"nofollow noopener\" target=\"_blank\">Connected App with client credentials<\/a>.<\/p>\n\n\n\n<pre><code class=\"language-typescript\">async function getSalesforceToken(): Promise&lt;string&gt; {\n  const response = await fetch(\n    `${process.env.SF_INSTANCE_URL}\/services\/oauth2\/token`,\n    {\n      method: &#39;POST&#39;,\n      headers: { &#39;Content-Type&#39;: &#39;application\/x-www-form-urlencoded&#39; },\n      body: new URLSearchParams({\n        grant_type: &#39;client_credentials&#39;,\n        client_id: process.env.SF_CLIENT_ID!,\n        client_secret: process.env.SF_CLIENT_SECRET!,\n      }),\n    }\n  );\n  const data = await response.json();\n  return data.access_token;\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Creating Leads<\/h3>\n\n\n\n<pre><code class=\"language-typescript\">async function processEvent(event: any) {\n  const { event: eventType, timestamp, data } = event;\n  const token = await getSalesforceToken();\n  const baseUrl = process.env.SF_INSTANCE_URL;\n\n  if (eventType === &#39;install.tracked&#39; || eventType === &#39;referral.created&#39;) {\n    await fetch(`${baseUrl}\/services\/data\/v59.0\/sobjects\/Lead`, {\n      method: &#39;POST&#39;,\n      headers: {\n        &#39;Content-Type&#39;: &#39;application\/json&#39;,\n        &#39;Authorization&#39;: `Bearer ${token}`,\n      },\n      body: JSON.stringify({\n        FirstName: &#39;Deep Link&#39;,\n        LastName: `Lead ${data.token || &#39;Unknown&#39;}`,\n        Company: &#39;Unknown&#39;,\n        LeadSource: &#39;Deep Link&#39;,\n        Deep_Link_Campaign__c: data.campaign || &#39;&#39;,\n        Deep_Link_Platform__c: data.platform || &#39;&#39;,\n        Deep_Link_Token__c: data.token || &#39;&#39;,\n        Install_Date__c: timestamp,\n        Status: &#39;New&#39;,\n      }),\n    });\n  }\n\n  if (eventType === &#39;referral.completed&#39;) {\n    \/\/ Find and update the existing lead\n    const search = await fetch(\n      `${baseUrl}\/services\/data\/v59.0\/query?q=${encodeURIComponent(\n        `SELECT Id FROM Lead WHERE Deep_Link_Token__c = &#39;${data.token}&#39; LIMIT 1`\n      )}`,\n      {\n        headers: { &#39;Authorization&#39;: `Bearer ${token}` },\n      }\n    );\n    const result = await search.json();\n    if (result.records?.length &gt; 0) {\n      await fetch(\n        `${baseUrl}\/services\/data\/v59.0\/sobjects\/Lead\/${result.records[0].Id}`,\n        {\n          method: &#39;PATCH&#39;,\n          headers: {\n            &#39;Content-Type&#39;: &#39;application\/json&#39;,\n            &#39;Authorization&#39;: `Bearer ${token}`,\n          },\n          body: JSON.stringify({\n            Status: &#39;Converted&#39;,\n            Conversion_Date__c: timestamp,\n          }),\n        }\n      );\n    }\n  }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Custom Fields<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Create these custom fields on the Lead object in Salesforce Setup:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>Deep_Link_Campaign__c<\/code> (Text)<\/li>\n<li><code>Deep_Link_Platform__c<\/code> (Text)<\/li>\n<li><code>Deep_Link_Token__c<\/code> (Text, External ID)<\/li>\n<li><code>Install_Date__c<\/code> (DateTime)<\/li>\n<li><code>Conversion_Date__c<\/code> (DateTime)<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Making <code>Deep_Link_Token__c<\/code> an External ID enables upsert operations, which simplifies the create-or-update pattern.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Pipedrive Integration<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/developers.pipedrive.com\/docs\/api\/v1\" rel=\"nofollow noopener\" target=\"_blank\">Pipedrive&#39;s API<\/a> is simpler than Salesforce and works well for smaller sales teams.<\/p>\n\n\n\n<pre><code class=\"language-typescript\">const PIPEDRIVE_TOKEN = process.env.PIPEDRIVE_API_TOKEN!;\nconst PIPEDRIVE_URL = `https:\/\/${process.env.PIPEDRIVE_DOMAIN}.pipedrive.com\/api\/v1`;\n\nasync function processEvent(event: any) {\n  const { event: eventType, timestamp, data } = event;\n\n  if (eventType === &#39;install.tracked&#39; || eventType === &#39;referral.created&#39;) {\n    \/\/ Create a person in Pipedrive\n    const person = await fetch(`${PIPEDRIVE_URL}\/persons?api_token=${PIPEDRIVE_TOKEN}`, {\n      method: &#39;POST&#39;,\n      headers: { &#39;Content-Type&#39;: &#39;application\/json&#39; },\n      body: JSON.stringify({\n        name: `Deep Link Lead (${data.token || &#39;unknown&#39;})`,\n        \/\/ Custom fields (use your Pipedrive field keys)\n        &#39;1a2b3c&#39;: data.campaign || &#39;&#39;,  \/\/ Campaign custom field\n        &#39;4d5e6f&#39;: data.platform || &#39;&#39;,  \/\/ Platform custom field\n      }),\n    });\n\n    const personData = await person.json();\n\n    \/\/ Optionally create a deal\n    await fetch(`${PIPEDRIVE_URL}\/deals?api_token=${PIPEDRIVE_TOKEN}`, {\n      method: &#39;POST&#39;,\n      headers: { &#39;Content-Type&#39;: &#39;application\/json&#39; },\n      body: JSON.stringify({\n        title: `Referral: ${data.token || &#39;Unknown&#39;}`,\n        person_id: personData.data.id,\n        stage_id: 1, \/\/ First stage of your pipeline\n      }),\n    });\n  }\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Matching Webhook Events to CRM Records<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The biggest challenge in CRM integration is identity. A <code>link.clicked<\/code> or <code>install.tracked<\/code> event identifies a device and a campaign, not a person. You can&#39;t create a meaningful CRM record from an IP address and a platform string alone.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">There are three strategies for bridging this gap:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1. Match on Referral Token<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If the deep link is a referral link, the <code>data<\/code> object includes a token that maps to a specific referral. Your app&#39;s backend knows which user created that referral, so you can look up the referrer and create a CRM record with full attribution.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">2. Match on Downstream Registration<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The most common pattern: when a user installs the app and registers, your app&#39;s backend creates the CRM record and attaches the deep link attribution data that was captured at install time. The webhook provides real-time notification that an install happened; the app backend provides the user identity.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">3. Enrichment After the Fact<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Create a &quot;stub&quot; CRM record from the webhook event (with campaign, platform, and token), then enrich it later when the user registers. Your app sends a second API call to the CRM with the user&#39;s email and name, matching on the deep link token.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">No-Code Alternative: Zapier<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If you don&#39;t want to build and host a receiver, <a href=\"https:\/\/tolinku.com\/blog\/webhook-zapier-integration\/\">Zapier<\/a> can connect Tolinku webhooks to HubSpot, Salesforce, and Pipedrive without code. The tradeoff: no signature verification and limited transformation logic. For low-volume, non-sensitive integrations, Zapier is a reasonable starting point.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Handling Retries and Duplicates<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Tolinku retries failed webhook deliveries (3 retries at 1 minute, 5 minutes, and 30 minutes). Your CRM integration must handle duplicate events gracefully.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>For HubSpot<\/strong>: Use the contact deduplication built into the API. Upserting by email or a custom unique field prevents duplicates.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>For Salesforce<\/strong>: Use External IDs on the Lead or Contact object. The <code>upsert<\/code> endpoint creates a new record or updates an existing one based on the External ID value.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>For Pipedrive<\/strong>: Search for existing persons by custom field before creating. Pipedrive doesn&#39;t have a native upsert operation.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">As a general practice, generate a deduplication key from the webhook payload (a hash of the event content) and check for its existence before creating records. See the <a href=\"https:\/\/tolinku.com\/blog\/webhook-retry-logic\/\">webhook retry logic guide<\/a> for more on idempotent processing.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What Your Sales Team Sees<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">After integration, a CRM record for a referral-driven lead might show:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Lead Source<\/strong>: Deep Link<\/li>\n<li><strong>Campaign<\/strong>: <code>instagram-story-may<\/code><\/li>\n<li><strong>Platform<\/strong>: iOS<\/li>\n<li><strong>Link Token<\/strong>: <code>summer-promo<\/code><\/li>\n<li><strong>Install Date<\/strong>: 2026-05-21 09:14:00<\/li>\n<li><strong>Referral Source<\/strong>: user-abc123<\/li>\n<li><strong>Conversion Date<\/strong>: 2026-05-21 09:32:00<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">This gives the sales team immediate context: the lead came from an Instagram campaign, installed on iOS, and converted within 18 minutes of install. That&#39;s the kind of attribution data that changes how you follow up.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Configure your webhooks in your <a href=\"https:\/\/tolinku.com\/docs\/user-guide\/webhooks\/\">Tolinku Appspace<\/a> and start routing deep link attribution data to where your team already works.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Sync deep link data to your CRM via webhooks. Update lead records, track touchpoints, and attribute conversions in Salesforce, HubSpot, and more.<\/p>\n","protected":false},"author":2,"featured_media":1143,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"CRM Integration with Deep Link Webhooks","rank_math_description":"Sync deep link data to your CRM via webhooks. Update lead records, track touchpoints, and attribute conversions in Salesforce, HubSpot, and more.","rank_math_focus_keyword":"webhook CRM integration","rank_math_canonical_url":"","rank_math_facebook_title":"","rank_math_facebook_description":"","rank_math_facebook_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-webhook-crm-integration.png","rank_math_facebook_image_id":"","rank_math_twitter_title":"","rank_math_twitter_description":"","rank_math_twitter_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-webhook-crm-integration.png","footnotes":""},"categories":[15],"tags":[28,285,20,287,263,286,61],"class_list":["post-1144","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-engineering","tag-attribution","tag-crm","tag-deep-linking","tag-hubspot","tag-integrations","tag-salesforce","tag-webhooks"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1144","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/comments?post=1144"}],"version-history":[{"count":2,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1144\/revisions"}],"predecessor-version":[{"id":2257,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/1144\/revisions\/2257"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/1143"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=1144"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=1144"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=1144"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}