Scripe relaunch is live.See what's new

API v1 · Resources

CRM sync

CRM sync sends the people who engage with your LinkedIn posts — the people in Community → Leads — to your CRM, with their Fit, their engagements, their signals and their company. HubSpot and Attio are connected from Settings → Integrations → CRM in Scripe. For anything else, use the signed webhook described here, or pull the same people from the API.

CRM sync is in a pilot: it is available to invited workspaces on the Business plan, and a workspace admin connects it.


What a person looks like

The webhook and the pull endpoint carry the same object:

json
{
  "id": "evt_7f3c2a9e1b5d4c08a6f2e1d3c4",
  "type": "person.qualified",
  "provenance": "LINKEDIN_ENGAGER",
  "person": {
    "id": "sp_h3k7q2m9x4b8c1v6n5z0w2r7ty",
    "firstName": "Ada",
    "lastName": "Lovelace",
    "fullName": "Ada Lovelace",
    "headline": "Head of Growth at Acme",
    "title": "Head of Growth",
    "linkedinUrl": "https://www.linkedin.com/in/ada",
    "email": null
  },
  "company": {
    "id": "9c2b7e41f0a35d68",
    "name": "Acme",
    "domain": "acme.io",
    "linkedinUrl": "https://www.linkedin.com/company/acme",
    "industry": "Software",
    "size": "51-200",
    "fit": 3.8,
    "peopleEngaged": 3
  },
  "fit": {
    "score": 4.2,
    "band": "BALANCED",
    "label": "Good fit",
    "person": 4.5,
    "company": 3.8,
    "reasons": ["Head of Growth", "Software, 51-200"]
  },
  "signal": { "strength": 78, "intent": "QUESTION" },
  "engagements": {
    "count": 4,
    "comments": 2,
    "reactions": 2,
    "reposts": 0,
    "mentions": 0,
    "firstAt": "2026-09-30T08:02:00.000Z",
    "lastAt": "2026-10-08T09:12:00.000Z",
    "profiles": ["Eva Example"],
    "lastPostUrl": "https://www.linkedin.com/feed/update/urn:li:share:7380000000000000000",
    "latest": {
      "kind": "COMMENT",
      "excerpt": "How do you handle onboarding for larger teams?",
      "postTitle": "Three things we changed in onboarding",
      "postUrl": "https://www.linkedin.com/feed/update/urn:li:share:7380000000000000000",
      "at": "2026-10-08T09:12:00.000Z"
    }
  },
  "source": "Engager",
  "url": "https://scripe.io/org_.../community/leads?person=sp_h3k7q2m9x4b8c1v6n5z0w2r7ty",
  "occurredAt": "2026-10-08T09:12:00.000Z"
}
  • person.id is the key to store. It is stable for a person in your workspace and the same on every event and every pull, so upsert on it. It is opaque: it is not LinkedIn's identifier, and nothing of LinkedIn's internal ids is ever sent.
  • company.id is Scripe's key for the company, the same for every person who works there.
  • email is always null. People who engage on LinkedIn share no email address with Scripe.
  • fit.score runs from 1 to 5. band and label say the same thing in words: Strong fit, Good fit, Partial fit, Not a fit.
  • engagements.latest.excerpt is only present for 48 hours after the comment was made; after that it is null. Names and headlines are refreshed from LinkedIn within a day.
  • An admin can switch individual fields off (headline, Fit, the reasons, signal, engagements, profiles, source, the link back); a field that is switched off arrives as null. person.id, the name, the LinkedIn URL and the title always travel.
  • Fields are added over time. Ignore fields you do not recognise.

The webhook

Add the webhook in Settings → Integrations → CRM: an https address that is reachable from the internet. Scripe shows the signing secret (whsec_…) once; save it before you close the dialog. A new secret can be made there at any time, and the old one stops at once.

Events

typeSent when
person.qualifiedA person is sent for the first time — by hand from Community → Leads, or by the automatic rule once their Fit reaches the workspace's minimum.
person.updatedA person you already received engaged again, or their Fit changed.
person.objectedA person objected to Scripe processing their data. See below.

person.objected carries the person's key and nothing else:

json
{
  "id": "evt_obj_4c1e7a9d2b5f",
  "type": "person.objected",
  "person": { "id": "sp_h3k7q2m9x4b8c1v6n5z0w2r7ty" },
  "occurredAt": "2026-10-09T12:00:00.000Z"
}

Scripe sends that person nothing more, in any workspace. What you do with the copy you hold is yours to decide — typically, delete it or mark it.

Request

http
POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: Scripe-Webhooks/1.0
X-Scripe-Event: person.qualified
X-Scripe-Delivery: evt_7f3c2a9e1b5d4c08a6f2e1d3c4
X-Scripe-Attempt: 1
Webhook-Signature: t=1760000000,v1=5257a869e7…

X-Scripe-Delivery is the event's id and stays the same on every retry of one delivery, so dedupe on it. Several people may arrive in one second; each is its own request.

Verifying the signature

The signature is the same as for Scripe's other webhooks: HMAC-SHA-256 over "<t>.<raw body>" with your signing secret, hex encoded, in Webhook-Signature as t=<unix seconds>,v1=<hex>. See Verifying signatures for code, and reject timestamps more than five minutes old.

Answers, retries and pausing

  • Answer 2xx within 10 seconds. Process the person afterwards.
  • 429 and 5xx answers, and timeouts, are retried — 8 attempts in all, 1 minute, 5 minutes, 30 minutes, 2 hours, 6, 8 and 10 hours apart (about 27 hours). A Retry-After header is honoured.
  • Other 4xx answers are retried on the same schedule, in case you fix the receiver.
  • 401, 403 and 410 mean your endpoint no longer accepts Scripe: delivery stops and the admin who added the webhook is told. Resume it in Settings → Integrations → CRM once it is fixed.
  • Redirects are not followed: point the webhook at its final address.

Pulling people

http
GET /v1/crm-people?limit=50&minFit=3.5
Authorization: Bearer <OAuth access token>

For an integration that polls instead of receiving the webhook. It answers the people CRM sync would send — the workspace's engagers with a Fit at or above minFit, newest engagement first — as the objects above.

  • OAuth only, scope people:read. An API key is answered 403 scope_missing: CRM sync is offered per person, so a pull is judged as the person the token acts for. people:read is never part of the read or write alias: request it by name, so no existing connection gains it.
  • A workspace admin (403 admin_required otherwise), on a plan that includes CRM sync (403 plan_not_eligible otherwise), in a workspace where CRM sync is available to them (404 not_found otherwise).
  • Every pull that returns people is recorded in the workspace's audit log first; if that record cannot be written the answer is 503 service_unavailable and nothing is returned. Retry.
  • People are judged the moment you pull, as they are when they are sent: someone who objected, someone your workspace hid or excluded, and someone whose name Scripe cannot refresh right now are left out. A page can therefore hold fewer people than limit; keep paging while pagination.has_more is true.
ParameterDefaultNotes
limit501–100; above 100 is clamped.
cursor—pagination.next_cursor of the previous page. Opaque: pass it back unchanged. It is valid only for the workspace that issued it; anything else is 400 bad_cursor, and the loop starts over without one.
minFit3.51–5.
since—ISO time: only people whose last engagement is at or after it. Use the time of your previous poll.
json
{
  "data": [ { "id": "evt_p_…", "type": "person.qualified", "person": { "id": "sp_…" } } ],
  "pagination": { "next_cursor": "v1.Hq3…", "has_more": true }
}

The full schema is in the OpenAPI reference tab under CRM sync.