API quickstart

A working call in under a minute.

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.

What you get

1

Tables with relations

A spreadsheet worksheet, an Airtable table, or an extracted PDF becomes a table. You link columns yourself, for example invoices.customer_id → customers.id.
2

REST

List tables, filter rows, page with limit and offset, and embed a linked row with include=. An OpenAPI 3.1 spec is generated for the workspace.
3

GraphQL and MCP

The same tables are available as a GraphQL endpoint and as an MCP server for Claude and Cursor. MCP tools are read-only.

Try it now

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.

List tables

curl -H "Authorization: Bearer dq_demo_readonly" \
  "https://docquery.io/api/v1/tables"

List due invoices, with the customer

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"

Fetch one row

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.

MCP: list tools

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"}'

MCP: query invoices

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}}}'

Use your own key

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.

Query parameters on row lists

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.
  • Any other query parameter is an exact-match filter on a column slug. The cell is compared as a string to the parameter value. Unknown parameters are treated as column filters, so a typo matches no rows rather than being ignored. 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

GraphQL

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.

OpenAPI

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.

MCP client config

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"
    }
  }
}

Your own workspace

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.