Authentication
Cleavr Connect uses OAuth 2.0 for secure authentication. This allows your platform to act on behalf of your users while maintaining security and control.
Security Notice: Never expose your client_secret in client-side code or public repositories.
OAuth 2.0 Flow
OAuth 2.0 authentication flow between your platform and Cleavr Connect.
Implementation Guide
Step 1: Register Your Platform
Contact us at this form to register your platform. You'll receive:
{ "client_id": "plat_live_a1b2c3d4e5f6", "client_secret": "sk_live_xxxxxxxxxxxxx", "webhook_secret": "whsec_xxxxxxxxxxxxx"}Step 2: Authorization Request
Redirect users to our authorization endpoint:
const authorizationUrl = new URL('https://api.cleavr.fr/oauth/authorize'); authorizationUrl.searchParams.append('client_id', CLIENT_ID); authorizationUrl.searchParams.append('redirect_uri', 'https://yourapp.com/callback'); authorizationUrl.searchParams.append('response_type', 'code'); authorizationUrl.searchParams.append('scope', 'receivables:write users:write webhooks:read'); authorizationUrl.searchParams.append('state', generateSecureRandomState());// Redirect user window.location.href = authorizationUrl.toString();Available Scopes
| Scope | Description |
|---|---|
users:read | Read user information |
users:write | Create and update users |
receivables:read | View receivables |
receivables:write | Create and manage receivables |
webhooks:read | View webhook events |
webhooks:write | Configure webhooks |
reports:read | Access recovery reports |
commission:read | View commission rates |
Best Practice: Request only the scopes you need. Start with minimal permissions and expand as needed.
Step 3: Handle Callback
After user authorization, Cleavr redirects back to your redirect_uri:
https://yourapp.com/callback?code=auth_code_xxxxx&state=your_state_valueVerify the state parameter to prevent CSRF attacks:
app.get('/callback', async (req, res) => { const { code, state } = req.query;
// Verify state matches what you sent if (state !== session.oauth_state) { return res.status(400).send('Invalid state parameter'); }
// Exchange code for token const token = await exchangeCodeForToken(code); // Store token securely await storeTokenForUser(req.user.id, token);
res.redirect('/dashboard');});Step 4: Exchange Code for Token
Exchange the authorization code for an access token:
async function exchangeCodeForToken(code) { const response = await fetch('https://api.cleavr.fr/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'authorization_code', code: code, redirect_uri: 'https://yourapp.com/callback', client_id: CLIENT_ID, client_secret: CLIENT_SECRET, }), });return response.json(); }Response:
{ "access_token": "clv_tok_xxxxxxxxxxxxx", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "clv_ref_xxxxxxxxxxxxx", "scope": "receivables:write users:write webhooks:read"}Step 5: Make Authenticated Requests
Include the access token in the Authorization header:
const response = await fetch('https://api.cleavr.fr/v1/receivables', { method: 'POST', headers: { 'Authorization': Bearer ${accessToken}, 'Content-Type': 'application/json', 'Cleavr-Version': '2025-08-28' }, body: JSON.stringify(receivableData) });Token Management
Refresh Tokens
Access tokens expire after 1 hour. Use the refresh token to get a new access token:
async function refreshAccessToken(refreshToken) { const response = await fetch('https://api.cleavr.fr/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: CLIENT_ID, client_secret: CLIENT_SECRET, }), });return response.json(); }Token Storage Best Practices
Security Critical: Store tokens encrypted and never in client-side storage.
// DO: Store encrypted in databaseawait db.tokens.create({ user_id: userId, access_token: encrypt(token.access_token), refresh_token: encrypt(token.refresh_token), expires_at: new Date(Date.now() + token.expires_in * 1000)});
// DON'T: Store in localStorage or cookieslocalStorage.setItem('token', token); // ❌ Never do thisAutomatic Token Refresh
Implement automatic token refresh:
class CleavrClient { constructor(tokenStore) { this.tokenStore = tokenStore; }
async makeRequest(url, options = {}) { let token = await this.tokenStore.getToken();
// Check if token needs refresh if (this.isTokenExpired(token)) { token = await this.refreshToken(token.refresh_token); await this.tokenStore.saveToken(token); }
const response = await fetch(url, { ...options, headers: { ...options.headers, 'Authorization': `Bearer ${token.access_token}` } });
// Handle 401 - token might have been revoked if (response.status === 401) { token = await this.refreshToken(token.refresh_token); await this.tokenStore.saveToken(token);
// Retry request with new token return fetch(url, { ...options, headers: { ...options.headers, 'Authorization': `Bearer ${token.access_token}` } }); }
return response; }
isTokenExpired(token) { return Date.now() >= token.expires_at - 60000; // Refresh 1 min early }}Revoking Access
Users can revoke access at any time. Handle revocation gracefully:
// Revoke token programmaticallyasync function revokeToken(token) { await fetch('https://api.cleavr.fr/oauth/revoke', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ token: token, token_type_hint: 'access_token', client_id: CLIENT_ID, client_secret: CLIENT_SECRET, }), });}Error Handling
Common OAuth Errors
| Error | Description | Solution |
|---|---|---|
invalid_request | Missing required parameters | Check all required params are included |
invalid_client | Invalid client_id or client_secret | Verify your credentials |
invalid_grant | Invalid or expired authorization code | Request a new authorization |
unauthorized_client | Client not authorized for grant type | Contact support |
unsupported_grant_type | Invalid grant_type parameter | Use authorization_code or refresh_token |
invalid_scope | Requested scope is invalid | Check available scopes |
Error Response Example
{ "error": "invalid_grant", "error_description": "The provided authorization code is invalid or has expired", "error_uri": "https://docs.cleavr.fr/connect/errors#invalid_grant"}Security Considerations
1. PKCE (Recommended for SPAs)
For single-page applications, use PKCE for added security:
// Generate code verifier and challengeconst codeVerifier = generateRandomString(128);const codeChallenge = await sha256(codeVerifier);
// Include in authorization requestauthUrl.searchParams.append('code_challenge', codeChallenge);authUrl.searchParams.append('code_challenge_method', 'S256');
// Include verifier when exchanging codebody.append('code_verifier', codeVerifier);2. State Parameter
Always use a cryptographically secure random state:
function generateState() { return crypto.randomBytes(32).toString('hex');}3. Redirect URI Validation
- Use exact match validation
- Always use HTTPS in production
- Register all redirect URIs with Cleavr
4. Token Rotation
Implement token rotation for enhanced security:
// Each refresh invalidates the previous refresh tokenconst newTokens = await refreshAccessToken(currentRefreshToken);// currentRefreshToken is now invalidTesting
Test Environment
Use test credentials in development:
const config = { development: { apiUrl: 'https://api-test.cleavr.fr', clientId: 'plat_test_xxxxx', clientSecret: 'sk_test_xxxxx' }, production: { apiUrl: 'https://api.cleavr.fr', clientId: 'plat_live_xxxxx', clientSecret: 'sk_live_xxxxx' }};Test User Flow
- Use test mode credentials
- Create test users with prefix
test_ - Submit test receivables
- Verify webhook delivery
- Check token refresh flow
Code Examples
const { CleavrConnect } = require('@cleavr/connect-node');const client = new CleavrConnect({ clientId: process.env.CLEAVR_CLIENT_ID, clientSecret: process.env.CLEAVR_CLIENT_SECRET, });// Get authorization URL const authUrl = client.oauth.getAuthorizationUrl({ redirectUri: 'https://yourapp.com/callback', scope: ['receivables:write', 'users:write'], state: 'unique_state_value' });Next Steps
Once authentication is working:
