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 |
Unique employee identifier from your system |
Unique group identifier |
Birthday (YYYY-MM-DD) |
Company name (for multi-company workspaces) |
Competence area or specialty |
Cost center identifier |
Department name |
If true, validates data and returns operations without making changes |
Employment type ("Full-time", "Part-time", etc.) |
User's first name |
Full-time equivalent percentage ("100", "50", etc.) |
List of groups/teams the user belongs to |
Parent group ID for hierarchical structures |
User role in group: "admin" or "member" (only these 2 roles supported) |
Whether user participates in engagement surveys for this group |
Whether user is a manager ("Yes" or "No") |
Preferred language code (en, fi, sv, etc.) |
User's last name |
Manager's employee ID (preferred) |
Manager's full name (fallback display name) |
Manager's email (use if managerExternalId unavailable) |
Office city location |
Primary role in the organization |
Office or site location |
Employment start date (YYYY-MM-DD) |
Organizational unit |
Primary email address (for full users). Cannot be combined with loginCode |
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)