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.
Create and protect an API key
- Sign in to Account.
- Under Developer API, name and create a key.
- 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.
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.
Poll until terminal
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_KEYCompleted 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.
Handle status, code, and retryability together
payment_required. required_usage_units states the need; add units before retrying.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
}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.
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"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"])