For agents
Everything a member can do in WayWiser is an HTTP route on the api, and every route accepts an API key — so Claude Code, an MCP client or a script can find the tender, analyse the pack, draft and review answers, and report on the pipeline. Outputs are decision support with citations, never advice; evaluator output is a review, never a score forecast.
Last updated 16 September 2026
API keys
An API key is a second credential beside the sign-in session. A company admin mints one under Company → API keys for any member of the company. It acts as that member — every audit row names the person and says it was their key — with a role ceiling of member or admin that is never higher than the member’s own role, optionally read-only, expiring after 90 days by default and 365 at most. It can be revoked at any time and stops within the same second; what the key may do (the member’s role and grants) is cached for up to 30 s per instance, cleared at once by the product’s own membership routes.
The secret is shown once, on that page, and never stored — only its SHA-256 is. It looks like ww_live_ followed by 43 URL-safe characters.
Authentication and the first call
Send the secret as a bearer token, and call GET /me first: it returns who the key acts as, activeCompanyId, role, and actor — the key’s id, name, read-only flag and expiry. Under a key, companies lists the pinned company only, whatever else the person belongs to.
curl -s https://waywiser-api-zxgq66afoq-nw.a.run.app/me -H 'Authorization: Bearer ww_live_…'One key, one company
A key is pinned to the company it was minted in. There is nothing to switch: every row you read or write is that company’s, and another company’s rows answer 404 exactly as if they did not exist. Leave the browser’s X-Company-Id header out — naming another company is a 400 company_mismatch.
What a key can never do
The identity and tenancy plane is for people, whatever the key’s ceiling: members and invitations, corpus grants, keys themselves, the workspace’s deletion and export, and the whole staff plane. Each answers 403 human_only_route and none appears in the reference. Company configuration — the profile, settings, the export logo, corpora — stays open to an admin-ceiling key.
Rate limits and 429s
Limits are per key (an agent never eats its person’s browser quota, nor the reverse), per api instance, per minute. A refusal is 429 with a Retry-After header and retryAfterSeconds in the body; wait that long and retry the same call. Once the company’s daily AI token ceiling is spent, paid routes answer 429 ai_budget_exceeded — back off for an hour.
| Bucket | Routes | Limit |
|---|---|---|
global | every route | 300 requests per minute per key |
ai | analyse, draft, review, fix, tighten, redraft, batch email/reply, interview, incumbent search, reingest, regrade | 10 per minute per key, plus the company’s daily AI token ceiling |
heavy | plan, fast path, export, evidence refresh | 10 per minute per key |
upload | the raw-bytes uploads, document registration, pursue | 30 per minute per key |
mcp | POST /mcp — the envelope, on top of each tool’s route bucket | 240 per minute per key |
search | POST /search | 60 per minute per key |
Long-running work: 202 and operation
Anything that parses a pack or calls a model runs as a background job. The route answers 202 at once and the body carries an operation pointer — the kind of work, the id to poll and the GET path that reports it. From POST /questions/:id/draft:
{ "status": "queued", "alreadyQueued": false,
"operation": { "kind": "draft", "targetId": "<questionId>",
"poll": "/activity?target=<questionId>" } }POST /tenders/:id/extract answers with the extraction row itself (id, status) plus the pointer, and no alreadyQueued.
GET that path until state is done or failed. Extractions, documents, drafts, reviews and clarification drafts poll through /activity?target=, which returns the one row with its state, error and an honest progress (real stages, or elapsed against the observed typical duration — never a percentage). Other kinds name the object’s own status route. Poll every 3–5 s for the first minute, then every 15–30 s: an extraction typically takes 4–8 minutes, a draft or review under a minute. A repeat of the same POST while the job is in flight returns the same 202 with alreadyQueued: true — one paid job, never two — except extract, clarification redraft and reingest, which answer 409 conflict while a run is in flight: do not retry unchanged; poll the run you already hold. A 503 queue_unavailable from the enqueuing POST means nothing was queued — retry that POST later.
Uploads
Uploads are raw bytes, not multipart: the file as the body with Content-Type: application/octet-stream, the percent-encoded name in X-Filename. The extension decides the parser (.pdf, .docx, .xlsx, .pptx, .odt, .ods; the facts bank takes .pdf and .docx only), the bytes are sniffed against it, and 60 MiB per file is the ceiling.
curl -s -X POST https://waywiser-api-zxgq66afoq-nw.a.run.app/tenders/<tenderId>/documents/upload \
-H 'Authorization: Bearer ww_live_…' \
-H 'Content-Type: application/octet-stream' \
-H 'X-Filename: ITT%20pack.pdf' \
--data-binary @ITT-pack.pdfIdempotency: natural keys, no Idempotency-Key header
Every POST a person could repeat is idempotent at the api by the action’s natural identity, so an agent that times out simply retries the same call. Creates dedupe by content and return the existing row with 200 (a tender by title and buyer within a minute, a corpus by name, a fact by type, label and value, a draft block by text and first citation, a comment by author, body and thread, a clarification by topic); enqueues dedupe by the row’s own marker; state transitions keep the first stamp; a block move carries the position you saw. Raw-byte uploads are the one exception — the bytes are the identity. Each operation in the reference states its rule as x-waywiser-idempotency.
Errors
Every non-2xx body is { error, code, issues?, hint?, retryAfterSeconds? }. error is the sentence a person sees; code is a closed vocabulary and is what to branch on.
| Code | Status | What it means |
|---|---|---|
validation_failed | 400 | A body, query or header failed validation; issues[] names each path. |
unauthenticated | 401 | No usable credential: header missing, secret unknown, or a bad token. |
forbidden | 403 | Known identity, not allowed — or the route wants an admin-ceiling key. |
no_active_company | 403 | The member the key acts as has no active company. |
read_only_key | 403 | The key is read-only and the request writes. |
human_only_route | 403 | The identity/tenancy plane — a person does this in the web app. |
company_mismatch | 400 | X-Company-Id names another company. Drop the header. |
api_key_required | 403 | The MCP endpoint takes an API key only; a signed-in session is refused. |
key_revoked | 401 | The key was revoked. |
key_expired | 401 | The key passed its expiry. |
key_membership_gone | 401 | The member the key acts as left the company. |
not_found | 404 | No such row in this company (another company’s rows look the same). |
conflict | 409 | Not in the right state; the message says what to do. Do not retry unchanged. |
payload_too_large | 413 | JSON bodies are capped at 4 MiB, uploads at 60 MiB per file. |
unsupported_file | 415 / 422 | The upload’s kind or bytes are not what the name claims. |
rate_limited | 429 | A per-key bucket is spent; wait retryAfterSeconds, then retry. |
ai_budget_exceeded | 429 | The company’s daily AI token ceiling is spent; back off an hour. |
queue_unavailable | 503 | The job queue is down; nothing was queued. Retry later. |
service_unavailable | 503 | A dependency the route needs is not reachable. Retry later. |
internal | 5xx | Ours. Retry once, then report it. |
The workflows
- Qualify a notice → a bid.
GET /notices→ pursue one (orPOST /tenders) → upload the ITT pack →POST /tenders/:id/extract(202) → go/no-go prefill, run and interview →POST /gng/:id/decide(ask the person) →POST /tenders/:id/plan→ per question: workspace, evidence, draft (202), review (202), fix flagged claims → readiness → export → submit (ask the person; never setforceyourself) → outcome. - Maintain the library. Corpora, documents, uploads, re-ingest;
POST /searchto see what the drafts are grounded in. - Maintain the facts bank. Facts and graded uploads; expired facts are marked and never enter a draft.
- Clarifications. Log → batch into one email (202) → mark sent → paste the reply (202) → record the answers.
- Report on the pipeline.
GET /tenders,GET /tenders/:id/page, the action log,GET /activity, benchmarks and the cost floor.
Quick starts
curl
export WAYWISER_API_KEY=ww_live_…
curl -s https://waywiser-api-zxgq66afoq-nw.a.run.app/me -H "Authorization: Bearer $WAYWISER_API_KEY"
curl -s "https://waywiser-api-zxgq66afoq-nw.a.run.app/tenders?stage=live" -H "Authorization: Bearer $WAYWISER_API_KEY"
curl -s -X POST https://waywiser-api-zxgq66afoq-nw.a.run.app/tenders/<tenderId>/extract -H "Authorization: Bearer $WAYWISER_API_KEY"
curl -s "https://waywiser-api-zxgq66afoq-nw.a.run.app/activity?target=<extractionId>" -H "Authorization: Bearer $WAYWISER_API_KEY"Python
import os, time, requests
API = "https://waywiser-api-zxgq66afoq-nw.a.run.app"
s = requests.Session()
s.headers["Authorization"] = f"Bearer {os.environ['WAYWISER_API_KEY']}"
tender_id = s.get(f"{API}/tenders", params={"stage": "live"}).json()["items"][0]["id"]
r = s.post(f"{API}/tenders/{tender_id}/extract")
if r.status_code == 429:
time.sleep(r.json()["retryAfterSeconds"]); r = s.post(f"{API}/tenders/{tender_id}/extract")
r.raise_for_status()
op = r.json()["operation"]
while True:
item = s.get(f"{API}{op['poll']}").json()["items"][0]
if item["state"] in ("done", "failed"):
break
time.sleep(5)Claude Code (MCP)
The same key drives an MCP server (Streamable HTTP, stateless) at POST /mcp: a curated set of tools — the pipeline, notices, tender documents and analysis, questions and drafting, clarifications, go/no-go, the facts bank, the library and search, export and outcome, activity — each of which is one of the routes above called in-process with your key, so the company pinning, the role ceiling, read-only keys, rate limits, the AI budget and the audit trail are exactly the ones this page describes. A tool call a route refuses carries the same error body as REST, flagged isError; an unknown tool or arguments its schema rejects are JSON-RPC errors from the MCP SDK, without a code. One JSON-RPC message per POST — a batch array is refused 400 — and the envelope has a bucket of its own, 240 calls per minute per key. The endpoint takes a key only: a signed-in session is refused with api_key_required.
claude mcp add --transport http waywiser https://waywiser-api-zxgq66afoq-nw.a.run.app/mcp \
--header "Authorization: Bearer ww_live_…"Or, checked into a project without the secret, .mcp.json with the key in the environment:
{
"mcpServers": {
"waywiser": {
"type": "http",
"url": "https://waywiser-api-zxgq66afoq-nw.a.run.app/mcp",
"headers": { "Authorization": "Bearer ${WAYWISER_API_KEY}" }
}
}
}Start with whoami — it also lists the tools a read-only key is refused on: every tool whose route is not a GET, search-knowledge and export-tender included. Tools that queue work return operation to poll with get-operation; lists page by cursor; uploads are base64 and capped at 10 MB decoded (the refusal names the curl one-liner for anything bigger); export-tender returns the .docx as an embedded resource; decide-go-no-go and submit-tender are the person’s decision. The full tool table is in the served guide at https://waywiser-api-zxgq66afoq-nw.a.run.app/docs/agents.md (§12).
Reference
- OpenAPI 3.1 — every operation with its request schema (the same validation the route runs), responses and the async / idempotency / rate-limit extensions. Any OpenAPI client generator works against it.
- The guide as markdown — what an agent reads; this page is the same text for a person.
- llms.txt — the short index.
- Security — how the data behind these routes is protected.