

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-timeUse case type
syncReal-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.
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
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.
Field Mapping
Map these fields between your apps.
| Field | API Name | |
|---|---|---|
| Required | ||
| Full Name | name | |
| Email Address | emails | |
6 optional fields▸ show
| Phone Number | phone_numbers |
| Job Title | title |
| Slack User ID | custom_fields |
| Slack Handle | details |
| Contact Source | tags |
| Date Added | custom_fields |
Step-by-Step Setup
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.
- 1Go to api.slack.com/apps and click 'Create New App'
- 2Choose 'From scratch', name the app, select your workspace, click 'Create App'
- 3In the left sidebar click 'Event Subscriptions'
- 4Toggle 'Enable Events' to On — leave the Request URL blank for now
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;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.
- 1Click '+ New Workflow' in your n8n dashboard
- 2Click the '+' button to add the first node
- 3Search for 'Webhook' and select it
- 4Set HTTP Method to POST
- 5Click 'Listen for Test Event' and copy the Test URL shown
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'.
- 1Paste the n8n Test URL into the 'Request URL' field
- 2Wait for the green 'Verified' checkmark to appear
- 3Click 'Add Bot Event' under 'Subscribe to Bot Events'
- 4Search for and select 'member_joined_channel'
- 5Click 'Save Changes' at the bottom of the page
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'.
- 1Click 'OAuth & Permissions' in the left sidebar
- 2Under 'Bot Token Scopes' click 'Add an OAuth Scope'
- 3Add 'channels:read', 'users:read', and 'users:read.email'
- 4Click 'Install to Workspace' and click 'Allow'
- 5Copy the 'Bot User OAuth Token' (xoxb-...)
- 6In Slack, open each target channel and type '/invite @YourAppName'
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'.
- 1Click '+' after the Webhook node to add a new node
- 2Search for 'HTTP Request' and select it
- 3Set Method to GET
- 4Set URL to 'https://slack.com/api/users.info'
- 5Under 'Query Parameters' add key 'user' with value '{{ $json.body.event.user }}'
- 6Under 'Authentication' select 'Header Auth', name: Authorization, value: Bearer xoxb-your-token
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 }}" }] }'.
- 1Click '+' after the Slack profile fetch node
- 2Add another HTTP Request node
- 3Set Method to POST
- 4Set URL to 'https://api.copper.com/developer_api/v1/people/search'
- 5Add headers: X-PW-AccessToken, X-PW-Application (developer_api), Content-Type (application/json)
- 6Set the JSON body to search by email as shown in the detail
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.
- 1Click '+' after the Copper search node
- 2Search for 'IF' and select it
- 3Set left value to '{{ $json.length }}'
- 4Set operator to 'Equal'
- 5Set right value to 0
- 6Connect the TRUE output to the next step (contact creation)
- 7Connect or leave the FALSE output unconnected
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 }}'.
- 1Click the TRUE output of the IF node and add an HTTP Request node
- 2Set Method to POST, URL to 'https://api.copper.com/developer_api/v1/people'
- 3Add the same three Copper headers as Step 6
- 4Set the JSON body using the field mapping in the Field Mapping section below
- 5Click 'Test Step' to verify a contact is created
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.
- 1Click the connection line between the Slack fetch node and the Copper search node
- 2Click '+' to insert a new node
- 3Search for 'Code' and select it
- 4Set the language to 'JavaScript'
- 5Paste the pro tip code into the editor
- 6Click 'Test Step' to confirm the output shows normalized fields
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;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.
- 1Open the Webhook node panel and copy the Production URL
- 2Go to api.slack.com/apps > Your App > Event Subscriptions
- 3Replace the Test URL with the Production URL
- 4Confirm the green 'Verified' badge reappears
- 5Return to n8n and click the 'Inactive' toggle to switch the workflow to 'Active'
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.
- 1Have a user join a monitored Slack channel
- 2In n8n, click 'Executions' in the left sidebar
- 3Confirm a new execution appears and all nodes show green
- 4Open Copper and navigate to People
- 5Search for the test user's email and confirm the contact record is correct
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
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.
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.
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 Messages — Extend 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 Slack — Use 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 Create — After 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
How to Share Notion Meeting Notes to Slack with Pipedream
~15 min setup
How to Share Notion Meeting Notes to Slack with Power Automate
~15 min setup
How to Share Notion Meeting Notes to Slack with n8n
~20 min setup
How to Send Notion Meeting Notes to Slack with Zapier
~8 min setup
How to Share Notion Meeting Notes to Slack with Make
~12 min setup
How to Create Notion Tasks from Slack with Pipedream
~15 min setup