> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://developer.deel.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://developer.deel.com/_mcp/server.

# OAuth2

> Implement OAuth2 authentication for user-authorized app access

## What is OAuth2?

OAuth 2.0 is the industry-standard protocol for authorization. It allows your app to access specific data in user accounts with explicit user consent, without requiring users to share their passwords.

* **Secure Access**: Users authorize without sharing passwords
* **Granular Permissions**: Request only the scopes your app needs
* **Token-Based**: Access and refresh tokens for secure API calls
* **User Consent**: Explicit approval for each permission

## How OAuth2 Works

OAuth2 uses the authorization code grant flow:

```mermaid
sequenceDiagram
    participant User
    participant YourApp
    participant Deel

    User->>YourApp: Request to connect Deel
    YourApp->>Deel: Redirect to OAuth grant screen
    Deel->>User: Show consent screen
    User->>Deel: Authorize app
    Deel->>YourApp: Return authorization code
    YourApp->>Deel: Exchange code for tokens
    Deel->>YourApp: Access token + Refresh token
    YourApp->>Deel: API request with access token
    Deel->>YourApp: API response
```

The flow ensures secure, controlled access by:

1. Requiring explicit user consent
2. Using temporary authorization codes
3. Providing time-limited access tokens
4. Supporting granular permissions through scopes

## Creating an OAuth2 App

#### Navigate to Apps

Go to **More** → **Developer** → **Apps** tab in your Deel dashboard

#### Create new app

Click **Create new app**

#### Select app type

Choose the appropriate app type:

#### Organization App

**Organization App**: Generate organization-level access tokens

**Use for:**

* Contract data reading
* Timesheets management
* Invoice adjustments
* SCIM API integration
* Accounting data access

Provides access to all organization resources based on granted scopes.

#### Personal App

**Personal App**: Generate personal access tokens (user-level access)

**Use for:**

* Contract creation
* SSO integrations
* User-specific operations
* Employee self-service features

Limited to resources the authorizing user can access.

#### Fill out app details

Provide the following information:

* **App Name**: Displayed to users during authorization
* **Description**: Explain what your app does
* **Redirect URI**: Where users return after authorization (e.g., `https://yourapp.com/callback`)
* **Logo**: Optional - shown on consent screen

#### Save credentials

After creation, you'll receive:

* **Client ID**: Public identifier for your app
* **Client Secret**: Secret key for authenticating your app

The client secret is only shown once. Store it securely immediately. If you lose it, you'll need to regenerate it.

## OAuth2 Flow Implementation

### Step 1: Request Permissions

Redirect users to Deel's authorization endpoint with the required parameters:

```
https://app.deel.com/oauth2/authorize?client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope={SCOPES}&state={STATE}
```

**Required Parameters:**

| Parameter      | Description                                                                       |
| -------------- | --------------------------------------------------------------------------------- |
| `client_id`    | Your app's client ID                                                              |
| `redirect_uri` | URL where user returns after authorization (must match registered URI)            |
| `scope`        | Space-separated list of requested scopes (e.g., `contracts:read contracts:write`) |
| `state`        | Random string to prevent CSRF attacks (you validate this on return)               |

**Example URL:**

```bash
https://app.deel.com/oauth2/authorize?client_id=abc123&redirect_uri=https://yourapp.com/callback&scope=contracts:read%20contracts:write&state=random_string_xyz
```

**`Node.js`**

```javascript Node.js
// Generate authorization URL
const authUrl = new URL('https://app.deel.com/oauth2/authorize');
authUrl.searchParams.append('client_id', process.env.CLIENT_ID);
authUrl.searchParams.append('redirect_uri', 'https://yourapp.com/callback');
authUrl.searchParams.append('scope', 'contracts:read contracts:write');
authUrl.searchParams.append('state', generateRandomState()); // Implement CSRF protection

// Redirect user
res.redirect(authUrl.toString());
```

**`Python`**

```python Python
from urllib.parse import urlencode
import secrets

# Generate authorization URL
params = {
    'client_id': os.getenv('CLIENT_ID'),
    'redirect_uri': 'https://yourapp.com/callback',
    'scope': 'contracts:read contracts:write',
    'state': secrets.token_urlsafe(32)  # CSRF protection
}

auth_url = f"https://app.deel.com/oauth2/authorize?{urlencode(params)}"

# Redirect user
return redirect(auth_url)
```

### Step 2: User Authorizes

The user sees a consent screen showing:

* Your app name and logo
* What permissions (scopes) you're requesting
* Option to approve or deny

When they approve, they're redirected to your `redirect_uri` with an authorization code:

```
https://yourapp.com/callback?code=AUTH_CODE&state=random_string_xyz
```

Always verify the `state` parameter matches what you sent to prevent CSRF attacks.

### Step 3: Exchange Code for Access Token

Exchange the authorization code for an access token by making a POST request:

```bash
curl -X POST 'https://app.deel.com/oauth2/tokens' \
  -H 'Authorization: Basic BASE64_ENCODED_CREDENTIALS' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=authorization_code' \
  -d 'code=AUTH_CODE' \
  -d 'redirect_uri=https://yourapp.com/callback'
```

**Authentication:**

* Use HTTP Basic authentication
* Encode your credentials: `base64(client_id:client_secret)`

**Example encoding:**

```bash
echo -n "client_id:client_secret" | base64
```

**`Node.js`**

```javascript Node.js
const axios = require('axios');

// Encode credentials
const credentials = Buffer.from(
  `${process.env.CLIENT_ID}:${process.env.CLIENT_SECRET}`
).toString('base64');

// Exchange code for token
const response = await axios.post(
  'https://app.deel.com/oauth2/tokens',
  new URLSearchParams({
    grant_type: 'authorization_code',
    code: authCode,
    redirect_uri: 'https://yourapp.com/callback'
  }),
  {
    headers: {
      'Authorization': `Basic ${credentials}`,
      'Content-Type': 'application/x-www-form-urlencoded'
    }
  }
);

const { access_token, refresh_token, expires_in } = response.data;
```

**`Python`**

```python Python
import requests
import base64
import os

# Encode credentials
credentials = base64.b64encode(
    f"{os.getenv('CLIENT_ID')}:{os.getenv('CLIENT_SECRET')}".encode()
).decode()

# Exchange code for token
response = requests.post(
    'https://app.deel.com/oauth2/tokens',
    headers={
        'Authorization': f'Basic {credentials}',
        'Content-Type': 'application/x-www-form-urlencoded'
    },
    data={
        'grant_type': 'authorization_code',
        'code': auth_code,
        'redirect_uri': 'https://yourapp.com/callback'
    }
)

tokens = response.json()
access_token = tokens['access_token']
refresh_token = tokens['refresh_token']
```

**Response:**

```json
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 2592000,
  "refresh_token": "def502000a2b3c4d5e6f...",
  "scope": "contracts:read contracts:write"
}
```

* **Access tokens** are valid for **30 days** (2,592,000 seconds)
* **Refresh tokens** are valid for **90 days**

### Step 4: Make Authenticated Requests

Use the access token to make API requests. OAuth2 requests require **two headers**:

```bash
curl -X GET 'https://api.letsdeel.com/rest/contracts' \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'x-client-id: CLIENT_ID'
```

OAuth2 requests require **both** the `Authorization` header AND the `x-client-id` header. Missing either will result in a 401 error.

**`Node.js`**

```javascript Node.js
const axios = require('axios');

const deelAPI = axios.create({
  baseURL: 'https://api.letsdeel.com/rest',
  headers: {
    'Authorization': `Bearer ${accessToken}`,
    'x-client-id': process.env.CLIENT_ID
  }
});

// Make authenticated request
const contracts = await deelAPI.get('/contracts');
console.log(contracts.data);
```

**`Python`**

```python Python
import requests
import os

headers = {
    'Authorization': f'Bearer {access_token}',
    'x-client-id': os.getenv('CLIENT_ID')
}

response = requests.get(
    'https://api.letsdeel.com/rest/contracts',
    headers=headers
)

contracts = response.json()
```

**`Go`**

```go Go
package main

import (
    "net/http"
    "os"
)

func main() {
    client := &http.Client{}
    req, _ := http.NewRequest(
        "GET",
        "https://api.letsdeel.com/rest/contracts",
        nil
    )

    req.Header.Add("Authorization", "Bearer " + accessToken)
    req.Header.Add("x-client-id", os.Getenv("CLIENT_ID"))

    resp, err := client.Do(req)
    // Handle response...
}
```

## Token Rotation

Access tokens expire after 30 days. Refresh them before expiration to maintain uninterrupted access.

### Refreshing access tokens

Make a POST request with your refresh token:

```bash
curl -X POST 'https://app.deel.com/oauth2/tokens' \
  -H 'Authorization: Basic BASE64_ENCODED_CREDENTIALS' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'grant_type=refresh_token' \
  -d 'refresh_token=REFRESH_TOKEN' \
  -d 'redirect_uri=https://yourapp.com/callback'
```

**`Python`**

```python Python
import requests
import base64
import os

def refresh_access_token(refresh_token):
    credentials = base64.b64encode(
        f"{os.getenv('CLIENT_ID')}:{os.getenv('CLIENT_SECRET')}".encode()
    ).decode()

    response = requests.post(
        'https://app.deel.com/oauth2/tokens',
        headers={
            'Authorization': f'Basic {credentials}',
            'Content-Type': 'application/x-www-form-urlencoded'
        },
        data={
            'grant_type': 'refresh_token',
            'refresh_token': refresh_token,
            'redirect_uri': 'https://yourapp.com/callback'
        }
    )

    return response.json()  # Contains new access_token and refresh_token
```

**`Node.js`**

```javascript Node.js
const axios = require('axios');

async function refreshAccessToken(refreshToken) {
  const credentials = Buffer.from(
    `${process.env.CLIENT_ID}:${process.env.CLIENT_SECRET}`
  ).toString('base64');

  const response = await axios.post(
    'https://app.deel.com/oauth2/tokens',
    new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      redirect_uri: 'https://yourapp.com/callback'
    }),
    {
      headers: {
        'Authorization': `Basic ${credentials}`,
        'Content-Type': 'application/x-www-form-urlencoded'
      }
    }
  );

  return response.data; // Contains new access_token and refresh_token
}
```

**Response includes:**

* New access token (valid for another 30 days)
* Token expiration time (2,592,000 seconds)
* New refresh token (use this for next refresh)
* Token type (Bearer)
* Authorized scopes

Refresh tokens are **single-use**. Each successful token refresh invalidates the previous refresh token and returns a new one. If you attempt to reuse a refresh token that has already been exchanged, the server returns an `invalid_grant` error. Reuse of a refresh token may indicate token compromise, and the authorization server may revoke all tokens associated with that grant as a security measure.

**Proactive rotation**: Do not wait until tokens expire. Set up a scheduled job to refresh tokens every 25 days to avoid disruption. Always store the new refresh token returned in the response, as the previous one is no longer valid.

### Best practices for token rotation

#### Automatic refresh

Implement automatic token refresh in your application:

* Check token expiration before each request
* Refresh proactively (5 days before expiration)
* Handle refresh failures gracefully
* Always store and use the newly returned refresh token after each refresh, as the previous one is immediately invalidated
* Store new tokens securely

#### Handle expired tokens

If a request fails with 401:

1. Attempt to refresh the token
2. Retry the original request with the new token
3. If the refresh fails with `invalid_grant`, the refresh token may have already been used (not just expired). Prompt the user to re-authorize

#### Single-use refresh tokens

Deel implements refresh token rotation as defined in RFC 9700 Section 4.14.2:

* Each refresh token can only be exchanged **once** for a new access token and refresh token pair
* After a successful exchange, the previous refresh token becomes inactive immediately
* If a refresh token is used more than once, the authorization server treats this as a potential compromise signal
* The server may revoke all tokens associated with the grant to protect the account
* Always replace your stored refresh token with the new one returned in the refresh response

#### Secure token storage

* Store tokens encrypted at rest
* Never expose tokens in URLs or logs
* Use secure session storage for web apps
* Implement token revocation on logout

## Scopes Reference

Request only the scopes your application needs. Each API endpoint specifies which scopes are required - check the [API Reference](/reference) for details.

**Scope patterns**: Scopes follow the pattern `{resource}:read` or `{resource}:write` (e.g., `contracts:read`, `timesheets:write`).

**Common scope combinations:**

| Use Case                  | Recommended Scopes                                        |
| ------------------------- | --------------------------------------------------------- |
| Read-only contract viewer | `contracts:read`                                          |
| Contract management app   | `contracts:read contracts:write`                          |
| Timesheet integration     | `timesheets:read timesheets:write`                        |
| Invoice management        | `invoice-adjustments:read invoice-adjustments:write`      |
| Full workforce management | `contracts:read contracts:write people:read people:write` |
| Accounting integration    | `accounting:read contracts:read`                          |

## Recommended App Types by Use Case

| Integration Type       | Recommended App Type | Reason                         |
| ---------------------- | -------------------- | ------------------------------ |
| Contract data reading  | Organization App     | Access all org contracts       |
| Contract creation      | Personal App         | User-specific permissions      |
| Timesheets management  | Organization App     | Org-wide timesheet access      |
| Invoice adjustments    | Organization App     | Manage all org invoices        |
| SCIM API               | Organization App     | User provisioning across org   |
| Accounting integration | Organization App     | Financial data for entire org  |
| SSO integration        | Personal App         | Individual user authentication |

## Troubleshooting

#### 401 Unauthorized Error

**Common causes:**

* Invalid or expired access token
* Missing `x-client-id` header
* Token doesn't have required scopes

**Solutions:**

* Refresh the access token
* Verify both `Authorization` and `x-client-id` headers are present
* Check token scopes match endpoint requirements

#### 403 Forbidden Error

**Common causes:**

* Token lacks required scopes
* User doesn't have permission to access resource
* App type mismatch (personal app trying to access org resources)

**Solutions:**

* Review requested scopes during authorization
* Use organization app for org-wide resources
* Verify user has appropriate permissions

#### Invalid Grant Error

**When exchanging code or refreshing token:**

* Authorization code already used
* Refresh token expired
* Refresh token already used (refresh tokens are single-use)
* `redirect_uri` does not match

**Solutions:**

* Authorization codes are single-use only
* Request new authorization if the refresh token has expired
* Store the new refresh token from each refresh response and discard the old one. If you receive this error unexpectedly, it may indicate token compromise — re-authorize the user
* Ensure `redirect_uri` exactly matches the registered URI

#### State Mismatch

**Cause:**

* CSRF protection - returned `state` doesn't match sent state

**Solutions:**

* Store state value in session before redirect
* Validate returned state matches stored value
* Generate new state for each authorization request

## Security Best Practices

#### Validate State Parameter

Always verify the state parameter to prevent CSRF attacks

#### Secure Client Secret

Never expose client secret in client-side code or version control

#### Use HTTPS

Redirect URIs must use HTTPS (except localhost for development)

#### Minimal Scopes

Request only the permissions your app actually needs

#### Token Encryption

Store access and refresh tokens encrypted at rest

#### Implement Logout

Revoke tokens when users disconnect or log out

## Next Steps

#### [API Tokens](/api/authentication)

Learn about simpler token-based authentication

#### [Scopes Reference](/api/authentication#available-scopes)

View all available OAuth2 scopes

#### [Test in Sandbox](/api/sandbox)

Try OAuth2 flow in sandbox environment

#### [Webhooks](/api/webhooks/quickstart)

Receive real-time updates via webhooks