{"openapi":"3.1.0","info":{"title":"tunnel public API","version":"1.0.0","summary":"Search verified businesses, check what their verification actually proves, and message them.","description":"The public, agent-facing surface of the tunnel knowledge base.\n\nReading needs no credentials, and neither does ASKING. Only messaging a business does, and\nregistration is self-service.\n\nIf you want to know something about a business, try `ask` before `landline`. A question the\nregistry can settle costs you nothing, costs the business nothing, and is answered immediately;\nopening a conversation puts it in front of a person who may be serving a customer.\n\nTwo things worth knowing before you build against this:\n\n1. An absent field means we do not hold that information. It does not mean the business lacks it.\n   Do not write a confident sentence about a business from a gap in this data.\n2. `verification.level` is the verification answer. `humanReview` is about whether a person edited\n   the knowledge-base entry, which is a different question and is not a statement about the business.\n\nEndpoints not described here are internal and may change without notice.","contact":{"name":"Tunnel","url":"https://tunnelpowered.com/docs","email":"contact@tunnelpowered.com"}},"servers":[{"url":"https://api.tunnelpowered.com","description":"Production"}],"tags":[{"name":"knowledge-base","description":"Find and read business records. No credentials."},{"name":"ask","description":"Put a question to a listing and get an answer from stored facts, or an escalation to a person. No credentials. Try this before the landline: it is free to you and costs the business nothing."},{"name":"landline","description":"Reach the person behind a listing. Requires a registered agent."},{"name":"trust","description":"Verification status, signed attestations and the transparency log."},{"name":"identity","description":"Register an agent and get a token."},{"name":"mcp","description":"The Model Context Protocol endpoint."},{"name":"a2a","description":"The Agent2Agent v1.0 endpoint. Read-only."},{"name":"discovery","description":"Machine-readable descriptions of this service."}],"components":{"schemas":{"Error":{"type":"object","description":"A refusal an agent can act on. Branch on nothing here — the REST surface returns a human-readable message only. The MCP surface (`POST /api/mcp`) is the one that returns stable machine codes and a `fix`; prefer it if you need to handle failures programmatically.","properties":{"error":{"type":"string"}}},"HumanReview":{"type":"object","description":"Whether a person at tunnel edited this KB entry by hand. This is a statement about the RECORD, not about the business — it is not the verification status. Read `verification.level` for that.","required":["reviewed"],"properties":{"reviewed":{"type":"boolean"},"reviewedAt":{"type":["string","null"],"format":"date-time"},"reviewedBy":{"type":"string"}}},"Verification":{"type":"object","description":"Present on entity records. Absent on website records, which are not verified subjects.","required":["level","status","checkUrl"],"properties":{"level":{"type":["string","null"],"enum":["human","automated",null],"description":"`human` — a tunnel employee checked legal identity, channel control and that the service is real, and a second employee approved it. `automated` — machines confirmed the merchant controls the channels this record cites, and nothing more. `null` — not verified. Read this field; do not infer verification from the presence of a badge."},"status":{"type":"string","enum":["verified_by_human","verified_automated","unverified"]},"limitations":{"type":"array","items":{"type":"string"},"description":"Present when `level` is `automated`. States what the check did NOT establish. The same array is inside the signed attestation, so it cannot be stripped in transit without breaking the signature."},"checkUrl":{"type":"string","format":"uri","description":"Fetch this before a high-stakes action. Revocations propagate faster than caches expire."}}},"RecordSummary":{"type":"object","required":["kind","slug","name","entityType","humanReview","url"],"properties":{"kind":{"type":"string","enum":["entity","website"]},"slug":{"type":"string"},"name":{"type":"string"},"entityType":{"type":"string","enum":["company","merchant","organization","person","other","website"]},"description":{"type":["string","null"]},"humanReview":{"$ref":"#/components/schemas/HumanReview"},"verification":{"$ref":"#/components/schemas/Verification"},"url":{"type":"string","format":"uri"}}},"EntityDetail":{"allOf":[{"$ref":"#/components/schemas/RecordSummary"},{"type":"object","properties":{"identity":{"type":"object","description":"Address, city, region, country, phone, email — whichever the merchant supplied. An absent field means we do not hold it, NOT that the business lacks it.","additionalProperties":true},"socialProfiles":{"type":"object","additionalProperties":true},"offerings":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}}}},"faq":{"type":"array","items":{"type":"object","properties":{"question":{"type":"string"},"answer":{"type":"string"}}}},"website":{"type":["object","null"],"description":"Set only when a verified, non-archived website is linked to this entity.","properties":{"url":{"type":"string","format":"uri"},"kbUrl":{"type":"string","format":"uri"},"machineReadable":{"type":"object","additionalProperties":{"type":"string","format":"uri"}}}},"updatedAt":{"type":"string","format":"date-time"}}}]},"WebsiteRecord":{"allOf":[{"$ref":"#/components/schemas/RecordSummary"},{"type":"object","properties":{"websiteUrl":{"type":"string","format":"uri"},"businessIdentity":{"type":"object","additionalProperties":true},"summary":{"type":["string","null"]},"faq":{"type":"array","items":{"type":"object"}},"structuredPricing":{"type":"object"},"generatedAt":{"type":"string","format":"date-time","description":"When the crawl behind this record ran — not when you fetched it."},"machineReadable":{"type":"object","additionalProperties":{"type":"string","format":"uri"}},"note":{"type":"string","description":"Present instead of the knowledge fields when no crawl has completed yet."}}}]},"SearchResults":{"type":"object","required":["query","count","results"],"properties":{"query":{"type":"string"},"count":{"type":"integer"},"results":{"type":"array","items":{"$ref":"#/components/schemas/RecordSummary"}}}},"KbIndex":{"type":"object","required":["name","description","counts","endpoints"],"properties":{"name":{"type":"string"},"description":{"type":"string"},"counts":{"type":"object","properties":{"entities":{"type":"integer"},"websites":{"type":"integer"}}},"endpoints":{"type":"object","additionalProperties":{"type":"string"}}}},"Conversation":{"type":"object","description":"The thread between an agent and the person behind a listing.","properties":{"id":{"type":"integer"},"token":{"type":"string","description":"Returned once, when the conversation is opened. It is the only way to read replies — there is no endpoint that lists your conversations. Store it."},"subject":{"type":"string"},"status":{"type":"string"},"messages":{"type":"array","items":{"type":"object","properties":{"author":{"type":"string","enum":["agent","merchant"]},"body":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}}}}}},"AgentRegistration":{"type":"object","required":["name","operator_contact","platform_type"],"properties":{"name":{"type":"string","description":"The agent or product name a merchant will see."},"operator_contact":{"type":"string","description":"A real inbox belonging to whoever operates this agent. Merchants use it to reach a human."},"platform_type":{"type":"string"}}},"Credentials":{"type":"object","description":"The client secret is returned exactly once, in this response. It is not recoverable.","properties":{"client_id":{"type":"string"},"client_secret":{"type":"string"}}},"Token":{"type":"object","properties":{"access_token":{"type":"string"},"token_type":{"type":"string","enum":["Bearer"]},"expires_in":{"type":"integer"}}},"VerificationStatus":{"type":"object","description":"The live verification answer, with the signed attestation attached. Verify the signature against `/.well-known/jwks.json` rather than trusting this envelope.","properties":{"slug":{"type":"string"},"verification":{"$ref":"#/components/schemas/Verification"},"attestation":{"type":["object","null"]},"issuedAt":{"type":["string","null"],"format":"date-time"},"expiresAt":{"type":["string","null"],"format":"date-time"}}},"Pricing":{"type":"object","required":["currency","interval","plans"],"properties":{"currency":{"type":"string","description":"ISO 4217. EUR — the initial market is Moldova and Romania."},"interval":{"type":"string","enum":["month"]},"plans":{"type":"array","items":{"type":"object","required":["id","amount","currency"],"properties":{"id":{"type":"string"},"amount":{"type":"number"},"currency":{"type":"string"},"summary":{"type":["string","null"]}}}},"crawlerUpgrades":{"type":"array","items":{"type":"object"}},"notes":{"type":"object","description":"Facts that a price alone does not carry — notably that plans are not self-serve today.","additionalProperties":true}}},"TrustLogHead":{"type":"object","description":"The current head of the hash-chained transparency log.","properties":{"size":{"type":"integer"},"hash":{"type":"string"},"updatedAt":{"type":"string","format":"date-time"}}},"MandateAnswer":{"type":"object","description":"What the registry could settle. `resolved: false` is a normal answer and not an error — it means we do not hold the fact, which is a different statement from the business not having it.","properties":{"resolved":{"type":"boolean","description":"Answered from stored facts."},"answer":{"type":["object","null"],"description":"Present when resolved. Carries the value and the `basis` it was decided on."},"reply":{"type":"string","description":"The same answer as a sentence, in the language below."},"language":{"type":"string","description":"What we ANSWERED in — not necessarily what was asked in."},"mandate":{"type":"string","enum":["configured","not-configured"],"description":"Distinguishes \"they set bounds and this is outside them\" from \"nobody has ever configured this listing\". Same non-answer, different fix, and only one is worth escalating."},"escalation":{"type":["object","null"],"description":"Present when a person was asked. Carries the `ref` to poll.","properties":{"ref":{"type":"string"},"repeat":{"type":"boolean"}}},"subject":{"type":"object","properties":{"kind":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"}}}}}},"securitySchemes":{"agentBearer":{"type":"oauth2","description":"Register once, then exchange credentials for a token. Needed only for writes.","flows":{"clientCredentials":{"tokenUrl":"https://api.tunnelpowered.com/api/v1/agents/token","scopes":{}}}}}},"security":[{"agentBearer":[]}],"paths":{"/api/kb":{"get":{"tags":["knowledge-base"],"summary":"What this knowledge base holds, and where everything is","description":"Start here. Returns record counts and a map of every other endpoint.","security":[],"responses":{"200":{"description":"The index.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/KbIndex"}}}},"429":{"description":"Rate limited. `Retry-After` gives the number of seconds to wait. Reads are metered per IP, per declared client name and per registered agent; a declared name never raises the IP ceiling.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/kb/llms.txt":{"get":{"tags":["knowledge-base","discovery"],"summary":"The prose guide a model reads first","security":[],"responses":{"200":{"description":"Markdown guide.","content":{"text/plain":{"schema":{"type":"string"}}}},"429":{"description":"Rate limited. `Retry-After` gives the number of seconds to wait. Reads are metered per IP, per declared client name and per registered agent; a declared name never raises the IP ceiling.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/kb/search":{"get":{"tags":["knowledge-base"],"summary":"Find a business by name, topic or place","description":"Words are matched independently against name, location, description, offerings and FAQ, and most of them have to appear somewhere in a record. Accents are folded, so the unaccented spelling works. THIS MATCHES WORDS, NOT MEANINGS: a business calling itself a \"cofetărie\" is not returned for \"bakery\". An empty result is a normal answer, not an error — and it means \"not in this index\", which is not the same as \"not in business\". Omit `q` to list what the index holds.","security":[],"parameters":[{"name":"q","in":"query","schema":{"type":"string"},"description":"Name, topic or place."},{"name":"type","in":"query","schema":{"type":"string","enum":["company","merchant","organization","person","other","website"]}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":50,"default":20}}],"responses":{"200":{"description":"Ranked results.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchResults"}}}},"429":{"description":"Rate limited. `Retry-After` gives the number of seconds to wait. Reads are metered per IP, per declared client name and per registered agent; a declared name never raises the IP ceiling.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/kb/entities/{slug}":{"get":{"tags":["knowledge-base"],"summary":"A full business profile","security":[],"parameters":[{"name":"slug","in":"path","required":true,"description":"The record’s stable slug, as returned by search.","schema":{"type":"string","pattern":"^[a-z0-9-]+$"}},{"name":"format","in":"query","schema":{"type":"string","enum":["md","jsonld"]},"description":"Omit for JSON. `md` returns markdown, `jsonld` returns schema.org JSON-LD."}],"responses":{"200":{"description":"The profile, in the requested representation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntityDetail"}},"text/markdown":{"schema":{"type":"string"}},"application/ld+json":{"schema":{"type":"object"}}}},"404":{"description":"No such record in the knowledge base.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` gives the number of seconds to wait. Reads are metered per IP, per declared client name and per registered agent; a declared name never raises the IP ceiling.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/kb/websites/{slug}":{"get":{"tags":["knowledge-base"],"summary":"What a crawl established about a listed website","description":"Accepts the numeric id as well as the slug.","security":[],"parameters":[{"name":"slug","in":"path","required":true,"description":"The record’s stable slug, as returned by search.","schema":{"type":"string","pattern":"^[a-z0-9-]+$"}}],"responses":{"200":{"description":"The website record.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebsiteRecord"}}}},"404":{"description":"No such record in the knowledge base.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` gives the number of seconds to wait. Reads are metered per IP, per declared client name and per registered agent; a declared name never raises the IP ceiling.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/kb/entities/{slug}/messages":{"post":{"tags":["landline"],"summary":"Message the person behind a listing","description":"Opens a conversation. The message lands in the merchant’s dashboard inbox and they reply when they reply — this is asynchronous, and no response time is promised. Keep the `id` and `token` from the response; without the token the thread is unreadable.","parameters":[{"name":"slug","in":"path","required":true,"description":"The record’s stable slug, as returned by search.","schema":{"type":"string","pattern":"^[a-z0-9-]+$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["agentName","subject","message"],"properties":{"agentName":{"type":"string"},"subject":{"type":"string"},"message":{"type":"string"},"agentContact":{"type":"string"}}}}}},"responses":{"200":{"description":"Conversation opened.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Conversation"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such record in the knowledge base.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` gives the number of seconds to wait. Reads are metered per IP, per declared client name and per registered agent; a declared name never raises the IP ceiling.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/kb/websites/{slug}/messages":{"post":{"tags":["landline"],"summary":"Message the owner of a listed website","parameters":[{"name":"slug","in":"path","required":true,"description":"The record’s stable slug, as returned by search.","schema":{"type":"string","pattern":"^[a-z0-9-]+$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["agentName","subject","message"],"properties":{"agentName":{"type":"string"},"subject":{"type":"string"},"message":{"type":"string"},"agentContact":{"type":"string"}}}}}},"responses":{"200":{"description":"Conversation opened.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Conversation"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such record in the knowledge base.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` gives the number of seconds to wait. Reads are metered per IP, per declared client name and per registered agent; a declared name never raises the IP ceiling.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/kb/conversations/{id}":{"get":{"tags":["landline"],"summary":"Read a conversation and any replies","security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"token","in":"query","required":true,"schema":{"type":"string"},"description":"The token returned when the conversation was opened. It is the authorisation."}],"responses":{"200":{"description":"The thread.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Conversation"}}}},"404":{"description":"No conversation for that id/token pair.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` gives the number of seconds to wait. Reads are metered per IP, per declared client name and per registered agent; a declared name never raises the IP ceiling.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/kb/conversations/{id}/messages":{"post":{"tags":["landline"],"summary":"Follow up in an existing conversation","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token","message"],"properties":{"token":{"type":"string"},"message":{"type":"string"}}}}}},"responses":{"200":{"description":"The updated thread.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Conversation"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No conversation for that id/token pair.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` gives the number of seconds to wait. Reads are metered per IP, per declared client name and per registered agent; a declared name never raises the IP ceiling.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/kb/conversations/{id}/dispute":{"post":{"tags":["landline"],"summary":"Contest how an interaction was recorded","description":"For the agent’s side of a disagreement about what happened in a conversation.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token","reason"],"properties":{"token":{"type":"string"},"reason":{"type":"string"}}}}}},"responses":{"200":{"description":"Dispute recorded.","content":{"application/json":{"schema":{"type":"object","properties":{"disputeId":{"type":"integer"},"disputeStatus":{"type":"string"}}}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` gives the number of seconds to wait. Reads are metered per IP, per declared client name and per registered agent; a declared name never raises the IP ceiling.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/agents/register":{"post":{"tags":["identity"],"summary":"Register an agent","description":"Only needed to WRITE — reading the knowledge base never requires credentials. The client secret is returned once and is not recoverable.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentRegistration"}}}},"responses":{"200":{"description":"Registered.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Credentials"}}}},"429":{"description":"Rate limited. `Retry-After` gives the number of seconds to wait. Reads are metered per IP, per declared client name and per registered agent; a declared name never raises the IP ceiling.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/agents/token":{"post":{"tags":["identity"],"summary":"Exchange credentials for a bearer token (OAuth 2.1 client_credentials)","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["client_id","client_secret"],"properties":{"grant_type":{"type":"string","enum":["client_credentials"]},"client_id":{"type":"string"},"client_secret":{"type":"string"}}}}}},"responses":{"200":{"description":"An access token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Token"}}}},"401":{"description":"Bad credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` gives the number of seconds to wait. Reads are metered per IP, per declared client name and per registered agent; a declared name never raises the IP ceiling.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/merchants/{slug}/verification":{"get":{"tags":["trust"],"summary":"Live verification status and the signed attestation","description":"Treat a cached answer older than five minutes as stale. Revocations are immediate.","security":[],"parameters":[{"name":"slug","in":"path","required":true,"description":"The record’s stable slug, as returned by search.","schema":{"type":"string","pattern":"^[a-z0-9-]+$"}}],"responses":{"200":{"description":"The status.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerificationStatus"}}}},"404":{"description":"No such record in the knowledge base.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` gives the number of seconds to wait. Reads are metered per IP, per declared client name and per registered agent; a declared name never raises the IP ceiling.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/merchants/{slug}/trust-history":{"get":{"tags":["trust"],"summary":"Every verification event for this merchant, in order","security":[],"parameters":[{"name":"slug","in":"path","required":true,"description":"The record’s stable slug, as returned by search.","schema":{"type":"string","pattern":"^[a-z0-9-]+$"}}],"responses":{"200":{"description":"The history.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"No such record in the knowledge base.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` gives the number of seconds to wait. Reads are metered per IP, per declared client name and per registered agent; a declared name never raises the IP ceiling.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/merchants/{slug}/attestation.vc":{"get":{"tags":["trust"],"summary":"The live attestation as a W3C Verifiable Credential (VC-JWT)","description":"Short-lived by design — five minutes. Re-fetch rather than storing it.","security":[],"parameters":[{"name":"slug","in":"path","required":true,"description":"The record’s stable slug, as returned by search.","schema":{"type":"string","pattern":"^[a-z0-9-]+$"}}],"responses":{"200":{"description":"A compact JWS.","content":{"application/jwt":{"schema":{"type":"string"}}}},"404":{"description":"No such record in the knowledge base.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` gives the number of seconds to wait. Reads are metered per IP, per declared client name and per registered agent; a declared name never raises the IP ceiling.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/merchants/{slug}/badge.svg":{"get":{"tags":["trust"],"summary":"A verification badge, for humans","description":"Presentation only. Never treat the badge as the verification answer — read the JSON.","security":[],"parameters":[{"name":"slug","in":"path","required":true,"description":"The record’s stable slug, as returned by search.","schema":{"type":"string","pattern":"^[a-z0-9-]+$"}}],"responses":{"200":{"description":"An SVG.","content":{"image/svg+xml":{"schema":{"type":"string"}}}},"404":{"description":"No such record in the knowledge base.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` gives the number of seconds to wait. Reads are metered per IP, per declared client name and per registered agent; a declared name never raises the IP ceiling.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/trust-log/head":{"get":{"tags":["trust"],"summary":"Current head of the transparency log","security":[],"responses":{"200":{"description":"The head.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrustLogHead"}}}},"429":{"description":"Rate limited. `Retry-After` gives the number of seconds to wait. Reads are metered per IP, per declared client name and per registered agent; a declared name never raises the IP ceiling.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/trust-log/checkpoint":{"get":{"tags":["trust"],"summary":"The latest operator-signed checkpoint","security":[],"responses":{"200":{"description":"A signed checkpoint.","content":{"application/json":{"schema":{"type":"object"}}}},"429":{"description":"Rate limited. `Retry-After` gives the number of seconds to wait. Reads are metered per IP, per declared client name and per registered agent; a declared name never raises the IP ceiling.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/trust-log/checkpoints":{"get":{"tags":["trust"],"summary":"Archived checkpoints, for witnesses","security":[],"responses":{"200":{"description":"Checkpoints.","content":{"application/json":{"schema":{"type":"object"}}}},"429":{"description":"Rate limited. `Retry-After` gives the number of seconds to wait. Reads are metered per IP, per declared client name and per registered agent; a declared name never raises the IP ceiling.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/trust-log/entries":{"get":{"tags":["trust"],"summary":"A range of log entries","security":[],"parameters":[{"name":"from","in":"query","schema":{"type":"integer"}},{"name":"to","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"description":"Entries.","content":{"application/json":{"schema":{"type":"object"}}}},"429":{"description":"Rate limited. `Retry-After` gives the number of seconds to wait. Reads are metered per IP, per declared client name and per registered agent; a declared name never raises the IP ceiling.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/trust-log/entries/{seq}":{"get":{"tags":["trust"],"summary":"One log entry by sequence number","security":[],"parameters":[{"name":"seq","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"The entry.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"No such record in the knowledge base.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` gives the number of seconds to wait. Reads are metered per IP, per declared client name and per registered agent; a declared name never raises the IP ceiling.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/mcp":{"post":{"tags":["mcp"],"summary":"Model Context Protocol endpoint (Streamable HTTP, stateless)","description":"JSON-RPC 2.0. Call `initialize`, then `tools/list`, then `tools/call`. The tool set, argument schemas, response envelope and error codes are described there at runtime rather than duplicated here. Three of the six tools need no credentials. Prefer this surface over REST when you need machine-readable failures: it returns a stable `code` and an imperative `fix`, which the REST endpoints above do not.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["jsonrpc","method"],"properties":{"jsonrpc":{"type":"string","enum":["2.0"]},"id":{"type":["string","integer","null"]},"method":{"type":"string","enum":["initialize","ping","tools/list","tools/call"]},"params":{"type":"object"}}}}}},"responses":{"200":{"description":"A JSON-RPC response.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"A bearer token was supplied and is not valid.","content":{"application/json":{"schema":{"type":"object"}}}},"429":{"description":"Rate limited. `Retry-After` gives the number of seconds to wait. Reads are metered per IP, per declared client name and per registered agent; a declared name never raises the IP ceiling.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/pricing":{"get":{"tags":["discovery"],"summary":"What the plans cost","description":"Generated from the one place prices are defined, so it cannot disagree with the pricing page or the dashboard. `notes.selfServe` is `false` today: paid plans are assigned manually rather than bought online, and these are the prices they are assigned at.","security":[],"responses":{"200":{"description":"The price list.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pricing"}}}},"429":{"description":"Rate limited. `Retry-After` gives the number of seconds to wait. Reads are metered per IP, per declared client name and per registered agent; a declared name never raises the IP ceiling.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/a2a":{"post":{"tags":["a2a"],"summary":"Agent2Agent v1.0 endpoint (JSONRPC binding)","description":"JSON-RPC 2.0. The only implemented method is `SendMessage` — note the v1.0 name; `message/send` was the v0.3 spelling. It answers synchronously and returns a `Message`, never a `Task`, because the three skills are lookups with nothing to poll. `GetTask` and the rest answer -32601 with a reason. Read-only: no A2A skill can commit a business to anything. Skills and shapes are described by the Agent Card.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["jsonrpc","method"],"properties":{"jsonrpc":{"type":"string","enum":["2.0"]},"id":{"type":["string","integer","null"]},"method":{"type":"string","enum":["SendMessage"]},"params":{"type":"object","properties":{"message":{"type":"object","description":"A proto3-JSON A2A Message. A text part is `{\"text\":\"…\"}` — the oneof, not the v0.3 `{\"kind\":\"text\",…}`. Plain text is treated as a search; a data part naming a `skill` selects one explicitly.","properties":{"role":{"type":"string","enum":["ROLE_USER"]},"messageId":{"type":"string"},"contextId":{"type":"string"},"parts":{"type":"array","items":{"type":"object"}}}}}}}}}}},"responses":{"200":{"description":"A JSON-RPC response whose result is `{ \"message\": … }`.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"A bearer token was supplied and is not valid.","content":{"application/json":{"schema":{"type":"object"}}}},"429":{"description":"Rate limited. `Retry-After` gives the number of seconds to wait. Reads are metered per IP, per declared client name and per registered agent; a declared name never raises the IP ceiling.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/a2a/card":{"get":{"tags":["a2a","discovery"],"summary":"The Agent Card, for callers who found the endpoint before the well-known path","security":[],"responses":{"200":{"description":"An A2A v1.0 AgentCard.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/.well-known/agent-card.json":{"get":{"tags":["a2a","discovery"],"summary":"A2A v1.0 Agent Card at the path the spec fixes","description":"Unsigned. Cards MAY carry a JWS over JCS; ours does not yet, and `signatures` is absent rather than empty.","security":[],"responses":{"200":{"description":"An A2A v1.0 AgentCard.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/b/{slug}":{"get":{"tags":["discovery"],"summary":"The citable HTML profile page for a business (alias address)","description":"Server-rendered, with schema.org JSON-LD embedded. The address to CITE is https://tunnelpowered.com/business/{slug}, on the marketing host, and this response names it as canonical. This path is a permanent alias and will keep answering 200 — it was published to agents before the canonical existed, and a documented machine address must not turn into a redirect.","security":[],"parameters":[{"name":"slug","in":"path","required":true,"description":"The record’s stable slug, as returned by search.","schema":{"type":"string","pattern":"^[a-z0-9-]+$"}}],"responses":{"200":{"description":"An HTML page.","content":{"text/html":{"schema":{"type":"string"}}}},"404":{"description":"No such record in the knowledge base.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/business/{slug}":{"get":{"tags":["discovery"],"summary":"The citable HTML profile page for a business","description":"The canonical profile page. In production this path is served on the marketing host (https://tunnelpowered.com/business/{slug}) by an nginx proxy to this process; it also answers here, on the API host, which is deliberate — it lets an operator verify the page before the nginx reload lands, and the canonical tag settles which address counts.","security":[],"parameters":[{"name":"slug","in":"path","required":true,"description":"The record’s stable slug, as returned by search.","schema":{"type":"string","pattern":"^[a-z0-9-]+$"}}],"responses":{"200":{"description":"An HTML page.","content":{"text/html":{"schema":{"type":"string"}}}},"404":{"description":"No such record in the knowledge base.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/llms.txt":{"get":{"tags":["discovery"],"summary":"Platform-level guide for models","security":[],"responses":{"200":{"description":"Markdown.","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/robots.txt":{"get":{"tags":["discovery"],"summary":"Crawler policy and the sitemap pointer","security":[],"responses":{"200":{"description":"Robots rules.","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/api/public/ask":{"post":{"tags":["ask"],"summary":"Ask a structured question and get an answer from stored facts","description":"THE CHEAPEST PATH, and the one to try first. Three questions the registry can settle without a model call and without troubling anybody: `is_open_now`, `delivers_to`, `lead_time`. Free, unauthenticated and rate-limited.\n\nAn answer is not always available, and that is a normal response rather than an error. When the business has not configured the bounds for a question, `resolved` is false — read `mandate` to tell \"they set bounds and this is outside them\" from \"nobody has configured this listing\", because they need different things from you and only one of them is worth escalating.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["kind","slug","intent"],"properties":{"kind":{"type":"string","enum":["entity","website"]},"slug":{"type":"string","description":"From /api/kb/search."},"intent":{"type":"string","enum":["is_open_now","delivers_to","lead_time"]},"params":{"type":"object","description":"Per intent. `delivers_to` requires `place`; the others take none.","properties":{"place":{"type":"string"}}}}}}}},"responses":{"200":{"description":"The answer, or a stated absence of one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MandateAnswer"}}}},"404":{"description":"No such record in the knowledge base.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` gives the number of seconds to wait. Reads are metered per IP, per declared client name and per registered agent; a declared name never raises the IP ceiling.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/public/respond":{"post":{"tags":["ask"],"summary":"Ask in the buyer’s own words","description":"Free text in, an answer in the buyer’s language out. Use this when the question does not fit one of `/public/ask`’s three intents — not instead of it.\n\nCOSTS US A MODEL CALL, which is why it is rate-limited an order of magnitude harder than everything else here (default 10/minute/IP). Four languages are templated — en, ro, ru, de — and anything else is answered in English; the language we answered in is in the response.\n\nWhen the registry cannot settle it, the question is handed to a person and you get an escalation reference. Poll it at `/api/public/escalation/{ref}`. Nothing here binds the business.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["kind","slug","question"],"properties":{"kind":{"type":"string","enum":["entity","website"]},"slug":{"type":"string"},"question":{"type":"string","description":"The buyer’s words, in any language."}}}}}},"responses":{"200":{"description":"An answer, a request for detail, or an escalation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MandateAnswer"}}}},"404":{"description":"No such record in the knowledge base.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` gives the number of seconds to wait. Reads are metered per IP, per declared client name and per registered agent; a declared name never raises the IP ceiling.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/public/escalation/{ref}":{"get":{"tags":["ask"],"summary":"Has a person answered yet?","description":"Polled rather than pushed. The reference IS the credential — it is single-purpose and opens one question — so no registration is needed: an agent that had to register to read an answer would open a conversation instead, which is the expensive path this whole layer exists to avoid.\n\nA merchant may take hours. Poll accordingly; the limit is loose because this costs a row read, and it exists because a tight loop on a four-hour question helps nobody.","security":[],"parameters":[{"name":"ref","in":"path","required":true,"schema":{"type":"string"},"description":"From the escalation in a /public/respond answer."}],"responses":{"200":{"description":"The question, its state, and the answer once there is one.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"No such record in the knowledge base.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` gives the number of seconds to wait. Reads are metered per IP, per declared client name and per registered agent; a declared name never raises the IP ceiling.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/public/tools":{"get":{"tags":["ask","discovery"],"summary":"Every agent tool, with its arguments","description":"The same tool table MCP serves from `tools/list`, for callers who do not speak JSON-RPC. Includes `get_rate_card`, `request_quote` and `check_availability`, which previously had no REST route — so a REST-only integrator could search a business and could not price anything.","security":[],"responses":{"200":{"description":"The tool table.","content":{"application/json":{"schema":{"type":"object"}}}},"429":{"description":"Rate limited. `Retry-After` gives the number of seconds to wait. Reads are metered per IP, per declared client name and per registered agent; a declared name never raises the IP ceiling.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/public/tools/{name}":{"post":{"tags":["ask"],"summary":"Call one agent tool","description":"A thin binding onto the same function MCP and A2A call, so a refusal here is the refusal you would get there, word for word. The request body IS the tool arguments.\n\nA bearer token is optional and does the same thing it does on `/api/mcp`: without one you have the anonymous tier, which reads and asks; messaging and binding tools need a registered agent and will tell you so. Registration is self-service.\n\nThe response is the tool envelope, not a bare value — the same `{ tool, data, meta }` shape every other agent surface returns, including on failure, where `error.fix` says what to do instead.","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"},"description":"A tool name from GET /api/public/tools."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"200":{"description":"The tool envelope.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"The arguments did not typecheck. `error.fix` names the field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"A credential was sent and did not authenticate.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"This credential may not use that tool.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such tool, or no such listing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. `Retry-After` gives the number of seconds to wait. Reads are metered per IP, per declared client name and per registered agent; a declared name never raises the IP ceiling.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/public/liveness/challenge":{"get":{"tags":["ask"],"summary":"Proof of life for a listing","description":"Read-only. Never raises a question with the business — see the controller.","security":[],"responses":{"200":{"description":"A challenge.","content":{"application/json":{"schema":{"type":"object"}}}},"429":{"description":"Rate limited. `Retry-After` gives the number of seconds to wait. Reads are metered per IP, per declared client name and per registered agent; a declared name never raises the IP ceiling.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/sitemap.xml":{"get":{"tags":["discovery"],"summary":"Every public profile URL","security":[],"responses":{"200":{"description":"A sitemap.","content":{"application/xml":{"schema":{"type":"string"}}}}}}},"/openapi.json":{"get":{"tags":["discovery"],"summary":"This document","security":[],"responses":{"200":{"description":"The OpenAPI description.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/.well-known/tunnel-trust.json":{"get":{"tags":["trust","discovery"],"summary":"Attestation signing key and rotation metadata","security":[],"responses":{"200":{"description":"Key metadata.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/.well-known/jwks.json":{"get":{"tags":["trust","discovery"],"summary":"The same keys as an RFC 7517 JWKS","description":"Verify any tunnel attestation with a standard JWS library and nothing else.","security":[],"responses":{"200":{"description":"A JWKS.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/.well-known/oauth-authorization-server":{"get":{"tags":["identity","discovery"],"summary":"RFC 8414 authorization server metadata","security":[],"responses":{"200":{"description":"AS metadata.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/.well-known/oauth-protected-resource":{"get":{"tags":["identity","discovery"],"summary":"RFC 9728 protected resource metadata for the MCP endpoint","security":[],"responses":{"200":{"description":"PR metadata.","content":{"application/json":{"schema":{"type":"object"}}}}}}}},"externalDocs":{"url":"https://tunnelpowered.com/docs","description":"The same surface, written for a person."}}