API reference

Conventions, errors, and rate limits

Triguna status codes, plain-text error bodies, Retry-After handling, the rate-limit headers, and what each failure means for your credits.

On this page

Every endpoint shares one set of transport conventions.

Overview

ValueNotes
Base URL https://api.triguna.ai/v1
Auth ApiKey: tg_live_… Custom request header, not Authorization: Bearer.
Methods GET only No endpoint takes a request body.
Content type application/json Plain text on error.
Versioning Path segment only (/v1) No version header.
Billing 1 credit per successful record Errors and not-founds are never billed.

Status codes

StatusBodyMeaning
400 invalid identifier: … What you sent carries no LinkedIn identifier, usually a display name. Not billed.
401 invalid api key Missing, unknown, or revoked key. Not billed.
402 insufficient credits Your balance cannot cover the call. Not billed.
404 profile not found The identifier parsed but no such record exists. Not billed.
429 rate limited You exceeded your per-second allowance. Honour Retry-After.
503 service busy, retry shortly Transient capacity or upstream problem. Retryable.

Only 429 and 503 are worth retrying. 400, 401, 402, and 404 are terminal: retrying them burns wall-clock time and achieves nothing. Blanket “retry any non-2xx” logic turns a permanent 401 into a tight loop.

Rate limits

Limits are per account and are set per second, not per month. The default is 10 requests per second, adjustable per account, so a higher tier is a settings change rather than a code change.

Every response, success or 429, carries:

HeaderTypeMeaning
X-RateLimit-Limit integer Requests per second granted to your account.
X-RateLimit-Remaining integer Tokens left in your bucket right now.
Retry-After seconds Sent on 429. How long to wait before trying again.

Behind your per-account bucket there is a global outbound bucket that keeps total traffic inside the upstream provider’s quota. When that one is saturated your request waits briefly and then returns a clean 503 rather than queueing indefinitely.

Retries

const RETRYABLE = new Set([429, 503]);

async function withRetry(fn: () => Promise<Response>, max = 2): Promise<Response> {
  for (let attempt = 0; ; attempt++) {
    const res = await fn();
    if (!RETRYABLE.has(res.status) || attempt >= max) return res;

    const header = Number(res.headers.get('Retry-After'));
    const base = Number.isFinite(header) && header > 0 ? header * 1000 : 5_000;
    // Jitter, or a throttled batch retries in lockstep and recreates the burst.
    await sleep(base + Math.random() * 500);
  }
}
  • Honour Retry-After when it is present. Fall back to 5 seconds, and give up after about 60.
  • Watch X-RateLimit-Remaining and slow down before you hit the wall, rather than after.
  • Bound your retries. Unbounded retry against a rate limit is how a backfill becomes an outage of your own making.

Timeouts and throughput

Responses can be slow when a record is fetched live, so allow a generous client timeout; 60 seconds is a safe upper bound. If you can tolerate slightly older data, max_age_days turns most calls into a fast store read. See provenance and freshness.

  • There is no bulk endpoint. Enriching N entities costs N calls and N credits.
  • Store the raw JSON, not just the fields you parse today. A stored response lets you pick up unmapped fields later without paying for the call twice.
  • Stable keys: for people use member_urn, for companies use company_id. Slugs are user-editable and will break your joins.

A client shape that holds up

type Outcome =
  | { ok: true; record: unknown; source: string | null; fetchedAt: string | null }
  | { ok: false; kind: 'bad_identifier' | 'not_found' | 'rate_limited' | 'auth' | 'server';
      status: number; detail: string };

async function fetchPerson(profileId: string): Promise<Outcome> {
  const res = await withRetry(() =>
    fetch(`${BASE}/people/profile?profile_id=${encodeURIComponent(profileId)}`, {
      headers: { ApiKey: KEY },
      signal: AbortSignal.timeout(60_000),
    })
  );

  if (res.ok) {
    return {
      ok: true,
      record: await res.json(),
      source: res.headers.get('X-Data-Source'),
      fetchedAt: res.headers.get('X-Fetched-At'),
    };
  }

  const detail = await res.text(); // plain text, never JSON
  const kind =
    res.status === 400 ? 'bad_identifier' :
    res.status === 404 ? 'not_found' :
    res.status === 429 ? 'rate_limited' :
    [401, 402].includes(res.status) ? 'auth' : 'server';

  return { ok: false, kind, status: res.status, detail };
}

Each kind has a different owner: bad_identifier and not_found are yours, auth is a billing or credentials problem, rate_limited is a pacing decision, and server is ours.

Checking your balance

curl -H "ApiKey: $TRIGUNA_API_KEY" https://api.triguna.ai/v1/credits
{ "credits": 4820 }

This call is free and does not count against your rate limit budget in any meaningful way. Poll it from a dashboard rather than inferring your balance from 402s.