> ## Documentation Index
> Fetch the complete documentation index at: https://tryklint.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Fetch /llms.txt for the complete page index before exploring further. Every docs page is available as markdown by appending .md to its URL. Coding agents should start at /for-agents/coding-agents.md.

# Klint for coding agents

> Self-contained reference for AI coding agents: auth, both search grammars, entity GETs, pagination, errors, recommended defaults, and common mistakes.

One page with the whole Klint contract. Paste this page's `.md` URL into your coding agent, or fetch it directly; it is self-contained.

## Which doc to fetch when

| Resource                                   | Fetch when                                                    |
| ------------------------------------------ | ------------------------------------------------------------- |
| This page (`/for-agents/coding-agents.md`) | Building an integration: the condensed contract               |
| `/llms.txt`                                | Discovering every page before going deeper                    |
| Any page + `.md` suffix                    | One topic in depth (e.g. `/instagram/search/posts-search.md`) |

## Basics

* Base URL: `https://api.tryklint.ai`
* Auth on every request: `Authorization: Bearer <api-key>` (env var `KLINT_API_KEY`)
* JSON bodies in, JSON out; every non-2xx is the same compact JSON problem object
* IDs are opaque, prefixed, and durable: `ig_post_…`, `ig_account_…`, `ig_media_…`, `ig_audio_…`. Handles are mutable display identity; never store them as keys.

## Choose the operation

| Task state                                              | Operation                            |
| ------------------------------------------------------- | ------------------------------------ |
| Have an idea, want matching content                     | `POST /v1/instagram/posts/search`    |
| Want people/accounts, not individual posts              | `POST /v1/instagram/accounts/search` |
| Have a post `id`, want media + transcript + everything  | `GET /v1/instagram/posts/{id}`       |
| Have an account `id` (e.g. from a result's `author.id`) | `GET /v1/instagram/accounts/{id}`    |

## Request grammar in one look

Search posts: top-level `post` block; semantic `query` and/or `filters`; author constraints nest under `post.author`:

```json theme={null}
{
  "post": {
    "query": "UGC-style grooming-product demonstrations",
    "filters": {
      "posted_at": {
        "gte": "2026-06-01T00:00:00Z"
      },
      "format": {
        "in": ["reel"]
      }
    },
    "author": {
      "filters": {
        "followers_count": {
          "gte": 5000,
          "lte": 50000
        },
        "niches": {
          "contains_any": ["pets"]
        }
      }
    }
  },
  "limit": 10
}
```

Search accounts: top-level `account` block; identity `query` and/or `filters`; publishing constraints nest under `account.posts`:

```json theme={null}
{
  "account": {
    "query": "sporty stay-at-home moms",
    "filters": {
      "followers_count": {
        "gte": 5000,
        "lte": 50000
      }
    },
    "posts": {
      "minimum_matching_posts": 3,
      "filters": {
        "posted_at": {
          "gte": "2026-04-01T00:00:00Z",
          "lt": "2026-07-01T00:00:00Z"
        }
      }
    }
  },
  "limit": 10
}
```

Rules that prevent almost every 422:

1. **Every filter value is an operator object** (`{"in": [...]}`, `{"contains_any": [...]}`, `{"contains_all": [...]}`, `{"gte": ..., "lte": ...}`) or a direct boolean. Never a bare value.
2. Singular fields (`format`, `account_type`, `primary_language`, `id`, `handle`, `audio.id`) take `in`. Collection fields (`hashtags`, `caption_mentions`, `topics`, `niches`, `creative_formats`, `hook.devices`, `hook.modalities`, `commercial_contexts`, `publishing_languages`, `declared_locations`) take exactly one of `contains_any` / `contains_all`. Scalars (`posted_at`, `views`, `likes`, `comments`, `followers_count`, `engagement_rate_by_views`, `view_ratio_to_account_baseline`) take ranges. `is_verified` / `has_paid_partnership_label` take direct booleans.
3. Fields AND together. There is **no negation, no OR, no exists**; the grammar is positive-only.
4. `sort` (`{"by": ..., "order": "asc"|"desc"}`) only on filters-only requests; a `query` owns its ordering and rejects `sort`.
5. In `account.posts.filters`, the complete predicate must hold on one single post; `minimum_matching_posts` (1–3) counts distinct posts that each satisfy all of it.
6. Hashtags without `#`, handles without `@` (Klint tolerates and strips leading symbols). Classification values are exact catalog IDs; Klint rejects unknown IDs with `candidates`, never silently empty.
7. Limits (provisional): 100 values per membership array, 20 filter fields per request, 2,000-char query, `limit` ≤ 100.

## Responses

* Search envelope: `{ "request_id", "data": [...], "has_more", "next_cursor" }`. GETs return the bare resource.
* `data` items are complete fixed-shape resources: post summaries or accounts. `null` = can't currently support the value; `[]` = affirmatively empty. Never treat `null` as `0` or `false`.
* Continue a result set by POSTing `{ "cursor": "...", "limit": <same-or-lower> }` to the same route, nothing else in the body. Cursors expire (\~15–30 min) → `410 expired-cursor` → run a new search.
* An empty `data` means no qualifying match **in what Klint has observed**, never proof the content doesn't exist on Instagram. Same for `404 not-tracked` on GETs.

## Recommended defaults

* Start with `limit: 10`; paginate only when the task actually consumes more.
* Put hard constraints (dates, counts, formats, languages, locations) in filters; keep the `query` for meaning. Describe content, not keywords: "UGC-style grooming-product demonstrations", not "dog grooming viral".
* For discovery-then-qualification workflows: posts search first, collect distinct `author.id`s, then account GETs, not the other way around.
* Full posts (`GET /posts/{id}`) are large; fetch them only when you need media, transcript, or on-screen text. The search summary already carries interpretation and metrics.

## Errors

| Status × type                 | Fix                                                                    | Retry same request?   |
| ----------------------------- | ---------------------------------------------------------------------- | --------------------- |
| 401 `authentication-required` | Send `Authorization: Bearer <key>`                                     | No                    |
| 403 `insufficient-credits`    | Top up                                                                 | No                    |
| 404 `not-tracked`             | Valid key, resource not observed; do **not** conclude it doesn't exist | No                    |
| 410 `expired-cursor`          | Start a new search                                                     | No                    |
| 422 `validation-failed`       | Fix each `errors[].pointer` per its `reason` and `detail`              | No                    |
| 429 `rate-limit-exceeded`     | Wait `Retry-After` seconds                                             | Yes                   |
| 500 / 503 / 504               | Backoff                                                                | Sometimes / yes / yes |

## Common mistakes

| Wrong                                                              | Correct                                                           |
| ------------------------------------------------------------------ | ----------------------------------------------------------------- |
| `{"query": "..."}` at the top level                                | Nest under the resource: `{"post": {"query": "..."}}`             |
| `"format": "reel"`                                                 | `"format": {"in": ["reel"]}`                                      |
| `"views": 100000`                                                  | `"views": {"gte": 100000}`                                        |
| `sort` alongside a `query`                                         | Filters-only requests sort; query requests rank by relevance      |
| `"format": {"not_in": ["image"]}`                                  | No negation exists; request what you want positively              |
| `"hashtags": {"contains_any": ["dogs"], "contains_all": ["cats"]}` | One operator per field                                            |
| Filtering `author` fields inside `post.filters`                    | Author fields go in `post.author.filters`                         |
| Storing `handle` as the account key                                | Store `id`; handles change                                        |
| Treating `has_paid_partnership_label: null` as `false`             | `null` is unknown; only observed `false` matches a `false` filter |
| Retrying a 422 unchanged                                           | It fails identically; fix the request                             |

## Availability caveat

Exact limits, bounds, and open items marked across these docs may change before launch; this page reflects the contract as of its last update. When something here disagrees with the [API reference](/docs/api-reference/overview), the reference wins.
