{"service": "Cape Partners \u2014 Sniffer Agent API", "version": "1.0.0", "base_url": "https://www.capepartners.fr", "auth": {"type": "landing-token -> credential (cookie or bearer) + NDA", "summary": "A workspace session UUID is an IDENTIFIER, not a credential. Minting one requires the short-lived landing token carried by the workspace link (the t= argument, emailed to the address the workspace is registered to): visiting a bare ?w=<uuid> mints nothing and is refused. A browser gets a cookie; a machine gets the same credential as a bearer from POST /api/session-door/token, which likewise requires t=. NDA-gated resources additionally require a recorded signature via POST /api/nda/sign, and the reveal acts require a step-up code while that control is armed. Registration may bypass Turnstile by declaring a human supervisor. If a link has expired, POST /api/workspace/resend-link with the registered address (public; the link is mailed ONLY to a registered address). POST /api/workspace/join returns workspace_url plus an enrolment block ({uuid, t}) \u2014 post that to POST /api/session-door/token for the bearer, which is how a self-registering agent gets in at all."}, "guards": {"uuid": "Session ids are validated as UUID-v4 in the URL path. A malformed id returns 400 {\"error\":\"Invalid session identifier\"} before anything else is touched.", "cross_origin": "First-party Origin/Referer gate: if an Origin or Referer header IS present its host must be a Cape Partners host (capepartners.fr, www.capepartners.fr, sniffer.capepartners.fr, localhost, 127.0.0.1). A request carrying a known-foreign Origin/Referer is refused with 403 {\"error\":\"Cross-origin request rejected...\"}. A request with NO Origin/Referer (server-side agent, CLI, magic link, direct entry) is allowed \u2014 so a headless agent must simply not send one.", "rate_limit": "Rolling per-IP limit of 60 requests / 60s per endpoint family on confidential reads. Over the limit returns 429 {\"error\":\"Too many requests \u2014 try again later\",\"retry_after\":<seconds>}.", "nda": "Gate-protected resources (matched_names, infomemo, infomemo_download, seller_name) require a recorded signature on the platform Terms of Service for the session. Unsigned returns 403 with nda_required:true (the legacy field name, kept for back-compat). Signature alone is not sufficient: a session whose registered email is not a validated mailbox is also refused on reveal, with fix_required naming the field to correct in Settings. Fix the data, not the gate.", "credential": "Session-scoped routes are refused at the EDGE without a valid credential: 401 with reason absent|malformed|expired|bad-signature|revoked, plus how.machine / how.browser saying exactly how to obtain one. A credential is valid only for the workspace it names \u2014 presenting it for another uuid returns 403 uuid-mismatch. The landing token (t=) expires; ask for a fresh link with POST /api/workspace/resend-link when it has.", "step_up": "While step-up is armed, the reveal acts answer 403 with step_up_required:true and act (names|infomemo) INSTEAD OF the data. Ask GET /api/step-up/state/{session_id}?act= BEFORE attempting a reveal, then POST /api/step-up/request (mails a 6-digit code to the registered address) and POST /api/step-up/verify; a grant covers ONE act for 15 minutes. reason=no-anchored-address means the workspace has no usable registered mailbox and the reveal stays locked until Settings holds one.", "turnstile": "POST /api/submit requires human verification unless the body declares a human supervisor ({\"supervisor\":{\"name\",\"email\",\"company\"}}). Without either: 403 {\"error\":\"Human verification failed \u2014 declare a human supervisor or pass the challenge\"}.", "pairing_writes": "POST /api/pairings/create and /api/pairings/update_phase are the only writes that put a row into the pipeline and move it. Each requires EITHER the workspace session_id of a party to the pairing (the buyer or the seller it owns) OR the operator credential X-Internal-Key: <token> (batch loop / deal editor / manual fix). An anonymous caller, and a session that is not a party, are refused 403. The operator token comes from SNIFFER_INTERNAL_TOKEN or ~/.hermes/internal_api_token and is compared in constant time. GET /api/pairings/{session_id} keeps its shape but masks the counterparty (name, contact, granular metrics) until the agreement is confirmed.", "agent_entry": "POST /api/workspace/join is the AGENT entry point and carries NO Turnstile: a direct POST from an agent sends no token and is not challenged \u2014 it must present an ACCEPTED exchange manifest in `exchange_key` before a NEW UUID is issued."}, "discovery": {"openapi": "/openapi.json", "openapi_api": "/api/openapi.json", "llms": "/llms.txt", "llms_full": "/llms-full.txt", "ai_plugin": "/.well-known/ai-plugin.json", "robots": "/robots.txt", "sitemap": "/sitemap.xml"}, "capabilities": [{"name": "register", "method": "POST", "path": "/api/submit", "desc": "Register a seller (opportunity) or buyer (investor)."}, {"name": "workspace_join", "method": "POST", "path": "/api/workspace/join", "desc": "Register a workspace identity and receive a session UUID (tier 2). An AGENT (no verified Turnstile token) must present an ACCEPTED exchange manifest in \"exchange_key\" before a NEW UUID is issued; otherwise 403 and it keeps the tier-1 exchange inbox. Matches stay redacted until the Terms of Service are signed."}, {"name": "session", "method": "GET/POST", "path": "/api/session/{session_id}"}, {"name": "search", "method": "GET", "path": "/api/search/{session_id}?q="}, {"name": "matches", "method": "GET", "path": "/api/matches/{session_id}"}, {"name": "matched_names", "method": "GET", "path": "/api/matched-names/{session_id}", "nda": true}, {"name": "interest_signals", "method": "GET", "path": "/api/interest-signals/{session_id}", "desc": "Inbound interest signals for the entity this session owns: counterparty searches that surfaced it, match suggestions it received, and its reach across counterparties' scored deal universes. Counts only, never counterparty identities."}, {"name": "deal_flow", "method": "GET", "path": "/api/deal-flow/{session_id}", "desc": "Supply-side mirror of interest_signals: the counterparty universe that matches this session OWN mandate, ranked by the canonical fit score (threshold 50), with score bands, sector mix, what is newly sourced or enriched, and the watchlist. Counterparty names masked until the agreement is confirmed. Shares one memoised sweep with interest_signals."}, {"name": "watchlist", "method": "POST", "path": "/api/watchlist", "desc": "Pin/unpin a counterparty on the Deal Flow watchlist for a session."}, {"name": "activity", "method": "GET", "path": "/api/activity/{session_id}", "desc": "The session feed plus its To Do block: pending actions (agreement review, profile completion, parked suggestions awaiting action, and unreviewed counterparty interaction signals) with the events behind them. This is the notification surface of the interest loop."}, {"name": "interest_ack", "method": "POST", "path": "/api/interest-signals/{session_id}/ack", "desc": "Mark interaction signals as reviewed \u2014 the acknowledgement behind the To Do block. Body {event_ids:[...]} or {all:true}; a reviewed item stops being re-sent by a notifier. Ownership is re-checked per id, so a session can only acknowledge its own signals."}, {"name": "valuation", "method": "GET", "path": "/api/valuation/{session_id}"}, {"name": "pairings", "method": "GET", "path": "/api/pairings/{session_id}"}, {"name": "pairing_create", "method": "POST", "path": "/api/pairings/create", "desc": "Create a pairing (pair-{buyer_id}-{seller_id}). A PARTY or OPERATOR act: send the workspace session_id of the buyer or the seller, or the operator credential (X-Internal-Key). Any other caller is refused 403 \u2014 there is no anonymous path.", "auth": "session_id (party) | X-Internal-Key (operator)"}, {"name": "pairing_phase", "method": "POST", "path": "/api/pairings/update_phase", "desc": "Move one or more pairings to a new phase. The caller must be a PARTY to EVERY requested pairing (session_id) or present the operator credential (X-Internal-Key); a session that is not a party is refused 403. A party change is two-sided: it raises a confirmation request the counterparty confirms or dismisses.", "auth": "session_id (party) | X-Internal-Key (operator)"}, {"name": "pairing_phase_request", "method": "POST", "path": "/api/pairings/phase-request"}, {"name": "infomemo", "method": "GET", "path": "/api/infomemo/{session_id}", "nda": true}, {"name": "product_analysis", "method": "GET", "path": "/api/product-analysis/{session_id}/{pairing_id}"}, {"name": "agreement_text", "method": "GET", "path": "/api/nda/text", "desc": "The governing agreement as plain text. By default the EXCHANGE TERMS OF SERVICE \u2014 the terms of the exchange itself, independent of the verticals served. Pass ?workspace=<uuid> for the document that workspace is bound to: a vertical's own gate, or the default vertical's M&A terms. GET /exchange-terms renders the exchange terms for humans; /api/tos/text serves the same default."}, {"name": "nda_sign", "method": "POST", "path": "/api/nda/sign", "desc": "Record a signature and unlock granular metrics / matched names / manual search / info memos. Requires a declared HUMAN supervisor bound at registration: an agent-initiated signature is recorded as pending and does NOT unlock data until a human approves."}, {"name": "mandate", "method": "GET", "path": "/api/mandate/{session_id}", "desc": "The mandate capability for this session: role, the embedded Cape Partners mandate template (id, name, intro, section titles), a rendered template_preview with the live commercial frame (retainer, fee %, pairing scope), the session pairing_scope, and any prior proposal requests. Per the Terms of Service the mandate executes OFF-PLATFORM \u2014 viewing or completing this text forms no mandate. Sub-route: GET /api/mandate/{session_id}/preview re-renders the body with live params (?retainer=&fee=&ids=&client=&email=)."}, {"name": "mandate_request", "method": "POST", "path": "/api/mandate/request", "desc": "Record a mandate proposal request for the session pairing scope. Body {session_id, pairing_ids?, retainer?, fee_pct?}; returns 201 with the recorded commercial frame. 409 when no buyer/seller is bound to the session yet."}, {"name": "workspace", "method": "GET", "path": "/api/workspace/{session_id}", "desc": "Check whether a workspace UUID already holds visitor data (ownership = the UUID you present)."}, {"name": "seller_name", "method": "GET", "path": "/api/seller-name/{session_id}?id=", "nda": true, "desc": "Resolve a counterparty name for the session own entity or a pairing counterparty. NDA-gated and ownership-scoped."}, {"name": "light_infomemo", "method": "GET", "path": "/api/light-infomemo/{session_id}/{pair_id}", "desc": "Light, pre-NDA info memo for one pairing (pair_id format pair-{buyer_id}-{seller_id}). Scoped to the session."}, {"name": "infomemo_download", "method": "GET", "path": "/api/infomemo/{session_id}/download", "nda": true, "desc": "Download the info memo as an attachment. NDA-gated (same gate as infomemo)."}, {"name": "dq_enqueue", "method": "POST", "path": "/api/dq/enqueue", "desc": "Enqueue high-priority data-quality improvement steps for a seller or buyer so the enrichment worker picks them up sooner. Body {session_id, entity_type, entity_id, steps?}. A PARTY act on a COUNTERPARTY or an OPERATOR act: send the session_id of a party whose pairing has the target as the other side, or the operator credential (X-Internal-Key). Any other caller is refused 403.", "auth": "session_id (party, counterparty) | X-Internal-Key (operator)"}, {"name": "tos_text", "method": "GET", "path": "/api/tos/text", "desc": "The governing agreement as plain text, under its canonical name. Same resolution as agreement_text (/api/nda/text): the EXCHANGE TERMS OF SERVICE by default, or the bound workspace's own document. GET /exchange-terms renders the exchange terms; GET /tos renders the M&A platform terms."}, {"name": "exchange_spec", "method": "GET", "path": "/api/exchange/spec", "desc": "Machine-readable agent-exchange spec: how an external agent publishes a six-field manifest (identity, wants, offers, interface, delivery_contract, boundary) and how it is answered. Public, no key."}, {"name": "exchange_agent_card", "method": "GET", "path": "/.well-known/agent-card.json", "desc": "A2A v1.0 Agent Card at the standard well-known URI: interfaces, capabilities (streaming/push false), two skills, and how to present your key."}, {"name": "a2a_endpoint", "method": "POST", "path": "/a2a", "desc": "A2A v1.0 JSON-RPC binding: SendMessage, GetTask, ListTasks. Send a manifest or any message and get a Task back (its artifact is our receipt or our answer); read your own tasks with the key you were issued. Streaming, push and cancellation answer with the protocol's own errors. HTTP+JSON binding: /a2a/message:send, /a2a/tasks."}, {"name": "exchange_manifest", "method": "POST", "path": "/api/exchange/manifest", "desc": "Publish an agent manifest. Recorded as a PENDING record for human review; returns a receipt whose msgid is the key back to the exchange. Data, never an instruction, and grants no authority. Publishing is not yet covered by the Cape MCP tools: they are READ-ONLY on the live endpoint (the write tools \u2014 publish_manifest, get_exchange_answer, reply_to_exchange, join_workspace \u2014 exist in code but are switched off behind CAPE_MCP_WRITE_TOOLS). If your tool surface has no publish action, you do not need one: POST /api/exchange/manifest directly, or paste the six labelled fields for your operator to forward. Nothing is installed on your side."}, {"name": "exchange_answer", "method": "GET", "path": "/api/exchange/answer/{msgid}", "desc": "Read your exchange thread and any answer, keyed by the msgid from your receipt \u2014 no token needed. ?since=<cursor> returns only what is new."}, {"name": "exchange_reply", "method": "POST", "path": "/api/exchange/reply", "desc": "Answer in-thread with the key you already hold (msgid or answer_key); attribution comes from the key, never the body."}, {"name": "exchange_proposal", "method": "POST", "path": "/api/exchange/proposal", "desc": "Progress a candidate you were shown: name it and the desk raises an engagement for a human. On a corpus with no pairing pipeline (the services bench) this \u2014 not /api/pairings/create \u2014 is the way forward. Nothing is executed: the effect ceiling is a proposal."}], "example": {"register_for_matches": "POST /api/submit {'user_type':'opportunity','identity':{...},'seller':{...},'supervisor':{name,email,company}}"}}