Error Handling
This guide covers all error responses from the Socialprofiler API and how to handle them properly.
Error Response Format
All errors follow a consistent format:
{
"code": 400,
"message": "Error description"
}
For field-level validation errors on the auth endpoints (/auth/signin, /auth/signup,
/auth/recovery, /auth/recovery/{hash}, /auth/google), the error is a soft error: HTTP
status is 200, and you must check status in the body instead:
{
"status": "error",
"fieldError": {
"field": "email",
"message": "user not found"
}
}
field is one of email, password, or promocode. Always check response.data.status === "error" for these endpoints — a 200 response is not automatically a success.
HTTP Status Codes
400 Bad Request
Invalid request parameters or validation errors.
Common causes:
- Missing required fields
- Invalid field format
- Invalid source code
- Report not found or not started
{
"code": 400,
"message": "Invalid request: source and username are required"
}
How to handle:
if (response.status === 400) {
const error = await response.json();
if (error.fieldError) {
showFieldError(error.fieldError.field, error.fieldError.message);
} else {
showError(error.message);
}
}
401 Unauthorized
Missing, invalid, or expired Bearer token / session on an endpoint that requires one.
Common causes:
- Missing
Authorizationheader - Expired or malformed JWT token
POST /create/{reportId}/confirmcalled by a guest session (not signed in)
{
"code": 401,
"message": "access denied"
}
Note: wrong email or password on POST /auth/signin is not a 401 — see "Soft Errors" above.
How to handle:
if (response.status === 401) {
// Token expired - re-authenticate
const newToken = await refreshToken();
// Retry original request with new token
return retryWithToken(originalRequest, newToken);
}
402 Payment Required
Subscription or credits needed.
Common causes:
- Report limit exceeded
- No active subscription
- Search limit reached
- Views limit reached
{
"code": 402,
"message": "to continue please upgrade your subscription"
}
Error variations:
| Message | Description |
|---|---|
to continue please upgrade your subscription | Signed-in user has no report credits — returned by POST /create (report creation), not by confirm |
You have reached search limit. Please Sign up to continue. | Unauthenticated caller hit the guest search limit (also from POST /create) |
to continue with a book of insta add-on please upgrade your subscription | No Book of Insta credits |
Note: confirming a report (POST /create/{reportId}/confirm) never returns 402 — the credit
check happens earlier, when the report is created. The credit itself is deducted transparently
the first time you fetch the finished report (GET /report/{reportId}); if you have none left at
that point, the report is returned with blurred/placeholder data instead of an error.
How to handle:
if (response.status === 402) {
showUpgradePrompt();
// Redirect to subscription page
window.location.href = '/subscription';
}
403 Forbidden
Access denied to the resource.
Common causes:
- Report already generated (can't modify)
- Report belongs to another user
- Account suspended
- Feature not available on plan
{
"code": 403,
"message": "access denied"
}
GET /create/{reportId} (and the other search/select endpoints) return 403 with
"report was already generated" once the report has moved past the search-and-select stage —
this is unrelated to confirm.
How to handle:
if (response.status === 403) {
const error = await response.json();
if (error.message === 'report was already generated') {
// Report is locked, redirect to view
redirectToReport(reportId);
} else {
showAccessDeniedError();
}
}
409 Conflict
Repeat confirm.
Cause:
POST /create/{reportId}/confirmcalled a second time on the same report — the profile selection was already locked in the first time.
{
"code": 409,
"message": "report profiles were already confirmed"
}
How to handle:
if (response.status === 409) {
// Already confirmed — treat as success and move on to waiting/fetching the report
proceedToWaitForReport(reportId);
}
404 Not Found
Resource doesn't exist.
Common causes:
- Invalid report ID
- Profile not found
- Interest not found
- Friend not found
{
"code": 404,
"message": "report not found"
}
GET /check/{reportId} also returns 404 (rather than 403) when the caller doesn't own the
report, to avoid revealing that the ID exists.
How to handle:
if (response.status === 404) {
showNotFoundError();
// Redirect to dashboard
window.location.href = '/dashboard';
}
408 Request Timeout
Search or operation timed out.
Common causes:
- Profile search took too long
- External service unavailable
- Network issues
{
"code": 408,
"message": "request can not be completed. Please try again later"
}
How to handle:
if (response.status === 408) {
showRetryPrompt();
// Retry after delay
await sleep(2000);
return retryRequest(originalRequest);
}
503 Service Unavailable
Backend service temporarily unavailable.
Common causes: a temporary outage or maintenance. Retry with backoff.
{
"code": 503,
"message": "service unavailable"
}
How to handle:
if (response.status === 503) {
showMaintenanceMessage();
// Retry with exponential backoff
await retryWithBackoff(originalRequest, {
maxRetries: 3,
initialDelay: 1000
});
}
Field-Specific Errors (Soft 200s)
These are all HTTP 200 responses with "status": "error" — see "Soft Errors" above.
Email Field
{
"status": "error",
"fieldError": {
"field": "email",
"message": "user not found"
}
}
Also used for a malformed email address ("message": "bad email") and for signing up with an
address that is already registered ("message": "user with this email was already registered").
Password Field
{
"status": "error",
"fieldError": {
"field": "password",
"message": "wrong password"
}
}
Promocode Field
Every promocode failure (not found, expired, already used, or malformed) returns the same generic message — the specific reason is not exposed to the client, only the field name:
{
"status": "error",
"fieldError": {
"field": "promocode",
"message": "Promo code is invalid, expired or was used up to the limit."
}
}
Profile Loading Errors (After Confirm)
After you call POST /create/:reportId/confirm, the API starts loading the selected account(s) and their subscriptions (follows). If loading fails for a profile (e.g. account turned private, service timeout, or follows could not be fetched), the API does not return an HTTP error for the whole report—instead it marks that profile as failed and continues. You detect this via progress/status APIs.
When it happens: During the loading phase, right after confirm. One or more profiles may fail while others succeed (e.g. Instagram loads, Twitter fails).
Where you see it:
-
SSE — Subscribe to
GET /sse/:reportId. You will receive afollowsevent withstatus: "error"and anerrorfield for the failed profile:{"type":"follows","status":"error","id":"123456789","source":"ig","error":"private"} -
Polling —
GET /check/:reportIdresponse can includeloadErroranderrorSourcewhen at least one profile failed to load. Use them to show which source failed and why.
Error codes (same as badReason and SSE error):
| Error Code | Description | User Action |
|---|---|---|
private | Profile is private | Try different profile |
notFound | Profile doesn't exist | Verify username |
zeroFollows | No follows to analyze | Try different profile |
optOut | User opted out | Cannot analyze |
unloaded | Temporary load failure (e.g. timeout fetching follows) | Retry with a new report if needed |
unmapped | Cannot map profile | Contact support |
unexpected | Unexpected failure | Retry with a new report if needed |
Important: The report credit is deducted when you first view the finished report
(GET /report/{reportId}), not on confirm. If loading fails for a profile, the report may still
be available with data for other profiles; the failed one will not have follows/interests data.
Handle status === "error" in SSE (or loadError from check) in your UI and inform the user that
one account could not be loaded.
Error Handling Best Practices
1. Implement Retry Logic
async function fetchWithRetry(url, options, maxRetries = 3) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url, options);
if (response.status === 503 || response.status === 408) {
// Retry on temporary errors
await sleep(Math.pow(2, i) * 1000);
continue;
}
return response;
} catch (error) {
lastError = error;
await sleep(Math.pow(2, i) * 1000);
}
}
throw lastError;
}
2. Token Refresh
refreshToken() below should call POST /auth/token/refresh, which mints a new JWT from the
session cookie set at sign-in (see Authentication).
class ApiClient {
async request(url, options) {
let response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${this.token}`
}
});
if (response.status === 401) {
// Try to refresh token
this.token = await this.refreshToken();
// Retry original request
response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${this.token}`
}
});
}
return response;
}
}
3. User-Friendly Messages
const ERROR_MESSAGES = {
'access denied': 'Please sign in to continue',
'to continue please upgrade your subscription': 'You\'ve used all your report credits. Upgrade your plan to continue.',
'report was already generated': 'This report is locked and can no longer be modified.',
'request can not be completed. Please try again later': 'The search is taking longer than expected. Please try again.',
'service unavailable': 'We\'re experiencing technical difficulties. Please try again in a few minutes.'
};
function getErrorMessage(apiError) {
return ERROR_MESSAGES[apiError.message] || apiError.message;
}
4. Logging for Debugging
async function apiCall(url, options) {
const startTime = Date.now();
try {
const response = await fetch(url, options);
const duration = Date.now() - startTime;
if (!response.ok) {
const error = await response.json();
console.error('API Error', {
url,
status: response.status,
error,
duration
});
}
return response;
} catch (error) {
console.error('Network Error', {
url,
error: error.message,
duration: Date.now() - startTime
});
throw error;
}
}
Common Error Scenarios
Scenario 1: Token Expired During Report Creation
async function createReport(username, source) {
try {
const response = await api.post('/create', { username, source });
return response.data;
} catch (error) {
if (error.status === 401) {
// Re-authenticate and retry
await api.authenticate();
return createReport(username, source);
}
throw error;
}
}
Scenario 2: Profile Not Found
async function addProfile(reportId, source, profileId) {
try {
await api.post(`/create/${reportId}/${source}/add`, { id: profileId });
} catch (error) {
if (error.status === 404) {
// Profile may have been removed, refresh search results
const profiles = await api.get(`/create/${reportId}/${source}`);
showProfileSelector(profiles);
return;
}
throw error;
}
}
Scenario 3: Handling Partial Failures
async function loadAllProfiles(reportId, profiles) {
const results = await Promise.allSettled(
profiles.map(p => addProfile(reportId, p.source, p.id))
);
const failed = results.filter(r => r.status === 'rejected');
if (failed.length > 0) {
console.warn(`${failed.length} profiles failed to load`);
showPartialLoadWarning(failed);
}
const succeeded = results.filter(r => r.status === 'fulfilled');
return succeeded.length > 0;
}