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:
{
"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.idis 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.idis Scripe's key for the company, the same for every person who works there.emailis alwaysnull. People who engage on LinkedIn share no email address with Scripe.fit.scoreruns from 1 to 5.bandandlabelsay the same thing in words: Strong fit, Good fit, Partial fit, Not a fit.engagements.latest.excerptis only present for 48 hours after the comment was made; after that it isnull. 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
type | Sent when |
|---|---|
person.qualified | A 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.updated | A person you already received engaged again, or their Fit changed. |
person.objected | A person objected to Scripe processing their data. See below. |
person.objected carries the person's key and nothing else:
{
"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
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
2xxwithin 10 seconds. Process the person afterwards. 429and5xxanswers, 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). ARetry-Afterheader is honoured.- Other
4xxanswers are retried on the same schedule, in case you fix the receiver. 401,403and410mean 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
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 answered403 scope_missing: CRM sync is offered per person, so a pull is judged as the person the token acts for.people:readis never part of thereadorwritealias: request it by name, so no existing connection gains it. - A workspace admin (
403 admin_requiredotherwise), on a plan that includes CRM sync (403 plan_not_eligibleotherwise), in a workspace where CRM sync is available to them (404 not_foundotherwise). - 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_unavailableand 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 whilepagination.has_moreistrue.
| Parameter | Default | Notes |
|---|---|---|
limit | 50 | 1–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. |
minFit | 3.5 | 1–5. |
since | — | ISO time: only people whose last engagement is at or after it. Use the time of your previous poll. |
{
"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.