Sell to State — Government Contracts Data API

Version 0.4.0

# Sell to State Data API Programmatic access to **government tenders and contract awards in 73 countries**, from official procurement sources: open tenders with bid deadlines, awards with the winner and the amount, public buyers and suppliers. Built for analysts, vendors, and **AI agents**. The [countries page](https://www.selltostate.com/countries/) lists each country and its sources. The same data behind the public [selltostate.com](https://selltostate.com) site and the [app](https://app.selltostate.com/search), available as JSON, CSV, and an **MCP server** your AI assistant can call directly. ## Base URL ``` https://app.selltostate.com ``` > The API is served from `https://app.selltostate.com` at the documented paths — use this host today. A dedicated `https://api.selltostate.com` machine-API host is planned for GA but is **not yet deployed** (it currently returns 404); do not target it until it is announced. ## 60-second quickstart 1. Create a free account at [app.selltostate.com](https://app.selltostate.com) and start a subscription (or trial). 2. Generate an API key under **Account → API keys** (`app.selltostate.com/account/keys`). It is shown **once** — store it. 3. Call the API: ```bash curl -s https://app.selltostate.com/api/v1/awards?q=cybersecurity&page_size=5 \ -H "x-api-key: sts_xxxxxxxx" ``` Open tenders in Mexico published in the last 30 days: ```bash curl -s "https://app.selltostate.com/api/v1/search?scope=tenders&country=mexico&lifecycle=open&published_days=30" \ -H "x-api-key: sts_xxxxxxxx" ``` Without an active subscription every data call returns **`402 Payment Required`** with a `payment_link` (and, for agents, a `machine_payment` affordance) — see **Subscriptions & the paywall** below. ## Authentication Two credential types, both resolved to one entitlement check: | Type | Header | Who | Get one | |---|---|---|---| | **API key** | `x-api-key: sts_…` | Humans / your backend | Dashboard → Account → API keys | | **OAuth 2.1 access token** | `Authorization: Bearer <access_token>` | MCP clients: Claude, ChatGPT, Claude Code, Gemini CLI | Sign-in and consent (see *Connect flow*) | | **Machine client** | `Authorization: Bearer client:<client_id>` | Autonomous agents | Self-register via DCR (`POST /api/oidc/register`) | - API keys are `sts_` + 48 hex chars, shown once, stored only as a SHA-256 hash, and revocable from the dashboard. - Machine clients self-register with no human in the loop (RFC 7591). A freshly registered client has **zero data scopes** until it subscribes — registration returns a `payment_link`, and the client can also pay autonomously via `POST /api/v1/pay`. - MCP clients sign in with OAuth 2.1 (the `OAuth2` scheme below): authorization code with PKCE (S256) and dynamic client registration at `POST /api/auth/mcp/register`. The user signs in and allows access on a consent page. See *Connect flow* below. ## Subscriptions & the paywall Data is gated by a single flat subscription (read live pricing at `GET /api/v1/price`). Until the caller's subject (`user:<id>` for a key, `client:<id>` for an agent) is active, **every data endpoint returns `402`**: ```json { "error": "payment_required", "message": "An active subscription is required to access award data.", "payment_link": "https://app.selltostate.com/checkout", "plan": "data-monthly", "machine_payment": { "endpoint": "https://app.selltostate.com/api/v1/pay", "method": "POST", "accepts": ["payment_method_token", "customer"], "scopes_granted": ["data:read", "data:export"] }, "price": "$49/mo", "currency": "usd" } ``` The `402` also carries a header advertising both paths: ``` WWW-Authenticate: Bearer realm="data", charge="…/checkout", charge_automated="…/api/v1/pay" ``` - **Humans** open `payment_link` (hosted Stripe Checkout). - **Agents** `POST /api/v1/pay` with a Stripe payment-method token and are charged off-session — no browser. On success the agent's very next data call returns `200`. **Plans & caps:** a 2-day trial is capped at **20 rows** per export/page; a paid plan returns up to **5,000 rows**. v1 entitlement is all-or-nothing — an active subscription grants every data scope. ## Scopes `data:read` (read awards), `data:export` (CSV export), `data:bulk` (bulk access). In v1 an active subscription grants all three. ## Pagination List endpoints take `page` (default 1) and `page_size` (default 100; hard-capped by your plan — 20 trial / 5,000 paid). Responses echo `total`, `page`, and `pageSize`. ## Filter errors (400) A bad filter value, an unknown param name, or an unknown country / sector slug is a `400`. Nothing runs and nothing is dropped in silence: the body lists every problem. ```json { "error": "invalid_filter", "errors": [{ "code": "invalid_filter", "param": "lifecycle", "value": "live", "allowed": ["open", "awarded", "cancelled", "planned"] }] } ``` Codes: `invalid_filter`, `unknown_param`, `unknown_country`, `unknown_sector`, `invalid_country`. Values of a fixed list match in any case (`Open`, `TRUE`). MCP returns the same body as the JSON-RPC error `data` (code `-32602`). ## Rate limits - **Data calls:** 120 requests/minute per credential (per API key or machine client; anonymous/`402` traffic is limited per source IP). - **Client registration:** 10 registrations/hour per IP. Over the limit → `429` with a `Retry-After` (seconds) header. ## Using the API from an AI agent (MCP) This API is a **Model Context Protocol (MCP) server**. Point any MCP-capable client (Claude Desktop, Claude Code, Cursor, your own agent) at the streamable-HTTP endpoint and it can search and export government tenders and contracts in 73 countries. An autonomous agent can also self-register and pay for access with no human in the loop. **Endpoint** ``` https://app.selltostate.com/api/mcp ``` ### Connect flow (OAuth 2.1) Claude, ChatGPT, Claude Code, Gemini CLI and other spec-compliant clients connect with no API key: 1. **Discover.** The client reads `GET /.well-known/oauth-protected-resource/api/mcp` (RFC 9728). The resource is `https://app.selltostate.com/api/mcp`. Then it reads `GET /.well-known/oauth-authorization-server` (RFC 8414) for the endpoints, PKCE (S256) and the scopes (`data:read`, `data:export`, `data:bulk`). 2. **Register.** The client registers with RFC 7591 Dynamic Client Registration at `POST /api/auth/mcp/register`. A redirect URI must be a loopback URL (`http://localhost:<port>/…`) or the hosted Claude or ChatGPT callback. 3. **Sign in.** The client opens `/api/auth/mcp/authorize`. The user signs in to Sell to State and selects **Allow access** on the consent page. The client gets a token at `/api/auth/mcp/token` and sends `Authorization: Bearer <access_token>` on each MCP call. 4. **Subscribe.** Data tools need an active plan. Without one, `tools/call` returns a `402` with a message and the account link. `tools/list` is open and needs no credential. ### Autonomous agents (no human) 1. **Register.** `POST /api/oidc/register` returns a `client_id` (zero data scopes) plus a `payment_link`. 2. **Authenticate.** Send `Authorization: Bearer client:<client_id>` on every MCP call. 3. **Subscribe.** Until the client's subject is entitled, every `tools/call` returns the same **`402`** body as REST (see *Subscriptions & the paywall*), with a human `payment_link` and the agent-native `machine_payment` affordance. The agent calls the `pay` tool (`POST /api/v1/pay`) with a Stripe payment-method token, is charged off-session, and its next `tools/call` returns data. ### Tool catalog `tools/list` returns **26 tools** (as of 0.4.0), each mirroring a REST endpoint 1:1 — search, entity detail, faceted search, lists, and saved searches — plus the autonomous `pay` tool. The full catalog (name · scope · REST endpoint · `requires_user_key`) is the machine-readable **`x-mcp-tools`** extension on this document; treat that as the source of truth. Highlights: `whoami` (ungated self-introspection), `search` (faceted), `get_buyer`/`get_supplier_detail`/`get_sector`/`get_tender` (entity detail; `get_buyer_detail` is a back-compat alias of `get_buyer`), and the owner-scoped `*_list`/`*_saved_search` tools that require a user `x-api-key` (an OIDC client token gets a `requires_user_key` result, mirroring the REST `403`). ### Raw JSON-RPC (manual test) List the tools (open, no auth): ```bash curl -s https://app.selltostate.com/api/mcp \ -H "content-type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` Call a tool (needs an entitled principal, else `402`): ```bash curl -s https://app.selltostate.com/api/mcp \ -H "content-type: application/json" \ -H "Authorization: Bearer client:<client_id>" \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"search_awards","arguments":{"q":"cybersecurity","year":2024}}}' ``` ### Example agent prompts Once the connector is attached, a user can simply ask: - *"Find UK tenders for payroll software that still accept bids."* - *"Who are the top suppliers of Countess of Chester Hospital NHS Foundation Trust?"* - *"Which German public contracts did Rohde & Schwarz Cybersecurity win? Export them as CSV."* - *"Show open tenders in Ireland about cybersecurity from the last 30 days."* If the agent hits a `402`, the affordance is self-explanatory: it can present the `payment_link` to the user, or — when authorised to spend — call `pay` and continue without interruption. ## Data notes Award amounts are in their **local award currency** (for example SGD in Singapore, GBP in the United Kingdom). Plan prices are in USD; in euro-area countries, checkout charges in EUR. `description` is the tender description, truncated to 240 chars in list/JSON responses. ## Support [[email protected]](mailto:[email protected]) · status & changelog below. --- ## What's new, 26 October 2026: sign in from Claude and ChatGPT, skills, n8n and Activepieces **OAuth 2.1 for MCP clients.** Claude (custom connector) and ChatGPT (custom MCP server) connect to `https://app.selltostate.com/api/mcp` with OAuth: the user signs in and allows access, with no API key. Dynamic client registration accepts loopback redirect URIs and the hosted Claude and ChatGPT callbacks. `GET /.well-known/oauth-protected-resource/api/mcp` names the MCP resource. See *Connect flow* above. **Agent skills.** `npx skills add getbeton/selltostate-skills` installs 5 skills (open tenders, buyer research, competitor wins, tender alerts, the API reference) for Claude Code, Codex, Cursor, Gemini CLI and other agents that read SKILL.md. **Packages on npm.** n8n community node `n8n-nodes-selltostate`; Activepieces piece `@selltostate/piece-sell-to-state`; local MCP server `npx -y selltostate-mcp` (stdio, with `STS_API_KEY`). ## What's new, 21 October 2026: polling triggers and MCP annotations **Polling triggers (`GET /api/v1/search`, MCP `search`).** A trigger in Zapier, n8n or your own job can now ask for the rows that are new since its last run: | Param | Values | |---|---| | `sort` | adds `published_desc` (the newest publish date first) and `added_desc` (the newest rows in Sell to State first). Tenders scope only. | | `added_after` | unix seconds. Only tenders added to Sell to State after this time. | | `published_days`, `awarded_days`, `date_days` | add `1` (the last day). `/api/v1/awards` keeps 7, 30, 90 and 365. | ```bash curl -s "https://app.selltostate.com/api/v1/search?scope=tenders&country=united-kingdom&q=software&sort=added_desc&added_after=1760000000" \ -H "x-api-key: sts_xxxxxxxx" ``` Keep the time of each run. Send it as `added_after` in the next run. Use the tender `slug` to remove duplicates. **MCP.** `tools/list` gives each tool a `title` and `annotations` (`readOnlyHint`, `destructiveHint`, `openWorldHint`). Data tools are read-only. List and saved-search writes are not read-only. `delete_list`, `remove_from_list` and `delete_saved_search` are destructive. `initialize` returns server `instructions`. A `402` on `tools/call` now has a human `error.message` with the account link. For a signed-in user (API key or OAuth), `error.data` is `{error: payment_required, message, account_url}`. A machine client (`Bearer client:<id>`) still gets the full `402` body with `payment_link` and `machine_payment`. ## What's new, 15 October 2026: strict filters **A bad filter is now a `400`.** Before, the API dropped a filter that it did not understand and returned the results without it. Now a bad value, an unknown param name, or an unknown country or sector returns `400`, and nothing runs. The body lists every problem (see **Filter errors (400)** above). | Request | Result | |---|---| | `GET /api/v1/search?scope=tenders&lifecycle=live` | `400 invalid_filter`, param `lifecycle` | | `GET /api/v1/search?scope=tenders&published_days=10` | `400 invalid_filter` (allowed: 7, 30, 90, 365) | | `GET /api/v1/search?per_page=100` | `400 invalid_filter` (1 to 50) | | `GET /api/v1/search?foo=1` | `400 unknown_param` | | `GET /api/v1/search?country=atlantis` | `400 unknown_country` | | `GET /api/v1/awards?page=0` | `400 invalid_filter` | Other rules: - Values of a fixed list match in any case: `lifecycle=Open` and `has_amount=TRUE` work. - `sort=relevance` is the default order. `sort=deadline` puts the nearest bid deadline first. - Years are whole numbers: `year_min=2024.5` is a `400`. **`/api/v1/awards` and `/api/v1/awards.csv`:** - New filters `supplier_slug` and `agency_slug` (exact match). `supplier` and `agency` still match part of the name. - New day windows `published_days`, `awarded_days`, `date_days` (7, 30, 90 or 365). - Paging is stable: a row never shows on two pages. `page` and `page_size` must be whole numbers from 1. - The rows include open notices. An open notice has no supplier and no `awarded_at`. **Entity endpoints:** `/api/v1/buyers`, `/buyers/{slug}`, `/suppliers`, `/suppliers/{slug}`, `/sectors/{slug}` and `/tenders/{id}` take `?country=<slug>`. Before, they always returned Singapore. Without `country`, the default is still `singapore`. **MCP:** the `search` tool lists every filter in its schema, with the allowed values. `search_awards` takes the new `/awards` filters. A bad argument is the JSON-RPC error `-32602`; its `data` is the same body as the REST `400`. **What to do:** if a call now returns `400`, read `errors`. Each entry names the param and the value that failed. ## What's new in 0.4.0 — REST · MCP · CLI parity 0.4.0 closes the gap between the three machine surfaces so **every capability is reachable the same way from REST, the MCP server, and the official CLI** (`@data-atlas/cli` → the `selltostate` binary). Same auth (`x-api-key` or `Bearer client:<id>`), same paywall, same entitlement caps. ### New REST endpoints (all under `/api/v1`, same machine paywall as `/awards`) | Endpoint | Returns | |---|---| | `GET /api/v1/whoami` | Self-introspection: `{subject, type, active, plan, scopes, export_cap}`. **No 402** — a valid-but-unentitled key returns `200` with `active:false` so an agent can decide to pay *before* doing work. Missing credential → `401`. | | `GET /api/v1/search` | Faceted full-text (Typesense) search over one `scope` (`tenders\|buyers\|suppliers\|sectors\|countries`): `{hits, found, page, per_page, facets}`. Search-budget rate limit (300/min). | | `GET /api/v1/buyers` · `GET /api/v1/buyers/{slug}` | Top buyers by spend · buyer detail (summary, yearly trend, sector breakdown, top suppliers). | | `GET /api/v1/suppliers` · `GET /api/v1/suppliers/{slug}` | Top suppliers by awarded total · supplier detail (summary, yearly trend, sectors, top buyers). | | `GET /api/v1/sectors` · `GET /api/v1/sectors/{slug}` | Curated sector taxonomy · sector detail (summary, yearly trend, active partners via `?kind=buyers\|suppliers`). | | `GET /api/v1/tenders/{id}` | Single award by numeric `award_id` **or** public tender page id, with related awards. | | `GET/POST /api/v1/lists` · `GET/PATCH/DELETE /api/v1/lists/{id}` · `POST/DELETE /api/v1/lists/{id}/items` | Saved award **lists** CRUD. **Owner-scoped: requires an `x-api-key` (user) credential** — OIDC client tokens get `403` (a list belongs to a real user). | | `GET/POST /api/v1/saved-searches` · `GET/PATCH/DELETE /api/v1/saved-searches/{id}` | Saved **searches** CRUD. Same owner-scoped rule as lists. | Entity-detail and partner tables are capped to your plan's row cap on trial keys (`table_cap` echoes the applied cap). ### MCP tool catalog (24 tools — `x-mcp-tools` on this document is the machine-readable source of truth) Every REST capability above has a matching MCP tool. `get_buyer` is canonical; `get_buyer_detail` is a kept back-compat alias. Lists/saved-searches tools require a user `x-api-key` (an OIDC client gets a `forbidden`/`requires_user_key` result, mirroring the REST `403`). `whoami` is ungated. ### Official CLI — `selltostate` `npm i -g @data-atlas/cli` ships the `selltostate` binary — a thin wrapper over this exact REST surface, 1:1 per command, with `--format table|json|csv`. Authenticate once with `selltostate auth login` (stores an `x-api-key` at `~/.config/selltostate/config.json`, mode `0600`) or pass `STS_API_KEY`. See the `x-cli` extension on this document for the full command map, and the [CLI page](https://selltostate.com/cli/) on the marketing site. ```bash selltostate auth login # verify + persist your x-api-key via /whoami selltostate whoami # {subject, active, plan, scopes, export_cap} selltostate search "cybersecurity" --scope tenders --year-min 2023 selltostate get buyer ministry-of-education selltostate export --supplier ACME -o awards.csv ```

Servers

Data

Search and export awarded government tenders. Requires an active subscription (else 402).

GET /api/v1/awards

Search awards and open notices

Paginated, filterable query over the awards of one country (`country` omitted = singapore), largest awarded amount first; `award_id` breaks ties, so pages never overlap. Requires an active subscription — otherwise returns `402` with a payment link. Row count per page is capped by your plan (20 trial / 5,000 paid). The rows include open notices: an open notice has no supplier and no awarded_at.

Parameters

  • country in query — Country slug. Omitted means singapore.
  • q in query — Text in the tender description (substring). A text with boolean syntax (OR, AND, NOT, "a phrase", parentheses, -word) matches whole words of the tender title, buyer and supplier instead, as the search q. At most 40 terms.
  • agency in query — Buyer slug, or text in the buyer name (substring).
  • agency_slug in query — Buyer slug, exact match.
  • supplier in query — Text in the supplier name (substring).
  • supplier_slug in query — Supplier slug, exact match.
  • category in query — Sector slug, for example it-software.
  • year in query — Award year (publication year when the award date is missing).
  • amount_min in query — Minimum awarded amount.
  • amount_max in query — Maximum awarded amount.
  • published_days in query — Published in the last N days (UTC days).
  • awarded_days in query — Awarded in the last N days (UTC days).
  • date_days in query — Published or awarded in the last N days (UTC days).
  • page in query — Page number, from 1.
  • page_size in query — Rows per page, from 1. Default 100; the plan cap and 1,000 apply.

GET /api/v1/awards.csv

Export awards and open notices as CSV

Same params as `searchAwards`, returned as `text/csv` (attachment `selltostate-awards.csv`). Columns: `country,tender_no,supplier,agency,category,awarded_amt,published_at,awarded_at,description`. The file is the top rows up to the plan cap (20 trial / 5,000 paid). Requires an active subscription; un-entitled callers are redirected to `/checkout`. The rows include open notices: an open notice has no supplier and no awarded_at.

Parameters

  • country in query — Country slug. Omitted means singapore.
  • q in query — Text in the tender description (substring). A text with boolean syntax (OR, AND, NOT, "a phrase", parentheses, -word) matches whole words of the tender title, buyer and supplier instead, as the search q. At most 40 terms.
  • agency in query — Buyer slug, or text in the buyer name (substring).
  • agency_slug in query — Buyer slug, exact match.
  • supplier in query — Text in the supplier name (substring).
  • supplier_slug in query — Supplier slug, exact match.
  • category in query — Sector slug, for example it-software.
  • year in query — Award year (publication year when the award date is missing).
  • amount_min in query — Minimum awarded amount.
  • amount_max in query — Maximum awarded amount.
  • published_days in query — Published in the last N days (UTC days).
  • awarded_days in query — Awarded in the last N days (UTC days).
  • date_days in query — Published or awarded in the last N days (UTC days).
  • page in query — Page number, from 1.
  • page_size in query — Rows per page, from 1. Default 100; the plan cap and 1,000 apply.

GET /api/v1/whoami

Self-introspection — auth, entitlement & scopes

Resolve the *calling* credential's identity, entitlement and granted scopes. **Deliberately different from the data paywall:** a valid-but-unentitled credential returns `200` with `active:false` (so an agent/CLI can decide to pay *before* doing work); a missing/invalid credential returns `401` (not `402`). Granted scopes are all data scopes when active, else empty (v1 is all-or-nothing). Shares the standard 120/min per-caller rate limit.

GET /api/v1/search

Faceted full-text search (one collection)

Faceted Typesense search over a single `scope` collection. Returns hits, total `found`, pagination, and `facets` (counts per facet value). Uses the **search budget** rate limit (300/min) shared with the public `/api/search` route. Behind the same machine paywall as `/awards` — no human-session fallback.

Parameters

  • q in query — Free text, at most 2,000 characters. Boolean syntax: OR, AND, NOT (uppercase), "a phrase", parentheses, -word (= NOT word). Two words with no operator mean AND. At most 300 terms. A text whose syntax does not parse runs as its plain words. When the top level is an OR of parenthesized groups, rows that match the first group come first.
  • scope in query — The collection to search. Default tenders.
  • country in query — Country slug, or comma-separated slugs (OR).
  • sector in query — Sector slugs (OR), for example it-software.
  • type in query — Procurement type, for example works, goods or services. Only some countries have it.
  • status in query — Countries scope only: data (the country has data) or coming_soon.
  • year_min in query — Earliest year (award year, else publication year).
  • year_max in query — Latest year (award year, else publication year).
  • amount_min in query — Minimum amount.
  • amount_max in query — Maximum amount.
  • lifecycle in query — Tender lifecycle values (OR).
  • lifecycle_mode in query — is (default) keeps the lifecycle values; is_not leaves them out.
  • has_amount in query — true: only rows with an amount. false: only rows without one.
  • published_days in query — Published in the last N days (UTC days). 1 is the last day, for polling.
  • awarded_days in query — Awarded in the last N days (UTC days).
  • date_days in query — Published or awarded in the last N days (UTC days).
  • accepts_bids in query — true: only open tenders whose bid deadline is still ahead.
  • sort in query — relevance (default), deadline (the nearest bid deadline first), published_desc (the newest publish date first) or added_desc (the newest rows in Sell to State first). Tenders scope only.
  • added_after in query — Unix seconds. Only tenders added to Sell to State after this time. With sort=added_desc, a polling trigger keeps the time of its last run and asks for the rows after it.
  • match in query — Text match mode for the suppliers and buyers scopes.
  • page in query — Page number, from 1.
  • per_page in query — Results per page, 1 to 50. Default 25.

GET /api/v1/buyers

Top buyers by spend

Top buying agencies ranked by total awarded spend. `?limit` (default 50, max 500). Trial keys are row-capped (`table_cap`).

Parameters

  • limit in query — Rows to return (default 50, max 500).
  • country in query — Country slug. Omitted means singapore. A malformed or unknown slug is a 400.

GET /api/v1/buyers/{slug}

Buyer (agency) detail

Full buyer profile: header summary, yearly spend trend, sector breakdown, and a paginated top-suppliers table (`?sup_page`). `404` when the slug is unknown.

Parameters

  • slug in path (required) — Buyer slug, e.g. ministry-of-education.
  • sup_page in query — Top-suppliers page (1-based).
  • country in query — Country slug. Omitted means singapore. A malformed or unknown slug is a 400.

GET /api/v1/suppliers

Top suppliers by awarded total

Top suppliers ranked by total awarded value. `?limit` (default 50, max 500). Trial keys are row-capped.

Parameters

  • limit in query — Rows to return (default 50, max 500).
  • country in query — Country slug. Omitted means singapore. A malformed or unknown slug is a 400.

GET /api/v1/suppliers/{slug}

Supplier detail

Full supplier profile: header summary, yearly awarded trend, sector breakdown, and a paginated top-buyers table (`?buyer_page`). `404` when the slug is unknown.

Parameters

  • slug in path (required) — Supplier slug.
  • buyer_page in query — Top-buyers page (1-based).
  • country in query — Country slug. Omitted means singapore. A malformed or unknown slug is a 400.

GET /api/v1/sectors

Sector taxonomy

The curated sector taxonomy (slug + name). Small reference list — no row cap.

GET /api/v1/sectors/{slug}

Sector detail

Sector profile: header summary, yearly trend, and active partners. `?kind=buyers|suppliers` (default buyers) selects the partner table; `?page` paginates it. `404` when the slug is unknown.

Parameters

  • slug in path (required) — Sector slug.
  • kind in query — Partner table: buyers | suppliers (default buyers).
  • page in query — Partner table page (1-based).
  • country in query — Country slug. Omitted means singapore. A malformed or unknown slug is a 400.

GET /api/v1/tenders/{id}

Single award (tender) detail

One award resolved by **numeric `award_id` OR public tender page id**, plus related awards (same tender / same buyer). Related lists are pre-capped; no trial row cap applies. `404` when unknown.

Parameters

  • id in path (required) — Numeric award_id or public tender page id.
  • country in query — Country slug. Omitted means singapore. A malformed or unknown slug is a 400.

Billing

Live pricing and autonomous (agent) payment.

GET /api/v1/price

Live subscription price

Public. Returns the active Stripe subscription price (USD), resolved live by lookup key, with a graceful fallback if Stripe is unreachable. No auth required.

POST /api/v1/pay

Activate a subscription autonomously (agent payment)

Agent-native payment. Present a Stripe payment-method token (`pm_…`) or an existing `customer` (`cus_…`) you control; the subscription is created server-side, off-session — no hosted Checkout page. On success the caller's subject-keyed entitlement flips active and the next data call returns `200`. A decline returns `402` (retryable) echoing `decline_code` and the same `machine_payment` affordance. Requires a machine credential (API key or `Bearer client:`).

Agents & MCP

Self-registration (DCR) and the MCP-over-HTTP tool server for AI agents.

POST /api/oidc/register

Dynamic Client Registration (RFC 7591)

Autonomous, zero-scope client registration for agents. Returns client credentials with **no data scopes** plus a `payment_link` whose Checkout activates exactly this client. Rate-limited 10/hour/IP. After paying (hosted link or `POST /api/v1/pay`), present `Authorization: Bearer client:<client_id>` on data calls.

POST /api/mcp

MCP-over-HTTP (JSON-RPC 2.0)

Model Context Protocol server. Methods: `tools/list` (open catalog) and `tools/call` (gated by subscription). `tools/call` resolves the machine principal (`x-api-key` or `Bearer client:`) and returns `402` if missing or un-entitled. As of 0.4.0 the catalog is **26 tools** at full REST parity (search, entity detail, faceted search, lists, saved searches) — owner-scoped list/saved-search tools require a user `x-api-key`. See `x-mcp-tools` for the catalog.

Discovery

OAuth/MCP discovery metadata (RFC 9728 / RFC 8414).

GET /.well-known/oauth-protected-resource

Protected Resource Metadata (RFC 9728)

Tells MCP/OAuth clients which authorization server and scopes apply to this resource. Origin-relative.

GET /.well-known/oauth-authorization-server

Authorization Server Metadata (RFC 8414)

Issuer, registration/authorize/token/jwks endpoints, PKCE methods, and supported scopes. The token endpoint implements the authorization_code + refresh_token grants (PKCE mandatory); there is no client_credentials token grant today — autonomous agents use the `Authorization: Bearer client:<client_id>` machine shim returned by DCR.

Lists

Saved award lists CRUD. Owner-scoped — requires a user x-api-key (OIDC clients get 403).

GET /api/v1/lists

List saved award lists

Every saved award list the caller can see. **Owner-scoped:** requires an `x-api-key` (user) credential. OIDC client (`Bearer client:`) tokens get `403` — a saved list/search belongs to a real user account.

POST /api/v1/lists

Create a saved list

Create a personal saved award list. **Owner-scoped:** requires an `x-api-key` (user) credential. OIDC client (`Bearer client:`) tokens get `403` — a saved list/search belongs to a real user account.

GET /api/v1/lists/{id}

Get a saved list + items

One list with its tender items joined to award data (paginated `?page`/`?page_size`, ≤100). The default response has tender items only, and `total` counts tender items only. Add `?types=tender,buyer,supplier` (any subset; `award` = `tender`) to also get the buyer and supplier items of the list: each item then has `type`, and `total` and paging count the requested types only. **Owner-scoped:** requires an `x-api-key` (user) credential. OIDC client (`Bearer client:`) tokens get `403` — a saved list/search belongs to a real user account.

Parameters

  • id in path (required) — List id.
  • page in query — 1-based page.
  • page_size in query — Items per page (≤100).
  • types in query — Optional. Comma-separated item types: tender, buyer, supplier (`award` = tender). Without it, items are tender items only. With it, each item has `type`: tender (award fields + country), buyer or supplier (country, slug, name). An empty or unknown type is a 400.

PATCH /api/v1/lists/{id}

Rename a saved list

Rename a list (creator or workspace owner). **Owner-scoped:** requires an `x-api-key` (user) credential. OIDC client (`Bearer client:`) tokens get `403` — a saved list/search belongs to a real user account.

Parameters

  • id in path (required) — List id.

DELETE /api/v1/lists/{id}

Delete a saved list

Delete a list and its items. **Owner-scoped:** requires an `x-api-key` (user) credential. OIDC client (`Bearer client:`) tokens get `403` — a saved list/search belongs to a real user account.

Parameters

  • id in path (required) — List id.

POST /api/v1/lists/{id}/items

Add a tender to a list

Add a tender by `tender_no` **or** `award_id`. Idempotent. **Owner-scoped:** requires an `x-api-key` (user) credential. OIDC client (`Bearer client:`) tokens get `403` — a saved list/search belongs to a real user account.

Parameters

  • id in path (required) — List id.

DELETE /api/v1/lists/{id}/items

Remove a tender from a list

Remove a tender by `tender_no`. **Owner-scoped:** requires an `x-api-key` (user) credential. OIDC client (`Bearer client:`) tokens get `403` — a saved list/search belongs to a real user account.

Parameters

  • id in path (required) — List id.

Saved Searches

Saved search CRUD. Owner-scoped — requires a user x-api-key (OIDC clients get 403).

GET /api/v1/saved-searches

List saved searches

Every saved search the caller can see (personal ∪ workspaces). **Owner-scoped:** requires an `x-api-key` (user) credential. OIDC client (`Bearer client:`) tokens get `403` — a saved list/search belongs to a real user account.

POST /api/v1/saved-searches

Save a search

Persist a search under a name. `kind:"atlas"` normalizes to the unified-search shape; a legacy Explorer filter set is stored as-is. **Owner-scoped:** requires an `x-api-key` (user) credential. OIDC client (`Bearer client:`) tokens get `403` — a saved list/search belongs to a real user account.

GET /api/v1/saved-searches/{id}

Get a saved search

One saved search by id (visible to caller, else `404`). **Owner-scoped:** requires an `x-api-key` (user) credential. OIDC client (`Bearer client:`) tokens get `403` — a saved list/search belongs to a real user account.

Parameters

  • id in path (required) — Saved-search id.

PATCH /api/v1/saved-searches/{id}

Rename a saved search

Rename a saved search (creator or workspace owner). **Owner-scoped:** requires an `x-api-key` (user) credential. OIDC client (`Bearer client:`) tokens get `403` — a saved list/search belongs to a real user account.

Parameters

  • id in path (required) — Saved-search id.

DELETE /api/v1/saved-searches/{id}

Delete a saved search

Delete a saved search. **Owner-scoped:** requires an `x-api-key` (user) credential. OIDC client (`Bearer client:`) tokens get `403` — a saved list/search belongs to a real user account.

Parameters

  • id in path (required) — Saved-search id.