Skip to content

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 .cursorrules file 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.md file 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.

Built by Determ — Media Monitoring & Analytics