For AI agents: a documentation index is available at the root level at /llms.txt and /llms-full.txt. Append /llms.txt to any URL for a page-level index, or .md for the markdown version of any page.
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
return response.json() # 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:
Attempt to refresh the token
Retry the original request with the new token
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 for details.
Scope patterns: Scopes follow the pattern {resource}:read or {resource}:write (e.g., contracts:read, timesheets:write).
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