> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.deel.wtf/api/stable/oauth/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.deel.wtf/_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 > **Warning** > > 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 ``` > **Warning** > > 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" } ``` > **Note** > > * **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' ``` > **Warning** > > 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 > **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. > **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. ### 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. > **Note** > > **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 > Build apps and integrations that extend and enhance the Deel services.