Tables with relations
invoices.customer_id
→ customers.id.
API quickstart
Each uploaded spreadsheet, Airtable base, or PDF becomes tables. Link
them with foreign keys, then read the same tables over REST, GraphQL,
and MCP. The commands below use the public read-only demo key
dq_demo_readonly
against the example customers and invoices.
invoices.customer_id
→ customers.id.
limit
and offset, and embed a linked row with include=.
An OpenAPI 3.1 spec is generated for the workspace.
No signup. The demo workspace has two tables, example_customers
and example_invoices, already linked by customer_id. The key allows REST GET and HEAD, and
POST /api/v1/mcp. Writes and GraphQL return 403
with read_only_demo.
curl -H "Authorization: Bearer dq_demo_readonly" \
"https://docquery.io/api/v1/tables"
status=due
keeps rows whose status is exactly due. include=customer_id
nests the linked customer under related.customer_id.
curl -H "Authorization: Bearer dq_demo_readonly" \
"https://docquery.io/api/v1/tables/example_invoices/rows?status=due&include=customer_id"
Each row has an integer id. That id is assigned when the
row is created, so it is not the invoice number and it is not the same
on every database. This reads the first due invoice's id, then requests GET /api/v1/tables/example_invoices/rows/:id. It needs
python3
only to print that id.
ID=$(curl -sS -H "Authorization: Bearer dq_demo_readonly" \
"https://docquery.io/api/v1/tables/example_invoices/rows?status=due&limit=1" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["data"][0]["id"])')
curl -H "Authorization: Bearer dq_demo_readonly" \
"https://docquery.io/api/v1/tables/example_invoices/rows/$ID?include=customer_id"
Every row includes source_page
and source_document. The demo invoices came from a CSV, so
both are null, as they are for spreadsheets and Airtable. On a table
extracted from a PDF, source_page
is the 1-based page when extraction could tell, and source_document
is id, filename, and url
for that file. PowerPoint and Google Slides use the slide number the
same way. The url opens the document page for a signed-in member.
The original file bytes are not stored, so it is not a download. Word,
ODT, and RTF rows leave source_page
null, and so does a row whose page could not be determined.
POST JSON-RPC to /api/v1/mcp. The same demo key works.
curl "https://docquery.io/api/v1/mcp" \
-H "Authorization: Bearer dq_demo_readonly" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
query_table
takes table_slug, an optional exact-match filter,
and a limit
(capped at 100). Each row includes source_page
(null on this CSV). The result also includes source_document
for the table.
curl "https://docquery.io/api/v1/mcp" \
-H "Authorization: Bearer dq_demo_readonly" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"query_table","arguments":{"table_slug":"example_invoices","filter":{"status":"due"},"limit":5}}}'
Sign up and open API docs. The workspace API key is at the top of that page. Send it on every request:
Authorization: Bearer YOUR_API_KEY
The same key is accepted as ?api_key=. One key covers REST,
GraphQL, and MCP. With your own key, POST /api/v1/tables/:slug/rows
creates a row, and PATCH or DELETE /api/v1/tables/:slug/rows/:id
updates or deletes one. The demo key cannot.
These apply to GET /api/v1/tables/:slug/rows. The single-row
route only reads include. It ignores limit, offset, and column filters.
limit — how many rows to return. Default 100, capped at 1000.
offset — how many matching rows to skip. Default 0. The list response
includes meta.total, meta.limit, and meta.offset.
include — comma-separated slugs of foreign-key columns on this table.
Linked rows are nested under related. A name that is
not a relation on this table is ignored. This also works on GET /api/v1/tables/:slug/rows/:id.
api_key, limit, offset, and
include
are reserved and are not filters.
GET /api/v1/tables/TABLE/rows?limit=100&offset=0&include=FOREIGN_KEY&COLUMN=VALUE
POST https://docquery.io/api/v1/graphql
with your own key. The demo key is refused with 403 read_only_demo. After you sign in, the GraphQL reference
is at /api-docs/graphql.
GET https://docquery.io/api/v1/openapi.json
returns the OpenAPI 3.1 spec for the workspace that owns the key. The
demo key can read the demo workspace's spec, because this route is a GET.
GET /api/v1/schema
returns the same JSON.
Cursor reads this from ~/.cursor/mcp.json
(or .cursor/mcp.json
in a project). Replace YOUR_API_KEY. The demo key works
here for read-only tools. Claude Code and Claude Desktop are on the MCP server page.
{
"mcpServers": {
"docquery": {
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
},
"url": "https://docquery.io/api/v1/mcp"
}
}
}
The demo tables are example data. A free workspace gets a private key with read and write, plus GraphQL.
Free plan: 5 documents and 10,000 rows, no card.
We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect