.md URL into your coding agent, or fetch it directly; it is self-contained.
Which doc to fetch when
Basics
- Base URL:
https://api.tryklint.ai - Auth on every request:
Authorization: Bearer <api-key>(env varKLINT_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
Request grammar in one look
Search posts: top-levelpost block; semantic query and/or filters; author constraints nest under post.author:
account block; identity query and/or filters; publishing constraints nest under account.posts:
- Every filter value is an operator object (
{"in": [...]},{"contains_any": [...]},{"contains_all": [...]},{"gte": ..., "lte": ...}) or a direct boolean. Never a bare value. - Singular fields (
format,account_type,primary_language,id,handle,audio.id) takein. Collection fields (hashtags,caption_mentions,topics,niches,creative_formats,hook.devices,hook.modalities,commercial_contexts,publishing_languages,declared_locations) take exactly one ofcontains_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_labeltake direct booleans. - Fields AND together. There is no negation, no OR, no exists; the grammar is positive-only.
sort({"by": ..., "order": "asc"|"desc"}) only on filters-only requests; aqueryowns its ordering and rejectssort.- 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. - Hashtags without
#, handles without@(Klint tolerates and strips leading symbols). Classification values are exact catalog IDs; Klint rejects unknown IDs withcandidates, never silently empty. - 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. dataitems are complete fixed-shape resources: post summaries or accounts.null= can’t currently support the value;[]= affirmatively empty. Never treatnullas0orfalse.- 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
datameans no qualifying match in what Klint has observed, never proof the content doesn’t exist on Instagram. Same for404 not-trackedon 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
queryfor 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.ids, 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.