Summary and recommendation
Intercom exposes two distinct API surfaces that serve different identity purposes and must not be conflated. The REST API at https://api.intercom.io manages contacts (end-users and leads) via /contacts and provides read-only access to admins (teammates) via /admins.
Teammate provisioning and deprovisioning is handled exclusively through the SCIM 2.0 API at https://api.intercom.io/scim/v2, which requires the Expert (Enterprise) plan and a separately generated SCIM token. Auth for the REST API uses Bearer token (server-side) or OAuth 2.0 (third-party integrations); SCIM uses its own Bearer token issued from the Security settings panel.
Building an accurate identity graph across both surfaces requires joining /admins (teammate records) with /contacts (user records) using shared email as the correlation key, since Intercom does not expose a unified identity object spanning both types.
API quick reference
| Has user API | Yes |
| Auth method | Bearer token (Access Token) for server-side integrations; OAuth 2.0 for third-party app integrations |
| Base URL | Official docs |
| SCIM available | Yes |
| SCIM plan required | Expert (Enterprise) |
Authentication
Auth method: Bearer token (Access Token) for server-side integrations; OAuth 2.0 for third-party app integrations
Setup steps
- Go to the Intercom Developer Hub (https://app.intercom.com/a/developer-signup) and create or open an app.
- For direct API access: navigate to 'Authentication' in your app settings and copy the Access Token; include it as 'Authorization: Bearer
' on every request. - For OAuth 2.0: register redirect URIs, implement the authorization code flow, exchange the code for an access token, and store the token securely.
- Set the 'Accept: application/json' and 'Content-Type: application/json' headers on all requests.
- Scope permissions are configured per-app in the Developer Hub under 'Permissions'.
Required scopes
| Scope | Description | Required for |
|---|---|---|
| Read contacts | Allows reading contact (user/lead) records. | GET /contacts, GET /contacts/{id}, POST /contacts/search |
| Write contacts | Allows creating, updating, and deleting contact records. | POST /contacts, PUT /contacts/{id}, DELETE /contacts/{id}, POST /contacts/merge |
| Read conversations | Allows reading conversation data associated with users. | GET /conversations |
| Read admins | Allows reading admin (teammate) records. | GET /admins, GET /admins/{id} |
| Read tags | Allows reading tags applied to contacts. | GET /tags |
| Write tags | Allows creating tags and tagging/untagging contacts. | POST /tags, POST /contacts/{id}/tags |
User object / data model
| Field | Type | Description | On create | On update | Notes |
|---|---|---|---|---|---|
| id | string | Intercom-generated unique contact ID. | auto-generated | immutable | Use this ID for all subsequent API calls. |
| external_id | string | Your application's unique identifier for the contact. | optional | updatable | Recommended for deduplication; used to match existing contacts. |
| string | Contact's email address. | optional (required if no external_id) | updatable | Used for deduplication. Duplicate emails across contacts will be merged. | |
| role | string (enum) | Contact role: 'user' or 'lead'. | required | updatable | Determines contact type. 'user' requires email or external_id. |
| name | string | Full name of the contact. | optional | updatable | |
| phone | string | Phone number in E.164 format. | optional | updatable | |
| avatar | object | Avatar image URL for the contact. | optional | updatable | Contains 'image_url' field. |
| signed_up_at | integer (Unix timestamp) | Timestamp when the user signed up. | optional | updatable | |
| last_seen_at | integer (Unix timestamp) | Timestamp of last seen activity. | optional | updatable | |
| last_replied_at | integer (Unix timestamp) | Timestamp of last reply from contact. | read-only | read-only | |
| unsubscribed_from_emails | boolean | Whether the contact has unsubscribed from emails. | optional | updatable | |
| location | object | Location data including city, country, region. | read-only (auto-detected) | read-only | Populated automatically from IP; not directly settable. |
| custom_attributes | object (key-value) | Custom data attributes defined in your Intercom workspace. | optional | updatable | Keys must be pre-defined in Intercom settings. Values can be string, number, boolean, or date. |
| tags | object (list) | Tags associated with the contact. | read-only (manage via /tags endpoint) | read-only (manage via /tags endpoint) | Use POST /contacts/{id}/tags to add/remove tags. |
| companies | object (list) | Companies the contact is associated with. | optional via companies array | updatable via /contacts/{id}/companies | Each entry references a company by company_id. |
| workspace_id | string | Intercom workspace the contact belongs to. | auto-assigned | immutable | |
| created_at | integer (Unix timestamp) | Timestamp when the contact was created in Intercom. | auto-generated | immutable | |
| updated_at | integer (Unix timestamp) | Timestamp of last update to the contact. | auto-generated | auto-updated |
Core endpoints
Create Contact
- Method: POST
- URL:
https://api.intercom.io/contacts - Watch out for: If a contact with the same email already exists, Intercom may return the existing contact rather than creating a duplicate. Always check the returned 'id'.
Request example
POST /contacts
Authorization: Bearer <token>
Content-Type: application/json
{
"role": "user",
"email": "jane@example.com",
"name": "Jane Doe",
"external_id": "usr_001"
}
Response example
{
"type": "contact",
"id": "64a1b2c3d4e5f6a7b8c9d0e1",
"role": "user",
"email": "jane@example.com",
"name": "Jane Doe",
"external_id": "usr_001",
"created_at": 1720000000
}
Retrieve Contact
- Method: GET
- URL:
https://api.intercom.io/contacts/{id} - Watch out for: Use Intercom's internal 'id', not 'external_id', in the URL path. To look up by external_id or email, use the Search endpoint.
Request example
GET /contacts/64a1b2c3d4e5f6a7b8c9d0e1
Authorization: Bearer <token>
Response example
{
"type": "contact",
"id": "64a1b2c3d4e5f6a7b8c9d0e1",
"role": "user",
"email": "jane@example.com",
"name": "Jane Doe",
"custom_attributes": {"plan": "pro"}
}
Update Contact
- Method: PUT
- URL:
https://api.intercom.io/contacts/{id} - Watch out for: Only include fields you want to change; omitted fields retain their current values. Custom attribute keys must be pre-defined in the workspace.
Request example
PUT /contacts/64a1b2c3d4e5f6a7b8c9d0e1
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "Jane Smith",
"custom_attributes": {"plan": "enterprise"}
}
Response example
{
"type": "contact",
"id": "64a1b2c3d4e5f6a7b8c9d0e1",
"name": "Jane Smith",
"custom_attributes": {"plan": "enterprise"}
}
Delete Contact
- Method: DELETE
- URL:
https://api.intercom.io/contacts/{id} - Watch out for: Deletion is permanent and removes all associated conversation history linkage. Consider archiving instead if data retention is needed.
Request example
DELETE /contacts/64a1b2c3d4e5f6a7b8c9d0e1
Authorization: Bearer <token>
Response example
{
"type": "contact_deleted",
"id": "64a1b2c3d4e5f6a7b8c9d0e1",
"deleted": true
}
List Contacts
- Method: GET
- URL:
https://api.intercom.io/contacts - Watch out for: List endpoint returns all contacts without filtering. For filtered retrieval, use the Search endpoint. Cursor-based pagination; do not use page offsets.
Request example
GET /contacts?per_page=50
Authorization: Bearer <token>
Response example
{
"type": "list",
"data": [{"id": "...", "email": "..."}],
"pages": {
"next": {"starting_after": "cursor_token_xyz"}
},
"total_count": 1200
}
Search Contacts
- Method: POST
- URL:
https://api.intercom.io/contacts/search - Watch out for: Search has a lower rate limit than standard endpoints. Complex nested queries using 'AND'/'OR' operators are supported but increase response time.
Request example
POST /contacts/search
Authorization: Bearer <token>
Content-Type: application/json
{
"query": {
"field": "email",
"operator": "=",
"value": "jane@example.com"
}
}
Response example
{
"type": "list",
"data": [{"id": "64a1b2c3d4e5f6a7b8c9d0e1", "email": "jane@example.com"}],
"total_count": 1
}
Merge Contacts
- Method: POST
- URL:
https://api.intercom.io/contacts/merge - Watch out for: Merge is irreversible. The 'from' contact (typically a lead) is deleted; its conversations are transferred to the 'into' contact (must be a user).
Request example
POST /contacts/merge
Authorization: Bearer <token>
Content-Type: application/json
{
"from": "lead_contact_id_abc",
"into": "user_contact_id_xyz"
}
Response example
{
"type": "contact",
"id": "user_contact_id_xyz",
"role": "user",
"email": "jane@example.com"
}
List Admins (Teammates)
- Method: GET
- URL:
https://api.intercom.io/admins - Watch out for: Admins (teammates) are managed separately from contacts. There is no API endpoint to create or delete admins; admin management is done via the Intercom UI or SCIM.
Request example
GET /admins
Authorization: Bearer <token>
Response example
{
"type": "admin.list",
"admins": [
{"type": "admin", "id": "1234567", "name": "Alice", "email": "alice@company.com"}
]
}
Rate limits, pagination, and events
- Rate limits: Intercom enforces per-app rate limits based on plan. Limits apply per access token per second and per minute. Rate limit headers are returned on every response.
- Rate-limit headers: Yes
- Retry-After header: Yes
- Rate-limit notes: Headers returned: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset (Unix timestamp). When limit is exceeded, API returns HTTP 429. Retry-After header is included in 429 responses. Bulk search and scroll endpoints have separate, lower limits.
- Pagination method: cursor
- Default page size: 50
- Max page size: 150
- Pagination pointer: starting_after (cursor value from pages.next.starting_after in response)
| Plan | Limit | Concurrent |
|---|---|---|
| Essential / Advanced (non-Enterprise) | 83 requests/second (approx. 5,000 requests/minute) | 0 |
| Expert / Enterprise | 167 requests/second (approx. 10,000 requests/minute) | 0 |
- Webhooks available: Yes
- Webhook notes: Intercom supports webhooks (called 'Notification Subscriptions') that send HTTP POST payloads to a configured URL when specified events occur. Webhooks are configured per-app in the Developer Hub.
- Alternative event strategy: For polling-based sync, use GET /contacts with cursor pagination and filter by updated_at.
- Webhook events: contact.created, contact.signed_up, contact.added_email, contact.tag.created, contact.tag.deleted, conversation.user.created, conversation.user.replied, conversation.admin.replied, conversation.admin.closed, conversation.admin.assigned, user.created, user.deleted, user.email.updated, user.unsubscribed
SCIM API status
SCIM available: Yes
SCIM version: 2.0
Plan required: Expert (Enterprise)
Endpoint: https://api.intercom.io/scim/v2
Supported operations: GET /scim/v2/Users – List provisioned users, GET /scim/v2/Users/{id} – Retrieve a user, POST /scim/v2/Users – Provision a new user (admin/teammate), PUT /scim/v2/Users/{id} – Replace user attributes, PATCH /scim/v2/Users/{id} – Update user attributes (activate/deactivate), DELETE /scim/v2/Users/{id} – Deprovision a user, GET /scim/v2/Groups – List groups (teams), POST /scim/v2/Groups – Create a group, PATCH /scim/v2/Groups/{id} – Update group membership, DELETE /scim/v2/Groups/{id} – Delete a group
Limitations:
- SCIM provisions Intercom admins (teammates), not end-user contacts.
- SSO must be configured and enabled before SCIM provisioning can be activated.
- Requires Expert (Enterprise) plan; not available on Essential or Advanced.
- Supported IdPs: Okta, Microsoft Entra ID (Azure AD), OneLogin.
- SCIM token is generated separately from the REST API access token.
- Deprovisioning via SCIM deactivates the admin but does not delete conversation history.
- Group (team) management via SCIM has limited attribute support compared to the UI.
Common scenarios
Three integration patterns cover the majority of production use cases. For SaaS sign-up sync, POST /contacts with role='user', email, external_id, and custom_attributes on registration; store the returned Intercom id and use PUT /contacts/{id} for subsequent profile updates.
For bulk attribute updates, POST /contacts/search with a custom_attribute filter, paginate results via the starting_after cursor, and update each record - note that search carries a stricter rate limit than GET /contacts, so implement exponential backoff on HTTP 429 and respect the Retry-After header.
For teammate lifecycle automation via SCIM with Okta, configure the SCIM base URL and Bearer token in the Okta Integration Network app, map userName to email, and rely on Okta group assignment to trigger POST /scim/v2/Users.
deprovisioning sends PATCH with active=false rather than DELETE, preserving conversation history but leaving open conversations unassigned.
Sync new SaaS sign-ups to Intercom as users
- On user registration in your app, call POST /contacts with role='user', email, external_id, and relevant custom_attributes (e.g., plan, company).
- Store the returned Intercom 'id' in your database alongside the user record.
- On subsequent profile updates (e.g., plan upgrade), call PUT /contacts/{id} with updated fields.
- To associate the user with a company, call POST /contacts/{id}/companies with the company_id.
Watch out for: If the user already exists (matched by email), Intercom returns the existing contact. Always use the returned 'id' rather than assuming a new record was created.
Bulk-search and update contacts by custom attribute
- Call POST /contacts/search with a query filtering on the target custom_attribute field and value.
- Iterate through paginated results using the 'starting_after' cursor from pages.next.
- For each contact, call PUT /contacts/{id} with the updated custom_attributes.
- Implement exponential backoff on HTTP 429 responses, respecting the Retry-After header.
Watch out for: Search has a lower rate limit than standard GET endpoints. For large datasets (>10,000 contacts), consider using the Intercom bulk export feature or Data Export API instead of repeated search calls.
Provision and deprovision teammates via SCIM with Okta
- Ensure the workspace is on the Expert (Enterprise) plan and SSO is configured.
- In Intercom settings, navigate to Security > SCIM Provisioning and generate a SCIM token.
- In Okta, add the Intercom app from the Okta Integration Network and configure SCIM base URL (https://api.intercom.io/scim/v2) and Bearer token.
- Map Okta user attributes to SCIM attributes (userName → email, givenName, familyName).
- Assign users/groups in Okta to trigger POST /scim/v2/Users provisioning in Intercom.
- To deprovision, unassign the user in Okta; Okta sends PATCH /scim/v2/Users/{id} with active=false, deactivating the admin in Intercom.
Watch out for: Deprovisioning deactivates the admin account but does not delete conversation history or reassign open conversations. Manually reassign conversations before deprovisioning to avoid orphaned workloads.
Why building this yourself is a trap
Several API behaviors produce silent failures or data integrity issues if not handled explicitly. external_id is immutable once set - plan the ID strategy before any bulk import, as there is no API path to correct it afterward.
Custom attribute keys must be pre-defined in workspace settings; POSTing an undefined key does not create it and the value is silently dropped. Contact deduplication by email means POST /contacts may return an existing record rather than a new one - always use the returned id, never assume creation.
Rate limits are pooled per access token across all integrations sharing a workspace, not per integration, so a high-volume integration can starve others. Cursor expiry on /contacts pagination means long-running sync jobs must not cache cursors across sessions.
Webhook payloads are signed with HMAC-SHA1 in the X-Hub-Signature header; skipping signature verification exposes the integration to spoofed events. Finally, SCIM deprovisioning deactivates the admin account but does not reassign open conversations - orphaned workloads must be handled out-of-band before the PATCH is sent.
Automate Intercom 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.