Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .ruby-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.7.6
2.7.8
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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'
10 changes: 6 additions & 4 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
94 changes: 93 additions & 1 deletion source/index.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://www.expensify.com/>
Expand All @@ -28,6 +37,89 @@ To use the API, you will need to generate API credentials.
Make sure to store the <code>partnerUserID</code> and <code>partnerUserSecret</code> pair you're given in a secure location, as you won't be shown them again.
</aside>

## 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).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
**Access:** OAuth2 partner authentication is currently in private beta. To request access, contact [concierge@expensify.com](mailto:concierge@expensify.com).
**Access:** OAuth2 partner authentication is currently in private beta. To request access, contact [partners@expensify.com](mailto:partners@expensify.com).

@NickTooker while we have this in private beta, I think we have any kenny keano email partners@ instead of Conci. You cool with that?


<aside class="warning">
Keep your <code>client_secret</code> 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.
</aside>

**Client secret:** Your `client_secret` is only shown once when first generated. If you lose it, go to <https://www.expensify.com/tools/integrations/> and click **"Generate new client secret."** This immediately invalidates your old secret — update all systems using it before regenerating.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
**Client secret:** Your `client_secret` is only shown once when first generated. If you lose it, go to <https://www.expensify.com/tools/integrations/> 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 <https://www.expensify.com/tools/integrations/partners> and click **"Generate new client secret."** This immediately invalidates your old secret — update all systems using it before regenerating.

Isn't this URL missing /partners else it goes to the page that exists now for the existing authentication method?


### 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
Expand Down Expand Up @@ -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
Expand Down
Loading