Skip to main content

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 Authorization header
  • Expired or malformed JWT token
  • POST /create/{reportId}/confirm called 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:

MessageDescription
to continue please upgrade your subscriptionSigned-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 subscriptionNo 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}/confirm called 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:

  1. SSE — Subscribe to GET /sse/:reportId. You will receive a follows event with status: "error" and an error field for the failed profile:

    {"type":"follows","status":"error","id":"123456789","source":"ig","error":"private"}
  2. Polling — GET /check/:reportId response can include loadError and errorSource when 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 CodeDescriptionUser Action
privateProfile is privateTry different profile
notFoundProfile doesn't existVerify username
zeroFollowsNo follows to analyzeTry different profile
optOutUser opted outCannot analyze
unloadedTemporary load failure (e.g. timeout fetching follows)Retry with a new report if needed
unmappedCannot map profileContact support
unexpectedUnexpected failureRetry 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;
}