Using Cursor AI Code Tracking API with FastAPI
If you need to capture every edit that Cursor AI makes to your codebase, the Cursor AI Code Tracking API is the tool you’re looking for. In a few minutes you can authenticate, pull change events, persist them, and build a simple dashboard – all inside a FastAPI service you already run in production.
Below is a step-by-step walkthrough that shows exactly how to do it, where things tend to break, and what you should keep an eye on. I’ve stripped out the fluff and kept the focus on things that actually matter when you’re shipping a product.
Overview of Cursor AI Code Tracking API
What does the Cursor AI Code Tracking API give you? It streams JSON payloads that describe each file change, the prompt that triggered it, and a timestamp. The endpoint is GET https://api.cursor.com/v1/tracking. It returns a paginated list of events, each looking like:
{ "id": "evt_12345", "file_path": "app/main.py", "change_type": "modification", "prompt": "Refactor the request handler", "diff": "-old line\n+new line", "created_at": "2024-08-14T12:34:56Z"}That’s all you need to reconstruct a timeline of AI-generated edits. The API is low-latency, but it enforces rate limits (30 calls per minute) and requires a valid API key for every request.
Authentication and API key setup
How do you securely store and use the Cursor API key? The service expects the key in an Authorization: Bearer <token> header. In production I keep the token in an environment variable called CURSOR_API_KEY. Never hard-code it.
import osfrom fastapi import Depends, HTTPException, status
def get_cursor_token() -> str: token = os.getenv("CURSOR_API_KEY") if not token: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Cursor API key not configured" ) return tokenIf the key is missing, the service fails fast with a 500 error – that’s intentional. It prevents you from silently sending unauthenticated requests and hitting obscure 401 responses later in the pipeline.
Common failure: forgetting to add the env var to your Docker or Kubernetes secret. The result is a cascade of 401 errors that look like “invalid credentials” but actually stem from a missing secret. Double-check your deployment manifest.
Integrating the API with a FastAPI service
What does the integration code look like? Below is a minimal FastAPI router that pulls the latest 100 events and returns them as a list.
import httpxfrom fastapi import APIRouter, Depends
router = APIRouter()
CURSOR_ENDPOINT = "https://api.cursor.com/v1/tracking"
@router.get("/cursor/events")async def get_cursor_events(token: str = Depends(get_cursor_token)): async with httpx.AsyncClient(timeout=10) as client: resp = await client.get( CURSOR_ENDPOINT, headers={"Authorization": f"Bearer {token}"}, params={"limit": 100} ) if resp.status_code != 200: # Propagate the exact error for debugging raise HTTPException( status_code=resp.status_code, detail=f"Cursor API error: {resp.text}" ) return resp.json()["events"]A few things to note:
- Async client – FastAPI runs on ASGI, so use
httpx.AsyncClientto avoid blocking the event loop. - Timeout – set a reasonable timeout (10 seconds here) to prevent hanging workers.
- Error propagation – surface the exact error; otherwise you’ll see generic 500s that make troubleshooting harder.
Trade-off: pulling 100 events on every request is cheap for a dev dashboard but could become expensive at scale. If you need real-time updates, consider a webhook or a background poller instead of on-demand fetching.
Persisting tracking data in PostgreSQL or Redis
Where should you store the events? Two common patterns:
PostgreSQL (relational, query-heavy)
Create a table that mirrors the JSON payload:
CREATE TABLE cursor_events ( id TEXT PRIMARY KEY, file_path TEXT NOT NULL, change_type TEXT NOT NULL, prompt TEXT, diff TEXT, created_at TIMESTAMPTZ NOT NULL);Insert with async SQLAlchemy:
from sqlalchemy.ext.asyncio import AsyncSessionfrom sqlalchemy import insertfrom models import CursorEvent # SQLAlchemy model matching the table
async def save_events(events: list[dict], db: AsyncSession): stmt = insert(CursorEvent).values(events) await db.execute(stmt) await db.commit()Failure mode: duplicate id values raise an integrity error. Wrap the insert in a try/except block and ignore duplicates if you’re polling the same page repeatedly.
Redis (fast, volatile, analytics)
If you only need recent changes for a dashboard, a Redis stream works well:
import aioredis
redis = await aioredis.from_url("redis://localhost:6379")
async def push_to_redis(events: list[dict]): for ev in events: await redis.xadd("cursor:events", ev)Redis gives you O(1) writes and easy range queries (XRANGE). The downside is data loss on restart unless you enable persistence, and you lose relational querying capabilities.
When NOT to use Redis: when you need to join events with other domain tables (e.g., linking a change to a ticket). In that case PostgreSQL is the safer bet.
Building a dashboard to visualize code changes
How can you turn stored events into a useful UI? I built a tiny Vue front-end that calls a FastAPI endpoint /dashboard/events. The endpoint reads from PostgreSQL and returns a paginated list with optional filters.
from fastapi import Query
@router.get("/dashboard/events")async def dashboard_events( skip: int = Query(0, ge=0), limit: int = Query(20, le=100), file_path: str | None = None, db: AsyncSession = Depends(get_db)): query = select(CursorEvent).order_by(CursorEvent.created_at.desc()) if file_path: query = query.where(CursorEvent.file_path == file_path) result = await db.execute(query.offset(skip).limit(limit)) return result.scalars().all()The UI shows a table with diff previews, timestamps, and the original prompt. Clicking a row expands the diff. I added a simple heat-map that counts changes per file; the query is a GROUP BY file_path aggregation.
Pitfall: pulling the full diff for every row can bloat the response. Send only a snippet (first 200 characters) and let the UI request the full diff on demand via another endpoint.
Security and best-practice considerations
What should you watch out for when exposing AI-generated code data? A few hard-earned lessons:
-
Never expose raw diff data to unauthenticated users. Diffs may contain secrets that the AI unintentionally inserted (API keys, passwords). Always scrub before rendering. A quick regex replace works for most cases, but consider a dedicated secret-detection step – see my AI generated code platform security checklist.
-
Rate-limit your own endpoint to avoid cascading failures if Cursor imposes stricter limits. A simple
slowapidecorator can throttle calls to/cursor/events. -
Validate the schema of incoming JSON. Cursor can evolve its payload format; using Pydantic models catches mismatches early:
from pydantic import BaseModel, Field
class CursorEventModel(BaseModel): id: str file_path: str change_type: str prompt: str | None diff: str | None created_at: datetime- Log responsibly. Store only event IDs and timestamps in your audit log. Full diffs belong in a secure data store, not in plain-text logs that may be shipped to external services.
If you’re already wrestling with code-quality regressions caused by AI, check out my post on Fixing AI Generated Code Quality Issues in Production for additional context.
FAQ
How often can I poll the Cursor API?
The official limit is 30 requests per minute. Exceeding it returns a 429 response. Use exponential back-off or a background worker that respects the limit.
Can I use the API without FastAPI?
Absolutely. The endpoint is language-agnostic; any HTTP client can call it. FastAPI just makes integration painless for Python services.
What happens if an event’s diff contains a secret?
Treat it as a security incident. Scrub the diff before persisting, rotate the leaked secret, and consider adding the event to a secret-detection pipeline.
Is there a webhook alternative?
Cursor currently only offers a pull-based API. If you need push notifications you’ll have to implement a poller that writes to a message queue (e.g., RabbitMQ) and lets downstream services consume the events.
Key Takeaways
- The Cursor AI Code Tracking API provides JSON change events; authenticate with a bearer token stored in an env var.
- Use
httpx.AsyncClientinside FastAPI to fetch events without blocking. - Persist events in PostgreSQL for relational queries or Redis streams for fast, temporary analytics.
- Build a lightweight dashboard that filters and paginates results; avoid sending full diffs unless requested.
- Scrub secrets, enforce rate limits, and validate payloads to keep the system secure.
If you’ve hit a wall trying to stitch these pieces together, I’m happy to help you get a production-ready pipeline up and running. Feel free to reach out via the hire page and we can tackle the rough spots together.
Working on something similar?
If you're building backend or AI systems and want a second set of senior eyes, let's talk.