Public REST API

One source engine. Any client.

Use the same Link2AI jobs, cache, quota, provenance, and source extraction engine used by Claude, Grok, and compatible MCP clients.

1 · Authenticate

Create and protect an API key

  1. Sign in to Account.
  2. Under Developer API, name and create a key.
  3. Copy it immediately; the full secret is shown once and only its hash is stored.

Send it on every API request as Authorization: Bearer l2ai_live_…. Keep it in a secret manager or environment variable—never source control, browser code, logs, or a URL. Revoke keys from Account when no longer needed.

2 · First request

POST /v1/read

curl https://api-production-0383e.up.railway.app/v1/read \
  -H "Authorization: Bearer $LINK2AI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://news.ycombinator.com/item?id=8863"}'

A fast read returns 200 completed. Work that continues asynchronously returns 202 processing. Both responses identify the job using id; job_id is the equivalent compatibility alias.

3 · Lifecycle

Poll until terminal

POST /v1/read200 completedor202 processing + idGET /v1/jobs/{id}

For a 202, wait briefly and poll with the same bearer header. Stop when status is completed, payment_required, or failed. Respect retry_after_seconds when present and only automate retries when retryable is true.

GET https://api-production-0383e.up.railway.app/v1/jobs/{id}
Authorization: Bearer $LINK2AI_API_KEY
Contract

Completed response

{
  "id": "7f…", "job_id": "7f…", "status": "completed",
  "source_type": "hackernews", "source_url": "https://…",
  "canonical_url": "https://news.ycombinator.com/item?id=8863",
  "source_key": "hackernews:8863", "title": "…", "creator": "…",
  "content": "AI-ready source text…", "transcript": "AI-ready source text…",
  "metadata": {
    "duration_seconds": null, "language": null, "transcript_source": null,
    "source_fetched_at": "…", "cache_expires_at": "…",
    "items_available": 42, "items_extracted": 42,
    "pages_available": null, "pages_extracted": null
  },
  "evidence": [], "source_completeness": "complete",
  "units_used": 1, "remaining_usage_units": 9
}

canonical_url is Link2AI's normalized source URL and source_key is its stable source identity. metadata carries source-specific counts and provenance; evidence carries timestamped or located excerpts when available.

source_completeness is complete or partial. For partial results inspect truncation_reason and content_sections_truncated; never silently treat partial content as the whole source.

Failures

Handle status, code, and retryability together

401Missing, invalid, or revoked bearer key. Create or replace the key; do not retry unchanged credentials.
402payment_required. required_usage_units states the need; add units before retrying.
422Supported request could not be extracted. Inspect error_code, message, and retryable.

Error bodies expose both error_code and the backward-compatible error alias with the same value. Request validation may also return 400. Failed, unsupported, private, login-required, empty, or inaccessible reads consume 0 units.

{
  "error": "SOURCE_UNAVAILABLE",
  "error_code": "SOURCE_UNAVAILABLE",
  "message": "The public source could not be read.",
  "retryable": true
}
Cost & cache

Usage, retries, cache, and refresh

Successful video reads use one unit per started processed minute. Successful text reads use roughly one unit per 2,500 estimated source tokens, with a one-unit minimum. units_used reports the charge for that result; remaining_usage_units reports the balance.

Ordinary retries and cache hits reuse the shared job/cache accounting and must not double-charge. Send {"url":"…","refresh":true} only when you intentionally need a fresh upstream fetch. Refresh can create new billable processing when successful, so do not use it as a polling or retry mechanism.

Discover

GET /v1/usage and GET /v1/sources

GET /v1/usage returns the authoritative shared balance used by REST and connected AI clients. GET /v1/sources returns the current supported-source policy; use it instead of hard-coding a separate client allowlist.

curl https://api-production-0383e.up.railway.app/v1/usage -H "Authorization: Bearer $LINK2AI_API_KEY"
curl https://api-production-0383e.up.railway.app/v1/sources -H "Authorization: Bearer $LINK2AI_API_KEY"
Complete examples

JavaScript

const base = "https://api-production-0383e.up.railway.app";
const headers = {
  Authorization: `Bearer ${process.env.LINK2AI_API_KEY}`,
  "Content-Type": "application/json"
};

let response = await fetch(`${base}/v1/read`, {
  method: "POST", headers,
  body: JSON.stringify({ url: "https://news.ycombinator.com/item?id=8863" })
});
let result = await response.json();
while (response.status === 202) {
  await new Promise(resolve => setTimeout(resolve, 2000));
  response = await fetch(`${base}/v1/jobs/${result.id}`, { headers });
  result = await response.json();
}
if (!response.ok) throw new Error(`${result.error_code}: ${result.message}`);
console.log(result.content);

Python

import os, time, requests

base = "https://api-production-0383e.up.railway.app"
headers = {"Authorization": f"Bearer {os.environ['LINK2AI_API_KEY']}"}
response = requests.post(f"{base}/v1/read", headers=headers,
    json={"url": "https://news.ycombinator.com/item?id=8863"})
result = response.json()
while response.status_code == 202:
    time.sleep(2)
    response = requests.get(f"{base}/v1/jobs/{result['id']}", headers=headers)
    result = response.json()
response.raise_for_status()
print(result["content"])