***

title: OAuth2
description: 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

<Steps>
  <Step title="Navigate to Apps">
    Go to **More** → **Developer** → **Apps** tab in your Deel dashboard
  </Step>

  <Step title="Create new app">
    Click **Create new app**
  </Step>

  <Step title="Select app type">
    Choose the appropriate app type:

    <Tabs>
      <Tab title="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.
      </Tab>

      <Tab title="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.
      </Tab>
    </Tabs>
  </Step>

  <Step title="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
  </Step>

  <Step title="Save credentials">
    After creation, you'll receive:

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

    <Warning>
      The client secret is only shown once. Store it securely immediately. If you lose it, you'll need to regenerate it.
    </Warning>
  </Step>
</Steps>

## 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
```

<CodeGroup>
  ```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
  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)
  ```
</CodeGroup>

### 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
```

<Warning>
  Always verify the `state` parameter matches what you sent to prevent CSRF attacks.
</Warning>

### 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
```

<CodeGroup>
  ```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
  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']
  ```
</CodeGroup>

**Response:**

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

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

### 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/v2/contracts' \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'x-client-id: CLIENT_ID'
```

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

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

  const deelAPI = axios.create({
    baseURL: 'https://api.letsdeel.com/rest/v2',
    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
  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/v2/contracts',
      headers=headers
  )

  contracts = response.json()
  ```

  ```go Go
  package main

  import (
      "net/http"
      "os"
  )

  func main() {
      client := &http.Client{}
      req, _ := http.NewRequest(
          "GET",
          "https://api.letsdeel.com/rest/v2/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...
  }
  ```
</CodeGroup>

## 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'
```

<CodeGroup>
  ```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
  ```

  ```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
  }
  ```
</CodeGroup>

**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

<Warning>
  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.
</Warning>

<Tip>
  **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.
</Tip>

### Best Practices for Token Rotation

<AccordionGroup>
  <Accordion title="Automatic refresh" icon="fa-light robot">
    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
  </Accordion>

  <Accordion title="Handle expired tokens" icon="fa-light clock">
    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
  </Accordion>

  <Accordion title="Single-use refresh tokens" icon="fa-light rotate">
    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
  </Accordion>

  <Accordion title="Secure token storage" icon="fa-light vault">
    * Store tokens encrypted at rest
    * Never expose tokens in URLs or logs
    * Use secure session storage for web apps
    * Implement token revocation on logout
  </Accordion>
</AccordionGroup>

## Scopes Reference

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

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

**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

<AccordionGroup>
  <Accordion title="401 Unauthorized Error" icon="fa-light triangle-exclamation">
    **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
  </Accordion>

  <Accordion title="403 Forbidden Error" icon="fa-light ban">
    **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
  </Accordion>

  <Accordion title="Invalid Grant Error" icon="fa-light circle-xmark">
    **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
  </Accordion>

  <Accordion title="State Mismatch" icon="fa-light shuffle">
    **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
  </Accordion>
</AccordionGroup>

## Security Best Practices

<AccordionGroup cols={2}>
  <Accordion title="Validate State Parameter" icon="fa-light shield-check">
    Always verify the state parameter to prevent CSRF attacks
  </Accordion>

  <Accordion title="Secure Client Secret" icon="fa-light lock">
    Never expose client secret in client-side code or version control
  </Accordion>

  <Accordion title="Use HTTPS" icon="fa-light globe">
    Redirect URIs must use HTTPS (except localhost for development)
  </Accordion>

  <Accordion title="Minimal Scopes" icon="fa-light filter">
    Request only the permissions your app actually needs
  </Accordion>

  <Accordion title="Token Encryption" icon="fa-light key">
    Store access and refresh tokens encrypted at rest
  </Accordion>

  <Accordion title="Implement Logout" icon="fa-light right-from-bracket">
    Revoke tokens when users disconnect or log out
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="API Tokens" icon="fa-light key" href="/api/authentication">
    Learn about simpler token-based authentication
  </Card>

  <Card title="Scopes Reference" icon="fa-light list" href="/api/authentication#available-scopes">
    View all available OAuth2 scopes
  </Card>

  <Card title="Test in Sandbox" icon="fa-light flask" href="/api/sandbox">
    Try OAuth2 flow in sandbox environment
  </Card>

  <Card title="Webhooks" icon="fa-light webhook" href="/api/webhooks/quickstart">
    Receive real-time updates via webhooks
  </Card>
</CardGroup>
