From 064f88f54f996cd2ad6e779d269488c0e75c0fff Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Tue, 18 Aug 2026 10:37:23 -1000 Subject: [PATCH 1/6] Document OAuth2 partner authentication and authToken usage Add a new section to the Authentication docs covering the full OAuth2 authorization code flow, including how to exchange a code for tokens, use the access_token as credentials.authToken in IS requests, and refresh tokens when they expire. Replace a dead link to oauth_partner_guide.html with this inline content. Also update nokogiri to >= 1.11.0 and .ruby-version to 2.7.8 so the docs build on Apple Silicon. --- .ruby-version | 2 +- Gemfile | 2 +- Gemfile.lock | 10 +++-- source/index.html.md | 90 +++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 97 insertions(+), 7 deletions(-) diff --git a/.ruby-version b/.ruby-version index 49cdd668e1c..6a81b4c8379 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.7.6 +2.7.8 diff --git a/Gemfile b/Gemfile index 95029e39a5c..99043e67b4f 100644 --- a/Gemfile +++ b/Gemfile @@ -8,5 +8,5 @@ gem 'middleman-autoprefixer', '~> 2.7' gem 'middleman-sprockets', '~> 4.1' gem 'rouge', '~> 3.2' gem 'redcarpet', '~> 3.4.0' -gem 'nokogiri', '~> 1.10.8' +gem 'nokogiri', '>= 1.11.0' gem 'sass' diff --git a/Gemfile.lock b/Gemfile.lock index 8d2f9eb0d8c..eb99a93f62a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -76,10 +76,11 @@ GEM middleman-syntax (3.2.0) middleman-core (>= 3.2) rouge (~> 3.2) - mini_portile2 (2.4.0) + mini_portile2 (2.8.9) minitest (5.14.1) - nokogiri (1.10.9) - mini_portile2 (~> 2.4.0) + nokogiri (1.15.7) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) padrino-helpers (0.13.3.4) i18n (~> 0.6, >= 0.6.7) padrino-support (= 0.13.3.4) @@ -88,6 +89,7 @@ GEM activesupport (>= 3.1) parallel (1.19.1) public_suffix (4.0.5) + racc (1.8.1) rack (2.2.2) rb-fsevent (0.10.4) rb-inotify (0.10.1) @@ -122,7 +124,7 @@ DEPENDENCIES middleman-autoprefixer (~> 2.7) middleman-sprockets (~> 4.1) middleman-syntax (~> 3.2) - nokogiri (~> 1.10.8) + nokogiri (>= 1.11.0) redcarpet (~> 3.4.0) rouge (~> 3.2) sass diff --git a/source/index.html.md b/source/index.html.md index 7cc70b95a1a..8676c7be905 100644 --- a/source/index.html.md +++ b/source/index.html.md @@ -28,6 +28,94 @@ To use the API, you will need to generate API credentials. Make sure to store the partnerUserID and partnerUserSecret pair you're given in a secure location, as you won't be shown them again. +If you are building a multi-user integration and need to access Expensify on behalf of multiple customers, see [OAuth2 partner authentication](#oauth2-partner-authentication) below. + +## Two authentication methods + +The `credentials` object in every `requestJobDescription` accepts either method: + +Method | Fields | When to use +------ | ------ | ----------- +Partner credentials | `partnerUserID` + `partnerUserSecret` | Server-to-server integrations with a single Expensify account +OAuth2 access token | `authToken` | Multi-user integrations acting on behalf of individual Expensify users + +## OAuth2 partner authentication + +To act on behalf of individual Expensify users, use the OAuth2 authorization code flow to obtain a short-lived access token. Pass that token as `credentials.authToken` in your requests. + +**Prerequisites:** Register OAuth2 credentials at . You receive a `client_id` (your `partnerUserID`) and a `client_secret`. Set a `redirect_uri` for your app. + +### Step 1 — Redirect the user to authorize + +``` +https://www.expensify.com/oauth/authorize + ?response_type=code + &scope=integrations:api + &client_id=YOUR_CLIENT_ID + &redirect_uri=YOUR_REDIRECT_URI + &state=RANDOM_STATE_VALUE +``` + +Generate a random `state` value and validate it in the callback to prevent CSRF attacks. + +### Step 2 — Exchange the authorization code for tokens + +After the user authorizes, Expensify redirects to `YOUR_REDIRECT_URI?code=AUTH_CODE&state=...`. Exchange the code for tokens: + +```shell +curl -X POST 'https://www.expensify.com/oauth/token' \ + -d 'grant_type=authorization_code' \ + -d 'code=AUTH_CODE' \ + -d 'redirect_uri=YOUR_REDIRECT_URI' \ + -d 'client_id=YOUR_CLIENT_ID' \ + -d 'client_secret=YOUR_CLIENT_SECRET' +``` + +Response: + +```json +{ + "access_token": "ABCDEF123...", + "refresh_token": "GHIJKL456...", + "expires_in": 7200 +} +``` + +- `access_token` expires in approximately 2 hours. Store it in memory only. +- `refresh_token` does not expire. Store it persistently (for example, in a database). +- Authorization codes expire in 2 minutes and can only be used once. + +### Step 3 — Use the access token in IS requests + +Pass `access_token` as `credentials.authToken` in any `requestJobDescription`: + +```shell +curl -X POST 'https://integrations.expensify.com/Integration-Server/ExpensifyIntegrations' \ + -d 'requestJobDescription={ + "type":"get", + "credentials":{ + "authToken":"ABCDEF123..." + }, + "inputSettings":{ + "type":"policyList" + } + }' +``` + +### Step 4 — Refresh the access token + +When the `access_token` expires, exchange the `refresh_token` for a new pair: + +```shell +curl -X POST 'https://www.expensify.com/oauth/token' \ + -d 'grant_type=refresh_token' \ + -d 'refresh_token=GHIJKL456...' \ + -d 'client_id=YOUR_CLIENT_ID' \ + -d 'client_secret=YOUR_CLIENT_SECRET' +``` + +Always store the new `refresh_token` returned in the response — each refresh issues a new one and invalidates the old one. + # Request format > API call format @@ -62,7 +150,7 @@ For every request, the `requestJobDescription` JSON parameter will need to conta Parameter | Type | Description --------- | ------- | ----------- type | String | The type of job to execute -credentials | JSON object | An object containing two key/values used to authenticate you: `partnerUserID` and `partnerUserSecret`. +credentials | JSON object | An object with either `partnerUserID` + `partnerUserSecret` (partner credentials) or a single `authToken` (OAuth2 access token). See [Authentication](#authentication). `inputSettings` | JSON Object | Additional information about the job to execute # Rate limits From d4e64b89cd094b46d6d972c6716b23b47ef58c2e Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Tue, 18 Aug 2026 10:47:08 -1000 Subject: [PATCH 2/6] Clarify OAuth2 access is private beta and improve state/secret docs --- source/index.html.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/source/index.html.md b/source/index.html.md index 8676c7be905..70417a04705 100644 --- a/source/index.html.md +++ b/source/index.html.md @@ -43,7 +43,9 @@ OAuth2 access token | `authToken` | Multi-user integrations acting on behalf of To act on behalf of individual Expensify users, use the OAuth2 authorization code flow to obtain a short-lived access token. Pass that token as `credentials.authToken` in your requests. -**Prerequisites:** Register OAuth2 credentials at . You receive a `client_id` (your `partnerUserID`) and a `client_secret`. Set a `redirect_uri` for your app. +**Access:** OAuth2 partner authentication is currently in private beta. To request access, contact [concierge@expensify.com](mailto:concierge@expensify.com). The Expensify team will provide you with a `client_id` (your `partnerUserID`) and a `client_secret` (your `partnerUserSecret`), along with a link to register your `redirect_uri`. + +**Regenerating your client secret:** If you lose your `client_secret`, go to and click **"Click here to regenerate your partnerUserSecret."** This immediately invalidates your old secret — update all systems using it before regenerating. ### Step 1 — Redirect the user to authorize @@ -56,7 +58,7 @@ https://www.expensify.com/oauth/authorize &state=RANDOM_STATE_VALUE ``` -Generate a random `state` value and validate it in the callback to prevent CSRF attacks. +Before redirecting the user, generate a random `state` value on your server and store it in the session. When the user returns to your callback URL, confirm that the `state` parameter matches what you stored — reject the request if it does not. This server-side check prevents CSRF attacks. ### Step 2 — Exchange the authorization code for tokens From ac662516822aa54841b561f8c4deb4631d021a68 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Tue, 18 Aug 2026 10:49:22 -1000 Subject: [PATCH 3/6] Add client_secret server-only warning and stronger state guidance --- source/index.html.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/source/index.html.md b/source/index.html.md index 70417a04705..ebf3ab4048b 100644 --- a/source/index.html.md +++ b/source/index.html.md @@ -45,6 +45,10 @@ To act on behalf of individual Expensify users, use the OAuth2 authorization cod **Access:** OAuth2 partner authentication is currently in private beta. To request access, contact [concierge@expensify.com](mailto:concierge@expensify.com). The Expensify team will provide you with a `client_id` (your `partnerUserID`) and a `client_secret` (your `partnerUserSecret`), along with a link to register your `redirect_uri`. + + **Regenerating your client secret:** If you lose your `client_secret`, go to and click **"Click here to regenerate your partnerUserSecret."** This immediately invalidates your old secret — update all systems using it before regenerating. ### Step 1 — Redirect the user to authorize @@ -58,7 +62,7 @@ https://www.expensify.com/oauth/authorize &state=RANDOM_STATE_VALUE ``` -Before redirecting the user, generate a random `state` value on your server and store it in the session. When the user returns to your callback URL, confirm that the `state` parameter matches what you stored — reject the request if it does not. This server-side check prevents CSRF attacks. +Before redirecting the user, generate a new cryptographically random `state` value on your server — for example, a UUID or a hex string from a secure random source — and store it in the user's session. Use a different value for every authorization request; never reuse a previous one. When the user returns to your callback URL, confirm that the `state` parameter matches what you stored, and reject the request if it does not or if it is missing. ### Step 2 — Exchange the authorization code for tokens From fa1b1040365c17b9122c5c1746831bb63a3d7bbe Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Tue, 18 Aug 2026 11:30:54 -1000 Subject: [PATCH 4/6] Note client_secret is only shown once when first generated --- source/index.html.md | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/source/index.html.md b/source/index.html.md index ebf3ab4048b..8662267c1ef 100644 --- a/source/index.html.md +++ b/source/index.html.md @@ -18,6 +18,15 @@ While the API is a self-serve tool, we have compiled some customer FAQs in [Expe # Authentication +The `credentials` object in every `requestJobDescription` accepts either method: + +Method | Fields | When to use +------ | ------ | ----------- +Credential based | `partnerUserID` + `partnerUserSecret` | Server-to-server integrations with a single Expensify account +OAuth2 access token | `authToken` | Multi-user integrations acting on behalf of individual Expensify users + +## Credential based authentication + To use the API, you will need to generate API credentials. 1. Create an Expensify account at @@ -28,28 +37,17 @@ To use the API, you will need to generate API credentials. Make sure to store the partnerUserID and partnerUserSecret pair you're given in a secure location, as you won't be shown them again. -If you are building a multi-user integration and need to access Expensify on behalf of multiple customers, see [OAuth2 partner authentication](#oauth2-partner-authentication) below. - -## Two authentication methods - -The `credentials` object in every `requestJobDescription` accepts either method: - -Method | Fields | When to use ------- | ------ | ----------- -Partner credentials | `partnerUserID` + `partnerUserSecret` | Server-to-server integrations with a single Expensify account -OAuth2 access token | `authToken` | Multi-user integrations acting on behalf of individual Expensify users - ## OAuth2 partner authentication To act on behalf of individual Expensify users, use the OAuth2 authorization code flow to obtain a short-lived access token. Pass that token as `credentials.authToken` in your requests. -**Access:** OAuth2 partner authentication is currently in private beta. To request access, contact [concierge@expensify.com](mailto:concierge@expensify.com). The Expensify team will provide you with a `client_id` (your `partnerUserID`) and a `client_secret` (your `partnerUserSecret`), along with a link to register your `redirect_uri`. +**Access:** OAuth2 partner authentication is currently in private beta. To request access, contact [concierge@expensify.com](mailto:concierge@expensify.com). -**Regenerating your client secret:** If you lose your `client_secret`, go to and click **"Click here to regenerate your partnerUserSecret."** This immediately invalidates your old secret — update all systems using it before regenerating. +**Client secret:** Your `client_secret` is only shown once when first generated. If you lose it, go to and click **"Click here to regenerate your partnerUserSecret."** This immediately invalidates your old secret — update all systems using it before regenerating. ### Step 1 — Redirect the user to authorize From 2cfc694fe06d50a38888c84b820257975df59ba5 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Tue, 18 Aug 2026 11:32:13 -1000 Subject: [PATCH 5/6] Fix client secret button label to "Generate new client secret" --- source/index.html.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/index.html.md b/source/index.html.md index 8662267c1ef..2c372b79ef4 100644 --- a/source/index.html.md +++ b/source/index.html.md @@ -47,7 +47,7 @@ To act on behalf of individual Expensify users, use the OAuth2 authorization cod Keep your client_secret on the server only. Never expose it in client-side code, browser requests, or mobile apps. Any code a user can read or intercept must not contain it. -**Client secret:** Your `client_secret` is only shown once when first generated. If you lose it, go to and click **"Click here to regenerate your partnerUserSecret."** This immediately invalidates your old secret — update all systems using it before regenerating. +**Client secret:** Your `client_secret` is only shown once when first generated. If you lose it, go to and click **"Generate new client secret."** This immediately invalidates your old secret — update all systems using it before regenerating. ### Step 1 — Redirect the user to authorize From ac9fc4da363c6eea32dae41a6d523a6e5f821da3 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Mon, 31 Aug 2026 08:14:31 -1000 Subject: [PATCH 6/6] Address review feedback: use partners@ email and fix integrations URL - Change contact email from concierge@ to partners@expensify.com - Add /partners path to the tools/integrations URL for client secret regeneration --- source/index.html.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/index.html.md b/source/index.html.md index 2c372b79ef4..f78332cf0b8 100644 --- a/source/index.html.md +++ b/source/index.html.md @@ -41,13 +41,13 @@ Make sure to store the partnerUserID and partnerUserSecret Keep your client_secret on the server only. Never expose it in client-side code, browser requests, or mobile apps. Any code a user can read or intercept must not contain it. -**Client secret:** Your `client_secret` is only shown once when first generated. If you lose it, go to and click **"Generate new client secret."** This immediately invalidates your old secret — update all systems using it before regenerating. +**Client secret:** Your `client_secret` is only shown once when first generated. If you lose it, go to and click **"Generate new client secret."** This immediately invalidates your old secret — update all systems using it before regenerating. ### Step 1 — Redirect the user to authorize