Appearance
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"
}| Field | Type | Description |
|---|---|---|
type | string (URI) | Always "about:blank" — no custom problem type URIs are emitted |
title | string | The HTTP reason phrase ("Bad Request", "Not Found", …) |
status | integer | The HTTP status code |
detail | string | Human-readable error message |
instance | string | The request path (e.g. /v2/organizations/100/keywords) |
code | string | Optional. 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)
| Status | Name | Description |
|---|---|---|
| 400 | Bad Request | The request is malformed. Check query parameters, request body, and data types. |
| 401 | Unauthorized | Authentication token is missing or invalid. See Authentication. |
| 403 | Forbidden | The token is valid but you lack permission to access this resource. |
| 404 | Not Found | The requested resource does not exist. Verify IDs and paths. |
| 409 | Conflict | The request conflicts with the current state (e.g. the resource already exists in an incompatible way). |
| 417 | Expectation Failed | The request body is invalid or unparseable. |
| 429 | Too Many Requests | You have exceeded the rate limit. Wait the number of seconds indicated by X-Rate-Limit-Retry-After-Seconds before retrying. |
Server Errors (5xx)
| Status | Name | Description |
|---|---|---|
| 500 | Internal Server Error | An unexpected error occurred on the server. Retry after a short delay. |
| 502 | Bad Gateway | An upstream service returned an error. Retry shortly. |
| 503 | Service Unavailable | The 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: 30Fix: 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
| Endpoint | Limit | Scope |
|---|---|---|
GET /v2/me | 200 req/s | Per user |
GET /v2/languages | 100 req/60s | Per IP |
GET /v2/locations | 100 req/60s | Per IP |
GET /v2/organizations/{orgId}/tags | 100 req/s | Per user |
POST /v2/organizations/{orgId}/keywords | 2 req/10s | Per user |
POST .../mentions/count (keyword & group) | 100 req/60s | Per user |
POST .../mentions/scroll (keyword & group) | 100 req/60s | Per 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
| Scenario | Header | Description |
|---|---|---|
| Request allowed | X-Rate-Limit-Remaining | Number of remaining tokens in the bucket |
| Rate limit exceeded | X-Rate-Limit-Retry-After-Seconds | Seconds 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:
- Read the
X-Rate-Limit-Retry-After-Secondsheader - Wait for the indicated number of seconds
- Retry the request
- 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
- Always check the status code before parsing the response body
- Retry only transient errors (
429,500,502,503) — do not retry400,401,403, or404 - Use exponential backoff — start at 1 second and double the wait each time
- Set a maximum retry count — 3 retries is usually sufficient
- Log errors with the full response body for debugging
- Respect rate limits — if you consistently hit
429, reduce your request frequency rather than relying on retries - Check
X-Rate-Limit-Remaining— monitor this header to throttle proactively before hitting the limit - Be aware of shared buckets — all mention count endpoints share one bucket; all mention scroll endpoints share another
Next Steps
- Authentication — Set up and secure your API token
- Getting Started — Make your first API call
- API Reference — Explore all available endpoints