Summary and recommendation
HaloPSA exposes a REST API authenticated via OAuth 2.0 client_credentials grant. The base URL is instance-specific (https://{your-subdomain}.halopsa.com/api), and there is no shared multi-tenant endpoint. Tokens are short-lived; implement refresh logic before expiry to avoid mid-run auth failures.
Key scopes for identity lifecycle work are read:agents, edit:agents, read:customers, edit:customers, and read:teams. Scope assignment is configured per API application in Configuration > Integrations > HaloPSA API. There is no native SCIM 2.0 endpoint - all provisioning must go through /Agent and /Customer REST endpoints.
When building an identity graph against HaloPSA, note that agents and end users (contacts) are distinct object types at separate endpoints (/Agent vs. /Customer), with different permission models and license implications. Conflating the two in a sync pipeline is a common integration error that produces duplicate records or incorrect seat consumption.
API quick reference
| Has user API | Yes |
| Auth method | OAuth 2.0 (client_credentials grant) |
| Base URL | Official docs |
| SCIM available | No |
| SCIM plan required | Enterprise |
Authentication
Auth method: OAuth 2.0 (client_credentials grant)
Setup steps
- In HaloPSA, navigate to Configuration > Integrations > HaloPSA API.
- Create a new API application; select 'Client ID and Secret (Services)' as the authentication method.
- Assign the required permission scopes to the application.
- Note the Client ID and Client Secret.
- POST to https://{instance}.halopsa.com/auth/token with grant_type=client_credentials, client_id, client_secret, and scope.
- Use the returned access_token as a Bearer token in the Authorization header for all API requests.
Required scopes
| Scope | Description | Required for |
|---|---|---|
| read:agents | Read agent (technician) records | GET /Agent |
| edit:agents | Create and update agent records | POST /Agent, DELETE /Agent |
| read:customers | Read customer/contact records | GET /Customer |
| edit:customers | Create and update customer/contact records | POST /Customer, DELETE /Customer |
| read:teams | Read team records | GET /Team |
| edit:teams | Create and update team records | POST /Team |
User object / data model
| Field | Type | Description | On create | On update | Notes |
|---|---|---|---|---|---|
| id | integer | Unique agent identifier | auto-assigned | required in URL | Read-only |
| name | string | Full display name of the agent | required | optional | |
| string | Primary email address | required | optional | Used for login and notifications | |
| firstname | string | Agent first name | optional | optional | |
| surname | string | Agent surname | optional | optional | |
| username | string | Login username | optional | optional | Defaults to email if not set |
| isactive | boolean | Whether the agent account is active | optional | optional | Set to false to deactivate without deleting |
| team_id | integer | ID of the team the agent belongs to | optional | optional | References /Team endpoint |
| role | string | Agent role/permission level | optional | optional | Maps to roles configured in HaloPSA |
| phone | string | Agent phone number | optional | optional | |
| mobile | string | Agent mobile number | optional | optional | |
| department_id | integer | Department assignment | optional | optional | |
| site_id | integer | Site/location assignment | optional | optional | |
| timezone | string | Agent timezone string | optional | optional | |
| language | string | Preferred language code | optional | optional |
Core endpoints
List Agents
- Method: GET
- URL:
/api/Agent - Watch out for: Returns all agents by default; use page_no and page_size for pagination. Inactive agents are included unless filtered.
Request example
GET /api/Agent?page_size=50&page_no=1
Authorization: Bearer {token}
Response example
{
"record_count": 120,
"agents": [
{"id": 1, "name": "Jane Smith", "email": "jane@example.com", "isactive": true}
]
}
Get Agent by ID
- Method: GET
- URL:
/api/Agent/{id} - Watch out for: Returns 404 if agent ID does not exist.
Request example
GET /api/Agent/42
Authorization: Bearer {token}
Response example
{
"id": 42,
"name": "Jane Smith",
"email": "jane@example.com",
"team_id": 3,
"isactive": true
}
Create Agent
- Method: POST
- URL:
/api/Agent - Watch out for: A license seat must be available; creating an agent when all seats are consumed will return an error.
Request example
POST /api/Agent
Content-Type: application/json
{"name": "John Doe", "email": "john@example.com", "team_id": 3}
Response example
{
"id": 99,
"name": "John Doe",
"email": "john@example.com",
"isactive": true
}
Update Agent
- Method: POST
- URL:
/api/Agent - Watch out for: HaloPSA uses POST (not PATCH/PUT) for both create and update on Agent. Include the id field in the body to trigger an update.
Request example
POST /api/Agent
Content-Type: application/json
{"id": 99, "name": "John Doe Updated", "isactive": false}
Response example
{
"id": 99,
"name": "John Doe Updated",
"isactive": false
}
Delete Agent
- Method: DELETE
- URL:
/api/Agent/{id} - Watch out for: Deletion is permanent. Consider setting isactive=false instead to preserve audit history.
Request example
DELETE /api/Agent/99
Authorization: Bearer {token}
Response example
HTTP 200 OK
{}
List Customers (Contacts)
- Method: GET
- URL:
/api/Customer - Watch out for: The /Customer endpoint returns both companies and contacts depending on query parameters; use the 'type' filter to distinguish.
Request example
GET /api/Customer?page_size=50&page_no=1
Authorization: Bearer {token}
Response example
{
"record_count": 300,
"customers": [
{"id": 10, "name": "Acme Corp", "email": "contact@acme.com"}
]
}
Create/Update Customer
- Method: POST
- URL:
/api/Customer - Watch out for: Same POST-for-upsert pattern as Agent; include id in body to update an existing record.
Request example
POST /api/Customer
Content-Type: application/json
{"name": "New Contact", "email": "new@client.com", "site_id": 5}
Response example
{
"id": 201,
"name": "New Contact",
"email": "new@client.com"
}
List Teams
- Method: GET
- URL:
/api/Team - Watch out for: Team IDs are required when assigning agents; retrieve this list to resolve team names to IDs.
Request example
GET /api/Team
Authorization: Bearer {token}
Response example
{
"teams": [
{"id": 3, "name": "Support Tier 1"}
]
}
Rate limits, pagination, and events
- Rate limits: HaloPSA does not publish explicit rate limit tiers in official documentation. Limits are enforced at the instance/hosting level and may vary between cloud-hosted and self-hosted deployments.
- Rate-limit headers: No
- Retry-After header: No
- Rate-limit notes: No official rate limit figures published. Self-hosted instances are subject to server resource constraints. Contact HaloPSA support for cloud throttling details.
- Pagination method: offset
- Default page size: 50
- Max page size: 1000
- Pagination pointer: page_size / page_no
| Plan | Limit | Concurrent |
|---|---|---|
| Cloud (all plans) | Not publicly documented | 0 |
- Webhooks available: No
- Webhook notes: HaloPSA does not expose a native outbound webhook system for user/agent lifecycle events in its official documentation. Integrations are typically achieved via polling the REST API or using the built-in automation/workflow engine to trigger actions.
- Alternative event strategy: Use HaloPSA's built-in Automation rules or scheduled API polling to detect agent/customer changes.
SCIM API status
- SCIM available: No
- SCIM version: Not documented
- Plan required: Enterprise
- Endpoint: Not documented
Limitations:
- No native SCIM 2.0 provisioning endpoint is available in HaloPSA.
- SAML SSO is supported (Azure AD, AuthPoint) but does not include SCIM-based user provisioning.
- User provisioning must be handled via the REST API (/Agent, /Customer endpoints) or manual processes.
Common scenarios
Three core automation scenarios cover the agent lifecycle. First, provision on join: POST to /api/Agent with name, email, team_id, and role.
Retrieve team_id first via GET /api/Team. A failed seat check returns an error in the response body - do not rely on HTTP status alone to confirm success.
Second, deactivate on offboard: POST to /api/Agent with {"id": {agent_id}, "isactive": false}. Prefer this over DELETE /api/Agent/{id} - deletion is permanent and breaks referential integrity in historical ticket data.
Third, sync team assignments from HR: paginate GET /api/Agent (page_no, page_size up to 1000), build a team name-to-id map from GET /api/Team, then POST individual updates per agent. There is no bulk update endpoint; each change is a separate request. Pagination is offset-based using page_no (1-indexed) and page_size; use record_count from the response to calculate total pages.
Provision a new agent when an employee joins
- Obtain OAuth 2.0 access token via POST to /auth/token with client_credentials grant and edit:agents scope.
- GET /api/Team to retrieve the target team_id for the new agent.
- POST /api/Agent with name, email, team_id, and role fields to create the agent.
- Verify the returned id and isactive=true to confirm successful provisioning.
- If a license seat error is returned, alert the admin - no seats are available.
Watch out for: Agent creation will fail silently or return an error if named license seats are exhausted. Always check response body for error messages, not just HTTP status.
Deactivate an agent when an employee offboards
- Obtain access token with edit:agents scope.
- GET /api/Agent?search={email} to resolve the agent's id.
- POST /api/Agent with {"id": {agent_id}, "isactive": false} to deactivate the account.
- Confirm response returns isactive: false.
Watch out for: Prefer deactivation (isactive=false) over DELETE to preserve ticket history and audit trails. Deleted agents may cause referential issues in historical ticket data.
Sync agent team assignments from an external HR system
- Obtain access token with read:agents, edit:agents, and read:teams scopes.
- GET /api/Team to build a name-to-id mapping for all teams.
- GET /api/Agent with pagination to retrieve all current agents.
- For each agent whose team differs from the HR system record, POST /api/Agent with {"id": {agent_id}, "team_id": {new_team_id}}.
- Log all changes with the returned response for audit purposes.
Watch out for: There is no bulk update endpoint; each agent update requires a separate POST request. For large agent counts, implement rate-aware batching with delays between requests.
Why building this yourself is a trap
HaloPSA's API is functional but carries several integration traps not surfaced in standard documentation. Rate limits are not publicly documented and vary between cloud-hosted and self-hosted instances; implement exponential backoff on 429 and 503 responses as a baseline.
The POST-for-upsert pattern used on /Agent and /Customer deviates from REST conventions - omitting the id field on an intended update silently creates a duplicate record instead of returning an error.
Self-hosted instances may run different API versions with missing or modified endpoints compared to cloud; always validate against the instance's own Swagger UI at /apidoc/.
For teams maintaining an identity graph across multiple tools, HaloPSA's lack of outbound webhooks means lifecycle events must be detected via polling - there is no push notification for agent creation, deactivation, or team reassignment.
Pair the HaloPSA API with an orchestration layer such as an MCP server with 60+ deep IT/identity integrations to avoid building redundant polling logic per event type.
Automate HaloPSA workflows without one-off scripts
Stitchflow builds and maintains end-to-end IT automation across your SaaS stack, including apps without APIs. Built for exactly how your company works, with human approvals where they matter.