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

How to Log Slack Messages to Copper CRM with Pipedream

When a Slack message is posted in a designated customer channel, Pipedream instantly logs it as an Activity on the matching Copper contact record.

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

Best for

Sales and CS teams using Slack for customer conversations who need every key message captured in Copper without copy-pasting manually

Not ideal for

Teams logging every single Slack message — this workflow works best when triggered by emoji reactions or specific channel naming conventions, not raw message volume

Sync type

real-time

Use case type

sync

Real-World Example

💡

A 12-person SaaS company runs dedicated Slack channels named after each customer (e.g. #client-acme, #client-globex). Their CS team tags messages with a 📋 emoji when something important is said — a complaint, a renewal signal, a feature request. Before this workflow, those insights lived only in Slack and were never visible in Copper. Now, each tagged message is logged as a Note activity on the right Copper contact within 30 seconds.

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 Pipedream

Copy the pre-built Pipedream blueprint and paste it straight into Pipedream. 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 or a user account with the channels:history, reactions:read, and users:read OAuth scopes granted
Copper CRM account with API access enabled — find your API key under Copper Settings > Integrations > API Keys
Copper user email address that matches the X-PW-UserEmail authentication header (must be an active Copper user)
Pipedream account on any plan — the free tier supports up to 10,000 events/month which is sufficient for typical customer logging volume
Slack channels following a consistent naming convention that maps to Copper contact or company names (e.g. #client-acme maps to Acme Corp in Copper)

Field Mapping

Map these fields between your apps.

FieldAPI Name
Required
Activity Details (Note Body)details
Activity Type IDactivity_type[id]
Parent Typeparent[type]
Parent IDparent[id]
4 optional fields▸ show
Slack Channel Name
Slack User Display Name
Message Timestamp
Slack Message Permalink

Step-by-Step Setup

1

pipedream.com > Workflows > New Workflow

Create a new Pipedream Workflow

Go to pipedream.com and sign in. Click 'New Workflow' in the top-right corner of the dashboard. Give your workflow a name like 'Slack → Copper: Log Customer Messages'. You'll land on the workflow builder canvas with an empty trigger slot at the top.

  1. 1Click 'New Workflow' in the top-right of the Pipedream dashboard
  2. 2Type a workflow name: 'Slack → Copper: Log Customer Messages'
  3. 3Click 'Create' to open the workflow canvas
What you should see: You should see the workflow canvas with a grey 'Add a trigger' block at the top and an empty step area below it.
2

Workflow Canvas > Add a trigger > Slack > New Reaction Added

Add the Slack trigger: New Reaction Added

Click the 'Add a trigger' block. Search for 'Slack' in the app list and select it. Choose the trigger 'New Reaction Added' — this fires whenever someone adds an emoji reaction to a message. Using a reaction (e.g. 📋) as the trigger gives your team control over exactly which messages get logged to Copper, rather than logging every message in a channel.

  1. 1Click the grey 'Add a trigger' block
  2. 2Type 'Slack' in the search box and select the Slack app
  3. 3Scroll to find 'New Reaction Added' and click it
  4. 4Click 'Connect Slack' and authenticate with your workspace via OAuth
  5. 5Set the 'Reaction' field to the emoji your team will use, e.g. 'clipboard' (without colons)
What you should see: You should see a green 'Connected' badge next to your Slack account name, and the trigger configuration panel will show your selected emoji and workspace.
Common mistake — The 'New Reaction Added' trigger fires for every reaction matching that emoji — across your entire workspace, not just customer channels. If a popular emoji is chosen, you'll process noise. Pick an uncommon one like 📋 or 🗂️ that your team won't accidentally use.
Pipedream
+
click +
search apps
Slack
SL
Slack
Add the Slack trigger: New R…
Slack
SL
module added
3

Workflow Canvas > Trigger Step > Generate Test Event

Test the Slack trigger to capture a sample event

Before building further steps, you need a real event payload to map fields against. Click 'Generate Test Event' in the trigger panel. Then go to your Slack workspace and add your chosen reaction to any message in a customer channel. Return to Pipedream — within 15 seconds you should see the event data populate in the trigger output panel on the right side of the screen.

  1. 1Click 'Generate Test Event' in the Slack trigger panel
  2. 2Switch to your Slack workspace and react to any message with your designated emoji
  3. 3Return to Pipedream and wait 10-15 seconds
  4. 4Click the test event that appears to expand the full JSON payload
What you should see: You should see a JSON object with fields including event.item.channel, event.item.ts, event.user, and event.reaction. The message text is not included in the reaction event payload — you'll fetch it in the next step.
Common mistake — The reaction event payload does NOT include the original message text. It only gives you the channel ID and message timestamp (ts). You must make a separate Slack API call in the next step to fetch the message content.
Pipedream
▶ Deploy & test
executed
Slack
Copper
Copper
🔔 notification
received
4

Workflow Canvas > + Add a step > Run Node.js code

Fetch the original Slack message text via API

Click '+ Add a step' below the trigger, then select 'Run Node.js code'. This step calls the Slack conversations.history API with the channel ID and timestamp from the trigger to retrieve the actual message text. You'll use the Pipedream $ helper and your connected Slack account's OAuth token to make the authenticated request. Paste the code from the pro_tip_code section into this code step.

  1. 1Click '+ Add a step' below the Slack trigger
  2. 2Select 'Run Node.js code' from the step type list
  3. 3Rename the step to 'fetch_message' by clicking the step title
  4. 4Paste the Node.js code into the code editor
  5. 5Click 'Test' to run the step against your sample event
What you should see: The step output panel should show a JSON object with a messages array. The first item's text field contains the original Slack message content.
Common mistake — The conversations.history API requires the channels:history OAuth scope on your Slack app. If you get a 'missing_scope' error, your connected Slack account doesn't have this permission — you'll need to reconnect with an account that has the correct scopes.
5

Workflow Canvas > + Add a step > Run Node.js code

Identify the Copper contact by email or name

Add another Node.js code step. This step calls the Copper People Search API to find the contact that matches the Slack channel name or a known email. The simplest approach: parse the channel name (e.g. #client-acme) to extract 'acme', then query Copper's /v1/people/search endpoint with that term. If your Slack channel names don't map to company names, you'll need a lookup table or to use a Slack user's email instead.

  1. 1Click '+ Add a step' below the fetch_message step
  2. 2Select 'Run Node.js code'
  3. 3Rename the step to 'find_copper_contact'
  4. 4Paste your contact lookup code referencing steps.trigger.event.item.channel
  5. 5Click 'Test' and verify the output includes a valid Copper contact ID
What you should see: The step output should include a numeric Copper person ID and the contact's full name, confirming the right record was matched.
Common mistake — Copper's People Search is fuzzy — if your channel name is 'client-acme-corp' and the Copper company is listed as 'Acme Corporation', the search may return zero results or the wrong contact. Normalize your channel names to match Copper company names exactly, or maintain a Pipedream data store with channel-to-contact-ID mappings.
6

Workflow Canvas > + Add a step > Run Node.js code

Fetch the Slack user's display name

The reaction event gives you a Slack user ID (e.g. U04XYZABC), not a human name. Add a Node.js step that calls the Slack users.info API to resolve this to a real name. This matters because the Copper activity log will show who logged the message — 'sarah.jones' is more useful than 'U04XYZABC' when a manager reviews the contact history.

  1. 1Click '+ Add a step' below find_copper_contact
  2. 2Select 'Run Node.js code'
  3. 3Rename the step to 'resolve_slack_user'
  4. 4Call the Slack users.info API using the event.user field from the trigger
  5. 5Click 'Test' and confirm the output includes the user's real_name or display_name
What you should see: Step output should include the Slack user's display name and email, ready to include in the Copper activity note body.
7

Workflow Canvas > + Add a step > Run Node.js code

Create a Copper Activity (Note) via API

Add a final Node.js code step that POSTs to Copper's /v1/activities endpoint. Set the parent type to 'person', the parent ID to the contact ID found in step 5, and the details field to a formatted string combining the message text, the Slack user's name, the channel name, and the message timestamp. Copper's activity API uses an activity_type_id — the default 'Note' type ID is typically 0, but you should verify this in your Copper account under Settings > Activity Types.

  1. 1Click '+ Add a step' below resolve_slack_user
  2. 2Select 'Run Node.js code'
  3. 3Rename the step to 'create_copper_activity'
  4. 4Reference the contact ID from steps.find_copper_contact.$return_value
  5. 5POST to https://api.copper.com/developer_api/v1/activities with your API key and user email headers
  6. 6Click 'Test' and confirm the response includes a numeric activity ID
What you should see: Copper's API returns a 200 response with a JSON body containing the new activity's ID and a details field matching the message you sent. Check the contact record in Copper — the note should appear under the Activity tab within seconds.
Common mistake — Copper requires two authentication headers on every API request: X-PW-AccessToken (your API key) and X-PW-UserEmail (the email of a valid Copper user). Missing either one returns a 401. Store both in Pipedream's Environment Variables, not hardcoded in the step.
8

Workflow Canvas > find_copper_contact step > Settings > Error Handling

Add error handling for failed contact lookups

Not every Slack channel will have a matching Copper contact. Add a conditional check after the find_copper_contact step: if the contact ID is null or empty, route to a fallback step that sends a Slack DM to the user who added the reaction, telling them the contact wasn't found in Copper. This prevents silent failures and keeps your team informed. In Pipedream, use a 'Continue workflow on error' setting or an explicit null check in code.

  1. 1Click the find_copper_contact step to open its settings panel
  2. 2Scroll to the 'Error Handling' section
  3. 3Toggle on 'Continue workflow on error'
  4. 4Add an if/else check at the top of your create_copper_activity step: if no contact ID, call $.flow.exit('No matching Copper contact found')
  5. 5Optionally add a Slack 'Send a Direct Message' step to alert the user who reacted
What you should see: When a reaction is added in a channel with no matching Copper contact, the workflow exits cleanly and (optionally) the user receives a Slack DM explaining the lookup failed.
9

Workflow Canvas > Deploy button (top right) > Executions tab

Deploy and test end-to-end

Click 'Deploy' in the top-right corner of the workflow canvas. The workflow is now live and listening for reactions. Go to Slack, find a real customer message in a customer channel, and add your designated reaction emoji. Watch the Pipedream workflow executions list — you should see a new run appear within 5 seconds. Click into the run to see each step's input/output and confirm all steps passed green.

  1. 1Click the blue 'Deploy' button in the top-right of the canvas
  2. 2Switch to your Slack workspace and add the designated emoji reaction to a customer message
  3. 3Return to Pipedream and click the 'Executions' tab in the left sidebar
  4. 4Click the most recent execution to open the run detail view
  5. 5Check each step's output for green status indicators
What you should see: All steps should show green checkmarks. The final step's output should include the Copper activity ID. Opening the Copper contact record should show the new Note in the Activity timeline with the message text, sender name, and timestamp.

This single Node.js step handles all four API calls in sequence — fetch the Slack message, resolve the user's name, look up the Copper contact, and create the activity — with proper error handling at each stage. Paste this into a single 'Run Node.js code' step after your Slack trigger, replacing the four separate steps if you want a more compact workflow.

JavaScript — Code Stepimport axios from 'axios';
▸ Show code
import axios from 'axios';
export default defineComponent({
  async run({ steps, $ }) {

... expand to see full code

import axios from 'axios';

export default defineComponent({
  async run({ steps, $ }) {
    const SLACK_TOKEN = process.env.SLACK_BOT_TOKEN;
    const COPPER_API_KEY = process.env.COPPER_API_KEY;
    const COPPER_USER_EMAIL = process.env.COPPER_USER_EMAIL;
    const CUSTOMER_CHANNEL_PREFIX = 'client-';

    const { channel, ts, user } = steps.trigger.event.item;

    // 1. Fetch the original message text
    const historyRes = await axios.get('https://slack.com/api/conversations.history', {
      headers: { Authorization: `Bearer ${SLACK_TOKEN}` },
      params: { channel, latest: ts, inclusive: true, limit: 1 },
    });
    if (!historyRes.data.ok) throw new Error(`Slack history error: ${historyRes.data.error}`);
    const message = historyRes.data.messages?.[0];
    if (!message) $.flow.exit('Message not found in channel history');
    const messageText = message.text;

    // 2. Resolve Slack user display name
    const userRes = await axios.get('https://slack.com/api/users.info', {
      headers: { Authorization: `Bearer ${SLACK_TOKEN}` },
      params: { user },
    });
    if (!userRes.data.ok) throw new Error(`Slack user error: ${userRes.data.error}`);
    const displayName = userRes.data.user?.profile?.display_name || userRes.data.user?.real_name || 'Unknown';

    // 3. Resolve channel name and guard against non-customer channels
    const chanInfoRes = await axios.get('https://slack.com/api/conversations.info', {
      headers: { Authorization: `Bearer ${SLACK_TOKEN}` },
      params: { channel },
    });
    if (!chanInfoRes.data.ok) throw new Error(`Slack channel error: ${chanInfoRes.data.error}`);
    const channelName = chanInfoRes.data.channel?.name || '';
    if (!channelName.startsWith(CUSTOMER_CHANNEL_PREFIX)) {
      $.flow.exit(`Skipped: #${channelName} is not a customer channel`);
    }
    const companySearchTerm = channelName.replace(CUSTOMER_CHANNEL_PREFIX, '').replace(/-/g, ' ');

    // 4. Search for matching Copper contact
    const copperHeaders = {
      'X-PW-AccessToken': COPPER_API_KEY,
      'X-PW-UserEmail': COPPER_USER_EMAIL,
      'X-PW-Application': 'developer_api',
      'Content-Type': 'application/json',
    };
    const searchRes = await axios.post(
      'https://api.copper.com/developer_api/v1/people/search',
      { page_size: 5, name: companySearchTerm },
      { headers: copperHeaders }
    );
    const contacts = searchRes.data;
    if (!contacts || contacts.length === 0) {
      $.flow.exit(`No Copper contact found for search term: ${companySearchTerm}`);
    }
    if (contacts.length > 1) {
      console.log(`Multiple contacts matched: ${contacts.map(c => c.name).join(', ')} — using first result`);
    }
    const contact = contacts[0];

    // 5. Build the note body
    const messageDate = new Date(parseFloat(ts) * 1000).toISOString().replace('T', ' ').substring(0, 16) + ' UTC';
    const permalink = `https://slack.com/archives/${channel}/p${ts.replace('.', '')}`;
    const noteBody = [
      `[Slack Log] ${messageDate}`,
      `Channel: #${channelName}`,
      `Logged by: ${displayName}`,
      '',
      messageText,
      '',
      permalink,
    ].join('\n');

    // 6. Create Copper Activity
    const activityRes = await axios.post(
      'https://api.copper.com/developer_api/v1/activities',
      {
        parent: { id: contact.id, type: 'person' },
        type: { category: 'note' },
        details: noteBody,
      },
      { headers: copperHeaders }
    );

    return {
      copper_activity_id: activityRes.data.id,
      contact_name: contact.name,
      contact_id: contact.id,
      channel_name: channelName,
      logged_by: displayName,
      message_preview: messageText.substring(0, 100),
    };
  },
});

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 Pipedream for this if your team has even minimal comfort reading Node.js — you don't need to write the code from scratch, but you do need to paste it and understand what the variables mean. Pipedream's webhook processing is genuinely fast here: the Slack reaction event hits Pipedream's endpoint, the four API calls run in under 3 seconds, and the Copper note appears before the reacting user has switched tabs. The other reason to pick Pipedream: you need four separate API calls in sequence (Slack history, Slack users.info, Copper search, Copper create), and chaining those in a no-code tool like Zapier requires four separate steps with fragile field mapping. In Pipedream, it's one code block with real error handling. The one scenario where you'd pick something else: if your entire team is non-technical and you need others to modify the workflow without touching code — use Make instead.

Cost

Pipedream's free tier gives you 10,000 workflow executions per month. Each Slack reaction that triggers this workflow costs exactly one execution. At 50 customer conversations logged per day, that's 1,500 executions per month — well inside the free tier. If you scale to 400+ logged messages per day, you'll cross into Pipedream's Basic plan at $19/month. Compare that to Zapier, where this workflow requires a multi-step Zap on the Professional plan at $49/month minimum. Make's free tier covers 1,000 operations per month, but this workflow uses roughly 8 operations per run (each API call counts), so you'd hit the ceiling at 125 reactions/month — faster than you'd expect.

Tradeoffs

Zapier can connect Slack reactions to Copper using their native app integrations, but Zapier has no native Copper 'Create Activity' action — you'd need to use Zapier's Webhooks step to hit the Copper API directly, which puts you in the same code-adjacent territory as Pipedream but with less flexibility and a higher price. Make handles the sequential API calls well using its HTTP modules and has a cleaner visual flow for non-developers, but Make's error handling for null contact lookups requires conditional routing that gets messy fast. n8n is a strong alternative if you self-host — the Copper and Slack nodes exist, and the workflow builder is visual enough for technical non-coders, plus self-hosting eliminates per-execution cost entirely. Power Automate has no Copper connector at all, making it a non-starter here. Pipedream wins because the code step is the right tool for this specific chain of API calls, and the execution speed on webhooks is the fastest of any platform tested.

Three things you'll hit after setup. First: Copper's People Search is not exact-match. If your Slack channel is #client-acme and Copper has the company listed as 'Acme Inc.' with the contact under a different name, the search returns zero results. Build your channel-to-contact-ID lookup table in Pipedream's built-in Data Store early, not after you've had three logging failures. Second: Slack's conversations.history API has a rate limit of 50 requests per minute per workspace. If your team all simultaneously reacts to a burst of messages, you'll hit 429 errors. Add a try/catch with a 1-second retry delay in your code step. Third: Copper's activity API requires the activity_type category OR the type ID — not both. If you pass both and they're inconsistent, the API returns a 422 with a vague 'unprocessable entity' message. Use category: 'note' and omit the numeric ID, or fetch the exact ID from /v1/activity_types first.

Ideas for what to build next

  • Log Copper to Slack: close the loopBuild a reverse workflow that posts to the customer's Slack channel whenever a Copper deal stage changes — so the team sees CRM updates without leaving Slack.
  • Add a daily digest to CopperCreate a scheduled Pipedream workflow that summarizes all Slack-logged activities from the past 24 hours and posts them as a single batch note on each contact, reducing notification noise.
  • Attach message to Copper Opportunity, not just ContactExtend the find_copper_contact step to also search for open Opportunities linked to that contact, and log the Slack message against the active deal record for better pipeline visibility.

Related guides

Was this guide helpful?
Slack + Copper overviewPipedream profile →