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_ALBUM or REELS
  • filter[publishedAfter], filter[publishedBefore]: an ISO 8601 date or datetime, such as 2026-01-31 or 2026-01-31T09:00:00Z
  • sort: -published_at, published_at, -publishedAt, publishedAt, -id or id
  • include[]: children or comments, in array form (include[]=children&include[]=comments)
  • format: jpg or webp

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 429 responses, 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:

  1. If the status isn't 200, read errors[0].code as for REST.
  2. If the body has errors, report each message, and each entry in extensions.validation if present.
  3. 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

Next steps