Skip to content

New adaptor: Copper CRM #1807

Description

@jackohilts

Request

We want to build a new adaptor for the Copper CRM Developer API (REST, JSON). Copper is a Google Workspace-native CRM.

Driving use case: "Create or update Copper leads from new lemlist activities" (Zapier template for reference). A lemlist activity (email sent/opened/replied/bounced, LinkedIn visit, call ended, etc.) arrives in OpenFn, and the workflow creates or updates the matching Lead in Copper. This issue covers the Copper side; the lemlist side is tracked in a sibling issue.

To start, this adaptor should:

  1. Handle authentication (API key, see Credentials below). Every request must send these headers, set automatically from state.configuration:

    Header Value
    X-PW-AccessToken API key
    X-PW-Application developer_api (constant)
    X-PW-UserEmail email of the user who generated the key
    Content-Type application/json
  2. Base URL: https://api.copper.com/developer_api/v1 (default; allow override via configuration.baseUrl). Do not use the old api.prosperworks.com domain.

  3. Generic HTTP helpers: request(method, path, body, options) plus get, post, put, delete so any unwrapped endpoint can be called.

  4. upsertLead(properties, match) wrapping PUT /leads/upsert. This is the core operation for the use case.

    • match is { field_name, field_value }; supported match fields are name, email, or a custom field.
    • Copper returns 422 when more than one lead matches (with the matching IDs if ≤30). Surface this as a clear error that includes the IDs, not a generic HTTP failure.
    • Also support the custom-field variant (PUT /leads/upsert by custom field, see docs) via an option.
  5. Lead helpers: getLead(id) (GET /leads/{id}), searchLeads(query, options) (POST /leads/search, with paging via page_number / page_size, max 200), createLead, updateLead(id, properties).

  6. createActivity(parent, activity) wrapping POST /activities, so a workflow can log the lemlist event (e.g. "Email replied in campaign X") on the Lead's timeline. parent is { type: 'lead', id }. Pair with a listActivityTypes() helper (GET /activity_types) so users can find the right activity_type.id.

  7. Lookup helpers that users need to build a valid lead payload: listLeadStatuses() (GET /lead_statuses), listCustomerSources() (GET /customer_sources), listCustomFieldDefinitions() (GET /custom_field_definitions), listUsers() (GET /users, for assignee_id).

  8. Rate limits: 180 requests/minute (rolling window), bulk endpoints 3 req/sec. On 429, back off and retry (configurable, sensible default). Don't hammer the API in each() loops.

  9. Results in state.data, consistent paging, and errors that pass through Copper's validation message without logging the API key, user email, or lead PII.

  10. Unit tests with mocks for every operation, plus JSDoc examples for each.

Out of scope for v1 (follow-up issues if needed): People, Companies, Opportunities, Projects, Tasks, file upload, OAuth2 partner flow, Copper webhook subscription management.

Credentials

  • Login credentials: No shared OpenFn account yet. Sign up for Copper's 14-day free trial (no card needed; trial includes API access). Use an OpenFn Google Workspace account, then save the login + API key to LastPass as Copper Trial - Adaptor Dev and note the expiry date here.
  • Test record(s): The trial workspace ships with sample data. Create 2 to 3 test Leads by hand (one with a custom field) so you can test both email-match and custom-field-match upserts, plus the 422 multiple-match case (create two leads with the same name).
  • Authentication method(s): API key + user email via custom headers (see table above). Generate the key in Copper under System Settings > API Keys > Generate API Key (admins can see all keys). Copper also supports OAuth2.0 for partner apps; not required for v1. Auth docs, request headers.
  • Suggested configuration-schema.json: apiKey (required, sensitive), userEmail (required), baseUrl (optional, defaults to the URL above).

Sample Code

// state.data is a lemlist activity payload (webhook body), see the lemlist adaptor issue
upsertLead(
  {
    name: `${$.data.leadFirstName} ${$.data.leadLastName}`,
    email: { email: $.data.leadEmail, category: 'work' },
    company_name: $.data.leadCompanyName,
    tags: ['lemlist', $.data.campaignName],
  },
  { field_name: 'email', field_value: $.data.leadEmail }
);

createActivity(
  { type: 'lead', id: $.data.id },
  {
    type: { category: 'user', id: 123456 }, // from listActivityTypes()
    details: `lemlist: ${$.references.at(-1).type} in "${$.references.at(-1).campaignName}"`,
  }
);

// generic escape hatch
post('/leads/search', { emails: ['someone@example.com'], page_size: 25 });

Resources

Logo Links

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Priority Level

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions