People API
People Endpoints
Section titled “People Endpoints”GET /api/people
Section titled “GET /api/people”Get all synced people from Productive.
Permission: view:people
Response:
{ "people": [ { "id": "uuid", "productive_id": "12345", "user_id": "user-uuid-or-null", "first_name": "Jane", "last_name": "Doe", "email": "jane@dotcollective.com.au", "title": "Senior Developer", "avatar_url": "https://...", "manager_id": "uuid-of-manager", "synced_at": "2026-03-03T10:00:00" } ], "synced_at": "2026-03-03T10:00:00", "stale": false}Data is cached in D1 with a 6-hour TTL. If stale, a background refresh is triggered automatically while serving the cached data.
GET /api/people/org-chart
Section titled “GET /api/people/org-chart”Get people structured as a tree for org chart visualization.
Permission: view:people
Response:
{ "roots": [ { "id": "uuid", "productive_id": "12345", "user_id": "user-uuid", "first_name": "Jane", "last_name": "Doe", "title": "CEO", "avatar_url": "https://...", "manager_id": null, "synced_at": "2026-03-03T10:00:00", "children": [ { "id": "uuid", "first_name": "John", "last_name": "Smith", "title": "VP Engineering", "children": [] } ] } ], "synced_at": "2026-03-03T10:00:00", "stale": false}People with no manager are tree roots. Children are sorted alphabetically by name at each level.
GET /api/people/:id/profile
Section titled “GET /api/people/:id/profile”Get employee profile fields for a person (permission-scoped). All data is read directly from the people table (the source of truth for employee/HR data). The person does not need a linked user_id — this enables profiles for people synced from Xero who don’t have an app account.
Permission: view:people (data scoping varies by access level)
Response:
{ "profile": { "employment": { "start_date": "2023-01-15", "end_date": null, "employment_status": "Active", "employment_basis": "FULLTIME", "location": "Melbourne" }, "contact": { "phone": "03 1234 5678", "mobile": "0400 123 456", "personal_email": "jane@gmail.com", "home_address": "{\"AddressLine1\":\"123 Main St\",\"City\":\"Melbourne\"}" }, "demographics": { "gender": "F", "date_of_birth": "1990-05-20" }, "financial": { "super_fund_name": "Australian Super", "super_fund_type": "REGULATED", "super_member_number": "123456789", "bank_account_name": "Jane Doe", "bank_bsb": "063-000", "bank_account_number": "12345678" } }}Data scoping by access level:
- All levels:
employmentfields (status, basis, dates, location) - Executive / Head (subtree) / Manager (direct reports):
contactanddemographicsfields - Executive only:
financialfields (bank account, super fund details),xero_mapping(link status and last sync timestamp)
GET /api/people/:id/notes
Section titled “GET /api/people/:id/notes”List confidential notes for a person, filtered by the viewer’s access level against each note’s visibility setting.
Permission: view:people (data scoping by access level + manager hierarchy)
Access:
- Executive: All notes on any person
- Head: Notes visible to
headon people in their subtree - Manager: Notes visible to
manageron their direct reports
Response:
{ "notes": [ { "id": "uuid", "person_id": "person-uuid", "author_id": "user-uuid", "author_name": "Jane Doe", "author_picture": "https://...", "content": "<p>Performance discussion notes...</p>", "visibility": "executive,head,manager", "created_at": "2026-03-07T09:00:00", "updated_at": "2026-03-07T09:00:00" } ]}POST /api/people/:id/notes
Section titled “POST /api/people/:id/notes”Add a confidential note to a person’s profile.
Permission: Same access rules as GET
Request:
{ "content": "<p>Note content (HTML)</p>", "visibility": ["executive", "head", "manager"]}Response: { "id": "uuid" } (201)
The visibility array controls which access levels can see the note. executive is always included regardless of input.
PUT /api/people/:id/notes/:noteId
Section titled “PUT /api/people/:id/notes/:noteId”Update a note’s content and/or visibility.
Permission: Executive only
Request:
{ "content": "<p>Updated content</p>", "visibility": ["executive", "head"]}Response: { "ok": true }
DELETE /api/people/:id/notes/:noteId
Section titled “DELETE /api/people/:id/notes/:noteId”Delete a note permanently.
Permission: Executive only
Response: { "ok": true }
GET /api/people/:id/xero-matches
Section titled “GET /api/people/:id/xero-matches”Find matching Xero employees for a person. If exactly one high-confidence match is found (email or full name), auto-links and returns auto_linked: true.
Permission: manage:people (executive only)
Response:
{ "matches": [ { "employee_id": "xero-guid", "first_name": "Jane", "last_name": "Doe", "email": "jane@dotcollective.com.au", "status": "ACTIVE", "confidence": "exact_email", "score": 100 } ], "auto_linked": true, "xero_employee_id": "xero-guid"}POST /api/people/:id/xero-link
Section titled “POST /api/people/:id/xero-link”Manually link a person to a Xero employee.
Permission: manage:people (executive only)
Request: { "xero_employee_id": "xero-guid" }
Response: { "ok": true }
Returns 409 if the Xero employee is already linked to another user.
LinkedIn Endpoints
Section titled “LinkedIn Endpoints”PUT /api/people/:id/linkedin-url
Section titled “PUT /api/people/:id/linkedin-url”Update a person’s LinkedIn URL.
Permission: manage:people
Request: { "linkedin_url": "https://linkedin.com/in/janedoe" }
Response: { "ok": true }
Pass null to remove the URL.
GET /api/people/:id/linkedin-activities
Section titled “GET /api/people/:id/linkedin-activities”Return cached LinkedIn activities for a person.
Permission: view:people (data scoped by access level — executive: all, head: subtree, manager: direct reports)
Response:
{ "activities": [ { "id": "uuid", "person_id": "person-uuid", "type": "post", "activity_id": "scrapin-id", "content": "Excited to announce...", "reaction_type": null, "related_post_text": null, "related_post_url": null, "reactions_count": 42, "comments_count": 5, "activity_date": "2026-03-01", "activity_url": "https://linkedin.com/feed/...", "fetched_at": "2026-03-09 10:00:00" } ], "fetched_at": "2026-03-09 10:00:00"}Activities are cached in D1. Types: post, comment, reaction.
POST /api/people/:id/linkedin-activities/fetch
Section titled “POST /api/people/:id/linkedin-activities/fetch”Trigger a fresh fetch from Scrapin (3 credits: posts + comments + reactions). Replaces cached activities.
Permission: manage:people
Response: Same as GET (fresh data)
Returns 400 if the person has no linkedin_url.
GET /api/people/:id/change-alerts
Section titled “GET /api/people/:id/change-alerts”Return change alert history and subscription status.
Permission: view:people (data scoped same as activities)
Response:
{ "alerts": [ { "id": "uuid", "person_id": "person-uuid", "subscription_id": "scrapin-sub-id", "linkedin_url": "https://linkedin.com/in/janedoe", "changes_detected": "[{\"field\":\"Title\",\"old\":\"Engineer\",\"new\":\"Senior Engineer\"}]", "received_at": "2026-03-08 14:30:00" } ], "subscription_id": "scrapin-sub-id", "subscribed_at": "2026-03-01 09:00:00"}POST /api/people/:id/change-alerts/subscribe
Section titled “POST /api/people/:id/change-alerts/subscribe”Subscribe to Scrapin profile change alerts.
Permission: manage:people
Response: { "ok": true, "subscription_id": "uuid" }
Returns 400 if no linkedin_url, 502 if Scrapin API fails.
POST /api/people/:id/change-alerts/unsubscribe
Section titled “POST /api/people/:id/change-alerts/unsubscribe”Unsubscribe from change alerts.
Permission: manage:people
Response: { "ok": true }
Webhook Endpoints
Section titled “Webhook Endpoints”POST /api/webhooks/scrapin
Section titled “POST /api/webhooks/scrapin”Public endpoint (no auth) that receives Scrapin change alert webhooks. Validates subscriptionId against known subscriptions in people.alert_subscription_id. Normalizes the incoming profile, computes diff against last snapshot, and stores the alert.
Request (from Scrapin):
{ "subscriptionId": "uuid", "data": { "person": { ... } }, "metadata": { "webhookType": "person" }}Response: { "ok": true }
Admin Connection Endpoints
Section titled “Admin Connection Endpoints”GET /api/admin/connections/scrapin/alerts-config
Section titled “GET /api/admin/connections/scrapin/alerts-config”Check if the Scrapin webhook URL is configured.
Permission: Executive only
Response: { "configured": true, "webhook_url": "https://app.nucleus.fast/api/webhooks/scrapin" }
POST /api/admin/connections/scrapin/alerts-config
Section titled “POST /api/admin/connections/scrapin/alerts-config”Register the webhook URL with Scrapin. Automatically constructs the URL from the request origin.
Permission: Executive only
Response: { "ok": true, "webhook_url": "https://app.nucleus.fast/api/webhooks/scrapin" }