Teamspective Public API Documentation

Last updated: December 8, 2025

1. Overview

The Teamspective Public API allows you to synchronize your organization structure with Teamspective and access engagement survey results. This API is designed for HRIS integrations where you maintain the complete source of truth in your HRIS system and sync it to Teamspective.

Key Concepts:

- Full State Synchronization: The employee import endpoint replaces your Teamspective workspace state with the data you provide

- Source of Truth: Your HRIS or HR system should be the authoritative source - Teamspective mirrors that state

- Read-Only Endpoints: Use the query endpoints (teams, questions, cohorts, results) to extract engagement data

Base URL: https://app.teamspective.com/api/v1

API Version: 1.0

2. Authentication

All API requests require authentication using a Bearer token (API Key).

2.1. Obtaining an API Key

API keys must be requested from your Teamspective Customer Success Manager or support team:

1. Contact your CSM or email support@teamspective.com

2. Request a Public API key for your workspace

3. Store the key securely - it will only be provided once

2.2. Using the API Key

Include the API key in the Authorization header of all requests:

```http
Authorization: Bearer apikey_your_key_here
```

2.3. Security Best Practices

- Never commit API keys to version control

- Store keys in environment variables or secure credential managers

- Contact support immediately if a key is compromised to request a replacement

- Keep your API key confidential - it provides full access to your workspace data

3. Endpoints

3.1. Import Employees

Synchronize your complete organization structure with Teamspective. This endpoint replaces the current workspace state with the provided data.

Endpoint: POST /api/v1/employees

---

CRITICAL: Full State Replacement

This API performs a full synchronization of your workspace state. Any employee or team membership not included in the import will be removed:

- Employees not included in the import will be removed from the workspace

- Team memberships not included will be removed from those teams

- Always include ALL active employees and their complete team memberships in every import

- Partial updates are not supported - you must send the complete current state

- Empty imports are rejected as a safety measure to prevent accidental deletion

---

Request Body:

```json

{

  "employees": [

    {

      "employeeId": "EMP001",

      "email": "alice@example.com",

      "firstName": "Alice",

      "lastName": "Anderson",

      "language": "en",

      "managerExternalId": "MGR001",

      "managerUserEmail": "manager@example.com",

      "managerName": "Bob Manager",

      "startDate": "2023-01-15",

      "gender": "Female",

      "department": "Engineering",

      "title": "Senior Engineer",

      "unit": "Product",

      "costCenter": "CC001",

      "site": "Helsinki Office",

      "isManager": "Yes",

      "company": "Example Corp",

      "team": "Platform Team",

      "fte": "100",

      "seniority": "Senior",

      "employeeType": "Full-time",

      "birthday": "1990-05-20",

      "competence": "Backend Engineering",

      "officeCity": "Helsinki",

      "primaryRole": "Software Engineer",

      "groups": [

        {

          "id": "TEAM-ENG",

          "name": "Engineering",

          "parentId": null,

          "role": "admin",

          "surveyParticipant": true

        },

        {

          "id": "TEAM-BACKEND",

          "name": "Backend Team",

          "parentId": "TEAM-ENG",

          "role": "member",

          "surveyParticipant": true

        }

      ]

    },

    {

      "employeeId": "PROJ001",

      "loginCode": "PROJECT-ALPHA-001",

      "firstName": "Charlie",

      "lastName": "Chen",

      "groups": [

        {

          "id": "PROJECT-ALPHA",

          "name": "Project Alpha",

          "surveyParticipant": false

        }

      ]

    }

  ],

  "dryRun": false

}

```

Field Descriptions:

Field

Type

Required

Description

employeeId

string

Yes

Unique employee identifier from your system

groups[].id

string

Yes

Unique group identifier

groups[].name

string

Yes

Group name

birthday

string

No

Birthday (YYYY-MM-DD)

company

string

No

Company name (for multi-company workspaces)

competence

string

No

Competence area or specialty

costCenter

string

No

Cost center identifier

department

string

No

Department name

dryRun

boolean

No

If true, validates data and returns operations without making changes

employeeType

string

No

Employment type ("Full-time", "Part-time", etc.)

firstName

string

No

User's first name

fte

string

No

Full-time equivalent percentage ("100", "50", etc.)

gender

string

No

Gender

groups

array

No

List of groups/teams the user belongs to

groups[].parentId

string

No

Parent group ID for hierarchical structures

groups[].role

string

No

User role in group: "admin" or "member" (only these 2 roles supported)

groups[].surveyParticipant

boolean

No

Whether user participates in engagement surveys for this group

isManager

string

No

Whether user is a manager ("Yes" or "No")

language

string

No

Preferred language code (en, fi, sv, etc.)

lastName

string

No

User's last name

managerExternalId

string

No

Manager's employee ID (preferred)

managerName

string

No

Manager's full name (fallback display name)

managerUserEmail

string

No

Manager's email (use if managerExternalId unavailable)

officeCity

string

No

Office city location

primaryRole

string

No

Primary role in the organization

seniority

string

No

Seniority level

site

string

No

Office or site location

startDate

string

No

Employment start date (YYYY-MM-DD)

team

string

No

Team name

title

string

No

Job title

unit

string

No

Organizational unit

email

string

Conditional\*

Primary email address (for full users). Cannot be combined with loginCode

loginCode

string

Conditional\*

Passwordless login code (for lite users without email). Cannot be combined with email

\*Authentication Requirement: Exactly one of email or loginCode must be provided for each employee. Provide email for full users who should receive email notifications, or loginCode for lite users (e.g., project participants) who authenticate with a code only.

Response (200 OK):

```json

{

  "result": "Successfully synced employees",

  "details": {

    "userOperations": {

      "createUsers": [...],

      "addUsers": [...],

      "removeUsers": [...]

    },

    "groupOperations": {

      "groupsToAdd": [...],

      "groupsToRename": [...],

      "groupsToMove": [...],

      "groupUserOperations": [...]

    }

  }

}

```

Dry Run Response (200 OK):

```json

{

  "result": "Dry run complete",

  "details": {

    // Same structure as above, showing planned operations

  }

}

```

**Error Response (400 Bad Request):**

```json

{

  "status": "bad-request",

  "reason": "Validation failed",

  "errors": {

    "employees": {

      "0": {

        "email": "Invalid email"

      }

    }

  }

}

```

Notes:

- Full State Synchronization: This endpoint performs a complete state replacement. Any employee or team membership not included in the import will be removed. Always send the complete current state of your organization in each import. The API compares the incoming data with the current workspace state and:

- Creates new employees

- Updates existing employee attributes

- Removes employees not in the import from the workspace

- Adds users to teams specified in their groups array

- Removes users from teams where they previously had integration-sourced membership but are no longer listed

- User Authentication Types:

- **Full users** (with email): Regular users who receive email notifications. Provide email only, do not include loginCode.

- **Lite users** (with loginCode): Limited users without email (e.g., project participants). Provide loginCode only, do not include email.

- **Constraint:** Exactly one of email or loginCode must be provided per user

- **Dry Run:** Set dryRun: true to validate your payload and see what changes would be made without affecting the database. **Always test with dry run first** to verify the sync will perform as expected.

- **Safety Check:** Empty employee arrays are rejected to prevent accidental deletion of your entire workspace

- **Idempotency:** Importing the same data multiple times is safe - the API will only make necessary changes

- **Group Hierarchy:** Use parentId to create nested team structures. Example hierarchy:

```

Workspace (Acme Corp)

├── Engineering (parentId: null)

│ ├── Backend Team (parentId: TEAM-ENG)

│ │ └── Platform Squad (parentId: TEAM-BACKEND)

│ └── Frontend Team (parentId: TEAM-ENG)

├── Design (parentId: null)

│ ├── Product Design (parentId: TEAM-DESIGN)

│ └── Brand Design (parentId: TEAM-DESIGN)

└── Operations (parentId: null)

```

To create this structure, import employees with groups like:

```json

{

"employees": [

{

"employeeId": "EMP001",

"email": "alice@example.com",

"groups": [

{

"id": "TEAM-ENG",

"name": "Engineering",

"parentId": null,

"role": "admin"

},

{

"id": "TEAM-BACKEND",

"name": "Backend Team",

"parentId": "TEAM-ENG",

"role": "member"

},

{

"id": "TEAM-PLATFORM",

"name": "Platform Squad",

"parentId": "TEAM-BACKEND",

"role": "member"

}

]

}

]

}

```

- **Group Roles (only 2 roles supported for groups):**

- "admin" - Can manage team settings and members

- "member" - Regular team member

- Note: Workspace-level roles include additional options (`observer`, analyst), but group-level roles only support admin and member

- **Survey Participation:** Set surveyParticipant: false to exclude users from surveys for specific groups while keeping them as members

3.2. List Teams

Retrieve all teams/groups in the workspace, including hierarchy information.

**Endpoint:** GET /api/v1/teams

**Response (200 OK):**

```json

{

"result": "ok",

"data": [

{

"teamId": 123,

"teamName": "Engineering",

"parentTeamId": 100

},

{

"teamId": 124,

"teamName": "Backend Team",

"parentTeamId": 123

}

]

}

```

**Notes:**

- Workspace itself is included as a team with parentTeamId equal to its own teamId

- Use parentTeamId to understand team hierarchy

3.3. List Questions

Retrieve all engagement survey questions available in the workspace.

**Endpoint:** GET /api/v1/questions

**Response (200 OK):**

```json

{

"result": "ok",

"data": [

{

"questionId": 456,

"questionTag": "enps",

"title": "How likely are you to recommend this company?",

"name": "eNPS"

},

{

"questionId": 457,

"questionTag": "wellbeing",

"title": "How would you rate your current wellbeing?",

"name": "Wellbeing"

}

]

}

```

**Notes:**

- questionTag is a stable identifier for standard questions (e.g., "enps", "wellbeing")

- Use questionId or questionTag when fetching results

3.4. List Cohorts

Retrieve all cohort dimensions and values available in the workspace.

**Endpoint:** GET /api/v1/cohorts

**Response (200 OK):**

```json

{

"result": "ok",

"data": [

{

"key": "department",

"options": [

{

"cohortId": 789,

"value": "Engineering",

"count": 25

},

{

"cohortId": 790,

"value": "Design",

"count": 12

}

]

},

{

"key": "seniority",

"options": [

{

"cohortId": 791,

"value": "Senior",

"count": 15

},

{

"cohortId": 792,

"value": "Junior",

"count": 10

}

]

}

]

}

```

**Notes:**

- Cohorts are automatically generated from user attributes

- Use cohortId when filtering results by cohort

- count shows number of users in each cohort

3.5. Get Question Results

Retrieve aggregated engagement survey results for a specific question, filtered by team or cohort.

**Endpoint:** POST /api/v1/engagement/results/question

**Request Body (by Team):**

```json

{

"teamId": 123,

"questionId": 456

}

```

**Request Body (by Cohort):**

```json

{

"cohortId": 789,

"questionTag": "enps"

}

```

**Field Descriptions:**

| Field | Type | Required | Description |

| ------------- | ------ | ----------- | -------------------------------------------------------------------------- |

| teamId | number | Conditional | Filter results by team (mutually exclusive with cohortId) |

| cohortId | number | Conditional | Filter results by cohort (mutually exclusive with teamId) |

| questionId | number | Conditional | Question identifier (mutually exclusive with questionTag) |

| questionTag | string | Conditional | Question tag like "enps", "wellbeing" (mutually exclusive with questionId) |

**Constraints:**

- Must provide either teamId OR cohortId (not both)

- Must provide either questionId OR questionTag (not both)

**Response (200 OK):**

```json

{

"result": "ok",

"data": [

{

"tag": "enps",

"group": {

"groupType": "team",

"groupId": 123,

"teamName": "Engineering"

},

"series": [

{

"date": "2024-01-15",

"score": 45.5,

"answerCount": 20,

"distribution": {

"promoters": 12,

"passives": 6,

"detractors": 2

}

}

]

}

]

}

```

**Error Response (404 Not Found):**

```json

{

"message": "Team not found",

"status": "not-found"

}

```

**Error Response (403 Forbidden):**

```json

{

"message": "Unauthorized: Team does not belong to workspace",

"status": "forbidden"

}

```

**Notes:**

- Results are aggregated over a rolling time window (default: 12 weeks for questions)

- Minimum anonymity thresholds apply (typically 5 responses)

- Use questionTag for standard questions when possible (more stable than IDs)

- Results include historical data as time series

4. Error Handling

4.1. HTTP Status Codes

| Code | Meaning | Common Causes |

| ---- | --------------------- | ---------------------------------------------------------------- |

| 200 | Success | Request completed successfully |

| 400 | Bad Request | Invalid request body, missing required fields, validation errors |

| 403 | Forbidden | Missing or invalid API key, insufficient permissions |

| 404 | Not Found | Requested resource (team, question, cohort) not found |

| 500 | Internal Server Error | Unexpected server error (contact support) |

4.2. Error Response Format

**Validation Errors (400):**

```json

{

"status": "bad-request",

"reason": "Validation failed",

"errors": {

"employees": {

"0": {

"email": "Invalid email"

}

}

}

}

```

**Resource Errors (404, 403):**

```json

{

"message": "Team not found",

"status": "not-found"

}

```

4.3. Common Errors

4.3.1. Missing Authentication

```http

HTTP/1.1 403 Forbidden

{

"status": "forbidden",

"message": "Unauthorized: No authentication header"

}

```

**Solution:** Include Authorization: Bearer apikey_xxx header

4.3.2. Invalid API Key

```http

HTTP/1.1 403 Forbidden

{

"status": "forbidden",

"message": "Unauthorized: Invalid token"

}

```

**Solution:** Verify your API key is correct and not revoked

4.3.3. Validation Errors

```http

HTTP/1.1 400 Bad Request

{

"status": "bad-request",

"reason": "Validation failed",

"errors": {

"employees": {

"0": {

"email": "Invalid email"

}

}

}

}

```

**Solution:** Check the error details and fix the invalid fields

4.3.4. Empty Employee Array

```http

HTTP/1.1 400 Bad Request

{

"status": "bad-request",

"reason": "Validation failed",

"errors": {

"employees": "Array must contain at least 1 element(s)"

}

}

```

**Solution:** Provide at least one employee in the array

5. Usage Guidelines

**Request Volume:**

- The API is designed to handle typical HRIS synchronization workloads

- For high-volume integrations or frequent polling, please contact support to discuss your use case

- Implement exponential backoff for retries on transient errors

**Best Practices:**

- Send all active employees in a single import request (the API can handle thousands of employees)

- Schedule regular syncs during off-peak hours when possible

- Cache reference data (teams, questions, cohorts) rather than fetching on every request

- Use dry run mode during development and testing to avoid unnecessary database operations

- For organizations with 10,000+ employees, contact support to discuss performance optimization

6. Best Practices

6.1. Always Send Complete State

**CRITICAL:** The API performs full state synchronization. Every import must include:

- **All active employees** in your organization

- **All team memberships** for each employee

- Complete attribute data for each user

Missing employees or memberships will be interpreted as deletions. Never send partial updates.

```javascript

// WRONG: Only sending changed employees

const changedEmployees = getEmployeesModifiedToday()

await syncEmployees({ employees: changedEmployees }) // Will remove everyone else!

// CORRECT: Always send complete state

const allActiveEmployees = getAllActiveEmployees()

await syncEmployees({ employees: allActiveEmployees })

```

6.2. Use Dry Run for Testing

Always test new integrations with dryRun: true first, especially before your first production sync:

```javascript

// Test your payload

const testResponse = await fetch('/api/v1/employees', {

method: 'POST',

headers: {

'Authorization': Bearer ${apiKey},

'Content-Type': 'application/json'

},

body: JSON.stringify({

employees: [...], // ALL active employees

dryRun: true // No database changes

})

});

// Review planned operations

console.log(testResponse.details);

// Then run for real

const realResponse = await fetch('/api/v1/employees', {

method: 'POST',

headers: {

'Authorization': Bearer ${apiKey},

'Content-Type': 'application/json'

},

body: JSON.stringify({

employees: [...], // Same complete list

dryRun: false // Apply changes

})

});

```

6.3. Handle Errors Gracefully

```javascript

try {

const response = await fetch('/api/v1/employees', {

method: 'POST',

headers: {

Authorization: Bearer ${apiKey},

'Content-Type': 'application/json',

},

body: JSON.stringify({ employees }),

})

if (!response.ok) {

const error = await response.json()

console.error('Import failed:', error)

// Handle specific error types

if (error.errors) {

// Validation errors - fix data and retry

} else if (response.status === 403) {

// Auth error - check API key

}

}

} catch (err) {

// Network error - retry with exponential backoff

}

```

6.4. Use Stable Identifiers

- **Prefer questionTag over questionId** - Tags are stable across workspaces

- **Use employeeId consistently** - This links users across imports

- **Use groups[].id for team matching** - Ensures teams are updated, not duplicated

6.5. Minimize Survey Impact

Set surveyParticipant: false for:

- Contractors who shouldn't participate in company-wide surveys

- Project-specific users with limited scope

- External stakeholders

- Managers who need access to view another group's results, but their own answers should not contribute to those groups

6.6. Monitor Import Results

Always check the response details:

```javascript

const result = await importEmployees(data)

console.log('Created:', result.details.userOperations.createUsers.length)

console.log('Added to workspace:', result.details.userOperations.addUsers.length)

console.log('Removed:', result.details.userOperations.removeUsers.length)

console.log('New teams:', result.details.groupOperations.groupsToAdd.length)

// Verify the sync completed as expected

const totalChanges =

result.details.userOperations.createUsers.length +

result.details.userOperations.addUsers.length +

result.details.userOperations.removeUsers.length

if (totalChanges > 0) {

console.log(`✓ Sync complete: ${totalChanges} user operations`)

}

```

7. Integration Examples

7.1. Sync from HRIS

```javascript

async function syncFromHRIS() {

// CRITICAL: Fetch ALL active employees, not just recent changes

const hrisEmployees = await fetchAllActiveEmployeesFromHRIS()

// Transform to Teamspective format

const employees = hrisEmployees.map((emp) => ({

employeeId: emp.id,

// Provide EITHER email OR loginCode (not both)

...(emp.email ? { email: emp.email } : { loginCode: emp.projectCode }),

firstName: emp.firstName,

lastName: emp.lastName,

department: emp.department,

title: emp.jobTitle,

managerExternalId: emp.managerId,

groups: emp.teams.map((team) => ({

id: team.id,

name: team.name,

role: team.isLead ? 'admin' : 'member',

})),

}))

// Import to Teamspective

const response = await fetch('https://app.teamspective.com/api/v1/employees', {

method: 'POST',

headers: {

Authorization: Bearer ${process.env.TEAMSPECTIVE_API_KEY},

'Content-Type': 'application/json',

},

body: JSON.stringify({ employees }),

})

return await response.json()

}

```

7.2. Fetch eNPS for Department

```javascript

async function getDepartmentENPS(departmentCohortId) {

const response = await fetch('https://app.teamspective.com/api/v1/engagement/results/question', {

method: 'POST',

headers: {

Authorization: Bearer ${process.env.TEAMSPECTIVE_API_KEY},

'Content-Type': 'application/json',

},

body: JSON.stringify({

cohortId: departmentCohortId,

questionTag: 'enps',

}),

})

const result = await response.json()

return result.data[0].series // Time series data

}

```

8. Support

For API support, please contact:

- Email: support@teamspective.com

- Status Page: https://teamspectivestatus.com

9. Changelog

9.1. Version 1.0 (2025-12-02)

Added:

- Group role support (`role: "admin"` | "member") - only these 2 roles supported for groups

- Survey participation control (`surveyParticipant: boolean`)

- Support for employee attributes including profile data, manager relationships, and organizational metadata

- Dry run support for employee imports (`dryRun: boolean`)

- Validation error responses now include status and reason fields (non-breaking addition)

- Support for lite users with loginCode (passwordless authentication without email)

Changed (Breaking):

- Email/loginCode constraint: Exactly one of email or loginCode must be provided per user

- Users can no longer have both email and loginCode simultaneously

Fixed:

- All user attributes are now properly processed (previously some were ignored)