Stitchflow
Coupa logo

Coupa User Management API Guide

API workflow

How to automate user lifecycle operations through APIs with caveats that matter in production.

UpdatedMar 9, 2026

Summary and recommendation

Coupa exposes a REST API at https://{instance}.coupahost.com/api with two auth methods: legacy API key (OAUTH_KEY header) and OAuth 2.0 client credentials (Authorization: Bearer). Note that all responses default to XML - you must explicitly set Accept: application/json on every request or response parsing will fail.

There is no native SCIM 2.0 endpoint; Okta's Coupa integration uses the Core API directly, not the SCIM protocol, and no SCIM endpoint is publicly documented.

For teams building identity graph pipelines, the user object carries employee-number, manager (id + login reference), roles array, and user-groups array - enough to reconstruct org hierarchy and access topology, but role and group IDs are instance-specific integers that must be resolved via /api/roles and /api/user_groups before any write operation.

Pagination is offset-based with a hard ceiling of 50 records per page; there is no way to increase this limit, so full user enumeration requires iterating offset=0, 50, 100 until an empty array is returned.

API quick reference

Has user APIYes
Auth methodAPI Key (OAUTH_KEY header) or OAuth 2.0 (Bearer token)
Base URLOfficial docs
SCIM availableNo
SCIM plan requiredEnterprise

Authentication

Auth method: API Key (OAUTH_KEY header) or OAuth 2.0 (Bearer token)

Setup steps

  1. Log in to Coupa as an administrator.
  2. Navigate to Setup > Integrations > API Keys.
  3. Click 'Create API Key', assign a name, and select the appropriate permissions/scopes.
  4. Copy the generated API key and pass it in the OAUTH_KEY request header for API key auth.
  5. For OAuth 2.0: register an OAuth2 application under Setup > Integrations > OAuth2/OpenID Connect Clients, obtain client_id and client_secret, then POST to https://{instance}.coupahost.com/oauth2/token with grant_type=client_credentials to receive a Bearer token.
  6. Include the Bearer token in the Authorization header for subsequent requests.

Required scopes

Scope Description Required for
core.user.read Read access to user records. GET /api/users, GET /api/users/:id
core.user.write Create and update user records. POST /api/users, PUT /api/users/:id
core.user.delete Delete or deactivate user records. DELETE /api/users/:id

User object / data model

Field Type Description On create On update Notes
id integer Unique Coupa internal user ID. system-assigned read-only Used as path parameter for PUT/DELETE.
login string Username for login. required optional Must be unique across the instance.
email string User's email address. required optional Used for notifications and SSO matching.
firstname string User's first name. required optional
lastname string User's last name. required optional
active boolean Whether the user account is active. optional (defaults true) optional Set to false to deactivate without deleting.
purchasing-user boolean Grants purchasing permissions. optional optional
expense-user boolean Grants expense submission permissions. optional optional
roles array[object] List of role objects assigned to the user. optional optional Each role object contains id and name.
user-groups array[object] User group memberships. optional optional
default-address object Default shipping/billing address. optional optional
default-locale string Locale code (e.g., en-US). optional optional
timezone string User's timezone string. optional optional
manager object Reference to the user's manager (id, login). optional optional
employee-number string HR employee identifier. optional optional
department object Department association. optional optional
account-type object Account type classification. optional optional
sso-identifier string Identifier used for SAML SSO matching. optional optional Required when SSO is enforced.
created-at datetime Record creation timestamp (ISO 8601). system-assigned read-only
updated-at datetime Last update timestamp (ISO 8601). system-assigned read-only

Core endpoints

List Users

  • Method: GET
  • URL: https://{instance}.coupahost.com/api/users
  • Watch out for: Returns max 50 records per page. Iterate using offset=0, 50, 100, etc. until an empty array is returned.

Request example

GET /api/users?offset=0
Accept: application/json
Authorization: Bearer {token}

Response example

[
  {
    "id": 101,
    "login": "jdoe",
    "email": "jdoe@example.com",
    "firstname": "Jane",
    "lastname": "Doe",
    "active": true
  }
]

Get User by ID

  • Method: GET
  • URL: https://{instance}.coupahost.com/api/users/{id}
  • Watch out for: Returns 404 if user does not exist; no partial match support on this endpoint.

Request example

GET /api/users/101
Accept: application/json
Authorization: Bearer {token}

Response example

{
  "id": 101,
  "login": "jdoe",
  "email": "jdoe@example.com",
  "active": true,
  "roles": [{"id": 5, "name": "Requester"}]
}

Query Users by Field

  • Method: GET
  • URL: https://{instance}.coupahost.com/api/users?email[eq]={email}
  • Watch out for: Coupa uses bracket-notation filter operators (eq, in, gt, lt). Unsupported operators return 400.

Request example

GET /api/users?email[eq]=jdoe@example.com
Accept: application/json
Authorization: Bearer {token}

Response example

[
  {
    "id": 101,
    "login": "jdoe",
    "email": "jdoe@example.com"
  }
]

Create User

  • Method: POST
  • URL: https://{instance}.coupahost.com/api/users
  • Watch out for: login and email must be unique. Duplicate login returns 422 with validation errors in the response body.

Request example

POST /api/users
Content-Type: application/json
Authorization: Bearer {token}

{"login":"jdoe","email":"jdoe@example.com","firstname":"Jane","lastname":"Doe","active":true}

Response example

{
  "id": 101,
  "login": "jdoe",
  "email": "jdoe@example.com",
  "active": true,
  "created-at": "2024-01-15T10:00:00Z"
}

Update User

  • Method: PUT
  • URL: https://{instance}.coupahost.com/api/users/{id}
  • Watch out for: Coupa uses PUT (full/partial replacement semantics vary by field). Omitting roles array does not clear roles; explicitly pass empty array to remove all roles.

Request example

PUT /api/users/101
Content-Type: application/json
Authorization: Bearer {token}

{"active":false,"roles":[{"id":5}]}

Response example

{
  "id": 101,
  "login": "jdoe",
  "active": false,
  "updated-at": "2024-06-01T12:00:00Z"
}

Deactivate User

  • Method: PUT
  • URL: https://{instance}.coupahost.com/api/users/{id}
  • Watch out for: Coupa does not support hard-delete of users via API in most configurations. Setting active=false is the recommended deprovisioning method.

Request example

PUT /api/users/101
Content-Type: application/json
Authorization: Bearer {token}

{"active":false}

Response example

{
  "id": 101,
  "active": false,
  "updated-at": "2024-06-01T12:00:00Z"
}

Get User Roles

  • Method: GET
  • URL: https://{instance}.coupahost.com/api/roles
  • Watch out for: Role IDs are instance-specific. Always query /api/roles to resolve role names to IDs before assigning.

Request example

GET /api/roles
Accept: application/json
Authorization: Bearer {token}

Response example

[
  {"id": 5, "name": "Requester"},
  {"id": 8, "name": "Approver"}
]

Get User Groups

  • Method: GET
  • URL: https://{instance}.coupahost.com/api/user_groups
  • Watch out for: User group IDs must be resolved before assigning via the users endpoint. Group membership is managed through the user object's user-groups array.

Request example

GET /api/user_groups
Accept: application/json
Authorization: Bearer {token}

Response example

[
  {"id": 3, "name": "Finance Team"},
  {"id": 7, "name": "Procurement"}
]

Rate limits, pagination, and events

  • Rate limits: Coupa does not publish explicit rate limit tiers in public documentation. Practical limits are enforced per instance and negotiated at the enterprise contract level. Excessive requests may result in HTTP 429 responses.
  • Rate-limit headers: No
  • Retry-After header: No
  • Rate-limit notes: No official rate limit headers documented. Coupa recommends implementing exponential backoff on 429 or 503 responses. Bulk operations should be batched and spaced to avoid throttling.
  • Pagination method: offset
  • Default page size: 50
  • Max page size: 50
  • Pagination pointer: offset
Plan Limit Concurrent
Enterprise (all plans) Not publicly documented; instance-level throttling applies 0
  • Webhooks available: No
  • Webhook notes: Coupa does not offer native outbound webhooks for user lifecycle events. Event-driven integration is achieved via polling the API or using Coupa's Integration Framework (CIF) for scheduled data exports.
  • Alternative event strategy: Poll GET /api/users with updated-at[gt]={timestamp} filter to detect changes. Coupa's Integration Framework supports scheduled flat-file exports for bulk sync.

SCIM API status

  • SCIM available: No
  • SCIM version: Not documented
  • Plan required: Enterprise
  • Endpoint: Not documented

Limitations:

  • No native SCIM 2.0 endpoint is documented by Coupa.
  • User provisioning via Okta is achieved through Okta's Coupa app integration using the Coupa Core API (not SCIM protocol).
  • Okta Group Linking maps Okta groups to Coupa roles/groups via API calls.
  • Microsoft Entra ID and OneLogin integrations similarly use SAML 2.0 for SSO; provisioning relies on the Core API or manual processes.
  • No SCIM endpoint has been publicly documented as of the policy date.

Common scenarios

Three provisioning scenarios cover the majority of integration work against the Coupa user API.

Provisioning a new employee: Resolve role IDs via GET /api/roles and group IDs via GET /api/user_groups, then POST /api/users with login, email, firstname, lastname, active=true, roles, user-groups, employee-number, manager reference, and sso-identifier. If SSO is enforced, sso-identifier must match the IdP NameID attribute exactly at creation time - there is no fallback login path once SSO is active.

Syncing role changes from IdP group membership: Fetch the current user record via GET /api/users?login[eq]={username}, compute the desired roles array by merging IdP-driven changes with existing roles not managed by the IdP, then PUT /api/users/{id} with the full updated array. PUT replaces whatever roles array is sent; omitting roles not managed by your integration will silently remove them.

Offboarding: GET /api/users?email[eq]={email} to resolve the Coupa user ID, then PUT /api/users/{id} with active=false. Optionally pass roles=[] and user-groups=[] to strip access. Hard-delete via DELETE is generally unavailable if the user has associated purchase orders, invoices, or expense reports - deactivation is the supported deprovisioning path.

Provision a new employee from HR system

  1. Resolve required role IDs: GET /api/roles and match by name.
  2. Resolve user group IDs: GET /api/user_groups and match by name.
  3. POST /api/users with login, email, firstname, lastname, active=true, roles array, user-groups array, employee-number, manager reference, and sso-identifier.
  4. Verify response returns HTTP 201 with the new user id.
  5. Store the Coupa user id in the HR system for future updates.

Watch out for: If SSO is enforced, the sso-identifier must be set at creation time and match the IdP attribute exactly, or the user will be unable to log in.

Sync role changes from IdP group membership

  1. Detect group membership change in IdP (Okta/Entra) via IdP webhook or scheduled poll.
  2. Resolve the corresponding Coupa role ID via GET /api/roles.
  3. GET /api/users?login[eq]={username} to retrieve the Coupa user id and current roles array.
  4. Compute the new desired roles array (add or remove role objects).
  5. PUT /api/users/{id} with the full updated roles array.
  6. Confirm HTTP 200 response and updated roles in the response body.

Watch out for: PUT replaces the roles array with whatever is sent. Always fetch current roles first and merge, or you will inadvertently remove existing roles not managed by the IdP.

Offboard a departing employee

  1. GET /api/users?email[eq]={email} to retrieve the Coupa user id.
  2. PUT /api/users/{id} with active=false to deactivate the account.
  3. Optionally PUT /api/users/{id} with roles=[] and user-groups=[] to remove all access.
  4. Confirm HTTP 200 and active=false in the response.

Watch out for: Deactivated users retain their historical transaction records in Coupa. Hard-delete is generally not available via API if the user has associated purchase orders, invoices, or expense reports.

Why building this yourself is a trap

The most common integration failure mode is assuming PUT behaves like PATCH. Sending a partial roles array on any update silently removes roles not included in the payload - always fetch current state and merge before writing.

A second trap is filter syntax: Coupa uses bracket-notation operators (field[eq], field[in], field[cont]) and will return HTTP 400 for unsupported operator forms; standard field=value equality does not work reliably across all fields.

Rate limits are not publicly documented and are enforced at the instance level. There are no rate-limit headers in responses, and no Retry-After header on 429s - implement exponential backoff and space bulk operations to avoid throttling.

Webhooks for user lifecycle events do not exist; event-driven sync requires polling GET /api/users with an updated-at[gt]={timestamp} filter, which means your identity graph reflects Coupa state only as frequently as your poll interval allows.

Automate Coupa 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.

Every app coverage, including apps without APIs
60+ app integrations plus browser automation for apps without APIs
IT graph reconciliation across apps and your IdP
Less than a week to launch, maintained as APIs and admin consoles change
SOC 2 Type II. ~2 hours of your team's time

UpdatedMar 9, 2026

* Details sourced from official product documentation and admin references.

Keep exploring

Related apps

Abnormal Security logo

Abnormal Security

API Only
AutomationAPI only
Last updatedMar 2026

Abnormal Security is an enterprise email security platform focused on detecting and investigating threats such as phishing, account takeover (ATO), and vendor email compromise. It does not support SCIM provisioning, which means every app in your stack

ActiveCampaign logo

ActiveCampaign

API Only
AutomationAPI only
Last updatedFeb 2026

ActiveCampaign uses a group-based permission model: every user belongs to exactly one group, and all feature-area access (Contacts, Campaigns, Automations, Deals, Reports, Templates) is configured at the group level, not per individual. The default Adm

ADP logo

ADP

API Only
AutomationAPI only
Last updatedFeb 2026

ADP Workforce Now is a mid-market to enterprise HCM platform that serves as the HR source of record for employee data — payroll, benefits, time, and talent. User access is governed by a hybrid permission model: predefined security roles (Security Maste