# SweMetrics API Guide for LLMs > **Base URL:** `https://swemetrics.se/duckdb` > **Interactive Docs:** https://swemetrics.se/duckdb/docs > **OpenAPI Spec:** https://swemetrics.se/duckdb/openapi.json A REST API providing access to 476M+ scholarly works via DuckDB. Designed for analysts, tools, and LLMs. --- ## Quick Start ```bash # Get 5 most-cited works from 2023 curl -H "X-API-Key: YOUR_KEY" \ "https://swemetrics.se/duckdb/api/works?filter=publication_year:2023&sort=cited_by_count:desc&limit=5" ``` --- ## Authentication Three methods (use any one): | Method | Example | |--------|---------| | **Header** (preferred) | `-H "X-API-Key: YOUR_KEY"` | | **Query param** | `?api_key=YOUR_KEY` | | **Basic auth** | `-u apikey:YOUR_KEY` | **Roles:** `reader` (REST CRUD reads only), `analyst` (REST reads + raw SQL + MCP tools), `editor` (CRUD on tables), `admin` (+ execute + export) > **MCP note:** All MCP tools issue SQL and require `QUERY` permission. The `reader` role does **not** have this — use `analyst` or higher for any MCP client (including claude.ai remote MCP integration). Query parameter auth works for **all endpoints including `/duckdb/mcp`**, making it suitable for clients that cannot set custom headers (e.g. claude.ai remote MCP integration — register `https://swemetrics.se/duckdb/mcp?api_key=YOUR_KEY` as a remote server). --- ## MCP Access (recommended for LLMs) The `/duckdb/mcp` endpoint implements the [Model Context Protocol](https://modelcontextprotocol.io) via streamable-HTTP. It is the most ergonomic interface for LLMs: tools are self-describing, SQL queries run directly against the local parquet snapshot, and the `export` tool avoids flooding context windows with row data. ### Client configuration To connect from **Claude Code** or any MCP client, add this server entry to `~/.claude/mcp.json` (replace key): ```json { "mcpServers": { "swemetrics": { "type": "http", "url": "https://swemetrics.se/duckdb/mcp", "headers": { "X-API-Key": "YOUR_KEY" } } } } ``` Or from the command line: ```bash claude mcp add --transport http swemetrics https://swemetrics.se/duckdb/mcp \ --header "X-API-Key: YOUR_KEY" ``` ### Raw JSON-RPC handshake ```json POST https://swemetrics.se/duckdb/mcp X-API-Key: YOUR_KEY Content-Type: application/json {"jsonrpc":"2.0","id":1,"method":"initialize", "params":{"protocolVersion":"2024-11-05","capabilities":{}, "clientInfo":{"name":"my-agent","version":"0.1"}}} ``` ### Standard Tools | Tool | Permission | Description | |------|-----------|-------------| | `database_info` | query | Tables, schemas, macros, extensions; includes `estimated_row_count` per table and `export_base_url` when public exports are configured | | `list_tables` | query | Tables (and optionally views) with column counts | | `describe` | query | Column names and types for a table or view | | `schema` | query | All tables + columns — use `compact=true` for ~70% fewer tokens, `table_pattern` to filter | | `summarize` | query | Per-column stats (min/max/avg/null%/approx_unique) — accepts `table` or `sql` param (use `sql` with SAMPLE for large tables) | | `value_counts` | query | Top-N distinct values with count, relative frequency (%), and cumulative frequency | | `help` | query | DuckDB documentation browser — no args for TOC, topic keyword for section content | | `sample` | query | Unbiased reservoir sample — better than LIMIT for exploration | | `column_search` | query | Find which tables contain a column matching a name/LIKE pattern | | `row_counts` | query | Row count for one or all tables (fast on parquet-backed views) | | `sample_by_id_range` | query | Reservoir sample filtered by integer ID range — efficient parquet row-group skipping | | `explain` | query | EXPLAIN or EXPLAIN ANALYZE a SQL statement — shows the query plan | | `view_definition` | query | Return the SQL definition of a view | | `relationships` | query | Discover likely join keys by finding column names shared across tables | | `time_range` | query | Min/max/count and span for all date/timestamp columns in a table | | `query` | query | Read-only SQL — result capped at `max_rows` (default 200) | | `export` | query | Run SQL, write to file, return URL — **use for large result sets**. Set `public=true` for an auth-free URL suitable for cross-domain imports | | `server_status` | query | Memory usage, spill-to-disk status, and key DuckDB settings | | `import_remote` | query | Download a remote .parquet/.csv/.json file, store locally, register as `FROM alias()` table macro — requires `DUCKDB_IMPORTS_DIR` on server | | `list_imports` | query | List active remote imports with alias, source URL, row count, columns, and expiry time | | `drop_import` | query | Drop a named import's macro and delete its local file immediately | | `execute` | execute | Write SQL: INSERT, UPDATE, DELETE, CREATE TABLE AS SELECT, etc. (requires `execute` permission — admin role by default) | ### Custom OpenAlex Tools These tools call the live OpenAlex API and are ideal for resolving names to numeric IDs before querying the local snapshot. **Entity lookup (autocomplete):** | Tool | Parameter | Returns | |------|-----------|---------| | `api_search_works` | `searchterm` | work_id, doi, display_name, cited_by_count | | `api_search_authors` | `searchterm` | author_id, orcid, display_name, works_count | | `api_search_institutions` | `searchterm` | institution_id, ror, display_name, works_count | | `api_search_sources` | `searchterm` | source_id, display_name, works_count | | `api_search_topics` | `searchterm` | topic_id, display_name, wikipedia_url | | `api_search_funders` | `searchterm` | funder_id, ror, display_name | | `api_search_publishers` | `searchterm` | publisher_id, display_name | | `api_search_doi` | `doi` | work_id, doi, display_name | | `api_search_orcid` | `orcid` | author_id, orcid, display_name | | `api_search_id` | `searchterm` | Generic OpenAlex autocomplete (any entity type) | **Full search (returns rich result objects from OpenAlex API):** | Tool | Parameter | Description | |------|-----------|-------------| | `api_works_search` | `searchterms` | Full-text search for works via OpenAlex works API | | `api_authors_search` | `searchterms` | Filter-based author search via OpenAlex authors API | **Text analysis:** | Tool | Parameter | Description | |------|-----------|-------------| | `api_aboutness_keywords` | `title` | Extract keywords from a title via OpenAlex text API | | `api_aboutness_topics` | `title` | Extract topic, subfield, and field assignments from a title | **Vector search (QBOT):** | Tool | Parameters | Description | |------|-----------|-------------| | `api_find_qbot` | `search_query`, `result_limit` | Semantic search via lance vector index — returns works with fulltext PDF/XML URLs | | `api_works_qbot` | — | All QBOT-indexed works with fulltext URLs | **Documentation readers:** | Tool | Parameters | Description | |------|-----------|-------------| | `read_api_fields` | `entity_name`, `entity_url` | Parse field definitions from OpenAlex documentation pages | | `read_docs` / `docs` | `entity_url` | Read structured content from any URL | ### MCP Resources Reference documents available as MCP resources. Fetch before writing complex DuckDB SQL — the syntax differs significantly from standard SQL. > **Note:** Most MCP clients (including Claude Code) do not auto-fetch resources — request them explicitly with `resources/read`. Use `resources/list` to discover what is available. Four resources are always available (built into the binary): | URI | Content | |-----|---------| | `duckdb://docs/sql-syntax` | FROM-first, GROUP BY ALL, SELECT * EXCLUDE/REPLACE, PIVOT/UNPIVOT, ASOF/LATERAL joins, lambdas, direct parquet queries | | `duckdb://docs/visualization` | SQL templates for time series, bar charts, scatter plots, heatmaps, and ASCII text plots via the `textplot` extension | | `duckdb://docs/functions` | LIST/STRUCT/MAP nested type reference, regexp (RE2), JSON operators, unnest patterns, QUALIFY clause | | `duckdb://docs/datadomain-meta` | COMMENT ON syntax, tags MAP, `macro_descriptions` table pattern, all `duckdb_*()` system catalog functions with example queries | Deployment-specific resources (mounted via `DUCKDB_MCP_DOCS_DIR`): | URI | Content | |-----|---------| | `duckdb://docs/suhf-bibliometrics` | SUHF 2025 guidance for responsible evaluative bibliometrics in Sweden — 12 recommendations, indicator choice, swemetrics column mapping | ```json {"jsonrpc":"2.0","id":1,"method":"resources/list","params":{}} {"jsonrpc":"2.0","id":2,"method":"resources/read", "params":{"uri":"duckdb://docs/sql-syntax"}} ``` ### Tool: `help` — DuckDB documentation browser Call with no arguments for a table of contents; call with a section ID or keyword for section content: ```json {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"help","arguments":{}}} {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"help","arguments":{"topic":"group-by-all"}}} ``` ### Recommended workflow for bibliometric queries The recommended pattern is: **resolve → explore → query → export**. 1. **Resolve names to IDs** — use `api_search_authors`, `api_search_institutions`, `api_search_topics` etc. to get numeric IDs from the live OpenAlex API 2. **Explore the local snapshot** — use `schema(compact=true)`, `row_counts`, `value_counts` to understand the data before writing joins 3. **Write targeted SQL** — use `query` for small results, `export` for large ones; follow the CTE-first pattern below for multi-table joins 4. **Fetch syntax docs if needed** — call `help("from-first")` or fetch `duckdb://docs/sql-syntax` before writing complex queries **Example:** ``` api_search_institutions("KTH") → institution_id: 86987016 query("SELECT ... FROM works_authorships WHERE institution_id = 86987016 ...") export(sql="SELECT ...", format="parquet") → download URL ``` ### Token-saving tips | Situation | Tool | Why | |-----------|------|-----| | Get all table structures | `schema(table_pattern='works%', compact=true)` | **Always filter** — 57 tables; unfiltered compact schema is very large | | Find a column across tables | `column_search("work_id")` | One call, no SQL | | Understand a categorical column | `value_counts(table, column)` | Returns count + pct + cumulative_pct | | Large result set | `export` | Returns URL (~15 tokens), not row data | | Cross-domain join needed | `database_info` on both servers → `export(public=true)` on the smaller → `import_remote` on the larger → `query` → `drop_import` | 4 tool calls replaces manual S3 workflow; `estimated_row_count` guides which direction to push data | | Unfamiliar DuckDB syntax | `help` or fetch `duckdb://docs/sql-syntax` | Fetch once, write correctly | --- ## Cross-Domain Queries (MCP) When you have MCP connections to two or more caddy-duckdb servers (e.g. swemetrics + kthworks-mcp), you can join their data without S3 or manual file transfer. The workflow is four MCP tool calls: **Step 1 — discover** what each server has and where exports land: ```json {"method":"tools/call","params":{"name":"database_info","arguments":{}}} ``` The response includes `estimated_row_count` per table (use this to decide which server exports — push data from the smaller/filtered source to the larger one) and `export_base_url` (only present when the server supports public exports). **Step 2 — export** from the smaller/filtered server with `public=true`: ```json {"method":"tools/call","params":{"name":"export","arguments":{ "sql": "SELECT orcid, display_name FROM diva_authors WHERE orcid IS NOT NULL", "format": "parquet", "public": true }}} ``` Returns `{"url": "https://kthworks.vm-app.cloud.cbh.kth.se/duckdb/public-exports/.parquet", "rows": 6934, ...}`. **Step 3 — import** on swemetrics (the server with the large OpenAlex table): ```json {"method":"tools/call","params":{"name":"import_remote","arguments":{ "url": "https://kthworks.vm-app.cloud.cbh.kth.se/duckdb/public-exports/.parquet", "alias": "diva_authors", "ttl_minutes": 60 }}} ``` Returns `{"row_count": 6934, "columns": ["orcid","display_name"], ...}`. **Step 4 — join** on swemetrics using `query`: ```json {"method":"tools/call","params":{"name":"query","arguments":{ "sql": "SELECT d.display_name, count(*) AS works, avg(w.fwci) AS mean_fwci FROM diva_authors() d JOIN authors a ON d.orcid = a.orcid JOIN works_authorships wa USING (author_id) JOIN works w USING (work_id) GROUP BY d.display_name ORDER BY works DESC LIMIT 20" }}} ``` **Step 5 — clean up:** ```json {"method":"tools/call","params":{"name":"drop_import","arguments":{"alias":"diva_authors"}}} ``` ### Cross-domain notes - `import_remote` only accepts `https://` URLs (SSRF protection). - The alias becomes a table macro in DuckDB's `:memory:` catalog — use `FROM alias()` (with parentheses) in all subsequent queries. - Re-importing the same alias replaces the previous import — idempotent. - Call `list_imports` to see active imports, their sizes, and time until expiry. - `import_remote` requires `DUCKDB_IMPORTS_DIR` on the *receiving* server; `export(public=true)` requires `DUCKDB_PUBLIC_EXPORTS_DIR` on the *exporting* server. --- ## Efficient Multi-Table Queries on Large Parquet Databases Several swemetrics tables exceed 1 billion rows. Naive joins will time out. Use the CTE-first pattern: **Rules:** 1. **Start with the smallest, most selective table** — narrow down the key IDs first 2. **Never use `SELECT *`** — project only the columns you actually need 3. **Use CTEs to stage results** — each CTE should be smaller than the previous 4. **Filter before joining** — use `WHERE x IN (FROM cte SELECT id)` to push filters down **Pattern — find works by a specific author at a specific institution:** ```sql WITH author_work_ids AS ( FROM works_authorships wa JOIN authors a USING (author_id) SELECT DISTINCT wa.work_id WHERE a.display_name ILIKE '%Maguire%' AND a.last_known_institution_display_name ILIKE '%KTH%' ), citation_counts AS ( FROM works_referenced_works SELECT referenced_work_id AS work_id, COUNT(*) AS citations_in_graph WHERE referenced_work_id IN (FROM author_work_ids SELECT work_id) GROUP BY referenced_work_id ) FROM author_work_ids aw JOIN works w USING (work_id) LEFT JOIN citation_counts cc USING (work_id) SELECT w.work_id, w.doi, w.display_name, w.publication_year, w.cited_by_count, COALESCE(cc.citations_in_graph, 0) AS citations_in_graph ORDER BY w.cited_by_count DESC ``` **What NOT to do (will time out):** ```sql -- BAD: joins two billion-row tables without narrowing first SELECT w.*, pl.source_is_core FROM works_authorships wa JOIN works_primary_location pl ON wa.work_id = pl.work_id WHERE wa.institution_id = 86987016 ``` **Tables to be especially careful with:** `works_referenced_works` (3B rows), `works_keywords` (2.9B), `works_related_works` (2.5B), `works_topics` (906M) — always filter with a CTE before joining. --- ## Endpoints | Endpoint | Method | Description | |----------|--------|-------------| | `/api` | GET | List all accessible tables and views with type info | | `/api/{table}` | GET | Query table/view with filters, pagination, sorting | | `/api/{table}` | POST | Insert records (editor+, tables only) | | `/api/{table}` | PUT | Update records (editor+, tables only) | | `/api/{table}` | DELETE | Delete records (editor+, tables only) | | `/api/{table}/columns` | GET | Get table/view schema and optional statistics | | `/view/{name}` | GET | Query api_-prefixed views | | `/view/{name}/columns` | GET | Get view schema and optional statistics | | `/export` | POST | Run SQL, write result to file, return URL | | `/exports/{filename}` | GET | Download exported file (no auth — UUID is capability token) | | `/public-exports/{filename}` | GET | Download a public export (no auth — only when `DUCKDB_PUBLIC_EXPORTS_DIR` is configured) | | `/query` | POST | Execute raw SQL (analyst+) | | `/mcp` | POST | MCP streamable-HTTP endpoint | | `/openapi.json` | GET | OpenAPI 3.0 spec (no auth) | | `/health` | GET | Health check (no auth) | --- ## Query Parameters | Parameter | Example | Description | |-----------|---------|-------------| | `filter` | `publication_year:2023,type:article` | Filter conditions (comma = AND) | | `sort` | `cited_by_count:desc,title:asc` | Sort order | | `limit` | `100` | Results per page (default: 20, max: 1000) | | `page` | `2` | Page number (offset pagination) | | `cursor` | `*` or `eyJ...` | Cursor pagination (use `*` for first page) | | `select` | `work_id,title,cited_by_count` | Return only these columns | | `group_by` | `publication_year` | Aggregate counts by column | --- ## Filter Syntax **Basic equality:** ``` ?filter=publication_year:2023 ``` **Multiple conditions (AND):** ``` ?filter=publication_year:2023,type:article,language:en ``` **Comparison operators:** ``` ?filter=cited_by_count:>100 # greater than ?filter=cited_by_count:<50 # less than ?filter=cited_by_count:>=100 # greater or equal ?filter=publication_year:!2020 # not equal ``` **Standard operator syntax (alternative):** ``` ?filter=cited_by_count:gt:100 ?filter=cited_by_count:gte:100 ?filter=cited_by_count:lt:50 ?filter=cited_by_count:lte:50 ?filter=type:ne:dataset ?filter=type:like:article% ?filter=type:in:article,book,review ``` --- ## Response Formats ### Standard List Response ```json { "data": [ {"work_id": 123, "title": "...", "cited_by_count": 500} ], "pagination": { "page": 1, "limit": 20, "total_rows": 476951866, "total_pages": 23847594 } } ``` ### Cursor Pagination Response ```json { "data": [...], "meta": { "count": 100, "per_page": 100, "next_cursor": "eyJzIjpbImNpdGVkX2J5X2NvdW50Il0..." } } ``` ### Group By Response ```json { "group_by": [ {"key": "article", "key_display_name": "article", "count": 256373696}, {"key": "book", "key_display_name": "book", "count": 7819905} ] } ``` ### Output Formats Set via `Accept` header: | Format | Accept Header | Use Case | |--------|---------------|----------| | JSON | `application/json` (default) | General use | | CSV | `text/csv` | Spreadsheets | | Parquet | `application/parquet` | Data analysis (5-10x smaller) | | Arrow | `application/vnd.apache.arrow.stream` | Zero-copy streaming | --- ## Common Query Patterns ### 1. Get top cited works ```bash curl -H "X-API-Key: KEY" \ "https://swemetrics.se/duckdb/api/works?sort=cited_by_count:desc&limit=10&select=work_id,display_name,cited_by_count" ``` ### 2. Filter by year and type ```bash curl -H "X-API-Key: KEY" \ "https://swemetrics.se/duckdb/api/works?filter=publication_year:2023,type:article&limit=50" ``` ### 3. Aggregate by publication year ```bash curl -H "X-API-Key: KEY" \ "https://swemetrics.se/duckdb/api/works?group_by=publication_year&filter=type:article" ``` ### 4. Aggregate by work type ```bash curl -H "X-API-Key: KEY" \ "https://swemetrics.se/duckdb/api/works?group_by=type" ``` ### 5. Cursor pagination for large exports ```bash # First page curl -H "X-API-Key: KEY" \ "https://swemetrics.se/duckdb/api/works?cursor=*&limit=1000&sort=work_id:asc" # Next page (use next_cursor from response) curl -H "X-API-Key: KEY" \ "https://swemetrics.se/duckdb/api/works?cursor=eyJzIjpbIndvcmtfaWQiXS...&limit=1000&sort=work_id:asc" ``` ### 6. Sparse fieldset (reduce payload) ```bash curl -H "X-API-Key: KEY" \ "https://swemetrics.se/duckdb/api/works?select=work_id,display_name,doi,publication_year&limit=100" ``` ### 7. Export as Parquet ```bash curl -H "X-API-Key: KEY" -H "Accept: application/parquet" \ "https://swemetrics.se/duckdb/api/works?filter=publication_year:2023&limit=10000" \ -o works_2023.parquet ``` --- ## Discovering Tables and Views Use `GET /api` to list all accessible tables and views: ```bash curl -H "X-API-Key: KEY" "https://swemetrics.se/duckdb/api" ``` Response: ```json { "tables": [ {"name": "works", "type": "view", "read_only": true}, {"name": "authors", "type": "view", "read_only": true}, {"name": "institutions", "type": "view", "read_only": true} ] } ``` Each entry includes: - `type`: `"table"` (supports full CRUD) or `"view"` (read-only queries only) - `read_only`: `true` for views (POST/PUT/DELETE will fail), omitted for tables Use `GET /api/{name}/columns` to inspect the schema of any table or view. ### Key Tables/Views | Name | Type | Description | Example Filter | |------|------|-------------|----------------| | `works` | view | 476M+ scholarly works | `type:article,publication_year:>2020` | | `authors` | view | Author profiles and metrics | `cited_by_count:>1000` | | `institutions` | view | Research institutions | — | | `works_authorships` | view | Work-author relationships | — | | `works_topics` | view | Work-topic assignments | — | | `topics` | view | Topic taxonomy | — | | `sources` | view | Journals and repositories | — | | `funders` | view | Funding organizations | — | *All data in the current deployment is served as read-only views over S3-hosted Parquet files. Use `GET /api` for the full current list.* --- ## Works Schema (Key Columns) *Use `GET /api/works/columns` for the full schema. Key columns:* | Column | Type | Description | |--------|------|-------------| | `work_id` | integer | Unique identifier | | `display_name` | string | Title of the work | | `title` | string | Title (alias) | | `doi` | string | Digital Object Identifier | | `publication_year` | integer | Year published | | `publication_date` | date | Full publication date | | `type` | string | article, book, dataset, etc. | | `language` | string | ISO 639-1 code (en, ja, de...) | | `cited_by_count` | integer | Number of citations | | `authors_count` | integer | Number of authors | | `is_oa` | boolean | Open access status | | `is_retracted` | boolean | Retraction status | | `has_fulltext` | boolean | Full text available | | `fwci` | float | Field-Weighted Citation Impact | > **Citation metrics are power-law distributed.** Median `cited_by_count` = 0 even though mean ≈ 6. The same applies to `fwci` and `referenced_works_count`. Use percentile columns (`q25`, `q50`, `q75` from `summarize`) rather than averages to describe typical works — averages are dominated by a small number of highly-cited outliers. > **Note on `api_works_food`:** this table contains duplicate columns (`doi_1`, `title_1`, `doi_2`, `title_2`) that are join artifacts in its macro definition. It has a subset of works only relevant for the food industry, focusing on sustainability. Prefer `works` for general queries. --- ## Best Practices for LLMs ### Do - Start with `GET /api` to discover available tables/views and check `read_only` before attempting writes - Use `GET /api/{table}/columns` to inspect schema before querying - Use `select` to request only needed columns (reduces tokens) - Use `cursor` pagination for iterating large datasets - Use `group_by` for aggregations instead of fetching all rows - Set reasonable `limit` values (100-1000) - Use Parquet format for data analysis pipelines - Use `schema(table_pattern=..., compact=true)` — always pass a pattern; 57 tables unfiltered produces a very large response - Use `row_counts()` to understand table sizes before joining — several tables exceed 1 billion rows ### Don't - Don't use random `page` numbers for sampling - Don't fetch all columns when you only need a few - Don't paginate beyond page 100 with offset pagination (use cursor instead) - Don't request unlimited results without filters - **Don't join `works_referenced_works` (3B rows), `works_keywords` (2.9B), `works_related_works` (2.5B), or `works_topics` (906M) without a filtering CTE first** — naive joins will time out - **Don't treat citation averages as typical** — `cited_by_count`, `fwci`, and `referenced_works_count` are heavily right-skewed (median = 0 even though mean ≈ 6); use percentiles (`q50`, `q75`) rather than averages to characterise "typical" works --- ## Responsible Bibliometrics (SUHF 2025) Swedish higher education institutions are guided by the SUHF working group's *Råd för utvärderande bibliometri i Sverige* (August 2025), which complements DORA, CoARA/ARRA, and the Leiden Manifesto. When helping with bibliometric analyses, apply the following: ### Indicator choice | Task | Use | Avoid | |------|-----|-------| | Compare citation impact across fields | `fwci`, `citation_normalized_percentile` | Raw `cited_by_count`, JIF, h-index | | Describe a typical work | `q50`, `q75` (from `summarize`) | `AVG(cited_by_count)` — median = 0, mean ≈ 6 | | Compare institutions or research groups | Field-normalized percentiles | Unnormalized totals | | Trend analysis | Year-by-year series from `works_counts_by_year` | Single-point snapshots | > **Never use Journal Impact Factor (JIF) or h-index to compare research across different > fields** (SUHF Rec 9, Leiden Manifesto principle 6). These indicators do not account for > field-specific publication and citation norms. ### Sample size - Analyses based on **fewer than 50 publications** are unreliable — state this explicitly and interpret with caution (SUHF Rec 6, 10). - Always include `COUNT(*) AS n_works` alongside citation indicators so the reader can judge reliability. - Use `row_counts()` before building analyses to understand the scale of each table. ### Reporting - Report **medians and percentiles** (`q25`, `q50`, `q75`) rather than means for all citation metrics — the distribution is heavily right-skewed (SUHF Rec 6). - State the **time window and data source** for every analysis (SUHF Rec 7, 10). - Avoid false precision: round indicators to 2 significant figures unless exact values are needed; note that `fwci` and percentile values depend on the OpenAlex snapshot date (SUHF Rec 11). - Do not directly compare indicator values from different analyses, time points, or data snapshots without noting the incomparability (SUHF Rec 12). ### Context - Bibliometric data describes *some* aspects of research output — it cannot replace expert judgement or peer review (SUHF Rec 4, Leiden Manifesto principle 1). - Publication and citation norms differ strongly by field. Swedish social science and humanities have lower citation counts than medicine or physics — use the `topics` and `primary_topic` columns to identify field context before interpreting raw numbers. - Be aware that evaluation models based on bibliometrics can be gamed. If asked to help design such a model, note this risk (SUHF Rec 1). ### swemetrics columns for responsible reporting | Column | Table | Use for | |--------|-------|---------| | `fwci` | `works` | Field-weighted citation impact (normalized) | | `citation_normalized_percentile` | `works_citation_normalized_percentile` | Percentile rank within field+year | | `cited_by_percentile_year` | `works_cited_by_percentile_year` | Percentile within publication year | | `counts_by_year` | `works_counts_by_year` | Citation trends over time | | `primary_topic` | `works_primary_topic` | Field classification for context | --- ## Error Handling | Code | Meaning | Action | |------|---------|--------| | 400 | Bad request | Check query syntax | | 401 | Unauthorized | Provide valid API key | | 403 | Forbidden | Insufficient permissions for operation | | 404 | Not found | Table doesn't exist | | 429 | Rate limited | Implement exponential backoff | | 500 | Server error | Retry with backoff | | 503 | Service unavailable | Export directory not configured on server | **Recommended retry strategy:** 1s, 2s, 4s, 8s delays, max 5 attempts. **MCP tool errors** are returned as text content prefixed with `"Error: "` rather than HTTP error codes — always check the content string, not just the HTTP status. --- ## Python Example ```python import requests API_KEY = "your_api_key" BASE_URL = "https://swemetrics.se/duckdb" def query_works(filters=None, sort=None, limit=100, select=None): params = {"limit": limit} if filters: params["filter"] = filters if sort: params["sort"] = sort if select: params["select"] = select response = requests.get( f"{BASE_URL}/api/works", headers={"X-API-Key": API_KEY}, params=params ) return response.json() # Get top 10 cited articles from 2023 results = query_works( filters="publication_year:2023,type:article", sort="cited_by_count:desc", limit=10, select="work_id,display_name,cited_by_count" ) for work in results["data"]: print(f"{work['display_name']}: {work['cited_by_count']} citations") ``` --- ## SQL Access (Analyst+) Analysts and admins can execute arbitrary DuckDB SQL: ```bash curl -X POST -H "X-API-Key: ADMIN_KEY" \ -H "Content-Type: application/json" \ -d '{"sql": "SELECT type, COUNT(*) as n FROM works GROUP BY type ORDER BY n DESC LIMIT 5"}' \ "https://swemetrics.se/duckdb/query" ``` --- ## Comparison with OpenAlex API This API is intentionally compatible with [OpenAlex](https://docs.openalex.org) query patterns: | Feature | OpenAlex | SweMetrics | |---------|----------|------------| | Filter syntax | `filter=year:2023` | `filter=publication_year:2023` | | Comparison ops | `cited_by_count:>100` | `cited_by_count:>100` | | Pagination | cursor only | cursor + offset | | Group by | `group_by=year` | `group_by=publication_year` | | Select fields | `select=id,title` | `select=work_id,title` | | Output formats | JSON, CSV | JSON, CSV, Parquet, Arrow | | Raw SQL | Not available | Available (admin) | --- ## Rate Limits Contact administrator for rate limit details. Implement client-side rate limiting for bulk operations. --- ## Support - **OpenAPI Spec:** https://swemetrics.se/duckdb/openapi.json - **Interactive Docs:** https://swemetrics.se/duckdb/docs - **Health Check:** https://swemetrics.se/duckdb/health