Skip to main content

Index and pack analysis · MCP and REST

Lucius Agent API

Search the Lucius tender index and analyse your own tender packs from your own agent or script, over MCP and plain REST, on the same index and analysis the product runs on.

What it is

The tender index, open to agents.

Lucius is a tender index and a page-cited verification engine. The index holds 250,000+ public procurement notices from the official sources named below, across 170+ buyer countries. The verification engine reads a tender pack and returns the requirements, each with a page citation.

The index tools are read-only: search notices, fetch one by id, and find notices similar to a given one. Search is hybrid, full text plus vector similarity. similar_tenders uses the stored embeddings directly.

Pack analysis runs the verification engine on your own tender documents, or on a public link to them, and returns the requirements with their pages (see pack analysis).

Google Cloud published an engineering write-up of how the platform runs: Solo founder runs a global tender platform on AlloyDB and MCP.

Sources in the index

TED · UK Find a Tender · UK Contracts Finder · PCS Scotland · SAM.gov · Grants.gov · World Bank · CanadaBuys · Ireland eTenders · AusTender · NZ GETS · GeBIZ · India CPPP · UNGM

The API returns 8 of them in full, 1 without snippets and 5 not at all, by each publisher's terms: data sources and licences.

Tools

Three tools, one shape.

The same three tools are exposed over MCP (by name) and over REST (by path). Every notice carries: title, buyer, country, deadline, value, the value's currency, CPV codes, source, source link, a snippet of up to 700 characters, and the attribution its source's licence asks for. A source whose terms do not cover the notice text comes without a snippet. A field the source did not give is null: a value with no stated currency has no currency. CPV codes come with TED and Public Contracts Scotland notices and with about a quarter of SAM.gov notices; on the other sources the list is empty, so search those with a query, not with cpv_prefix. The two pack analysis tools are described under pack analysis, below.

ToolRESTInputsReturns
search_tendersGET /api/v1/ext/tenders/searchquery, countries (ISO codes, comma separated), source, cpv_prefix, closing_within_days, limit, cursorresults with notice metadata, a snippet and a relevance score; mode; next_cursor for the next page
get_tenderGET /api/v1/ext/tenders/{id}idone notice: metadata, a snippet, and the documents and links its source lists for it
similar_tendersGET /api/v1/ext/tenders/{id}/similarid, countries, limitnotices similar to the given one, ranked by stored embeddings

MCP quick start

Add the server to your agent.

The MCP server speaks streamable HTTP at POST https://api.ailucius.com/api/v1/mcp and exposes search_tenders, get_tender and similar_tenders, and for pack analysis analyze_pack and get_pack_analysis.

Claude Code:

claude mcp add --transport http lucius-tenders https://api.ailucius.com/api/v1/mcp --header "Authorization: Bearer lk_live_..."

Any client that reads a JSON MCP config:

{
  "mcpServers": {
    "lucius-tenders": {
      "type": "http",
      "url": "https://api.ailucius.com/api/v1/mcp",
      "headers": { "Authorization": "Bearer lk_live_..." }
    }
  }
}

REST quick start

Three requests.

Search, fetch one notice, and find similar notices. Page through search results with the cursor parameter, using the next_cursor value from the previous response.

curl -H "Authorization: Bearer lk_live_..." \
  "https://api.ailucius.com/api/v1/ext/tenders/search?query=solar+panels&countries=UK,DE&closing_within_days=30&limit=10"

curl -H "Authorization: Bearer lk_live_..." \
  "https://api.ailucius.com/api/v1/ext/tenders/{id}"

curl -H "Authorization: Bearer lk_live_..." \
  "https://api.ailucius.com/api/v1/ext/tenders/{id}/similar?countries=DE&limit=5"

A real request and its answer on 6 October 2026: open notices for surveying services (CPV 71355) in Norway. With no query the search lists open notices by closing date, mode is listing and relevance is null. With a query, relevance is a number between 0 and 1 for a match by meaning and null for a match by keyword alone.

curl -H "Authorization: Bearer lk_live_..." \
  "https://api.ailucius.com/api/v1/ext/tenders/search?countries=NO&cpv_prefix=71355"
{
  "results": [
    {
      "id": "ea5d0480-6247-4dd4-91bc-ba40c7ca11fc",
      "title": "Framework agreement surveying services",
      "buyer": "Suldal Kommune",
      "country": "NO",
      "deadline_at": "2026-10-30T11:00:00+00:00",
      "value": 3000000.0,
      "currency": "NOK",
      "cpv_codes": ["71355000"],
      "source": "TED",
      "source_url": "https://ted.europa.eu/en/notice/-/detail/659182-2026",
      "snippet": "Suldal municipality is inviting tenders for a competition to establish a framework agreement for surveying services. The procured surveying services must be carried out in accordance with the…",
      "relevance": null,
      "attribution": "Source: TED (ted.europa.eu), © European Union, CC BY 4.0, summary by Lucius",
      "licence": "CC BY 4.0",
      "licence_url": "https://creativecommons.org/licenses/by/4.0/"
    }
  ],
  "count": 1,
  "mode": "listing",
  "next_cursor": null,
  "degraded": false,
  "attribution": "Changes by Lucius: fields are normalised, and where a result's attribution says summary or translation, its title or snippet is an English translation or summary written by Lucius, not the publisher's text. Each result's attribution is the credit its source's licence asks for; keep it with the notice when you show or pass it on. Licences per source: https://ailucius.com/developers#licences"
}

What search is, and what it is not

  • A search returns up to 25 notices a page and pages through at most 80 results: the 80 best matches for a query, or the 80 soonest to close when there is no query. Past that, next_cursor is null. To see more of a market, narrow the search with countries, cpv_prefix or closing_within_days.
  • It is a search, not an export. It does not hand over every notice in a country, and it does not report which notices changed since your last call. If you need that, write to davor@ailucius.com.
  • A query runs two searches, by meaning and by keyword, and merges them. When one of the two is down the answer still comes, with "degraded": true: the results are real, and a match the missing half would have found may be absent. When neither can run, the API answers 503 and the request is not counted or charged. An empty results list always means the search ran and nothing matched.
  • No results for a country can mean the API does not serve that country's portal. The licences table lists every source and what the API returns from it.
HTTP/1.1 503 Service Unavailable

{
  "detail": "Search is unavailable right now. This request was not counted or charged; try again in a minute."
}

Pack analysis

Send a tender pack, get its requirements back.

Send your own tender documents, or a public link to them, and Lucius runs the analysis the web app runs on an upload, with the same file types, size limits and checks. The result holds the title, buyer and deadline, the bid or no-bid call and why, the requirements, each with its original text quoted from the pack and the document and page it was read on, which of them decide the bid, and the forms the pack asks bidders to return once they have been mapped.

The analysis runs in the background. A submit answers 202 at once with an analysis_id; poll the status until it is complete or failed. Over MCP, analyze_pack takes a link and get_pack_analysis reads the result; files go to the REST route.

The analysis belongs to the key's workspace. It uses one of the workspace's monthly analyses under its Lucius plan, exactly like an upload in the app, and the team sees it in the app at app_url. Only that workspace's keys can read it, and they can read any of its analyses, including ones made in the app; another workspace's id answers 404. Sending the same file again within 24 hours returns the existing analysis, with deduplicated: true, without using another analysis.

An analysis started through the API does not send the results email and does not start a draft.

MCP toolRESTInputsReturns
REST onlyPOST /api/v1/ext/packsfiles: multipart/form-data, one files part per document, or one ZIP202 with analysis_id, status, deduplicated, status_url, app_url
analyze_packPOST /api/v1/ext/packs/urlurl: a public http(s) link to a tender document (PDF, Word, Excel or ZIP) or to a notice page that links to the documents202 with analysis_id, status, deduplicated, status_url, app_url
get_pack_analysisGET /api/v1/ext/packs/{id}id; requirements_offset (default 0), requirements_limit (default 200, at most 1,000), deciding_only (default false)status, progress and message; reason when failed; result when complete

Send several documents, one ZIP of the pack, or a link, then read the status and the result with the analysis_id:

curl -H "Authorization: Bearer lk_live_..." \
  -F "files=@invitation-to-tender.pdf" \
  -F "files=@specification.docx" \
  -F "files=@pricing-schedule.xlsx" \
  "https://api.ailucius.com/api/v1/ext/packs"

curl -H "Authorization: Bearer lk_live_..." \
  -F "files=@tender-pack.zip" \
  "https://api.ailucius.com/api/v1/ext/packs"

curl -H "Authorization: Bearer lk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"url": "https://..."}' \
  "https://api.ailucius.com/api/v1/ext/packs/url"

curl -H "Authorization: Bearer lk_live_..." \
  "https://api.ailucius.com/api/v1/ext/packs/{analysis_id}?requirements_limit=200"

A submit answers at once. status is queued, or the existing analysis's status when deduplicated is true:

HTTP/1.1 202 Accepted

{
  "analysis_id": "...",
  "status": "queued",
  "deduplicated": false,
  "status_url": "/api/v1/ext/packs/{analysis_id}",
  "app_url": "https://ailucius.com/dashboard/{analysis_id}"
}

Statuses

  • queued: Accepted, not started yet. progress is 0.
  • processing: Running. progress (0 to 100) and message say how far it is.
  • complete: Done. result holds the analysis and progress is 100.
  • failed: Stopped. reason says why, in plain words.

A complete analysis. The fields before result come with every status, and a failed one adds reason:

{
  "analysis_id": "...",
  "status": "complete",
  "progress": 100,
  "message": "...",
  "submitted_at": "...",
  "updated_at": "...",
  "app_url": "https://ailucius.com/dashboard/{analysis_id}",
  "result": {
    "title": "...",
    "buyer": "...",
    "deadline": "...",
    "summary": "...",
    "document": {
      "classification": "...",
      "kind": "...",
      "is_tender": true,
      "note": null
    },
    "verdict": {
      "call": "...",
      "recommendation": "...",
      "confidence": ...,
      "open_requirements": 0,
      "note": null,
      "stale": false,
      "summary": "...",
      "strengths": [...],
      "risks": [...],
      "competition": "...",
      "profile_incomplete": false,
      "deciding_requirements": [...]
    },
    "coverage": {
      "complete": true,
      "notes": [],
      "files": [
        {
          "name": "...",
          "pages": ...,
          "read": "full",
          "left_out_of_upload": false,
          "detail": null
        }
      ],
      "second_read": null
    },
    "documents": ["..."],
    "requirements_total": ...,
    "requirements_final": true,
    "requirements_locked": false,
    "locked_reason": null,
    "deciding_positions": [...],
    "requirements_offset": 0,
    "requirements_count": ...,
    "requirements_next_offset": null,
    "requirements": [
      {
        "position": 1,
        "requirement": "...",
        "requirement_en": "...",
        "original_text": "...",
        "severity": "...",
        "why": "...",
        "section": "...",
        "document": "...",
        "page": ...,
        "page_is_approximate": false,
        "citation": "...",
        "quote_verified": true,
        "deciding": true
      }
    ],
    "forms": [
      {
        "name": "...",
        "name_en": "...",
        "page": ...,
        "page_basis": "...",
        "section": "...",
        "mandatory": true,
        "purpose": "..."
      }
    ]
  }
}

The result

  • document says what was read. When is_tender is false (a bid somebody wrote, a signed contract, something else, or a document Lucius could not classify), verdict is null, no requirement is deciding and document.note says why.
  • verdict is the bid call, or null when the analysis made none. call is Bid, Bid with caveats or Skip, or Closed with closed_on when the deadline has passed; recommendation is GO, NO GO or CAUTION. When the workspace has not described its company, Lucius makes no call: call, recommendation and confidence are null, profile_incomplete is true, deciding_requirements lists the pass/fail gates, open_requirements counts the ones still unanswered and note says what to do. stale is true when the company details changed after the call was made.
  • coverage says how the pack was read. complete is true only when every file got the full read. notes lists every limit of this read in plain words: pages that could not be read, a file read as plain text, a file the upload did not take. files says for each file of a pack whether it was read in full, as text, in partial or not_read; it is null for a single document with nothing left out. While second_read is pending, a second read is still adding requirements and requirements_final is false: read the result again in a few minutes.
  • Each requirement carries original_text, quoted from the pack in its own language, and document, page and citation. page_is_approximate is true where the page is estimated, as it is in Word files. quote_verified is true when the quote was found in the pack text. severity is CRITICAL, HIGH, MEDIUM or LOW, and deciding is true for the requirements that decide the bid, the pass/fail gates first.
  • Requirements come in pages of requirements_limit (default 200, at most 1,000). Pass requirements_next_offset as requirements_offset for the next page; it is null on the last. deciding_only=true returns only the requirements that decide the bid.
  • On a plan without the full requirements list, only the requirements that decide the bid are returned, requirements_locked is true and locked_reason says how to unlock the rest in the app, as in the app itself.
  • forms lists the forms the pack asks bidders to return once they have been mapped, else null.

Limits

  • Files: PDF, Word (DOC, DOCX) and Excel (XLS, XLSX), up to 80 documents of at most 40 MB each, one files part per document. An image (PNG, JPG, WEBP, TIFF, BMP or GIF) is read on its own, as a single file.
  • Or one ZIP on its own, holding up to 80 documents and 300 MB once unpacked, at most 40 MB each. A ZIP sent with other files is refused.
  • One request carries at most 32 MB in all. For a larger pack, put it at a public link and use POST /api/v1/ext/packs/url.
  • A link is fetched the way the app's paste-a-link is: private and internal addresses are refused, every redirect is checked, each file fetched is capped at 15 MB, and a notice page is read for at most 5 of the documents it links to. The link starts with http:// or https:// and is at most 2,048 characters.
  • Links on UNGM (ungm.org), GeBIZ (gebiz.gov.sg), NZ GETS (gets.govt.nz), India CPPP (eprocure.gov.in) and AusTender (Approach to Market notices on tenders.gov.au) are refused with 400, as is a link that leads to one of them: their terms do not allow Lucius to pass their material on, and the index tools do not return them either. Download the documents from the portal and send them to POST /api/v1/ext/packs instead.

Metering

  • A submit is one request once the pack is accepted, over REST or MCP. A pack that is refused (a bad type, too large, over the plan) is not counted.
  • Reading a complete result is one request. Polls that answer queued, processing or failed are free: the API has no callback, so polling is how you wait.
  • Requests are priced like every other call: the first 100 each month are free for the workspace, then EUR 0.02 each from prepaid credit. See pricing.
  • The analysis itself counts against the plan, not the credit: each accepted pack uses one of the workspace's monthly analyses, like an upload in the app. A deduplicated submit uses none.

Errors

  • 400: Nothing usable was sent. No file, an unsupported type, a ZIP sent with other files, a bad or refused link, or an id that is not a uuid.
  • 402: The workspace has used its plan's monthly analyses, or its free API requests and API credit. The detail says which and what to do.
  • 403: The key reads the index only (a key made before pack analysis opened: make a new one), or its workspace has no owner, admin or editor who can start an analysis.
  • 404: No analysis with that id in the key's workspace, or the workspace limited that tender to some of its members: a key reads only what every member reads.
  • 409: The same file was sent in the last 24 hours and that copy cannot be read through the API, for example because the workspace limited it to some members. Open it in the Lucius app, or send the file again after 24 hours.
  • 413: A file is over the size limit.
  • 422: A file could not be read (corrupt, empty or password-protected), or a query value is out of range.
  • 429: The key's per-minute rate limit or daily result cap was reached.

Over MCP a refusal is an error result with the same status and message.

Authentication and limits

One header, two limits.

  • Every request carries Authorization: Bearer lk_live_.... A workspace owner or admin makes keys in Lucius, under Settings, Agent API. A key is shown once, when it is made; Lucius keeps only a hash of it.
  • 60 requests per minute and 10,000 results per day per key. Past either limit the API answers 429.
  • Keys expire after 30, 90, 180 or 365 days (90 unless you choose otherwise), and can be revoked at any time.
  • The index tools are read-only. Pack analysis writes one thing: the analysis it starts, stored in the key's workspace. The per-minute limit counts every call, free status polls included. A key made before pack analysis opened reads the index only: the pack routes answer it with 403 and the MCP server does not list the pack tools for it. Make a new key to use them.

Pricing

100 requests a month free to try, then EUR 20 per 1,000.

  • The first 100 requests each calendar month (UTC) are free for each workspace, so you can try it, however many keys it has. They run on the live index: there is no separate sandbox.
  • After that, each request costs EUR 0.02, which is EUR 20 per 1,000. A search returns up to 25 notices and counts as one request.
  • A request is one successful call to search_tenders, get_tender or similar_tenders, over MCP or REST, including a search that finds nothing. A call the API refuses or fails (any 4xx or 5xx answer) is not counted.
  • Pack analysis counts one request when a pack is accepted, and one when get_pack_analysis returns a complete result. Status checks that answer queued, processing or failed are free.
  • Paid requests come from prepaid credit, bought in Lucius in steps of EUR 20, 50 or 100 with a one-time payment. Credit is valid 12 months from the day you buy it. There is no subscription and nothing is charged automatically.
  • The credit is your spend limit. When this month's free requests are used and the credit is gone, the API answers HTTP 402 with a link to add credit. Over MCP the tool call returns an error result with status 402 and the same message.
  • API credit pays for requests, not analyses. Each pack analysis uses one of the workspace's monthly analyses on its plan, exactly like an upload in the app. The full requirement list and drafts are sold on the plans, which API credit does not change.

The answer when a workspace is out of free requests and credit:

HTTP/1.1 402 Payment Required

{
  "detail": "This workspace has used its 100 free API requests for this month and has no API credit left. Add credit at https://ailucius.com/settings?tab=api (from EUR 20, EUR 0.02 a request, valid 12 months). The free requests start again on 1 November 2026."
}

Data rules

Metadata and a snippet, with the source link.

  • Responses carry notice metadata and a snippet of up to 700 characters. The full notice text is not served.
  • Every notice carries a link to the official source portal. That is where the full text and the documents live.
  • For one notice, get_tender also returns the documents and links the source's own feed lists for it, each as the address on the publisher's side, with the type, format, date and name where the source gives them. An entry can be a bidding document, the notice's own page or a buyer's portal. Lucius serves no file. Find a Tender, Contracts Finder and SAM.gov list them today, SAM.gov as links alone; for the other sources the list is empty.
  • Email addresses are removed from snippets and from document names.
  • The notices are public records published by the source portals. Respect each portal's terms when you store or pass on what you fetch, and keep each notice's attribution with it.

Data sources and licences

Each source on its own terms.

Every result carries attribution, the credit its source's licence asks for, with licence and licence_url. Where Lucius translated a title or wrote the summary a snippet comes from, the attribution says so. Each response also carries a response-level attribution note.

We read each publisher's licence or terms before charging for the API. Sources whose terms do not allow a paid service to pass their notices on are not returned by the API.

SourceLicence or termsIn the API
TEDTEDCC BY 4.0, © European UnionMetadata, snippet and link
UK Find a TenderUK-FTSOpen Government Licence v3.0Metadata, snippet and link
UK Contracts FinderUK-OfficialOpen Government Licence v3.0Metadata, snippet and link
PCS ScotlandPCS-ScotlandOpen Government Licence v3.0Metadata, snippet and link
SAM.govSAM-GOVPublic domain (U.S. Government work)Metadata, snippet and link
Grants.govGRANTS-GOVGrants.gov API terms of serviceMetadata, snippet and link
World BankWORLD-BANKCC BY 4.0 (World Bank Dataset Terms)Metadata, snippet and link
CanadaBuysCanadaBuysOpen Government Licence - Canada 2.0Metadata, snippet and link
Ireland eTendersIE-eTendersCC BY 4.0 for notice metadata (data.gov.ie); no terms stated for notice textMetadata and link, no snippet
AusTenderAusTender-ATMApproach to market notices are outside its CC BY 3.0 AU licenceNot returned by the API
NZ GETSNZ-GETSGETS terms and conditionsNot returned by the API
GeBIZGeBIZGeBIZ terms of useNot returned by the API
India CPPPIN-CPPPCPPP copyright policyNot returned by the API
UNGMUNGMUNGM terms of useNot returned by the API

Get a key

Make a key in your Lucius workspace.

  1. Sign in to Lucius, or create a free account.
  2. Open Settings, then Agent API. Keys and credit are for workspace owners and admins.
  3. Name the key and copy it. It is shown once.
  4. Add it to your MCP client or your REST calls as above. The free requests start at once; add credit when you need more.

Questions, or a use case the limits above do not fit? Email davor@ailucius.com.