Appearance
AI Context File
Copy the entire block below and paste it into your AI assistant to give it full knowledge of the Determ API. This works with any AI tool -- ChatGPT, Claude, Cursor, GitHub Copilot, or any other LLM-based assistant.
How to use it:
- ChatGPT / Claude -- Start a new conversation, paste the block, then ask your question in the same message or in a follow-up.
- Cursor -- Paste the block into a
.cursorrulesfile in your project root for persistent context, or paste it into the chat for a one-off session. - Claude Code -- Paste the block into a
CLAUDE.mdfile in your project root, or include it directly in your prompt.
markdown
# Determ API -- Complete Reference
## Overview
Determ is a media monitoring platform. The public API lets you programmatically access mentions, create keywords, and retrieve metadata.
- **Base URL:** `https://api.mediatoolkit.com`
- **Authentication:** Bearer token in the `Authorization` header (`Authorization: Bearer YOUR_API_TOKEN`)
- **Response format:** JSON
- **Content-Type for POST requests:** `application/json`
---
## Endpoints
### GET /v2/me
Returns the authenticated user's profile, including organization memberships and roles.
**Headers:**
- `Authorization: Bearer YOUR_API_TOKEN` (required)
**Response fields:**
| Field | Type | Description |
|-------|------|-------------|
| `id` | int64 | User ID |
| `email` | string | Email address |
| `name` | string | Display name |
| `accessToken` | string | Current access token |
| `createdTime` | int64 | Account creation timestamp |
| `lastLoginTime` | int64 | Last login timestamp |
| `languageCode` | string | Preferred language code (e.g. `en`, `hr`) |
| `verified` | boolean | Whether the email is verified |
| `organizationIds` | array of int64 | Organization IDs the user belongs to |
| `organizationRoles` | object | Map of org ID to role object (`id`, `name`, `isAdmin`, `canUpdate`, `canInvite`, `canUpdateMentions`) |
**Example response:**
```json
{
"id": 12345,
"email": "user@example.com",
"name": "John Doe",
"languageCode": "en",
"verified": true,
"organizationIds": [100, 200],
"organizationRoles": {
"100": {
"id": "admin",
"name": "Admin",
"isAdmin": true,
"canUpdate": true,
"canInvite": true,
"canUpdateMentions": true
}
}
}
```
---
### GET /v2/organizations/{id}
Returns a single organization, including its groups, keywords, tags, member IDs, and subscription state. This is how you resolve the `groupId` and `keywordId` values that the mentions endpoints require. Returns 404 if the organization does not exist or the authenticated user has no access to it.
**Path parameters:**
- `id` (int64, required) -- Organization ID
**Response:**
```json
{
"id": 100,
"name": "Acme Inc.",
"groupIds": [10, 11],
"userIds": [12345, 67890],
"timeZone": "Europe/Zagreb",
"groups": [
{
"id": 10,
"name": "PR Team",
"color": "#4A90D9",
"colorNew": "blue",
"public": true,
"keywordIds": [200, 201],
"memberIds": [12345],
"invitedUsers": [
{ "email": "newuser@example.com", "roleId": "member" }
]
}
],
"keywords": [
{
"id": 200,
"name": "Acme",
"color": "#E94E77",
"colorNew": "red",
"active": true,
"editedAt": "2024-03-10T14:00:00Z",
"createdAt": "2024-01-15T09:30:00Z"
}
],
"tags": [
{ "id": 1, "name": "Important" }
],
"state": "ACTIVE",
"createdAt": "2024-01-15T09:30:00Z",
"currentMonthMentionsLimit": 50000
}
```
**Fields:** `id` (int64), `name` (string), `groupIds` (array of int64), `userIds` (array of int64), `timeZone` (string, IANA identifier), `groups` (array -- see below), `keywords` (array -- see below), `tags` (array of `{id, name}`), `state` (string, one of `BLOCKED` / `UNUSED` / `SUSPENDED` / `ACTIVE`), `createdAt` (string, date-time), `currentMonthMentionsLimit` (int64, mentions allowance for the current billing month).
**Group object:** `id` (int64), `name` (string), `color` (string, legacy hex), `colorNew` (string, current palette value), `public` (boolean, visible to all org members), `keywordIds` (array of int64), `memberIds` (array of int64), `invitedUsers` (array of `{email, roleId}` -- invited but not yet accepted).
**Keyword object:** `id` (int64), `name` (string), `color` (string, legacy hex), `colorNew` (string, current palette value), `active` (boolean), `editedAt` (string), `createdAt` (string).
---
### GET /v2/languages
Returns all supported languages. Use the returned codes when configuring keyword filters.
**Response:**
```json
{
"languages": [
{ "code": "en", "name": "English" },
{ "code": "hr", "name": "Croatian" },
{ "code": "de", "name": "German" }
]
}
```
**Fields:** `languages[].code` (string, ISO language code), `languages[].name` (string, English name).
---
### GET /v2/locations
Returns all supported locations. Use the returned codes when configuring keyword filters.
**Response:**
```json
{
"locations": [
{ "code": "US", "name": "United States" },
{ "code": "HR", "name": "Croatia" },
{ "code": "GB", "name": "United Kingdom" }
]
}
```
**Fields:** `locations[].code` (string, ISO country code), `locations[].name` (string, English name).
---
### GET /v2/organizations/{organizationId}/tags
Returns tags for a given organization, filtered by the provided tag IDs.
**Path parameters:**
- `organizationId` (int64, required)
**Query parameters:**
- `ids` (string, required) -- Comma-separated list of tag IDs (int64 values)
**Response:**
```json
{
"tags": [
{ "id": 1, "name": "Important" },
{ "id": 2, "name": "Reviewed" }
]
}
```
**Fields:** `tags[].id` (int64), `tags[].name` (string).
---
### POST /v2/organizations/{organizationId}/keywords
Creates a new keyword (monitoring query) within the specified organization.
**Path parameters:**
- `organizationId` (int64, required)
**Request body:**
| Field | Type | Description |
|-------|------|-------------|
| `groupId` | int64 | ID of the group to add the keyword to (required) |
| `name` | string | Display name for the keyword (required) |
| `keyword` | object | Keyword definition object (required) |
| `keyword.naturalQuery` | string | Search query. Supports boolean operators (`OR`, `AND`, quoted phrases) |
| `keyword.mayLangs` | array of string | Language codes to include (e.g. `["en", "de"]`) |
| `keyword.mayLocations` | array of string | Location codes to include (e.g. `["US", "GB"]`) |
| `keyword.maySourceTypes` | array of string | Source types to monitor (see Source Types enum) |
| `keyword.naturalFilters` | string | Filters in natural syntax, one `key:value,value` per line. Author filters: `author:` (include only) and `not_author:` (exclude). Quote values with spaces |
| `keyword.mayAuthors` | array of object | Include only these authors: `[{ "author": "Name", "caseSensitive": false }]`. Same as `author:` in `naturalFilters` |
| `keyword.notAuthors` | array of object | Exclude these authors, same object shape. Same as `not_author:` in `naturalFilters` |
**Example request:**
```json
{
"groupId": 456,
"name": "Tesla Mentions",
"keyword": {
"naturalQuery": "Tesla OR \"Elon Musk\"",
"naturalFilters": "not_author:\"Tesla Fan Club\"",
"mayLangs": ["en", "de"],
"maySourceTypes": ["WEB", "TWITTER", "FACEBOOK"]
}
}
```
Author matching: whole-string exact match (not substring), case-insensitive unless `caseSensitive` is true, accents not folded. Forum mentions have no author.
**Response fields:**
| Field | Type | Description |
|-------|------|-------------|
| `id` | int64 | Created keyword ID |
| `name` | string | Display name |
| `color` | string | Hex color code |
| `naturalQuery` | string | The search query |
| `active` | boolean | Whether the keyword is actively monitoring |
| `createdAt` | string (ISO 8601) | Creation timestamp |
| `editedAt` | string (ISO 8601) | Last edit timestamp |
---
### DELETE /v2/organizations/{organizationId}/keywords/{keywordId}
Permanently deletes a keyword (topic/query) and every mention collected under it.
**Path parameters:**
- `organizationId` (int64, required)
- `keywordId` (int32, required)
**Response:** `200 OK` with an empty body -- there is no JSON payload. Do not parse the response as JSON.
Destructive and only partly reversible: the collected mentions are permanently deleted and cannot be restored. The keyword setup may be recoverable through support; the mentions are not. Deleting the same keyword twice returns `404` on the second call, so treat `404` as "already gone" in retry logic.
---
### POST /v2/organizations/{organizationId}/keywords/{keywordId}/mentions/count
Returns the total number of mentions for a specific keyword, broken down by source type.
**Path parameters:**
- `organizationId` (int64, required)
- `keywordId` (int32, required)
**Request body (FeedsSearchQueryRequest):**
```json
{
"query": {
"feeds": [
{ "groupId": 200, "keywordId": 5001 }
],
"publishedTime": {
"relative": "LAST_7_DAYS"
},
"mentionFilter": {
"mentionType": { "any": ["WEB", "TWITTER"] },
"sentiment": { "any": ["POSITIVE", "NEUTRAL"] }
}
}
}
```
**Query fields explained:**
- `query.feeds` -- Optional array of `{ groupId, keywordId }` to scope the search. Omit to search all.
- `query.publishedTime` -- Time range filter based on publication time. Use `relative` (enum) for preset ranges, or `from`/`to` (ISO 8601 datetime) for custom ranges.
- `query.feedTime` -- Same structure as `publishedTime`, based on when the mention entered the feed.
- `query.mentionFilter` -- Supports `mentionType` and `sentiment` filters only. Each uses a **FilterClause** pattern with `any`, `all`, `must`, and `not` sub-arrays.
**Response:**
```json
{
"count": 1523,
"typeToCount": {
"WEB": 890,
"TWITTER": 423,
"FACEBOOK": 210
}
}
```
**Fields:** `count` (int64, total), `typeToCount` (object, breakdown by source type).
---
### POST /v2/organizations/{organizationId}/groups/{groupId}/mentions/count
Same as the keyword-level count endpoint, but scoped to a single group.
**Path parameters:**
- `organizationId` (int64, required)
- `groupId` (int64, required)
**Request body:** Same FeedsSearchQueryRequest as above (mentionType and sentiment filters only).
**Response:** Same format -- `count` and `typeToCount`.
---
### POST /v2/organizations/{organizationId}/keywords/{keywordId}/mentions/scroll
Returns a paginated list of mentions with full mention data for a specific keyword.
**Path parameters:**
- `organizationId` (int64, required)
- `keywordId` (int32, required)
**Request body (ScrollRequest):**
```json
{
"query": {
"publishedTime": { "relative": "LAST_7_DAYS" },
"mentionFilter": {
"sentiment": { "any": ["POSITIVE"] }
}
},
"paged": {
"count": 20,
"sorted": {
"direction": "DESC",
"property": "PUBLISHED_TIME"
}
},
"scrollToken": null
}
```
**ScrollRequest fields:**
- `query` -- FeedsSearchQueryRequest (same schema as count endpoints -- mentionType and sentiment filters only)
- `paged.count` (int32) -- Mentions per page. Maximum: 1000.
- `paged.sorted.direction` -- `ASC` or `DESC`
- `paged.sorted.property` -- Sort field (see Sort Properties enum)
- `scrollToken` (string) -- Token from previous response for next page. Omit on first request.
**Response:**
```json
{
"mentions": [
{
"id": 987654321,
"type": "WEB",
"title": "Tesla announces new factory",
"mention": "Tesla has announced plans to build a new factory...",
"url": "https://example.com/article/tesla-factory",
"author": "TechNews",
"reach": 50000,
"virality": 0.85,
"interaction": 1200,
"influenceScore": 78,
"autoSentiment": "POSITIVE",
"languages": ["en"],
"locations": ["US"],
"insertTime": 1705312800000,
"keywords": ["Tesla"]
}
],
"scrollToken": "eyJzZWFyY2hBZnRlciI6..."
}
```
**Mention object fields:**
| Field | Type | Description |
|-------|------|-------------|
| `id` | int64 | Mention ID |
| `type` | string | Source type |
| `title` | string | Title (may be null) |
| `mention` | string | Text snippet |
| `url` | string | Original URL |
| `author` | string | Author name/handle |
| `influencer` | string | Influencer name (if any) |
| `reach` | int32 | Estimated audience reach |
| `virality` | double | Virality score |
| `interaction` | int64 | Total interactions |
| `influenceScore` | int64 | Influence score |
| `autoSentiment` | string | Detected sentiment |
| `languages` | array of string | Detected languages |
| `locations` | array of string | Detected locations |
| `insertTime` | int64 | Ingestion timestamp (epoch ms) |
| `databaseInsertTime` | int64 | DB storage timestamp (epoch ms) |
| `image` | string | Associated image URL (if any) |
| `keywords` | array of string | Matched keywords |
**Pagination flow:**
1. First request: Send query without `scrollToken`. Response includes results + `scrollToken`.
2. Next page: Include the `scrollToken` from previous response. Keep the same `query` and `paged` params.
3. End: `scrollToken` is `null` or absent when no more results remain.
---
### POST /v2/organizations/{organizationId}/groups/{groupId}/mentions/scroll
Same as the keyword-level scroll endpoint, but scoped to a single group.
**Path parameters:**
- `organizationId` (int64, required)
- `groupId` (int64, required)
**Request body:** Same ScrollRequest as above.
**Response:** Same format -- `mentions` array and `scrollToken`.
---
## Enums
### Source Types
`WEB`, `FACEBOOK`, `TWITTER`, `FORUM`, `COMMENT`, `YOUTUBE`, `INSTAGRAM`, `REDDIT`, `TIKTOK`, `PRINT`, `RADIO`, `TV`, `BLUESKY`
### Sentiments
`POSITIVE`, `NEUTRAL`, `NEGATIVE`, `UNDEFINED`
### Relative Time Periods
`TODAY`, `YESTERDAY`, `LAST_24_HOURS`, `LAST_7_DAYS`, `LAST_30_DAYS`, `LAST_90_DAYS`, `THIS_MONTH`, `THIS_WEEK`, `LAST_WEEK`, `LAST_MONTH`
### Sort Properties
`PUBLISHED_TIME`, `FEED_TIME`, `REACH`, `VIRALITY`, `INTERACTIONS`, `INFLUENCE`, `ENGAGEMENT`
### Sort Directions
`ASC`, `DESC`
---
## Filter Clause Pattern
All mention filters use the same structure:
```json
{
"any": ["VALUE_1", "VALUE_2"],
"all": ["VALUE_3"],
"must": ["VALUE_4"],
"not": ["VALUE_5"]
}
```
- `any` -- Match any of the listed values (OR logic)
- `all` -- Match all of the listed values (AND logic)
- `must` -- Must match these values
- `not` -- Exclude these values
Most common usage is `any` for inclusive filtering and `not` for exclusions.
---
## Error Handling
**Error response format:** RFC 7807 Problem Details, sent 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"
}
```
**Fields:** `type` (string, always `"about:blank"` -- no custom problem type URIs), `title` (string, the HTTP reason phrase), `status` (int, the HTTP status code), `detail` (string, human-readable message), `instance` (string, the request path), `code` (string, **optional**).
`code` is only present on handled domain errors -- e.g. `invalid_param`, `missing_param`, `invalid_role`, `invalid_keyword` (400), `illegal_permission` (403), `not_exists` (404), `illegal_param` (409). It is absent on framework errors such as 401, 429, 500 and validation failures, so always treat it as optional.
**Do not assume the body is JSON.** Known exceptions:
- The `/v2/organizations/{orgId}/mentions/**` endpoints (count and scroll) return a plain `text/plain` body containing just the message for 400 and 404.
- 503 is typically produced by the edge proxy as `text/html`.
Parse defensively: check the status code first, then attempt to parse the body only if `Content-Type` says JSON, and fall back to the raw text.
**HTTP status codes:**
| Status | Meaning | Retry? |
|--------|---------|--------|
| 400 | Bad Request -- malformed request, bad enum, malformed date | No, fix the request |
| 401 | Unauthorized -- missing/invalid token | No, check token |
| 403 | Forbidden -- valid token, no permission | No, check access |
| 404 | Not Found -- resource does not exist | No, check IDs |
| 409 | Conflict -- conflicts with current state (e.g. already exists) | No, fix the request |
| 417 | Expectation Failed -- body invalid or unparseable | No, fix the body |
| 429 | Too Many Requests -- rate limit exceeded | Yes, see below |
| 500 | Internal Server Error | Yes, with backoff |
| 502 | Bad Gateway -- upstream service error | Yes, with backoff |
| 503 | Service Unavailable | Yes, with backoff |
**Rate limit headers:**
- `X-Rate-Limit-Remaining` -- on allowed requests, tokens left in the bucket. Use it to throttle proactively.
- `X-Rate-Limit-Retry-After-Seconds` -- on 429 responses, how many seconds to wait.
There is no `X-RateLimit-Limit` header; total bucket capacity is not discoverable from the response.
**Retry logic:** Up to 3 attempts, for the statuses marked retryable above. On 429, wait the number of seconds in `X-Rate-Limit-Retry-After-Seconds`; if that header is missing, fall back to exponential backoff starting at 1 second and doubling each attempt.
**Unexpected 429s:** limits are per user, per endpoint, but both mention *count* endpoints (keyword-level and group-level) share one bucket, and both *scroll* endpoints share another. Calling the keyword and group variants of the same operation draws from the same pool.
---
## Rules & Conventions
1. **Always include the Authorization header** with `Bearer YOUR_API_TOKEN` on every request.
2. **Use `scrollToken` for pagination** -- do not increment page numbers manually. Pass the token from the previous response. A single page returns at most 1000 mentions (`paged.count` <= 1000); page through results instead of trying to fetch everything at once.
3. **Date formats:** Use ISO 8601 (`2024-01-15T00:00:00Z`) for specific times, or the relative enum (`LAST_7_DAYS`) for preset ranges.
4. **ID types:** All IDs are int64 except `keywordId` which is int32.
5. **Keep query params stable during pagination** -- only update `scrollToken` between scroll requests.
6. **Rate limiting:** Per user, per endpoint -- see Error Handling above for shared buckets and 429 handling.
7. **Content-Type:** Always send `Content-Type: application/json` for POST requests.