Errors
Every error code a data lookup can return, whether it is worth retrying, how long to wait, and what to send us when you need help.
Every error returns the same envelope: a stable error code and a human-readable message. Branch on error, never on message, which may be reworded.
{
"error": "PROFILE_UNAVAILABLE",
"message": "This profile does not exist or has been deactivated."
}A request answered with an error is not billed.
Permanent errors
These fail the same way again. Do not retry the same request unchanged.
error | HTTP | Meaning and what to do |
|---|---|---|
UNAUTHORIZED | 401 | The token is missing, unknown, expired or revoked, or belongs to the other environment. Check the Authorization header |
CUSTOMER_SUSPENDED | 403 | Your account is suspended. Contact us |
INSUFFICIENT_SCOPE | 403 | The route needs a token with more rights than yours, for example creating a token. Ask us to do it |
PROFILE_RESTRICTED | 403 | /v1/profile only: the platform withholds this profile from public lookups, including a private account that could not be read |
POST_NOT_FOUND | 404 | The post no longer exists at that URL |
PROFILE_UNAVAILABLE | 404 | /v1/profile only: the account does not exist or was deactivated |
VALIDATION_ERROR | 422 | A query parameter or the request body is missing or malformed, or the URL is not a supported post URL. Nothing was read. Correct the request |
POST_NOT_ACCESSIBLE | 422 | The post is private or otherwise not public |
POST_AGE_RESTRICTED | 422 | The post is age-restricted and could not be read, even with reduced detail. Only its author can fix this for good, by reposting without the restriction |
POST_* and PROFILE_* codes are permanent for that post or profile, not forever: a post made public again, or a profile reactivated, reads normally, though not always straight away.
Retryable errors
These are temporary. Retry the same request after the wait shown.
error | HTTP | Retry-After | When to retry |
|---|---|---|---|
RATE_LIMITED | 429 | Always | You went over a rate limit, or refreshed one challenge's metrics again within 5 minutes. Wait for Retry-After |
SCRAPING_RATE_LIMITED | 429 | Always | Reads are temporarily rate limited, whatever your own usage. Wait for Retry-After |
INTERNAL_ERROR | 500 | No | An unexpected error on our side. Back off from a few seconds; contact us if it persists |
SCRAPING_ERROR | 502 | Sometimes | The post or profile could not be read, for an unclassified reason. Back off from a few seconds |
SCRAPING_UNAVAILABLE | 503 | Sometimes | Reads are temporarily unavailable. Back off from about 30 seconds |
UPSTREAM_TIMEOUT | 504 | No | The request timed out. Retry after a few seconds, then back off |
BATCH_TIMEOUT | — | — | Batch only, in a result's error.code: the URL was not finished within the batch's 100 seconds. Send it again in a smaller batch or with GET /v1/stats |
Whenever a response carries Retry-After, wait at least that many seconds, even on a code the table marks "Sometimes". Otherwise use exponential backoff with jitter: double the wait after each failure, add a random fraction of it, cap it at about five minutes, and give up after five or six attempts.
In a batch, each failed URL carries one of these codes in its result's error.code, an object rather than the plain error string of a single call. The batch itself still answers 200.
Routes that return these codes
GET /v1/statsandPOST /v1/stats/batchGET /v1/commentsGET /v1/profile, the only route that returns thePROFILE_*codes
On the engagement verification routes:
GET /admin/posts/thumbnailPOST …/comments/refreshPOST …/metrics/refreshPOST /admin/creators/{userId}/refresh(for profiles,404means the profile no longer exists)
Liker fetching reports failures on the run's status and errorMessage instead; see Instagram likers.
Handling pattern
This function sorts a response into a result, a retry or a permanent failure. Not every error body is the API's JSON envelope: a 502 from a load balancer in between, for example, is not, so the status code decides when there is no error to read.
const RETRYABLE = new Set([
"RATE_LIMITED",
"SCRAPING_RATE_LIMITED",
"INTERNAL_ERROR",
"SCRAPING_ERROR",
"SCRAPING_UNAVAILABLE",
"UPSTREAM_TIMEOUT",
]);
type Outcome =
| { kind: "ok"; body: unknown }
| { kind: "retry"; retryAfterSeconds?: number }
| { kind: "permanent"; error: string };
async function classify(res: Response): Promise<Outcome> {
if (res.ok) return { kind: "ok", body: await res.json() };
const retryAfter = Number(res.headers.get("Retry-After"));
const { error } = await res.json().catch(() => ({ error: undefined }));
const retryable = error ? RETRYABLE.has(error) : res.status === 429 || res.status >= 500;
if (retryable) return { kind: "retry", retryAfterSeconds: retryAfter > 0 ? retryAfter : undefined };
// POST_*, PROFILE_*, VALIDATION_ERROR and auth errors: permanent
return { kind: "permanent", error: error ?? `HTTP ${res.status}` };
}For the waiting and retrying around it, use the tested read() function in High-volume integration.
Get help with a request
Every response carries an X-Request-Id header. Log it next to the error, and quote it when you contact us about a request: it is how we find that request on our side. You can also send your own X-Request-Id, up to 128 letters, digits, _ or -, and the API uses it instead of generating one, so the id in your logs and ours is the same.
Last updated on