Skip to main content

Overview

Search resolves entities and pulls evidence from Arcmira’s media index (long-form video and podcasts on YouTube).
  • Base URL: https://api.arcmira.com, all paths under /v1.
  • Auth: Authorization: Bearer arc_sk_... or x-api-key: arc_sk_.... If both are sent, x-api-key wins.
  • Scope: all endpoints on this page need read (present on every key). Commercial endpoints additionally need a Pro+ tier AND the recommendations:read scope; verify with GET /v1/merecommendations_api_enabled and scopes.
  • Billing: lookups and searches are free. Mention/appearance rows cost 1 row each from the shared monthly pool (the first 5 rows of each distinct pull are free). Commercial rows bill at 10 rows each. GET /v1/me costs 0 rows; check usage.rows_remaining before large pulls.
  • Five entity types: person, organization, product, topic, channel. Only person has appearances.

Minimal working example

const H = { Authorization: `Bearer ${process.env.ARCMIRA_API_KEY}` };

const { entity } = await fetch(
  'https://api.arcmira.com/v1/entities/lookup?name=OpenAI&type=organization',
  { headers: H },
).then(r => r.json());

const { data } = await fetch(
  `https://api.arcmira.com/v1/mentions?entity_id=${entity.id}&limit=25`,
  { headers: H },
).then(r => r.json());
import os, httpx

H = {'Authorization': f"Bearer {os.environ['ARCMIRA_API_KEY']}"}

entity = httpx.get(
    'https://api.arcmira.com/v1/entities/lookup',
    params={'name': 'OpenAI', 'type': 'organization'}, headers=H,
).json()['entity']

rows = httpx.get(
    'https://api.arcmira.com/v1/mentions',
    params={'entity_id': entity['id'], 'limit': 25}, headers=H,
).json()['data']
curl 'https://api.arcmira.com/v1/entities/lookup?name=OpenAI&type=organization' \
  -H "Authorization: Bearer $ARCMIRA_API_KEY"
curl 'https://api.arcmira.com/v1/mentions?entity_id=ent_123881&limit=25' \
  -H "Authorization: Bearer $ARCMIRA_API_KEY"

Endpoints

GET /v1/entities/lookup: resolve to a canonical ID

Exact resolution. Pass name (+ optional type) or id. Follows merge chains; response is { "entity": { id, name, type, merged_from_id, route, ... } }. merged_from_id is non-null when the input was an alias. Free. Missing entities: 404 entity_not_found.

GET /v1/entities/search: ranked discovery

Fuzzy, ranked, free. q (required, min 2 chars), type, has_recommendations_data=true, limit (1-25, default 10). Single page, no cursor. Ordered by appearance_count desc. Rows include recommendations_summary only for Pro+ callers with recommendations:read; other callers get the same row without that field (no error).

GET /v1/search: resolve a name to a single entity

Single-result name resolution for a free-text q (+ optional type). Returns the best match as { found: true, ... } or { found: false }. The entity.id here is a raw number; prefix it as ent_{id} for the entities endpoints, or use lookup, which returns the public form. Single result, no pagination. Free. Use entities/search for ranked lists; use lookup when you already know the exact name or ID.

GET /v1/entities/: read canonical metadata

{ "entity": { id, canonical_id, name, type, url, image_url, appearance_count, is_canonical, merged_from_id, route } }. On organization/product entities, Pro+ + recommendations:read callers also get a recommendations_summary aggregate block (0 rows). appearance_count counts indexed mention/appearance rows for any entity type.

GET /v1/entities/cards: batched compact cards

ids as a comma-separated list of raw integer entity IDs (ids=12,844,1032, 1-50 of them); the ent_* form is not accepted here. Returns compact card objects under the requested IDs; unknown IDs are silently dropped. Free.

GET /v1/mentions: evidence rows

The primary evidence surface. At least one of entity_id or entity_name is required; if both are sent, entity_id wins.
ParameterTypeDefaultDescription
entity_idstringCanonical ent_*. Mutually exclusive with entity_name. Prefer this.
entity_namestringHuman-readable name; pair with entity_type to disambiguate.
entity_typeenumperson, organization, product, topic, channel.
channel_idstringRestrict to one YouTube channel ID (UC...).
channel_namestringSubstring match on source channel name.
qstringFree text over description, media title, entity name, channel name.
sentimentenumpositive, neutral, negative.
is_appearancebooleanPerson entities only; true on a non-person returns 400 appearances_person_only.
date_from / date_toISO dateInclusive bounds on published_at.
detailsenumfull attaches commercial rows per mention. Pro+ + recommendations:read; charges premium rate per attached item.
limitint201-100.
cursorstringOpaque, from prior next_cursor.

GET /v1/entities//mentions: entity-scoped variant

Identical shape and filters (minus entity_*); the entity rides in the path. Preferred for production: explicit, no URL-encoded names.

GET /v1/people//appearances: person appearances

The appearance subset for one person. Person-only: the equivalent route for any other type returns 400 appearances_person_only. Documented exception: this route and the related-entity lists below return { items, total, offset, limit, hasMore } (camelCase, offset-based, with a total) instead of the standard { data, has_more, next_cursor } envelope. No cursor is returned, so pages past the first are not reachable here today. For appearance rows with standard pagination, date filters, and the mention-row shape, prefer GET /v1/mentions?entity_id=...&is_appearance=true.
GET /v1/{people|organizations|products|topics|channels}/{slug}/{topics|people|organizations|products|channels}
GET /v1/channels/{slug}/guests
Filters: q (substring), field, sort, order. One row each. Same { items, total, ... } exception shape as appearances above; raise limit (max 100) and narrow with q instead of paging.

Commercial endpoints (Pro+ + recommendations:read)

GET /v1/recommendations                       ad reads, endorsements; entity_id|entity_name required
GET /v1/entities/{id}/recommendations         same, entity in path
GET /v1/channels/{channel_id}/sponsors        recurring sponsor rollup; channel_id is the YouTube UC... ID
Key params: mention_class (ad_read|endorsement|mention|all, default all), min_confidence (default 0.7), include_disputed (default false), min_ad_reads (sponsors, default 3), status (sponsors: active|lapsed|ended|uncertain). Full narrative: Commercial intelligence.

Response schema

Mention row (the shape you will parse most):
{
  "data": [
    {
      "id": "men_2049815",
      "appearance_id": 2049815,
      "entity": { "id": "ent_123881", "name": "OpenAI", "type": "organization" },
      "media": {
        "id": 9912345,
        "video_id": "abcd_1234",
        "title": "...",
        "url": "https://www.youtube.com/watch?v=abcd_1234",
        "published_at": "2026-04-12T14:00:00Z",
        "channel_id": "UCabc...",
        "view_count": 412034,
        "source_channel": { "id": "ent_5012", "name": "TBPN", "url": "https://www.youtube.com/@tbpn" }
      },
      "start_timestamp": "06:52",
      "end_timestamp": "07:58",
      "start_seconds": 412,
      "end_seconds": 478,
      "is_appearance": false,
      "description": "OpenAI's new model release...",
      "confidence": 0.93,
      "sentiment_score": 0.42,
      "sentiment": "positive",
      "referenced_url": null,
      "referenced_platform": null,
      "extracted_content": "..."
    }
  ],
  "has_more": true,
  "next_cursor": "eyJvZmZzZXQiOjI1fQ",
  "entity": { "id": "ent_123881", "name": "OpenAI", "type": "organization", "appearance_count": 0 }
}
FieldTypeNotes
idstringPublic men_* form. Use it in Community Review corrections.
start_seconds / end_secondsnumber | nullInteger seconds; read these. 0 means the full episode, no specific moment. Null when the analyzer could not locate the mention in time.
start_timestamp / end_timestampstring | nullDeprecated MM:SS (or HH:MM:SS) strings, kept until a changelog-announced removal. Same information as the seconds fields.
sentiment_scorenumber | nullRaw [-1, 1], null when not computed. sentiment is the bucketed label (positive > 0.2, negative < -0.2, else neutral).
is_appearancebooleanAlways false for non-person entities.
confidencenumberExtraction confidence [0, 1].
source_channelobject | nullNull when the video has not been linked to a channel entity (including some YouTube rows).
next_cursorstring | nullNull when has_more is false. Opaque; pass back verbatim.
With details=full, each row gains recommendations: { items: [...] } where items carry com_* IDs, mention_class, verbatim_quote, promo_code, and timestamps (start_seconds/end_seconds plus the deprecated MM:SS strings); items is [] when nothing matches.

Errors

Envelope on every error:
{ "error": { "type": "...", "code": "...", "message": "...", "doc_url": "https://docs.arcmira.com/errors#...", "request_id": "req_..." } }
CodeHTTPWhen
invalid_query400Param validation failed; error.message has the validator output.
appearances_person_only400Appearance surface or is_appearance=true on a non-person. Use mentions instead.
invalid_api_key401Missing, malformed, disabled, or internal (arc_int_*) key.
quota_exceeded402Monthly rows exhausted and on-demand disabled.
insufficient_scope403Key lacks the needed scope (message names it).
recommendations_not_enabled403Commercial route on a tier below Pro+, regardless of scope.
entity_not_found404ID or name did not resolve.
channel_not_found404Sponsors called with a channel ID that has no indexed media.
rate_limit_exceeded429Honor Retry-After.
Switch on error.code, never on error.message. Quote X-Request-Id to support.

Rate limits & idempotency

  • Per-key fixed 60-second window: free 60/min, paid (incl. Ultra) 240, teams/enterprise 600. Headers on every authenticated response: RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset; 429s add Retry-After.
  • Every data endpoint on this page is GET: no Idempotency-Key needed, retries are safe. The one write is POST /v1/feedback below; send Idempotency-Key on it.

Community Review

Every result on this page is disputable, free (0 rows), via POST /v1/feedback. Pick the type matching the surface you called, echo the reproducing query, and target rows by public ID:
You calledtypeCorrection targets
/v1/searchsearchThe resolved ent_* (or a missing_result); for bad_ranking, suggested_change carries observed_rank/expected_rank
/v1/entities/searchentities_searchent_* hits; issue_type: bad_ranking, wrong_entity_type, duplicate_entity, merge_suggestion, missing_result
/v1/entities/lookup, /v1/entities/{id}entitiesThe ent_*; wrong_entity_type, stale_metadata, merge_suggestion
/v1/mentionsmentionsmen_* rows; wrong_entity, missing_result
/v1/people/{slug}/appearancesappearancesAppearance rows; person_not_present, wrong_person, wrong_appearance_role
/v1/recommendationsrecommendationscom_* rows; reason: false_positive_ad_read, missed_ad_read, wrong_classification, …
/v1/channels/{id}/sponsorschannel_sponsorsSponsor ent_* with query.channel_id
Check any submission’s status later with GET /v1/feedback/{feedback_id}.
{
  "type": "mentions",
  "query": { "entity_id": "ent_123881", "sentiment": "positive", "limit": 25 },
  "request_id": "req_...",
  "corrections": [
    { "id": "men_2049815", "issue_type": "wrong_entity", "notes": "Row is about a different OpenAI merge candidate." }
  ]
}
Nothing auto-applies. Supports Idempotency-Key. Up to 100 corrections per submission. Full catalog: Community Review.

Common mistakes

Wrong parameters and patterns frequently generated for this API. Do not use the left column.
WrongCorrect
Authorization: arc_sk_... (bare key)Authorization: Bearer arc_sk_... or x-api-key: arc_sk_...
Using an arc_int_* session keyv1 rejects internal keys; use an arc_sk_* API key
Pulling mentions by entity_name in productionResolve once with /v1/entities/lookup, cache ent_*, pass entity_id
GET /v1/organizations/{slug}/appearancesAppearances are person-only; use /v1/mentions?entity_id=...
is_appearance=true with a non-person entityReturns 400 appearances_person_only; omit it
Parsing start_timestamp as a numberRead start_seconds/end_seconds (integer seconds); the legacy start_timestamp strings are deprecated
Calling /v1/recommendations with only read scopeNeeds Pro+ tier AND recommendations:read; check /v1/me first
Passing ent_* IDs to /v1/entities/cardsThat endpoint takes raw integers only (ids=12,844); strip the prefix
Decoding or constructing cursor valuesCursors are opaque; pass next_cursor back verbatim
Branching on error.messageSwitch on error.code; follow doc_url

Patterns and gotchas

  • Resolve-then-pull: lookups are free, metered rows are not; never burn rows on an ambiguous name.
  • recommendations_summary blocks are aggregates and cost 0 rows; row-level pulls are what bill.
  • details=full multiplies cost (mention rows + 10 rows per attached commercial item); use only when you need the overlay.
  • Merge-aware queries: querying a canonical ent_* includes rows from entities merged into it. If results look split across variants, that is a merge_suggestion for Community Review, not a client-side join.
  • has_more: false means next_cursor: null; stop paging.
  • Watch RateLimit-Remaining while iterating; back off before hitting 429.
  • Every response carries X-Request-Id; log it.

Complete examples

Full evidence pull with pagination and budget check:
const BASE = 'https://api.arcmira.com';
const H = { Authorization: `Bearer ${process.env.ARCMIRA_API_KEY}` };

// 0. Budget check (free)
const me = await fetch(`${BASE}/v1/me`, { headers: H }).then(r => r.json());
if (me.usage.rows_remaining < 500) throw new Error('row budget too low');

// 1. Resolve (free)
const { entity } = await fetch(
  `${BASE}/v1/entities/lookup?name=Ramp&type=organization`, { headers: H },
).then(r => r.json());

// 2. Page through positive mentions since April (1 row per row)
const rows = [];
let cursor: string | null = null;
do {
  const url = new URL(`${BASE}/v1/entities/${entity.id}/mentions`);
  url.searchParams.set('sentiment', 'positive');
  url.searchParams.set('date_from', '2026-04-01');
  url.searchParams.set('limit', '100');
  if (cursor) url.searchParams.set('cursor', cursor);
  const page = await fetch(url, { headers: H }).then(r => r.json());
  rows.push(...page.data);
  cursor = page.has_more ? page.next_cursor : null;
} while (cursor);

console.log(`${rows.length} rows`, rows[0]?.media.title);
import os, httpx

BASE = 'https://api.arcmira.com'
H = {'Authorization': f"Bearer {os.environ['ARCMIRA_API_KEY']}"}

me = httpx.get(f'{BASE}/v1/me', headers=H).json()
assert me['usage']['rows_remaining'] >= 500, 'row budget too low'

entity = httpx.get(
    f'{BASE}/v1/entities/lookup',
    params={'name': 'Ramp', 'type': 'organization'}, headers=H,
).json()['entity']

rows, cursor = [], None
while True:
    params = {'sentiment': 'positive', 'date_from': '2026-04-01', 'limit': 100}
    if cursor:
        params['cursor'] = cursor
    page = httpx.get(
        f"{BASE}/v1/entities/{entity['id']}/mentions", params=params, headers=H,
    ).json()
    rows.extend(page['data'])
    if not page['has_more']:
        break
    cursor = page['next_cursor']

print(len(rows), 'rows')