Authentication
The SavvyMoney API uses OAuth 2.0 for authentication. This guide covers how to obtain and use access tokens.
Overviewβ
There are two types of tokens:
| Token Type | Purpose | Lifetime |
|---|---|---|
| Access Token | Server-to-server API calls | 1 hour |
| User Token | User-specific operations (iframe, mobile) | 1 hour |
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β Your Server ββββββΆβ SavvyMoney ββββββΆβ Credit Data β
β β β Auth Server β β β
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β β
β Access Token β
βββββββββββββββββββββββββ
β β
β User Token β
βββββββββββββββββββββββββ
Obtaining Access Tokensβ
Client Credentials Flowβ
Use the client credentials grant to obtain an access token for server-to-server API calls.
curl -X POST "https://api.sandbox.savvymoney.com/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET"
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read write"
}
Code Examplesβ
const getAccessToken = async () => {
const response = await fetch('https://api.sandbox.savvymoney.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.SAVVY_CLIENT_ID,
client_secret: process.env.SAVVY_CLIENT_SECRET
})
});
if (!response.ok) {
throw new Error(`Authentication failed: ${response.status}`);
}
const data = await response.json();
return data.access_token;
};
@Service
public class SavvyMoneyAuthService {
private final RestTemplate restTemplate;
@Value("${savvymoney.client-id}")
private String clientId;
@Value("${savvymoney.client-secret}")
private String clientSecret;
@Value("${savvymoney.auth-url}")
private String authUrl;
public String getAccessToken() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "client_credentials");
body.add("client_id", clientId);
body.add("client_secret", clientSecret);
HttpEntity<MultiValueMap<String, String>> request =
new HttpEntity<>(body, headers);
ResponseEntity<TokenResponse> response = restTemplate.postForEntity(
authUrl + "/oauth/token",
request,
TokenResponse.class
);
return response.getBody().getAccessToken();
}
}
import requests
import os
def get_access_token():
response = requests.post(
'https://api.sandbox.savvymoney.com/oauth/token',
data={
'grant_type': 'client_credentials',
'client_id': os.environ['SAVVY_CLIENT_ID'],
'client_secret': os.environ['SAVVY_CLIENT_SECRET']
}
)
response.raise_for_status()
return response.json()['access_token']
Obtaining User Tokensβ
User tokens are required for user-specific operations like displaying credit scores in iframes or mobile apps.
curl -X POST "https://api.sandbox.savvymoney.com/v1/auth/user-token" \
-H "Authorization: Bearer {access_token}" \
-H "Content-Type: application/json" \
-d '{
"userId": "user-12345",
"email": "user@example.com",
"firstName": "John",
"lastName": "Doe"
}'
{
"userToken": "ut_eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 3600,
"expiresAt": "2024-01-15T11:30:00Z"
}
Required Fieldsβ
| Field | Type | Description |
|---|---|---|
userId | string | Your internal user identifier |
email | string | User's email address |
firstName | string | User's first name |
lastName | string | User's last name |
Optional Fieldsβ
| Field | Type | Description |
|---|---|---|
phone | string | User's phone number |
externalId | string | Additional external identifier |
Using Tokensβ
Bearer Authenticationβ
Include the access token in the Authorization header:
curl -X GET "https://api.savvymoney.com/v1/users/user-12345/credit-score" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
Token Managementβ
Caching Tokensβ
Tokens are valid for 1 hour. Implement caching to avoid unnecessary token requests:
class TokenManager {
constructor() {
this.token = null;
this.expiresAt = null;
}
async getToken() {
// Check if current token is still valid (with 5-minute buffer)
const now = Date.now();
const bufferMs = 5 * 60 * 1000; // 5 minutes
if (this.token && this.expiresAt && (this.expiresAt - now > bufferMs)) {
return this.token;
}
// Fetch new token
const response = await fetch('https://api.savvymoney.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: process.env.SAVVY_CLIENT_ID,
client_secret: process.env.SAVVY_CLIENT_SECRET
})
});
const data = await response.json();
this.token = data.access_token;
this.expiresAt = now + (data.expires_in * 1000);
return this.token;
}
invalidate() {
this.token = null;
this.expiresAt = null;
}
}
// Singleton instance
export const tokenManager = new TokenManager();
Handling Token Expirationβ
If a request fails with 401 Unauthorized, refresh the token and retry:
const makeAuthenticatedRequest = async (url, options = {}) => {
const token = await tokenManager.getToken();
const response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`
}
});
// If unauthorized, refresh token and retry once
if (response.status === 401) {
tokenManager.invalidate();
const newToken = await tokenManager.getToken();
return fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${newToken}`
}
});
}
return response;
};
Security Best Practicesβ
Credential Storageβ
- Never include credentials in client-side code
- Never commit credentials to version control
- Use environment variables or secure vaults
# .env file (never commit this!)
SAVVY_CLIENT_ID=your_client_id
SAVVY_CLIENT_SECRET=your_client_secret
// Node.js
const clientId = process.env.SAVVY_CLIENT_ID;
const clientSecret = process.env.SAVVY_CLIENT_SECRET;
Token Securityβ
- Store tokens securely - Use secure, encrypted storage
- Never log tokens - Mask tokens in logs
- Use HTTPS - All API calls must use HTTPS
- Rotate credentials - Periodically rotate client credentials
IP Allowlistingβ
For production environments, we recommend IP allowlisting. Contact support to configure allowed IP addresses for your account.
Scopesβ
Access tokens can be limited to specific scopes:
| Scope | Description |
|---|---|
read | Read-only access to user data |
write | Create and update operations |
admin | Administrative operations |
offers | Access to offer data |
webhooks | Manage webhook subscriptions |
Request specific scopes during authentication:
curl -X POST "https://api.savvymoney.com/oauth/token" \
-d "grant_type=client_credentials" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "scope=read offers"
Troubleshootingβ
Common Errorsβ
| Error | Cause | Solution |
|---|---|---|
invalid_client | Wrong client ID or secret | Verify credentials |
invalid_grant | Expired or invalid grant | Check grant type |
unauthorized_client | Client not authorized for grant type | Contact support |
invalid_scope | Requested scope not allowed | Use authorized scopes |
Debug Authenticationβ
Test your credentials:
curl -v -X POST "https://api.sandbox.savvymoney.com/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET"
Next Stepsβ
- Explore API Endpoints
- Set up Webhooks
- Review Security Best Practices