SQL API
Send read-only SQL to api.transitlab.nyc and get rows back as JSON, CSV or Parquet. Free key. Also a remote MCP server for AI agents.
The SQL API runs your query on our servers, against every open table, and sends back the answer. Nothing to download or install. It suits scripts, notebooks, small apps and AI agents that can make HTTPS calls but can’t run DuckDB.
If you want whole tables, or queries that read years of trips, download the Parquet files instead. They need no key.
Get a key
Section titled “Get a key”Enter your email address at transitlab.nyc/api-keys. We email you a key from hello@transitlab.nyc. It looks like tl_live_ and 32 letters and digits.
We keep only a fingerprint of the key, so we can’t send it again. If you lose it, ask for a new one. If it leaks, write to hello@transitlab.nyc and we’ll turn it off.
Run a query
Section titled “Run a query”Send the key in the Authorization header and the query as JSON:
curl https://api.transitlab.nyc/v1/sql \ -H "Authorization: Bearer $TRANSITLAB_API_KEY" \ -H "Content-Type: application/json" \ -d '{"sql": "select route_id, count(*) as stops from subway_stop_events where year = 2026 and month = 10 group by 1 order by 2 desc limit 5"}'The answer:
{ "columns": [{"name": "route_id", "type": "VARCHAR"}, {"name": "stops", "type": "BIGINT"}], "row_count": 5, "truncated": false, "elapsed_ms": 412, "bytes_scanned": 3145728, "rows": [["A", 41210], ["F", 38877], ...]}The body takes these fields:
| Field | Meaning |
|---|---|
sql | One DuckDB SELECT or WITH statement. Required. |
format | json (the default), csv or parquet. |
limit | Most rows to return, up to 10,000. The default is 1,000. truncated says whether there were more. |
from, to | Optional, YYYY-MM-DD or YYYY-MM. Read only the files for those days or months. |
You can also send the SQL itself as the body with Content-Type: text/plain.
CSV and Parquet answers carry the row count, truncation, time and bytes read in the X-Rows, X-Truncated, X-Elapsed-Ms and X-Bytes-Scanned headers.
curl https://api.transitlab.nyc/v1/sql \ -H "Authorization: Bearer $TRANSITLAB_API_KEY" \ -H "Content-Type: application/json" \ -d '{"sql": "select * from bus_lane_streets", "format": "parquet"}' -o streets.parquetPython
Section titled “Python”import os, requests
r = requests.post( "https://api.transitlab.nyc/v1/sql", headers={"Authorization": f"Bearer {os.environ['TRANSITLAB_API_KEY']}"}, json={"sql": "select count(*) from taxi_yellow_trips where year = 2026 and month = 8"}, timeout=60,)r.raise_for_status()print(r.json()["rows"])Find the tables
Section titled “Find the tables”These two need no key:
curl https://api.transitlab.nyc/v1/tablescurl https://api.transitlab.nyc/v1/tables/taxi_yellow_tripsThe first lists every table with its rows, size and the days it covers. The second gives one table’s columns, their meaning, and its files. The datasets pages say the same at more length.
Write queries that finish
Section titled “Write queries that finish”Each query stops after 30 seconds. Big tables (trips, bus positions, stop events) are one file a month or a day, with year and month columns. Filter on them, and the engine opens only those files:
select date_trunc('day', pickup_datetime) as day, count(*) as tripsfrom fhv_tripswhere year = 2026 and month = 5group by 1 order by 1A query over a whole trip table reads gigabytes and won’t finish in time. Count, average and group in SQL, and fetch raw rows only when you need them.
Only one read-only statement is allowed per request. Functions that read other files or servers (read_csv, read_parquet, glob and the like) are refused; name the tables instead.
Limits
Section titled “Limits”| Limit | Free key |
|---|---|
| Requests | 60 a minute, to any endpoint |
| Queries | 1,000 a day (UTC) |
| Query time | 10 minutes a day (UTC) |
| At once | 2 queries per key |
| Per query | 30 seconds, 10,000 rows, 10 MB |
The same query (ignoring spaces and comments) sent again within 10 minutes comes from a cache. It is fast and doesn’t count against the daily limits.
Every answer has RateLimit-Policy and RateLimit headers. Over a limit, you get 429 with a Retry-After header in seconds. When our servers are busy you may get 503, also with Retry-After. The engine sleeps when no one is using it, so the first query after a quiet spell takes a few seconds longer.
Errors
Section titled “Errors”Errors come back as JSON with an error code and a message in plain words.
| Status | When |
|---|---|
| 400 | The query isn’t one SELECT, reads something other than the tables, or has a mistake (sql_error; the message is DuckDB’s) |
| 401 | No key, or a key we don’t know or have turned off. The body links to the key page. |
| 408 | The query ran past 30 seconds |
| 422 | The query needed more memory than the engine allows |
| 429 | Over the per-minute limit (rate_limited), the daily limits (daily_queries, daily_compute) or two queries at once (concurrency) |
| 503 | The engine is busy or starting. Wait the Retry-After seconds and try again. |
Use it from an AI agent
Section titled “Use it from an AI agent”The same key works with our remote MCP server at https://api.transitlab.nyc/mcp. It offers the tools of the local transitlab mcp: list_tables, describe_table and run_sql. Use it when the agent can’t run programs on your computer.
Claude Code
claude mcp add --transport http transitlab https://api.transitlab.nyc/mcp \ --header "Authorization: Bearer $TRANSITLAB_API_KEY"Other clients. Point a streamable HTTP MCP client at https://api.transitlab.nyc/mcp and add the header Authorization: Bearer <your key>.
Queries from the agent count against the key’s limits like any other.
Reference
Section titled “Reference”The OpenAPI description covers every endpoint. GET https://api.transitlab.nyc/v1/health says whether the API is up and which catalog it serves.
Data from the API is under the same terms as the files: CC BY 4.0 for our tables. Credit Transit Lab and the original source. See license and credit.