Intermediate~20 min setupCommunication & CRMVerified April 2026
Slack logo
Copper logo

How to Sync Slack Members to Copper Contacts with n8n

Automatically creates a Copper contact whenever a new member joins a designated Slack channel, pulling their profile data directly from the Slack API.

Steps and UI details are based on platform versions at time of writing — check each platform for the latest interface.

Best for

Teams using Copper as their CRM who onboard partners or clients via dedicated Slack channels and need those people in Copper without manual data entry.

Not ideal for

If your Slack workspace has hundreds of channel joins per day — at that volume, a dedicated integration with batching is a better fit than per-event webhook processing.

Sync type

real-time

Use case type

sync

Real-World Example

💡

A 20-person consulting firm creates a dedicated Slack channel for each new client engagement. When external partners join the #engagement-acme channel, those people need to be in Copper immediately so account managers can log calls and emails against them. Before this workflow, someone on the ops team manually copied names and emails from Slack into Copper — typically a day or two after the channel join, by which time a call had already been missed.

What Will This Cost?

Drag the slider to your expected monthly volume.

/mo
505005K50K

Each platform counts differently — Zapier: 1 task per trigger. Make: 1 operation per module per record. n8n: 1 execution per run.

Prices shown for annual billing. Based on published pricing as of April 2026.

Estimated ROI

1000

min saved/mo

$583

labor value/mo

Free

no platform cost

Based on ~2 min manual effort per operation at $35/hr fully loaded labor cost.

Implementation

Skip the setup

Import this workflow directly into n8n

Copy the pre-built n8n blueprint and paste it straight into n8n. All modules, filters, and field mappings are already configured — you just need to connect your accounts.

Before You Start

Make sure you have everything ready.

Slack workspace admin access — needed to create a Slack app and approve the 'users:read.email' scope
Copper API key — found in Copper under Settings > Integrations > API Keys; you need a user account with contact creation permissions
n8n instance accessible via a public HTTPS URL — Slack cannot send webhooks to localhost; use n8n Cloud, a VPS, or a tunnel like ngrok for testing
Slack bot scopes: 'channels:read', 'users:read', 'users:read.email' — must be added before installing the app to the workspace
Bot invited to each target Slack channel — the 'member_joined_channel' event only fires in channels where your bot is a member

Field Mapping

Map these fields between your apps.

FieldAPI Name
Required
Full Namename
Email Addressemails
6 optional fields▸ show
Phone Numberphone_numbers
Job Titletitle
Slack User IDcustom_fields
Slack Handledetails
Contact Sourcetags
Date Addedcustom_fields

Step-by-Step Setup

1

api.slack.com/apps > Your App > Event Subscriptions

Create a Slack App and Enable Event Subscriptions

Go to api.slack.com/apps and click 'Create New App', then choose 'From scratch'. Name it something like 'n8n Contact Sync' and select your workspace. Once created, navigate to 'Event Subscriptions' in the left sidebar and toggle 'Enable Events' to On. You'll need to paste your n8n webhook URL here — set that up in Step 2 first, then come back.

  1. 1Go to api.slack.com/apps and click 'Create New App'
  2. 2Choose 'From scratch', name the app, select your workspace, click 'Create App'
  3. 3In the left sidebar click 'Event Subscriptions'
  4. 4Toggle 'Enable Events' to On — leave the Request URL blank for now
What you should see: You should see the Event Subscriptions page with the toggle switched to On and a Request URL field waiting for input.
Common mistake — Slack will send a challenge request to verify your webhook URL. Your n8n webhook node must be active and reachable at that URL before you paste it here — if n8n isn't listening, Slack will reject the URL and the toggle reverts to Off.

This Code node runs between the Slack users.info HTTP Request node and the Copper duplicate-check node. It normalizes all profile fields, sets safe fallbacks for missing data, and formats the phone and email into the array-of-objects structure Copper's API requires — so your later HTTP Request nodes can just reference clean fields instead of building nested JSON inside expressions.

JavaScript — Code Node// Code node: Normalize Slack profile for Copper API
▸ Show code
// Code node: Normalize Slack profile for Copper API
// Place between: HTTP Request (Slack users.info) → HTTP Request (Copper search)
const items = $input.all();

... expand to see full code

// Code node: Normalize Slack profile for Copper API
// Place between: HTTP Request (Slack users.info) → HTTP Request (Copper search)

const items = $input.all();
const results = [];

for (const item of items) {
  const user = item.json.user;
  const profile = user?.profile ?? {};

  // Normalize all fields with safe fallbacks
  const fullName = (user?.real_name || user?.name || '').trim();
  const email = (profile?.email || '').toLowerCase().trim();
  const phone = (profile?.phone || '').trim();
  const title = (profile?.title || '').trim();
  const slackHandle = (profile?.display_name || user?.name || '').trim();
  const slackUserId = user?.id || '';

  // Skip if no email — Copper requires it and the contact would be useless
  if (!email) {
    console.log(`Skipping user ${slackUserId}: no email in Slack profile`);
    continue;
  }

  // Build Copper-formatted arrays
  const copperEmails = [{ email, category: 'work' }];
  const copperPhones = phone ? [{ number: phone, category: 'mobile' }] : [];

  // Build the details string for Copper's notes field
  const details = [
    slackHandle ? `Slack: @${slackHandle}` : null,
    `Added via channel join automation on ${new Date().toISOString().split('T')[0]}`
  ].filter(Boolean).join(' | ');

  results.push({
    json: {
      fullName,
      email,           // plain string for duplicate search
      copperEmails,    // array for Copper create API
      copperPhones,    // array for Copper create API
      title,
      slackHandle,
      slackUserId,
      details,
      tags: ['slack-sync']
    }
  });
}

return results;
2

n8n > New Workflow > + Node > Webhook

Set Up the n8n Webhook Trigger Node

Open your n8n instance, click '+ New Workflow', then click the '+' node button and search for 'Webhook'. Select the Webhook node and set the HTTP Method to POST. Copy the 'Test URL' shown in the node panel — you'll paste this into Slack in the next step. Leave the node in 'Listen for Test Event' mode for now so Slack can verify it.

  1. 1Click '+ New Workflow' in your n8n dashboard
  2. 2Click the '+' button to add the first node
  3. 3Search for 'Webhook' and select it
  4. 4Set HTTP Method to POST
  5. 5Click 'Listen for Test Event' and copy the Test URL shown
What you should see: The Webhook node panel shows a Test URL ending in /webhook-test/[uuid] and the node is actively listening, indicated by the orange 'Waiting for test event' banner.
Common mistake — The Test URL and Production URL are different in n8n. The Test URL only works while you're actively listening in the editor. Once you activate the workflow, switch Slack's Request URL to the Production URL (the one without /webhook-test/).
n8n
+
click +
search apps
Slack
SL
Slack
Set Up the n8n Webhook Trigg…
Slack
SL
module added
3

api.slack.com/apps > Your App > Event Subscriptions > Subscribe to Bot Events

Register the Webhook URL in Slack and Subscribe to Channel Join Events

Go back to your Slack app's Event Subscriptions page and paste the n8n Test URL into the Request URL field. Slack will immediately send a challenge POST — n8n will auto-respond and Slack will show a green 'Verified' checkmark. Scroll down to 'Subscribe to bot events' and add the event 'member_joined_channel'. Click 'Save Changes'.

  1. 1Paste the n8n Test URL into the 'Request URL' field
  2. 2Wait for the green 'Verified' checkmark to appear
  3. 3Click 'Add Bot Event' under 'Subscribe to Bot Events'
  4. 4Search for and select 'member_joined_channel'
  5. 5Click 'Save Changes' at the bottom of the page
What you should see: The Request URL field shows a green 'Verified' badge and 'member_joined_channel' appears listed under 'Subscribe to Bot Events'.
Common mistake — Slack distinguishes between bot events and workspace events. Make sure you add 'member_joined_channel' under 'Bot Events', not 'Workspace Events' — otherwise the event fires for every channel in the workspace, not just channels your bot has been invited to.
4

api.slack.com/apps > Your App > OAuth & Permissions > Bot Token Scopes

Install the Slack App to Your Workspace and Invite It to Target Channels

In the Slack API dashboard, go to 'OAuth & Permissions' in the left sidebar. Under 'Bot Token Scopes' add the scopes: 'channels:read', 'users:read', and 'users:read.email'. Click 'Install to Workspace' at the top of the page and authorize. Copy the Bot User OAuth Token — it starts with xoxb-. Then in Slack itself, go to each channel you want to monitor and type '/invite @YourAppName'.

  1. 1Click 'OAuth & Permissions' in the left sidebar
  2. 2Under 'Bot Token Scopes' click 'Add an OAuth Scope'
  3. 3Add 'channels:read', 'users:read', and 'users:read.email'
  4. 4Click 'Install to Workspace' and click 'Allow'
  5. 5Copy the 'Bot User OAuth Token' (xoxb-...)
  6. 6In Slack, open each target channel and type '/invite @YourAppName'
What you should see: The OAuth & Permissions page shows a Bot User OAuth Token starting with xoxb- and each target Slack channel shows '@YourAppName was added to this channel'.
Common mistake — The 'users:read.email' scope requires workspace admin approval in some Slack plans. If you see a 'Restricted' error when requesting the scope, your Slack admin needs to approve the app before email addresses will be returned by the API.
5

n8n Workflow Editor > + Node > HTTP Request

Add an HTTP Request Node to Fetch the User's Full Slack Profile

The 'member_joined_channel' event only gives you a user ID, not name or email. Add an HTTP Request node after the Webhook node to call the Slack API and get the full profile. Set the method to GET, the URL to 'https://slack.com/api/users.info', and add a query parameter 'user' with value '{{ $json.body.event.user }}'. In the Authentication section, select 'Header Auth', set the header name to 'Authorization', and the value to 'Bearer xoxb-your-token'.

  1. 1Click '+' after the Webhook node to add a new node
  2. 2Search for 'HTTP Request' and select it
  3. 3Set Method to GET
  4. 4Set URL to 'https://slack.com/api/users.info'
  5. 5Under 'Query Parameters' add key 'user' with value '{{ $json.body.event.user }}'
  6. 6Under 'Authentication' select 'Header Auth', name: Authorization, value: Bearer xoxb-your-token
What you should see: When you click 'Test Step', the node returns a JSON object with 'ok: true' and a 'user' object containing 'name', 'real_name', 'profile.email', 'profile.title', and 'profile.phone'.
6

n8n Workflow Editor > + Node > HTTP Request (Copper Search)

Add a Copper Search Node to Check for Duplicate Contacts

Before creating a contact in Copper, check if one already exists with that email. Add another HTTP Request node. Set the method to POST, URL to 'https://api.copper.com/developer_api/v1/people/search', and add three headers: 'X-PW-AccessToken' with your Copper API key, 'X-PW-Application' with value 'developer_api', and 'Content-Type' with 'application/json'. In the body, set JSON body to '{ "emails": [{ "email": "{{ $node["HTTP Request"].json.user.profile.email }}" }] }'.

  1. 1Click '+' after the Slack profile fetch node
  2. 2Add another HTTP Request node
  3. 3Set Method to POST
  4. 4Set URL to 'https://api.copper.com/developer_api/v1/people/search'
  5. 5Add headers: X-PW-AccessToken, X-PW-Application (developer_api), Content-Type (application/json)
  6. 6Set the JSON body to search by email as shown in the detail
What you should see: The node returns an array — an empty array '[]' means no duplicate exists; an array with one or more objects means a contact with that email already exists in Copper.
Common mistake — Copper's search API is case-sensitive for email matching in some configurations. Normalize the email to lowercase in your expression — use '{{ $node["HTTP Request"].json.user.profile.email.toLowerCase() }}' — to avoid creating duplicates for '[email protected]' vs '[email protected]'.
7

n8n Workflow Editor > + Node > IF

Add an IF Node to Skip Existing Contacts

Add an IF node after the Copper search. Set the condition to check the length of the search results array. In the left value field enter '{{ $json.length }}', set the operator to 'Equal', and the right value to '0'. The TRUE branch means no contact exists — route this to the create step. The FALSE branch means a duplicate — connect it to a No Operation node or simply leave it unconnected to stop execution.

  1. 1Click '+' after the Copper search node
  2. 2Search for 'IF' and select it
  3. 3Set left value to '{{ $json.length }}'
  4. 4Set operator to 'Equal'
  5. 5Set right value to 0
  6. 6Connect the TRUE output to the next step (contact creation)
  7. 7Connect or leave the FALSE output unconnected
What you should see: The IF node shows two output branches labeled 'true' and 'false'. Running a test with an empty Copper result routes to the true branch.
8

n8n Workflow Editor > IF Node (true branch) > + Node > HTTP Request (Copper Create)

Add an HTTP Request Node to Create the Copper Contact

Connect an HTTP Request node to the TRUE branch of the IF node. Set method to POST, URL to 'https://api.copper.com/developer_api/v1/people'. Use the same three Copper headers from Step 6. In the JSON body, map the Slack profile fields to Copper's expected format — name, email, phone, and title. Reference the Slack profile data from the earlier HTTP Request node using expressions like '{{ $node["HTTP Request"].json.user.real_name }}'.

  1. 1Click the TRUE output of the IF node and add an HTTP Request node
  2. 2Set Method to POST, URL to 'https://api.copper.com/developer_api/v1/people'
  3. 3Add the same three Copper headers as Step 6
  4. 4Set the JSON body using the field mapping in the Field Mapping section below
  5. 5Click 'Test Step' to verify a contact is created
What you should see: The node returns a JSON object with a numeric 'id' field — that's the new Copper contact's ID — plus the name and email you sent. Log into Copper and confirm the contact appears under People.
Common mistake — Copper's People API expects emails and phone_numbers as arrays of objects, not plain strings. Sending 'email': '[email protected]' will return a 422 error. The correct format is 'emails': [{'email': '[email protected]', 'category': 'work'}].
9

n8n Workflow Editor > Between HTTP Request (Slack) and HTTP Request (Copper Search) > + Node > Code

Add a Code Node to Handle Missing Profile Fields Gracefully

Slack profiles are inconsistently filled out — phone and title are often blank. Add a Code node between the Slack profile fetch (Step 5) and the duplicate check (Step 6) to normalize the data and set fallback values. This prevents the Copper API call from failing due to null values. Paste the code from the Pro Tip section below into the Code node's 'JavaScript' editor.

  1. 1Click the connection line between the Slack fetch node and the Copper search node
  2. 2Click '+' to insert a new node
  3. 3Search for 'Code' and select it
  4. 4Set the language to 'JavaScript'
  5. 5Paste the pro tip code into the editor
  6. 6Click 'Test Step' to confirm the output shows normalized fields
What you should see: The Code node output shows a single item with fields: 'fullName', 'email', 'phone', 'title', 'slackUserId', and 'slackHandle' — with empty strings instead of nulls for missing values.

This Code node runs between the Slack users.info HTTP Request node and the Copper duplicate-check node. It normalizes all profile fields, sets safe fallbacks for missing data, and formats the phone and email into the array-of-objects structure Copper's API requires — so your later HTTP Request nodes can just reference clean fields instead of building nested JSON inside expressions.

JavaScript — Code Node// Code node: Normalize Slack profile for Copper API
▸ Show code
// Code node: Normalize Slack profile for Copper API
// Place between: HTTP Request (Slack users.info) → HTTP Request (Copper search)
const items = $input.all();

... expand to see full code

// Code node: Normalize Slack profile for Copper API
// Place between: HTTP Request (Slack users.info) → HTTP Request (Copper search)

const items = $input.all();
const results = [];

for (const item of items) {
  const user = item.json.user;
  const profile = user?.profile ?? {};

  // Normalize all fields with safe fallbacks
  const fullName = (user?.real_name || user?.name || '').trim();
  const email = (profile?.email || '').toLowerCase().trim();
  const phone = (profile?.phone || '').trim();
  const title = (profile?.title || '').trim();
  const slackHandle = (profile?.display_name || user?.name || '').trim();
  const slackUserId = user?.id || '';

  // Skip if no email — Copper requires it and the contact would be useless
  if (!email) {
    console.log(`Skipping user ${slackUserId}: no email in Slack profile`);
    continue;
  }

  // Build Copper-formatted arrays
  const copperEmails = [{ email, category: 'work' }];
  const copperPhones = phone ? [{ number: phone, category: 'mobile' }] : [];

  // Build the details string for Copper's notes field
  const details = [
    slackHandle ? `Slack: @${slackHandle}` : null,
    `Added via channel join automation on ${new Date().toISOString().split('T')[0]}`
  ].filter(Boolean).join(' | ');

  results.push({
    json: {
      fullName,
      email,           // plain string for duplicate search
      copperEmails,    // array for Copper create API
      copperPhones,    // array for Copper create API
      title,
      slackHandle,
      slackUserId,
      details,
      tags: ['slack-sync']
    }
  });
}

return results;
Slack fields
text
user
channel
ts
thread_ts
available as variables:
1.props.text
1.props.user
1.props.channel
1.props.ts
1.props.thread_ts
10

n8n Workflow Editor > Webhook Node > Production URL | api.slack.com > Event Subscriptions

Switch to Production URL and Activate the Workflow

Before activating, go back to your Webhook node and copy the Production URL (found in the node panel under 'Production URL' — it does not contain '/webhook-test/'). Go to api.slack.com/apps, navigate to Event Subscriptions, and replace the Test URL with the Production URL. Slack will re-verify it. Then return to n8n and click the 'Activate' toggle in the top right of the workflow editor.

  1. 1Open the Webhook node panel and copy the Production URL
  2. 2Go to api.slack.com/apps > Your App > Event Subscriptions
  3. 3Replace the Test URL with the Production URL
  4. 4Confirm the green 'Verified' badge reappears
  5. 5Return to n8n and click the 'Inactive' toggle to switch the workflow to 'Active'
What you should see: The n8n workflow editor shows a green 'Active' badge in the top right. The Slack Event Subscriptions page shows the Production URL as 'Verified'.
Common mistake — If you forget to update Slack's Request URL to the Production URL, Slack will keep sending events to the Test URL, which only works while the editor is open. Events fired when the editor is closed will be silently dropped — no error, no retry.
11

n8n > Executions | Copper > People

Test End-to-End with a Real Channel Join

Have a team member or a test account join one of the monitored Slack channels. Wait up to 30 seconds, then check n8n's Execution Log (left sidebar > Executions) to confirm a run was triggered and all nodes completed with green checkmarks. Then open Copper, go to People, and confirm the new contact appears with the correct name, email, phone, and title. Check that no duplicate was created if you run the test twice.

  1. 1Have a user join a monitored Slack channel
  2. 2In n8n, click 'Executions' in the left sidebar
  3. 3Confirm a new execution appears and all nodes show green
  4. 4Open Copper and navigate to People
  5. 5Search for the test user's email and confirm the contact record is correct
What you should see: n8n shows a successful execution with no red nodes. Copper shows one new contact with the Slack user's real name, email address, and any available phone and title from their Slack profile.
Common mistake — Map fields using the variable picker — don't type field names manually. Hand-typed variable names often have invisible spacing errors that produce blank output.
n8n
▶ Run once
executed
Slack
Copper
Copper
🔔 notification
received

Going live

Production Checklist

Before you turn this on for real, confirm each item.

Troubleshooting

Common errors and how to fix them.

Frequently Asked Questions

Common questions about this workflow.

Analysis

VerdictWhy n8n for this workflow

Use n8n for this if you're self-hosting and want full control over credentials, you need to transform Slack profile data before it hits Copper (the Code node is essential here), or you're already running n8n for other workflows and don't want another SaaS bill. The one scenario where you'd skip n8n: if your team has zero technical resources and needs this running in under an hour — Zapier's Slack + Copper integration requires no webhook setup and has pre-built auth for both apps, which cuts setup time from 45 minutes to about 10.

Cost

On cost: this workflow fires one execution per Slack channel join. At 50 new members per month across monitored channels, that's 50 executions — each execution runs 5-6 nodes, so roughly 250-300 node executions monthly. n8n Cloud's Starter plan ($20/month) includes 2,500 executions. Self-hosted n8n is free. Zapier's equivalent would consume 3 Zap steps per run (trigger + lookup + create), putting 50 events at 150 tasks/month — comfortably inside Zapier's free tier at low volume, but at 500 events/month you'd hit 1,500 tasks and need a $19.99/month plan. For most teams using this workflow, n8n self-hosted is free and Zapier paid is $20 — same cost, but n8n gives you the Code node.

Tradeoffs

Make handles this use case well and has a native Slack 'Watch Channel Members' module that removes the need to build your own webhook verification flow — that alone saves about 20 minutes of setup. Zapier's version is the fastest to configure but you cannot run custom deduplication logic without Code by Zapier, which requires a paid plan. Power Automate has a Slack connector but it's limited — the 'member_joined_channel' event isn't a supported trigger as of mid-2024, meaning you'd need to fall back to polling, which introduces a 15-minute delay. Pipedream has excellent Slack source support with pre-built event filtering, and its Node.js steps handle the Copper API formatting cleanly — it's a genuine alternative to n8n if you prefer writing code in a hosted environment rather than self-managing infrastructure. n8n wins here if you're already self-hosting it, because you're not paying extra for the Code node functionality that makes this workflow reliable.

Three things you'll hit after setup. First, Slack's 'member_joined_channel' event has a 3-second delivery window — if your n8n instance is slow to respond (cold start on a low-memory VPS), Slack will retry up to three times and you may process the same join event multiple times, creating duplicates despite your IF node. Add idempotency by storing processed Slack user IDs in n8n's static data or a simple Airtable log. Second, Copper's People search API does not support partial email matching — you must send the exact email string. If someone's Slack profile email differs from their Copper email by even one character (a nickname alias vs. primary address), the duplicate check returns empty and you'll create a second record. Third, Slack's users.info endpoint is rate-limited at 20 requests per minute on the basic tier — at normal channel join rates this is never an issue, but if you're bulk-inviting 30+ people to a channel at once, the concurrent webhook events will trigger parallel executions that may hit the limit and return 429 errors. Add a 3-second Wait node before the users.info call if you anticipate bulk invite events.

Ideas for what to build next

  • Add Copper Activity on Slack MessagesExtend this workflow to log a Copper activity every time an existing contact sends a message in the channel — gives your team a message history linked directly to the CRM contact record.
  • Reverse Sync: New Copper Contacts to SlackUse Copper's webhooks to post a Slack message to a #new-contacts channel whenever a contact is manually added in Copper — closes the loop for contacts created outside of Slack.
  • Enrich Contacts with Clearbit on CreateAfter creating the Copper contact, pass the email to Clearbit's Enrichment API to add company size, industry, and LinkedIn URL — three fields that Slack profiles never contain but Copper reports need.

Related guides

Was this guide helpful?
Slack + Copper overviewn8n profile →