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.
| Tool | REST | Inputs | Returns |
|---|---|---|---|
| search_tenders | GET /api/v1/ext/tenders/search | query, countries (ISO codes, comma separated), source, cpv_prefix, closing_within_days, limit, cursor | results with notice metadata, a snippet and a relevance score; mode; next_cursor for the next page |
| get_tender | GET /api/v1/ext/tenders/{id} | id | one notice: metadata, a snippet, and the documents and links its source lists for it |
| similar_tenders | GET /api/v1/ext/tenders/{id}/similar | id, countries, limit | notices 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_cursorisnull. To see more of a market, narrow the search withcountries,cpv_prefixorclosing_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 answers503and the request is not counted or charged. An emptyresultslist 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 tool | REST | Inputs | Returns |
|---|---|---|---|
| REST only | POST /api/v1/ext/packs | files: multipart/form-data, one files part per document, or one ZIP | 202 with analysis_id, status, deduplicated, status_url, app_url |
| analyze_pack | POST /api/v1/ext/packs/url | url: a public http(s) link to a tender document (PDF, Word, Excel or ZIP) or to a notice page that links to the documents | 202 with analysis_id, status, deduplicated, status_url, app_url |
| get_pack_analysis | GET /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
documentsays what was read. Whenis_tenderis false (a bid somebody wrote, a signed contract, something else, or a document Lucius could not classify),verdictis null, no requirement is deciding anddocument.notesays why.verdictis the bid call, or null when the analysis made none.callis Bid, Bid with caveats or Skip, or Closed withclosed_onwhen the deadline has passed;recommendationis GO, NO GO or CAUTION. When the workspace has not described its company, Lucius makes no call:call,recommendationandconfidenceare null,profile_incompleteis true,deciding_requirementslists the pass/fail gates,open_requirementscounts the ones still unanswered andnotesays what to do.staleis true when the company details changed after the call was made.coveragesays how the pack was read.completeis true only when every file got the full read.noteslists 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.filessays for each file of a pack whether it was read infull, astext, inpartialornot_read; it is null for a single document with nothing left out. Whilesecond_readis pending, a second read is still adding requirements andrequirements_finalis false: read the result again in a few minutes.- Each requirement carries
original_text, quoted from the pack in its own language, anddocument,pageandcitation.page_is_approximateis true where the page is estimated, as it is in Word files.quote_verifiedis true when the quote was found in the pack text.severityis CRITICAL, HIGH, MEDIUM or LOW, anddecidingis 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). Passrequirements_next_offsetasrequirements_offsetfor the next page; it is null on the last.deciding_only=truereturns 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_lockedis true andlocked_reasonsays how to unlock the rest in the app, as in the app itself. formslists 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
filespart 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://orhttps://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 toPOST /api/v1/ext/packsinstead.
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
403and 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_tenderorsimilar_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_analysisreturns 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
402with 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_tenderalso 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.
| Source | Licence or terms | In the API |
|---|---|---|
| TEDTED | CC BY 4.0, © European Union | Metadata, snippet and link |
| UK Find a TenderUK-FTS | Open Government Licence v3.0 | Metadata, snippet and link |
| UK Contracts FinderUK-Official | Open Government Licence v3.0 | Metadata, snippet and link |
| PCS ScotlandPCS-Scotland | Open Government Licence v3.0 | Metadata, snippet and link |
| SAM.govSAM-GOV | Public domain (U.S. Government work) | Metadata, snippet and link |
| Grants.govGRANTS-GOV | Grants.gov API terms of service | Metadata, snippet and link |
| World BankWORLD-BANK | CC BY 4.0 (World Bank Dataset Terms) | Metadata, snippet and link |
| CanadaBuysCanadaBuys | Open Government Licence - Canada 2.0 | Metadata, snippet and link |
| Ireland eTendersIE-eTenders | CC BY 4.0 for notice metadata (data.gov.ie); no terms stated for notice text | Metadata and link, no snippet |
| AusTenderAusTender-ATM | Approach to market notices are outside its CC BY 3.0 AU licence | Not returned by the API |
| NZ GETSNZ-GETS | GETS terms and conditions | Not returned by the API |
| GeBIZGeBIZ | GeBIZ terms of use | Not returned by the API |
| India CPPPIN-CPPP | CPPP copyright policy | Not returned by the API |
| UNGMUNGM | UNGM terms of use | Not returned by the API |
Get a key
Make a key in your Lucius workspace.
- Sign in to Lucius, or create a free account.
- Open Settings, then Agent API. Keys and credit are for workspace owners and admins.
- Name the key and copy it. It is shown once.
- 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.