Skip to content

Errors & Rate Limits

This page documents the error responses returned by the Determ API and how to handle them in your code.

Error Response Format

The API returns errors in the RFC 7807 Problem Details format with Content-Type: application/problem+json.

json
{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "Invalid parameter: from",
  "instance": "/v2/organizations/100/keywords",
  "code": "invalid_param"
}
FieldTypeDescription
typestring (URI)Always "about:blank" — no custom problem type URIs are emitted
titlestringThe HTTP reason phrase ("Bad Request", "Not Found", …)
statusintegerThe HTTP status code
detailstringHuman-readable error message
instancestringThe request path (e.g. /v2/organizations/100/keywords)
codestringOptional. Domain error code on handled domain exceptions (e.g. invalid_param, not_exists, illegal_permission). Not present on framework errors like 429, 500, validation failures.

Known exception

The /v2/organizations/{orgId}/mentions/** endpoints (count/scroll) return a plain text/plain body with just the error message for 400 and 404 — not the JSON structure above. Handle both cases if you parse errors for those endpoints.

HTTP Status Codes

Client Errors (4xx)

StatusNameDescription
400Bad RequestThe request is malformed. Check query parameters, request body, and data types.
401UnauthorizedAuthentication token is missing or invalid. See Authentication.
403ForbiddenThe token is valid but you lack permission to access this resource.
404Not FoundThe requested resource does not exist. Verify IDs and paths.
409ConflictThe request conflicts with the current state (e.g. the resource already exists in an incompatible way).
417Expectation FailedThe request body is invalid or unparseable.
429Too Many RequestsYou have exceeded the rate limit. Wait the number of seconds indicated by X-Rate-Limit-Retry-After-Seconds before retrying.

Server Errors (5xx)

StatusNameDescription
500Internal Server ErrorAn unexpected error occurred on the server. Retry after a short delay.
502Bad GatewayAn upstream service returned an error. Retry shortly.
503Service UnavailableThe server is temporarily unavailable (typically returned by the edge proxy as text/html, not JSON).

Common Error Scenarios

400 Bad Request

Typically caused by:

  • Invalid parameter values (bad enum, out-of-range numbers, malformed dates)
  • Missing required parameters
  • Malformed JSON in the request body
json
{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "detail": "Invalid parameter: from",
  "instance": "/v2/organizations/100/keywords",
  "code": "invalid_param"
}

Possible code values: invalid_param, missing_param, invalid_role, invalid_keyword.

401 Unauthorized

Your token is missing or invalid:

json
{
  "type": "about:blank",
  "title": "Unauthorized",
  "status": 401,
  "detail": "Missing Authentication Token",
  "instance": "/v2/organizations/100/keywords"
}

Fix: Verify that your Authorization header is formatted as Bearer YOUR_API_TOKEN with no extra spaces or characters. See Authentication.

403 Forbidden

Your token is valid but cannot access the requested resource:

json
{
  "type": "about:blank",
  "title": "Forbidden",
  "status": 403,
  "detail": "User does not have access to organization",
  "instance": "/v2/organizations/100/keywords",
  "code": "illegal_permission"
}

Fix: Confirm that your user account is a member of the organization referenced in the request path.

404 Not Found

The resource ID does not exist or has been deleted:

json
{
  "type": "about:blank",
  "title": "Not Found",
  "status": 404,
  "detail": "Keyword 12345 does not exist",
  "instance": "/v2/organizations/100/keywords/12345",
  "code": "not_exists"
}

Fix: Double-check resource IDs. Use the list endpoints to discover valid IDs.

409 Conflict

The request conflicts with current resource state:

json
{
  "type": "about:blank",
  "title": "Conflict",
  "status": 409,
  "detail": "Already exists",
  "instance": "/v2/organizations/100/keywords",
  "code": "illegal_param"
}

429 Too Many Requests

You are sending requests too quickly. The response includes an X-Rate-Limit-Retry-After-Seconds header telling you how long to wait:

json
{
  "type": "about:blank",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "No message available",
  "instance": "/v2/organizations/100/keywords"
}

Response headers:

X-Rate-Limit-Retry-After-Seconds: 30

Fix: Wait the indicated number of seconds, then retry. See the retry logic section below.

502 Bad Gateway

An upstream service returned an error to the API:

json
{
  "type": "about:blank",
  "title": "Bad Gateway",
  "status": 502,
  "detail": "Downstream service error"
}

Fix: Retry after a short delay with exponential backoff.

Rate Limiting

The API uses a token bucket algorithm (Bucket4j) to enforce rate limits. Limits are applied per user, per endpoint method — meaning each endpoint has its own independent bucket for each authenticated user.

For unauthenticated endpoints (languages, locations), rate limiting is applied per IP address.

Rate Limits by Endpoint

EndpointLimitScope
GET /v2/me200 req/sPer user
GET /v2/languages100 req/60sPer IP
GET /v2/locations100 req/60sPer IP
GET /v2/organizations/{orgId}/tags100 req/sPer user
POST /v2/organizations/{orgId}/keywords2 req/10sPer user
POST .../mentions/count (keyword & group)100 req/60sPer user
POST .../mentions/scroll (keyword & group)100 req/60sPer user

Shared Buckets for Mention Endpoints

Both count endpoints (keyword-level and group-level) share a single 100 req/60s bucket per user. The same applies to both scroll endpoints. This means calling keyword-level count and group-level count draws from the same token pool.

Response Headers

ScenarioHeaderDescription
Request allowedX-Rate-Limit-RemainingNumber of remaining tokens in the bucket
Rate limit exceededX-Rate-Limit-Retry-After-SecondsSeconds to wait before the next token becomes available

INFO

The API does not return an X-RateLimit-Limit header — the total bucket capacity is not discoverable from headers. Refer to the table above for limits.

When Rate Limited

When you exceed the limit, the API returns HTTP 429 immediately (the request is rejected before reaching the endpoint handler):

json
{
  "type": "about:blank",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "No message available",
  "instance": "/v2/organizations/100/keywords"
}

How to handle 429 responses:

  1. Read the X-Rate-Limit-Retry-After-Seconds header
  2. Wait for the indicated number of seconds
  3. Retry the request
  4. If no header is present, use exponential backoff (see below)

Handling Errors in Code

Retry Logic with Exponential Backoff

For transient errors (429, 500, 502, 503), implement retry logic with exponential backoff:

python
import time
import requests

API_TOKEN = "YOUR_API_TOKEN"
BASE_URL = "https://api.mediatoolkit.com"

def api_request(method, path, max_retries=3, **kwargs):
    """Make an API request with automatic retry for transient errors."""
    url = f"{BASE_URL}{path}"
    headers = {"Authorization": f"Bearer {API_TOKEN}"}

    for attempt in range(max_retries + 1):
        response = requests.request(method, url, headers=headers, **kwargs)

        if response.status_code in (429, 500, 502, 503):
            if attempt < max_retries:
                # Use server-provided retry delay for 429, fallback to exponential backoff
                retry_after = response.headers.get("X-Rate-Limit-Retry-After-Seconds")
                wait = int(retry_after) if retry_after else 2 ** attempt
                print(f"Got {response.status_code}, retrying in {wait}s...")
                time.sleep(wait)
                continue

        return response

    return response


# Usage
response = api_request("GET", "/v2/me")
if response.ok:
    print(response.json())
else:
    print(f"Error {response.status_code}: {response.text}")
javascript
const API_TOKEN = "YOUR_API_TOKEN";
const BASE_URL = "https://api.mediatoolkit.com";

async function apiRequest(method, path, options = {}, maxRetries = 3) {
  const url = `${BASE_URL}${path}`;
  const headers = {
    "Authorization": `Bearer ${API_TOKEN}`,
    ...options.headers,
  };

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const response = await fetch(url, { method, headers, ...options });

    if ([429, 500, 502, 503].includes(response.status)) {
      if (attempt < maxRetries) {
        // Use server-provided retry delay for 429, fallback to exponential backoff
        const retryAfter = response.headers.get("X-Rate-Limit-Retry-After-Seconds");
        const wait = retryAfter ? parseInt(retryAfter) * 1000 : Math.pow(2, attempt) * 1000;
        console.log(`Got ${response.status}, retrying in ${wait}ms...`);
        await new Promise((r) => setTimeout(r, wait));
        continue;
      }
    }

    return response;
  }
}

// Usage
const response = await apiRequest("GET", "/v2/me");
if (response.ok) {
  const data = await response.json();
  console.log(data);
} else {
  console.error(`Error ${response.status}: ${await response.text()}`);
}
php
<?php

$apiToken = "YOUR_API_TOKEN";
$baseUrl = "https://api.mediatoolkit.com";

function apiRequest($method, $path, $maxRetries = 3) {
    global $apiToken, $baseUrl;

    $url = $baseUrl . $path;

    for ($attempt = 0; $attempt <= $maxRetries; $attempt++) {
        $ch = curl_init($url);
        $responseHeaders = [];
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CUSTOMREQUEST => $method,
            CURLOPT_HTTPHEADER => [
                "Authorization: Bearer " . $apiToken,
            ],
            CURLOPT_HEADERFUNCTION => function ($ch, $header) use (&$responseHeaders) {
                $parts = explode(':', $header, 2);
                if (count($parts) === 2) {
                    $responseHeaders[trim($parts[0])] = trim($parts[1]);
                }
                return strlen($header);
            },
        ]);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if (in_array($httpCode, [429, 500, 502, 503])) {
            if ($attempt < $maxRetries) {
                // Use server-provided retry delay for 429, fallback to exponential backoff
                $retryAfter = $responseHeaders['X-Rate-Limit-Retry-After-Seconds'] ?? null;
                $wait = $retryAfter ? (int) $retryAfter : pow(2, $attempt);
                echo "Got $httpCode, retrying in {$wait}s...\n";
                sleep($wait);
                continue;
            }
        }

        return ["status" => $httpCode, "body" => $response];
    }

    return ["status" => $httpCode, "body" => $response];
}

// Usage
$result = apiRequest("GET", "/v2/me");
if ($result["status"] === 200) {
    print_r(json_decode($result["body"], true));
} else {
    echo "Error {$result['status']}: {$result['body']}\n";
}

Best Practices

  1. Always check the status code before parsing the response body
  2. Retry only transient errors (429, 500, 502, 503) — do not retry 400, 401, 403, or 404
  3. Use exponential backoff — start at 1 second and double the wait each time
  4. Set a maximum retry count — 3 retries is usually sufficient
  5. Log errors with the full response body for debugging
  6. Respect rate limits — if you consistently hit 429, reduce your request frequency rather than relying on retries
  7. Check X-Rate-Limit-Remaining — monitor this header to throttle proactively before hitting the limit
  8. Be aware of shared buckets — all mention count endpoints share one bucket; all mention scroll endpoints share another

Next Steps

Built by Determ — Media Monitoring & Analytics