ReplySparrow Docs

Two-way CRM sync

Push CRM contacts and first-party signals into ReplySparrow, and mirror rejected leads back so nobody gets contacted twice.

Why sync both ways

Your CRM knows who your customers, open deals, and opt-outs are. ReplySparrow knows who your agents found, who replied, and who a teammate rejected. When the two disagree, the same person gets two first messages, or a customer gets a cold invitation.

A two-way sync fixes that:

  • CRM to ReplySparrow. Push the contacts you want to work, and the actions they take in your product (trial signup, demo booked), as signals next to the ones ReplySparrow detects.
  • ReplySparrow to CRM. Mark rejected leads in the CRM, with the reason, so no rep picks them up again.
  • Both ways. Reject in ReplySparrow anyone your sales team already works, so campaigns skip them.

Send Authorization: Bearer YOUR_API_KEY. The base URL is https://app.replysparrow.com/api/v1. The key needs the contacts:read, contacts:write, and signals:write scopes. With the MCP server, your assistant runs the tools named in each step for you.

1. Push a CRM contact into ReplySparrow

POST /contacts creates the contact. first_name, last_name, and linkedin_url (the LinkedIn profile) are required. Pass list_id to drop the contact straight into a list a campaign reaches out to, or agent_id to have that source agent score it against its ICP.

MCP tool: contact_create, which takes full_name, linkedin_url, and an optional list_id.

curl --request POST \
  --header "Authorization: Bearer YOUR_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "first_name": "John",
    "last_name": "Doe",
    "linkedin_url": "https://www.linkedin.com/in/johndoe",
    "email": "john@techcorp.com",
    "company": "Tech Corp",
    "title": "Head of Sales",
    "list_id": "9f8e7d6c-0000-4000-8000-000000000002"
  }' \
  "https://app.replysparrow.com/api/v1/contacts"

The response (201) is the new contact. Store its id on the CRM record, in a field such as "ReplySparrow ID": every following call needs it.

ReplySparrow keeps one contact per LinkedIn profile in a workspace, so look the person up first with GET /contacts?search=john@techcorp.com (the search matches name, email, company, website, location, and job title) and reuse the id you find instead of creating a second contact.

When the person is in your excluded contacts, for example because their contact was deleted earlier, no contact is created and the call fails:

{
  "error": "contact_already_excluded",
  "detail": { "excluded_contact_id": "0192c4a1-7d2e-7f3a-9b10-5e6f7a8b9c0d", "kind": "linkedin", "value": "linkedin.com/in/johndoe" }
}

The person stays blocked until you remove the exclusion with Delete an excluded contact.

2. Push first-party signals

POST /contacts/{id}/signals adds what the lead did in your product or stack, next to the signals ReplySparrow agents detect. Describe the event in intent, add a short label in intent_keyword, and leave intent_type empty.

MCP tool: signals_manage, with action: "ingest", the contact as target_id, type: "custom", and the event as title. In a CRM workflow, backend, Zapier, Make, or n8n, call the API.

curl --request POST \
  --header "Authorization: Bearer YOUR_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{ "intent": "Booked a demo from the pricing page", "intent_keyword": "demo_booked" }' \
  "https://app.replysparrow.com/api/v1/contacts/8f1c2c3e-5b7a-4a51-9d0e-2b6f8f6d9c11/signals"

The response is { "created": true, "contact": { ... } }, with the contact's signals most recent first. A signal without intent_type is recorded on every call, so send each event once: a retry adds a duplicate.

3. Keep campaigns away from people sales already works

When a CRM contact becomes a customer, gets an open deal, or opts out, reject them in ReplySparrow. POST /contacts/{id}/reject works like the Reject button in the app: the contact's agent stops working it, pending first-touch drafts are rejected, and any campaign still running for it stops sending. contact_known_or_duplicate is the reason for "already known".

MCP tool: lead_action, with action: "skip" and the reason as text.

curl --request POST \
  --header "Authorization: Bearer YOUR_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{ "reason": "contact_known_or_duplicate" }' \
  "https://app.replysparrow.com/api/v1/contacts/8f1c2c3e-5b7a-4a51-9d0e-2b6f8f6d9c11/reject"

Other reason codes cover a competitor, a wrong company industry or type, size, or location, a wrong title or seniority, outdated data, or an irrelevant signal, and other takes a free-text reason_text. A rejection is reversible with POST /contacts/{id}/unreject (API only), which puts the contact back in its agent's pipeline to be scored again, not in a campaign: stopped campaign steps are not restarted.

For people who must never be imported again, DELETE /contacts/{id} removes the contact and adds its LinkedIn profile and email to your excluded contacts, so agents skip them and POST /contacts refuses them. Deleting cannot be undone: removing the exclusion later does not restore the contact.

4. Mirror rejected leads back to the CRM

Rejections made in ReplySparrow, by a teammate in the app or through the API, carry rejected_at, rejected_by_user_id, rejection_reason, and rejection_reason_text on the contact, and its state is rejected. Read them with GET /contacts, a list at a time with list_id, and copy every contact whose rejected_at is set onto its CRM record.

MCP tool: leads_list with status: "skipped", or contact_get for one contact. Try asking: "Which leads did my team reject this week, and why?"

curl --header "Authorization: Bearer YOUR_API_KEY" \
  "https://app.replysparrow.com/api/v1/contacts?list_id=9f8e7d6c-0000-4000-8000-000000000002&page=1&limit=100"
{
  "contacts": [
    {
      "id": "8f1c2c3e-5b7a-4a51-9d0e-2b6f8f6d9c11",
      "full_name": "Ada Lovelace",
      "email": "ada@example.com",
      "state": "rejected",
      "rejected_at": "2026-10-13 09:12:44",
      "rejected_by_user_id": "usr_17",
      "rejection_reason": "company_wrong_size",
      "rejection_reason_text": null
    }
  ],
  "total": 240,
  "page": 1,
  "limit": 100,
  "totalPages": 3
}

Set the CRM record to "Disqualified" or "Do not contact", and keep the reason. Walk page through totalPages, and run the scan on a schedule, for example every night.

The webhooks tell you when an agent qualifies a new lead (On Lead Created) and when a lead replies (On Lead Reply), signed with x-kairo-signature, which keeps the rest of the CRM record current. A rejection does not send one, so the scheduled scan is what catches rejections.

Every field is listed on Create a contact, Add a signal, Reject a lead, and List contacts.

On this page