API Documentation
REST JSON API. The public read endpoints work without a key at the free rate limit; send a customer API key to apply your plan's limit. Built for programmatic access to LeanForge keyword data.
Endpoints
Content type
Free rate limit (per user / IP)
Base URL
https://lean-forge.net/api/v1Authentication
The GET endpoints listed below are public: with no key you are bucketed by IP at the free limit. To use a paid plan's limit, send a customer API key with each request. Keys are created in your account (sign-in required) and look like lf_live_…. The full key is shown once, at creation — store it then; it cannot be retrieved later. If you lose it, revoke it and create a new one.
Either header form is accepted:
# Option 1: X-API-Key header curl https://lean-forge.net/api/v1/keywords/trending?limit=10 \ -H "X-API-Key: lf_live_YOUR_KEY" # Option 2: Authorization bearer curl https://lean-forge.net/api/v1/keywords/trending?limit=10 \ -H "Authorization: Bearer lf_live_YOUR_KEY"
- Read access only. A customer key identifies your account so your plan's rate limit applies. It cannot create, update, or delete keywords, categories, or posts, and it cannot manage keys — those require the admin secret or a signed-in session.
- Rate limit follows the plan. Keyed requests are bucketed per account (not per key) at the plan's limit: Free 100, Starter 1,000, Growth 10,000 requests per 15 minutes. Every response carries an
X-Planheader showing which plan was applied. - Keys per plan. Free 1, Starter 5, Growth 20 active keys. Revoke a key from your account and it is rejected on the very next request — keys are verified against the database on every call, with no cache.
- Errors. An invalid or revoked key returns
401with{"error":"unauthorized","reason":"invalid_api_key"}. A request with nolf_live_key is not rejected — it simply falls back to the anonymous free bucket.
Endpoints
/api/v1/keywordsList all keywords with pagination
/api/v1/keywords/trendingTrending keywords (rising direction)
/api/v1/keywords/exportDownload CSV or JSON keyword data
/api/v1/keywords/:slugGet a single keyword with trend data
/api/v1/keywords/:slug/next-actionsGet the deterministic content verdict
/api/v1/categoriesList all categories
/api/v1/categories/:slugGet a single category with its keywords
/api/v1/trendsList trend data points, filterable
/api/v1/trends/dailyGoogle Trends daily feed, proxied. Answers 503 upstream_unavailable when the feed cannot be read (Google has retired it; currently always 503), never an empty 200
/api/v1/trends/realtimeGoogle Trends "realtime" stories feed, proxied. Answers 503 upstream_unavailable when the feed cannot be read (Google has retired it; currently always 503), never an empty 200
/api/v1/trends/compareCompare 2-5 keywords side by side
/api/v1/trends/:keywordId/timeline90-day timeline for a keyword
/healthLiveness probe (Railway healthcheck)
/health/deepDeep probe: DB + Redis + uptime
/api/v1/blogList published blog posts
/api/v1/blog/:slugGet a published blog post
Example: fetch trending
curl https://lean-forge.net/api/v1/keywords/trending?limit=10
{
"data": [
{
"id": "...",
"term": "AI code review",
"slug": "ai-code-review",
"category": "ai",
"trendScore": 73,
"velocity": 23.4,
"direction": "rising",
"isActive": true,
"breakdown": {
"components": [
{ "key": "velocity7d", "label": "7-day velocity", "rawValue": 23.4, "subScore": 55.85, "weight": 0.45, "contribution": 25.13, "source": "Google Trends", "measured": true },
{ "key": "velocity30d", "label": "30-day velocity", "rawValue": 60, "subScore": 80, "weight": 0.2, "contribution": 16, "source": "Google Trends", "measured": true },
{ "key": "currentInterest", "label": "Current interest", "rawValue": 90, "subScore": 90, "weight": 0.35, "contribution": 31.5, "source": "Google Trends", "measured": true }
],
"composite": 72.63,
"trendScore": 73,
"measured": true,
"asOf": "2026-09-08T00:00:00.000Z"
}
}
],
"_meta": {
"basis": "rising",
"regent_cta": {
"headline": "Ready to rank for this keyword?",
"url": "https://seo-ai-regent.com/?ref=keyword-trend-api"
}
}
}JavaScript
const response = await fetch(
'https://lean-forge.net/api/v1/keywords/trending?limit=10'
);
if (!response.ok) throw new Error(`LeanForge API: ${response.status}`);
const { data, _meta } = await response.json();
console.log(_meta.basis, data);Python
import requests
response = requests.get(
"https://lean-forge.net/api/v1/keywords/trending",
params={"limit": 10},
timeout=10,
)
response.raise_for_status()
payload = response.json()
print(payload["_meta"]["basis"], payload["data"])Rate limiting
Requests are rate-limited per identity: requests carrying a customer API key or a signed-in session token are bucketed per account at that account's plan limit, and anonymous requests are bucketed per IP at the free limit. Every response includes X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (Unix seconds). When a request is rejected with 429, the response also includes a Retry-After header (seconds). The free tier allows 100 requests per 15-minute window; the Starter and Growth plans raise that to 1,000 and 10,000 requests per 15-minute window respectively.