Skip to content

People API

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 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 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: employment fields (status, basis, dates, location)
  • Executive / Head (subtree) / Manager (direct reports): contact and demographics fields
  • Executive only: financial fields (bank account, super fund details), xero_mapping (link status and last sync timestamp)

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 head on people in their subtree
  • Manager: Notes visible to manager on 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"
}
]
}

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.

Update a note’s content and/or visibility.

Permission: Executive only

Request:

{
"content": "<p>Updated content</p>",
"visibility": ["executive", "head"]
}

Response: { "ok": true }

Delete a note permanently.

Permission: Executive only

Response: { "ok": true }

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"
}

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.

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.

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.

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 }

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 }

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" }