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..2c372b79ef4 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,6 +37,89 @@ 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.
+## 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).
+
+
+
+**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
+
+```
+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
+```
+
+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
+
+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 +154,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