Error Handling
Introduction
When a REST or RSS request fails, Feedframer returns an HTTP error status and a JSON body in the JSON:API error format. Every error has a machine-readable code, so your code can switch on code rather than parse messages.
GraphQL works differently. See "GraphQL errors" below.
HTTP status codes
| Status | Meaning | Codes |
|---|---|---|
200 |
Success | |
401 |
Missing or invalid API key | MISSING_API_KEY, INVALID_API_KEY |
404 |
The API path doesn't exist | NOT_FOUND |
405 |
Method other than GET on a REST endpoint |
METHOD_NOT_ALLOWED |
422 |
One or more invalid query parameters | VALIDATION_ERROR |
500 |
Something went wrong on our side | INTERNAL_SERVER_ERROR |
There are no rate limits, so you'll never get a 429.
Error response format
Error responses have Content-Type: application/vnd.api+json, whatever Accept header you send. That includes the RSS feed: an RSS request with a bad key gets a JSON error, not XML. (Successful REST responses are application/json.)
The body is an errors array:
{
"errors": [
{
"status": "422",
"code": "VALIDATION_ERROR",
"title": "Invalid Parameter",
"detail": "The page[size] field must not be greater than 100.",
"source": {
"parameter": "page[size]"
}
}
]
}
| Field | Description |
|---|---|
status |
The HTTP status, as a string |
code |
Machine-readable error code. Switch on this. |
title |
Short summary |
detail |
Human-readable explanation. The wording may change, so don't match on it. |
source.parameter |
Only on VALIDATION_ERROR: the query parameter at fault, written the way you sent it (page[size], filter[type], include[]) |
Most errors have one entry. A 422 has one entry per problem.
Errors are never cached, so fixing the request fixes the response straight away.
Errors by code
401 MISSING_API_KEY
The request has no api_key query parameter. The key must be in the query string; headers such as Authorization are not read.
curl 'https://feedframer.com/api/v1/me'
{
"errors": [
{
"status": "401",
"code": "MISSING_API_KEY",
"title": "Authentication Required",
"detail": "An API key is required. Please provide your API key using the api_key parameter."
}
]
}
Fix: add ?api_key=YOUR_API_KEY. See Authentication.
401 INVALID_API_KEY
The key doesn't match a connected account. This happens when:
- the key is mistyped or cut short (keys are 64 lowercase hex characters)
- the key was regenerated: the old key stops working immediately
- the Instagram account was disconnected from Feedframer. Reconnecting it issues a new key.
curl 'https://feedframer.com/api/v1/me?api_key=0000000000000000000000000000000000000000000000000000000000000000'
{
"errors": [
{
"status": "401",
"code": "INVALID_API_KEY",
"title": "Authentication Failed",
"detail": "The provided API key is invalid."
}
]
}
A disconnected account's key returns exactly this body too.
Fix: copy the current key from the account's card on your Dashboard, where it's labelled "Feedframer API Token".
An account whose Instagram token has expired is different: its status shows as "Invalid Token" on the Dashboard, but its key keeps working and serves the last stored posts. Reconnect the account to resume updates; the key stays the same. If you also talk to Meta's API directly, that same failure surfaces there as OAuthException code 190; Instagram API error 190 lists which message means which cause.
422 VALIDATION_ERROR
One or more query parameters are invalid. There is one entry per problem, each with source.parameter. This request has four:
curl -g 'https://feedframer.com/api/v1/me?api_key=YOUR_API_KEY&page[size]=500&filter[type]=PHOTO&sort=likes&filter[publishedAfter]=notadate'
{
"errors": [
{
"status": "422",
"code": "VALIDATION_ERROR",
"title": "Invalid Parameter",
"detail": "The selected filter[type] is invalid.",
"source": { "parameter": "filter[type]" }
},
{
"status": "422",
"code": "VALIDATION_ERROR",
"title": "Invalid Parameter",
"detail": "The filter[publishedAfter] field must be a valid date.",
"source": { "parameter": "filter[publishedAfter]" }
},
{
"status": "422",
"code": "VALIDATION_ERROR",
"title": "Invalid Parameter",
"detail": "The page[size] field must not be greater than 100.",
"source": { "parameter": "page[size]" }
},
{
"status": "422",
"code": "VALIDATION_ERROR",
"title": "Invalid Parameter",
"detail": "The sort value must be one of: -published_at, published_at, -publishedAt, publishedAt, -id, id.",
"source": { "parameter": "sort" }
}
]
}
The order of the entries isn't significant. Other validation errors you might see:
| Request | source.parameter |
detail |
|---|---|---|
page[size]=0 |
page[size] |
The page[size] field must be at least 1. |
include=children |
include |
The include field must be an array. |
include[]=likes |
include[] |
The selected include[] is invalid. |
format=png |
format |
The selected format is invalid. |
Fix: correct the named parameter. The valid values are:
page[size]: 1 to 100, on both plans. On Free, values from 7 to 100 return 6 posts rather than an error.filter[type]:IMAGE,VIDEO,CAROUSEL_ALBUMorREELSfilter[publishedAfter],filter[publishedBefore]: an ISO 8601 date or datetime, such as2026-01-31or2026-01-31T09:00:00Zsort:-published_at,published_at,-publishedAt,publishedAt,-idoridinclude[]:childrenorcomments, in array form (include[]=children&include[]=comments)format:jpgorwebp
When you use curl, pass -g so the brackets aren't treated as a pattern. See Filtering & Sorting and Pagination.
404 NOT_FOUND
The path doesn't exist under /api. The only REST endpoints are /api/v1/me, /api/v1/accounts and /api/v1/rss. There is no /api/v1/posts: posts come from /api/v1/me.
curl 'https://feedframer.com/api/v1/posts?api_key=YOUR_API_KEY'
{
"errors": [
{
"status": "404",
"code": "NOT_FOUND",
"title": "Not Found",
"detail": "This API endpoint does not exist. See https://feedframer.com/docs/rest-api."
}
]
}
The path is checked before the key, so you get a 404 even with a missing or invalid key.
405 METHOD_NOT_ALLOWED
The REST endpoints only accept GET.
curl -X POST 'https://feedframer.com/api/v1/me?api_key=YOUR_API_KEY'
{
"errors": [
{
"status": "405",
"code": "METHOD_NOT_ALLOWED",
"title": "Method Not Allowed",
"detail": "This endpoint only accepts GET requests."
}
]
}
Like the 404, this is checked before the key.
500 INTERNAL_SERVER_ERROR
Something failed on our side. Your request may be fine. This is the body the API code returns (we can't safely trigger one to show you a live response):
{
"errors": [
{
"status": "500",
"code": "INTERNAL_SERVER_ERROR",
"title": "Internal Server Error",
"detail": "Something went wrong on our side. Please try again; if it keeps happening, contact [email protected]."
}
]
}
Fix: retry after a short delay (see "Retrying" below). If it keeps happening, check the status page and email [email protected] with the time and the URL you requested, minus your API key.
Other responses
Handle any status or code not listed here by its status: treat 4xx as a problem with the request and 5xx as temporary. A network failure, or a proxy between you and Feedframer, can also give you a response that isn't JSON at all, so don't assume the body parses.
Handling errors in code
Each sample below:
- returns the data on success
- reads
errors[]from any error response, falling back to the status if the body isn't JSON - stops on 401, 404, 405 and 422, because retrying won't help
- retries 5xx responses and network errors up to three times, waiting 1, then 2 seconds
JavaScript (fetch)
const BASE_URL = 'https://feedframer.com';
class FeedframerError extends Error {
constructor(status, errors) {
super(errors.map((e) => e.detail).join(' ') || `HTTP ${status}`);
this.status = status;
this.errors = errors; // the JSON:API errors array (may be empty)
this.code = errors[0]?.code ?? null;
}
}
async function request(path, params) {
const response = await fetch(`${BASE_URL}${path}?${new URLSearchParams(params)}`);
if (response.ok) {
return response.json();
}
let errors = [];
try {
errors = (await response.json()).errors ?? [];
} catch {
// Not JSON: keep the status only.
}
throw new FeedframerError(response.status, errors);
}
async function requestWithRetry(path, params, attempts = 3) {
for (let attempt = 1; ; attempt++) {
try {
return await request(path, params);
} catch (error) {
const apiError = error instanceof FeedframerError;
const retryable = !apiError || error.status >= 500; // network error or 5xx
if (!retryable || attempt === attempts) throw error;
await new Promise((resolve) => setTimeout(resolve, 2 ** (attempt - 1) * 1000));
}
}
}
async function loadPosts() {
try {
const me = await requestWithRetry('/api/v1/me', { api_key: 'YOUR_API_KEY' });
console.log(`Loaded ${me.posts.length} posts for @${me.username}`);
return me.posts;
} catch (error) {
if (!(error instanceof FeedframerError)) {
console.error('Could not reach Feedframer:', error.message);
} else if (error.code === 'MISSING_API_KEY' || error.code === 'INVALID_API_KEY') {
console.error('Check your API key on the Feedframer Dashboard.');
} else if (error.code === 'VALIDATION_ERROR') {
for (const e of error.errors) {
console.error(`Invalid ${e.source.parameter}: ${e.detail}`);
}
} else {
// NOT_FOUND, METHOD_NOT_ALLOWED, INTERNAL_SERVER_ERROR, or anything else
console.error(`Feedframer error ${error.status} ${error.code ?? ''}: ${error.message}`);
}
return [];
}
}
loadPosts();
PHP
<?php
class FeedframerException extends RuntimeException
{
public function __construct(public int $status, public array $errors)
{
$detail = implode(' ', array_column($errors, 'detail'));
parent::__construct($detail !== '' ? $detail : "HTTP {$status}");
}
public function code(): ?string
{
return $this->errors[0]['code'] ?? null;
}
}
function feedframerRequest(string $path, array $params): array
{
$ch = curl_init('https://feedframer.com' . $path . '?' . http_build_query($params));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
]);
$raw = curl_exec($ch);
if ($raw === false) {
// Network failure: status 0 marks it as retryable.
throw new FeedframerException(0, [['detail' => curl_error($ch)]]);
}
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$body = json_decode($raw, true);
if ($status === 200) {
return $body;
}
throw new FeedframerException($status, $body['errors'] ?? []);
}
function feedframerRequestWithRetry(string $path, array $params, int $attempts = 3): array
{
for ($attempt = 1; ; $attempt++) {
try {
return feedframerRequest($path, $params);
} catch (FeedframerException $e) {
$retryable = $e->status === 0 || $e->status >= 500;
if (! $retryable || $attempt === $attempts) {
throw $e;
}
sleep(2 ** ($attempt - 1));
}
}
}
try {
$me = feedframerRequestWithRetry('/api/v1/me', ['api_key' => 'YOUR_API_KEY']);
echo 'Loaded ' . count($me['posts']) . " posts for @{$me['username']}" . PHP_EOL;
} catch (FeedframerException $e) {
switch ($e->code()) {
case 'MISSING_API_KEY':
case 'INVALID_API_KEY':
echo 'Check your API key on the Feedframer Dashboard.' . PHP_EOL;
break;
case 'VALIDATION_ERROR':
foreach ($e->errors as $error) {
echo "Invalid {$error['source']['parameter']}: {$error['detail']}" . PHP_EOL;
}
break;
default:
// NOT_FOUND, METHOD_NOT_ALLOWED, INTERNAL_SERVER_ERROR, network errors
echo "Feedframer error {$e->status} " . ($e->code() ?? '') . ": {$e->getMessage()}" . PHP_EOL;
}
}
Python
import time
import requests
BASE_URL = "https://feedframer.com"
class FeedframerError(Exception):
def __init__(self, status, errors):
self.status = status
self.errors = errors # the JSON:API errors array (may be empty)
self.code = errors[0].get("code") if errors else None
super().__init__(" ".join(e.get("detail", "") for e in errors) or f"HTTP {status}")
def request(path, params):
response = requests.get(BASE_URL + path, params=params, timeout=10)
if response.ok:
return response.json()
try:
errors = response.json().get("errors", [])
except ValueError: # not JSON
errors = []
raise FeedframerError(response.status_code, errors)
def request_with_retry(path, params, attempts=3):
for attempt in range(1, attempts + 1):
try:
return request(path, params)
except FeedframerError as e:
if e.status < 500 or attempt == attempts:
raise
except requests.RequestException: # network error
if attempt == attempts:
raise
time.sleep(2 ** (attempt - 1))
try:
me = request_with_retry("/api/v1/me", {"api_key": "YOUR_API_KEY"})
print(f"Loaded {len(me['posts'])} posts for @{me['username']}")
except FeedframerError as e:
if e.code in ("MISSING_API_KEY", "INVALID_API_KEY"):
print("Check your API key on the Feedframer Dashboard.")
elif e.code == "VALIDATION_ERROR":
for error in e.errors:
print(f"Invalid {error['source']['parameter']}: {error['detail']}")
else:
# NOT_FOUND, METHOD_NOT_ALLOWED, INTERNAL_SERVER_ERROR, or anything else
print(f"Feedframer error {e.status} {e.code or ''}: {e}")
except requests.RequestException as e:
print(f"Could not reach Feedframer: {e}")
Retrying
- Retry 5xx responses and network errors (timeouts, refused connections), with a growing delay between attempts. Stop after a few tries.
- Don't retry 401, 404, 405 or 422. The same request will fail the same way until you change it.
- There are no rate limits and no
429responses, so you don't need to throttle or read rate-limit headers.
If requests keep failing, show the visitor something useful instead of an empty space: for example, the last posts you successfully loaded, or a link to your Instagram profile.
GraphQL errors
The GraphQL endpoint (/graphql) reports errors in two ways.
Missing or invalid API key: HTTP 401. The key is checked before the query runs, and you get the same JSON:API body as the REST API, with code set to MISSING_API_KEY or INVALID_API_KEY.
Anything wrong with the query: HTTP 200 with errors. Invalid type or format values give a validation error with details in extensions.validation, keyed by argument:
{
"errors": [
{
"message": "Validation failed for the field [me].",
"locations": [{ "line": 1, "column": 3 }],
"path": ["me"],
"extensions": {
"validation": {
"type": ["The selected type is invalid."]
}
}
}
],
"data": { "me": null }
}
Other problems, such as an unparseable date, an unsupported orderBy column or a syntax error, give an entry with just a message (plus locations, and sometimes path). Errors caught before the query runs have no data key at all.
So a GraphQL client should:
- If the status isn't 200, read
errors[0].codeas for REST. - If the body has
errors, report eachmessage, and each entry inextensions.validationif present. - Otherwise, read
data.
async function graphql(query, variables = {}) {
const response = await fetch(
`https://feedframer.com/graphql?api_key=${encodeURIComponent('YOUR_API_KEY')}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables }),
},
);
const body = await response.json();
if (!response.ok) {
const error = body.errors?.[0];
throw new Error(`${response.status} ${error?.code}: ${error?.detail}`);
}
if (body.errors) {
const messages = body.errors.flatMap((e) =>
e.extensions?.validation
? Object.entries(e.extensions.validation).map(([arg, msgs]) => `${arg}: ${msgs.join(' ')}`)
: [e.message],
);
throw new Error(messages.join('; '));
}
return body.data;
}
graphql('query { me { username posts { id type } } }')
.then((data) => console.log(`${data.me.posts.length} posts`))
.catch((error) => console.error(error.message));
Asking post(id:) for a post your key can't see is not an error: it returns {"data": {"post": null}}. See the GraphQL API reference for more.
Error codes reference
| Code | Status | Retry? | Meaning |
|---|---|---|---|
MISSING_API_KEY |
401 | No | No api_key parameter |
INVALID_API_KEY |
401 | No | Unknown, regenerated or disconnected key |
NOT_FOUND |
404 | No | Unknown API path |
METHOD_NOT_ALLOWED |
405 | No | Not a GET request |
VALIDATION_ERROR |
422 | No | Invalid query parameter; see source.parameter |
INTERNAL_SERVER_ERROR |
500 | Yes | Our fault; retry, then contact support |
Getting help
- Service status: feedframer.com/status
- Email: [email protected], or use the contact page
Next steps
- Authentication: fix 401 errors
- REST API: endpoints and parameters
- GraphQL API: queries and GraphQL errors