Stitchflow
Expensify logo

Expensify User Management API Guide

API workflow

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

UpdatedMar 11, 2026

Summary and recommendation

Expensify's Integration Server API uses a command-pattern design over HTTP POST with application/x-www-form-urlencoded encoding. Every request - regardless of operation - is serialized as a JSON string in a single requestJobDescription form field. There is no REST/JSON body API.

Authentication uses static partner credentials (partnerUserID + partnerUserSecret) passed as form fields in every request; there is no OAuth 2.0 flow.

The API returns HTTP 200 for many error conditions. Callers must inspect the responseCode field inside the JSON response body to determine actual success or failure. Rate limits are not numerically documented; empirical observation suggests a low threshold (tens of requests per minute).

Batch multiple employee operations into a single API call rather than looping per-user to stay within undocumented limits.

For teams building identity graph pipelines - mapping users across HR systems, IdPs, and SaaS apps - Expensify's user object supports managerEmail, approvalLimit, overLimitApprover, and two custom fields (customField1, customField2), enabling approval hierarchy data to be written programmatically alongside provisioning events.

A platform like Stitchflow, which operates as an MCP server with 60+ deep IT/identity integrations, can use these fields to keep Expensify's approval graph in sync with changes in the authoritative HR source.

API quick reference

Has user APIYes
Auth methodPartner credentials (partnerUserID + partnerUserSecret) passed as form-encoded parameters in every request. No OAuth 2.0 flow.
Base URLOfficial docs
SCIM availableYes
SCIM plan requiredControl Plan (enterprise/custom pricing)

Authentication

Auth method: Partner credentials (partnerUserID + partnerUserSecret) passed as form-encoded parameters in every request. No OAuth 2.0 flow.

Setup steps

  1. Log in to Expensify and navigate to https://www.expensify.com/tools/integrations/ to generate API credentials.
  2. Note the partnerUserID and partnerUserSecret displayed on that page.
  3. Include both values as form fields (requestJobDescription JSON blob) in every API request.
  4. Credentials are scoped to the Expensify account that generated them; use a dedicated service account for automation.

User object / data model

Field Type Description On create On update Notes
email string Primary identifier for a user/member in Expensify. required immutable Must be a valid email address. Used as the unique key across all policy operations.
role string Member role within a policy. Values: 'admin', 'auditor', 'user'. optional updatable Defaults to 'user' if omitted on invite.
isTerminated boolean Marks the employee record as terminated in an employee list (HR feed). optional updatable Used in the employee updater command; triggers offboarding workflow.
firstName string Employee first name, used in HR/employee feed integrations. optional updatable Part of the employee object in the updateEmployeeList command.
lastName string Employee last name. optional updatable Part of the employee object in the updateEmployeeList command.
managerEmail string Email of the employee's direct manager for approval workflow routing. optional updatable Must correspond to an existing Expensify user.
approvalLimit number Maximum report total (in cents) this employee can approve. optional updatable Relevant for multi-level approval workflows.
overLimitApprover string Email of approver for reports exceeding approvalLimit. optional updatable Used in advanced approval chain configuration.
customField1 string Custom employee attribute (e.g., employee ID, cost center). optional updatable Mapped to custom fields configured on the policy.
customField2 string Second custom employee attribute. optional updatable Same mapping rules as customField1.

Core endpoints

Add or update policy members

  • Method: POST
  • URL: https://integrations.expensify.com/Integration-Server/api
  • Watch out for: All employees in the payload are upserted; omitting an employee from the list does NOT remove them. Use the 'delete' action explicitly to remove members.

Request example

POST /Integration-Server/api
Content-Type: application/x-www-form-urlencoded

requestJobDescription={
  "type": "update",
  "credentials": {"partnerUserID": "user", "partnerUserSecret": "secret"},
  "inputSettings": {
    "type": "policyMembers",
    "policyID": "POLICY_ID",
    "employees": [{"email": "alice@example.com", "role": "user"}]
  }
}

Response example

{
  "responseCode": 200
}

Remove policy members

  • Method: POST
  • URL: https://integrations.expensify.com/Integration-Server/api
  • Watch out for: Deleted members lose access immediately. Ensure outstanding reports are submitted or reassigned before removal.

Request example

requestJobDescription={
  "type": "update",
  "credentials": {"partnerUserID": "user", "partnerUserSecret": "secret"},
  "inputSettings": {
    "type": "policyMembers",
    "policyID": "POLICY_ID",
    "employees": [{"email": "alice@example.com"}],
    "action": "delete"
  }
}

Response example

{
  "responseCode": 200
}

Update employee list (HR feed / approval hierarchy)

  • Method: POST
  • URL: https://integrations.expensify.com/Integration-Server/api
  • Watch out for: Setting isTerminated=true deprovisions the user from the policy but does not delete their Expensify account.

Request example

requestJobDescription={
  "type": "update",
  "credentials": {"partnerUserID": "user", "partnerUserSecret": "secret"},
  "inputSettings": {
    "type": "employees",
    "policyID": "POLICY_ID",
    "employees": [{
      "email": "bob@example.com",
      "managerEmail": "mgr@example.com",
      "isTerminated": false
    }]
  }
}

Response example

{
  "responseCode": 200
}

Get policy members list

  • Method: POST
  • URL: https://integrations.expensify.com/Integration-Server/api
  • Watch out for: Returns all members for the specified policy IDs in a single response object; no pagination.

Request example

requestJobDescription={
  "type": "get",
  "credentials": {"partnerUserID": "user", "partnerUserSecret": "secret"},
  "inputSettings": {
    "type": "policyMembers",
    "policyIDList": ["POLICY_ID"]
  }
}

Response example

{
  "responseCode": 200,
  "policyMembers": {
    "POLICY_ID": {
      "alice@example.com": {"role": "user"},
      "bob@example.com": {"role": "admin"}
    }
  }
}

Create expense report (on behalf of user)

  • Method: POST
  • URL: https://integrations.expensify.com/Integration-Server/api
  • Watch out for: The calling partner credentials must have admin rights on the policy to create reports on behalf of other users.

Request example

requestJobDescription={
  "type": "create",
  "credentials": {"partnerUserID": "user", "partnerUserSecret": "secret"},
  "inputSettings": {
    "type": "report",
    "policyID": "POLICY_ID",
    "report": {"title": "Q1 Expenses"},
    "employeeEmail": "alice@example.com"
  }
}

Response example

{
  "responseCode": 200,
  "reportID": "R123456789"
}

Rate limits, pagination, and events

  • Rate limits: Expensify enforces rate limits on the Integration Server API but does not publish explicit numeric thresholds in official documentation. Requests that exceed limits receive an HTTP 429 or an error response in the JSON body.
  • Rate-limit headers: No
  • Retry-After header: No
  • Rate-limit notes: Official docs do not specify rate limit headers. Implement exponential backoff on error responses. Bulk operations (e.g., policy member updates with multiple employees in one call) are preferred over looping single-user calls.
  • Pagination method: none
  • Default page size: 0
  • Max page size: 0
  • Pagination pointer: Not documented
Plan Limit Concurrent
All plans Not publicly documented; empirically observed limit is low (tens of requests per minute). 0
  • Webhooks available: No
  • Webhook notes: Expensify does not offer outbound webhooks for user lifecycle events (create, update, delete) via its Integration Server API. Export/report-level callbacks exist for report status changes but are not user-management webhooks.
  • Alternative event strategy: Poll the policyMembers GET command on a schedule to detect membership changes. For provisioning automation, use SCIM via Okta.

SCIM API status

  • SCIM available: Yes

  • SCIM version: 2.0

  • Plan required: Control Plan (enterprise/custom pricing)

  • Endpoint: Not publicly documented as a standalone URL; provisioning is configured through Okta's SCIM integration tile for Expensify. Activation must be requested via concierge@expensify.com.

  • Supported operations: Create user, Deactivate user, Update user attributes, Push groups (map Okta groups to Expensify policies)

Limitations:

  • SCIM is only supported via Okta as the identity provider; direct SCIM endpoint access is not documented for other IdPs.
  • Requires SAML SSO to be configured and domain verification completed before SCIM activation.
  • Must contact concierge@expensify.com to enable SCIM provisioning on the account.
  • Microsoft Entra ID SCIM support is referenced in context but not confirmed in official Expensify SCIM documentation; verify with Expensify support.
  • Group push maps Okta groups to Expensify workspace policies; granular role assignment via SCIM may be limited.

Common scenarios

Bulk onboarding: Collect new employee emails, roles, and managerEmail values from your HR system. Construct a single POST with type='update', inputSettings.

type='policyMembers', and an employees array containing all new members. The upsert behavior means re-sending existing members is safe.

Follow up with a type='employees' call to set customField1/customField2 and approvalLimit values if your approval workflow requires them.

Offboarding: Two distinct paths exist. Sending action='delete' immediately revokes policy access. Sending isTerminated=true via the employee feed triggers Expensify's offboarding workflow, which reassigns open reports to the terminated employee's manager before removing access. Choose based on whether outstanding report continuity matters for your offboarding policy. Neither path deletes the Expensify account; account deletion is not available via the API.

SCIM via Okta: SCIM 2.0 is available on the Control plan but requires SAML SSO, domain verification, and manual activation via concierge@expensify.com before the Okta integration tile can be configured. Okta group push maps groups to Expensify workspace policies. SCIM support for Microsoft Entra ID is referenced in third-party context but is not confirmed in official Expensify documentation - verify directly with Expensify support before building against it.

Bulk onboard new employees to a policy

  1. Collect new employee emails, roles, and manager emails from your HR system.
  2. Construct a single POST request to the Integration Server with type='update', inputSettings.type='policyMembers', and an employees array containing all new members.
  3. Include managerEmail and role fields for each employee to configure approval routing.
  4. Check responseCode=200 in the response; log any non-200 codes for retry.
  5. Optionally follow up with a type='employees' update call to set customField1/customField2 and approvalLimit values.

Watch out for: Batch all employees in one request to avoid rate limiting. The upsert behavior means re-sending existing members is safe but redundant.

Offboard a terminated employee

  1. Identify the employee's email and the policy IDs they belong to (retrieve via policyMembers GET if unknown).
  2. Send a POST with type='update', action='delete', and the employee's email for each affected policy.
  3. Alternatively, send a type='employees' update with isTerminated=true to trigger Expensify's offboarding workflow (reassigns open reports to manager).
  4. Confirm removal by re-fetching policyMembers and verifying the email is absent.

Watch out for: Using action='delete' immediately revokes access. Using isTerminated=true is softer and may preserve report history routing. Choose based on your offboarding policy.

Set up SCIM provisioning via Okta

  1. Ensure the Expensify account is on the Control plan with domain verification and SAML SSO already configured.
  2. Email concierge@expensify.com to request SCIM provisioning activation for your domain.
  3. Once activated, add the Expensify app from the Okta Integration Network in your Okta admin console.
  4. Configure SCIM provisioning in the Okta app settings using the credentials/token provided by Expensify support.
  5. Assign Okta users and groups to the Expensify app; Okta will push create/deactivate events via SCIM 2.0.
  6. Map Okta groups to Expensify workspace policies using Okta's group push feature.

Watch out for: SCIM activation is not self-serve and can take time to enable. Test with a pilot group before full rollout. Deprovisioning via SCIM deactivates policy membership but does not delete the Expensify account.

Why building this yourself is a trap

The most significant API caveat is the upsert-only behavior on member updates: omitting an employee from a policyMembers payload does not remove them. Removals require an explicit action='delete' call. Automation that syncs a full employee list without explicitly handling the delete path will silently leave terminated employees on policies.

Partner credentials are scoped to the Expensify account that generated them. If the generating account's password is rotated, the partnerUserSecret is invalidated. Use a dedicated admin service account for all API automation and document the credential dependency explicitly in your runbook.

The API has no endpoint to list all policies for an account or to create policies programmatically. Any workflow that needs to discover policy IDs must either hardcode them or retrieve them out-of-band.

There is also no pagination on the policyMembers GET response - all members are returned in a single object, which can become a performance consideration for large policies.

Automate Expensify 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 11, 2026

* Details sourced from official product documentation and admin references.

Keep exploring

Related apps

15Five logo

15Five

Full API + SCIM
AutomationAPI + SCIM
Last updatedFeb 2026

15Five uses a fixed role-based permission model with six predefined roles: Account Admin, HR Admin, Billing Admin, Group Admin, Manager, and Employee. No custom roles can be constructed. User management lives at Settings gear → People → Manage people p

1Password logo

1Password

Full API + SCIM
AutomationAPI + SCIM
Last updatedFeb 2026

1Password's admin console at my.1password.com covers the full user lifecycle — invitations, group assignments, vault access, suspension, and deletion — without any third-party tooling. Like every app that mixes role-based and resource-level permissions

8x8 logo

8x8

Full API + SCIM
AutomationAPI + SCIM
Last updatedFeb 2026

8x8 Admin Console supports full lifecycle user management — create, deactivate, and delete — across its X Series unified communications platform. Every app a user can access (8x8 Work desktop, mobile, web, Agent Workspace) is gated by license assignmen