{"openapi":"3.1.0","info":{"title":"Prowl REST API v1","summary":"Wallet, usage, tool catalog and API keys for agents using Prowl.","description":"The REST surface an agent uses to watch what it is spending.\n\nProwl's product surface is the MCP endpoint at `https://prowl.chat/mcp`, where an agent\ngains 448 market-intelligence tools and every call is metered against a prepaid USD\nwallet. This API is the other half of that arrangement: read the balance, read what each\ncall cost, and manage the keys that authorise them.\n\nAuthentication is a bearer token — either a JWT from sign-in or a `prowl_...` API key\ngenerated in MCP Home. Two operations need no credential at all, because a price list\nthat demands a key cannot be compared before you buy: `GET /api/v1/tools/pricing` and\n`GET /api/v1/tools/catalog`.\n","version":"1.0.0","contact":{"name":"Prowl","url":"https://prowl.chat/contact/","email":"support@prowl.chat"},"license":{"name":"Proprietary","url":"https://prowl.chat/terms/"}},"servers":[{"url":"https://prowl.chat","description":"Production"}],"externalDocs":{"description":"Prowl documentation","url":"https://prowl.chat/docs/"},"paths":{"/api/v1/wallet":{"get":{"summary":"Read wallet balances","description":"Returns the two pools a call is paid from: plan credit, which burns at the end of the billing period and is spent first, and top-up credit, which never expires. Figures are rounded to whole cents at this boundary.","operationId":"getWallet","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"prowl_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prowl Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletBalances"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing, malformed or expired credential."}},"security":[{"bearerAuth":[]}]}},"/api/v1/wallet/invocations":{"get":{"summary":"List recent metered calls","description":"Every tool call this account has been billed for, newest first, with what the provider charged and what the wallet was debited. Use it to reconcile a balance against the work that produced it.","operationId":"getWalletInvocations","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":10,"title":"Limit"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"prowl_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prowl Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvocationsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing, malformed or expired credential."}},"security":[{"bearerAuth":[]}]}},"/api/v1/usage/tools":{"get":{"summary":"List recent metered calls (alias)","description":"Identical to `GET /api/v1/wallet/invocations`; kept because usage and wallet are two words for the same question and clients reach for both.","operationId":"getUsageTools","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":10,"title":"Limit"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"prowl_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prowl Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvocationsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing, malformed or expired credential."}},"security":[{"bearerAuth":[]}]}},"/api/v1/usage/summary":{"get":{"summary":"Summarise spend over a window","description":"Spend rolled up over the last `days` days — totals, the twenty tools that cost the most, and the current balances alongside them so a budget decision needs one request rather than two.","operationId":"getUsageSummary","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","default":30,"title":"Days"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"prowl_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prowl Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageSummaryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing, malformed or expired credential."}},"security":[{"bearerAuth":[]}]}},"/api/v1/tools/pricing":{"get":{"summary":"Read the public price list","description":"What each tool costs to call, with no credential required — a price you cannot read before signing up is a price you cannot compare. Returns what a call debits from your wallet, and nothing about how that figure is arrived at.","operationId":"getToolsPricing","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PricingResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/tools/catalog":{"get":{"summary":"Browse the full tool catalog","description":"Every registered tool with what it does, who runs it, what it costs and where its documentation lives. No credential required. This is the endpoint to read when choosing which tool answers a question.","operationId":"getToolsCatalog","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CatalogResponse"}}}}}}},"/api/v1/tools/health":{"get":{"summary":"Read per-tool health","description":"Success rate and latest outcome per tool over a recent window, aggregated across all callers — no per-user data. A tool missing from the reply had no calls in the window, which reads as idle rather than broken.","operationId":"getToolsHealth","parameters":[{"name":"window_minutes","in":"query","required":false,"schema":{"type":"integer","default":60,"title":"Window Minutes"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"prowl_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prowl Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToolHealthResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing, malformed or expired credential."}},"security":[{"bearerAuth":[]}]}},"/api/v1/keys":{"post":{"summary":"Create an API key","description":"Mints a `prowl_...` key, optionally scoped to categories, spend limits and an IP allowlist. **The secret is returned once and is never retrievable again** — store it when you receive it.","operationId":"createKeys","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"prowl_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prowl Session"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateMCPKeyRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyCreatedResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing, malformed or expired credential."}},"security":[{"bearerAuth":[]}]},"get":{"summary":"List API keys","description":"Every key on the account with its scope, limits and last use. Secrets are never returned — only the prefix, which is enough to tell two keys apart.","operationId":"getKeys","parameters":[{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"prowl_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prowl Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing, malformed or expired credential."}},"security":[{"bearerAuth":[]}]}},"/api/v1/keys/{key_id}":{"delete":{"summary":"Revoke an API key","description":"Takes effect immediately: the next call presenting this key fails authentication. Revoking the key behind an OAuth connector kills that connector too, without touching the OAuth grant.","operationId":"revokeKeys","parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"string","title":"Key Id"}},{"name":"authorization","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"}},{"name":"prowl_session","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prowl Session"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevokedResponse"}}}},"404":{"description":"No such key on this account, or already revoked."},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing, malformed or expired credential."}},"security":[{"bearerAuth":[]}]}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"A `prowl_...` API key from MCP Home, or a JWT from sign-in. Sent as `Authorization: Bearer <token>`."}},"schemas":{"ApiKeyCreatedResponse":{"properties":{"id":{"type":"string","title":"Id"},"key":{"type":"string","title":"Key","description":"The secret. Shown exactly once; store it now."},"prefix":{"type":"string","title":"Prefix"},"name":{"type":"string","title":"Name"},"allowed_categories":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Allowed Categories"},"denied_categories":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Denied Categories"},"daily_spend_limit_usd":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Daily Spend Limit Usd"},"total_spend_limit_usd":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Total Spend Limit Usd"},"ip_allowlist":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Ip Allowlist"}},"type":"object","required":["id","key","prefix","name"],"title":"ApiKeyCreatedResponse","description":"The one response that carries the raw key — it is never retrievable again."},"ApiKeyListResponse":{"properties":{"keys":{"items":{"$ref":"#/components/schemas/ApiKeySummary"},"type":"array","title":"Keys"}},"type":"object","required":["keys"],"title":"ApiKeyListResponse"},"ApiKeySummary":{"properties":{"id":{"type":"string","title":"Id"},"prefix":{"type":"string","title":"Prefix","description":"First characters of the key, enough to tell keys apart."},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At"},"last_used_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Used At"},"is_active":{"type":"boolean","title":"Is Active"},"allowed_categories":{"items":{"type":"string"},"type":"array","title":"Allowed Categories"},"denied_categories":{"items":{"type":"string"},"type":"array","title":"Denied Categories"},"daily_spend_limit_usd":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Daily Spend Limit Usd"},"total_spend_limit_usd":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Total Spend Limit Usd"},"ip_allowlist":{"items":{"type":"string"},"type":"array","title":"Ip Allowlist"}},"type":"object","required":["id","prefix","is_active"],"title":"ApiKeySummary","description":"A key as it can be shown back. Never carries the secret or its hash."},"CatalogResponse":{"properties":{"tools":{"items":{"$ref":"#/components/schemas/CatalogRow"},"type":"array","title":"Tools"},"count":{"type":"integer","title":"Count"},"categories":{"items":{"type":"string"},"type":"array","title":"Categories","description":"Every category present, sorted."}},"type":"object","required":["tools","count","categories"],"title":"CatalogResponse","description":"Every registered tool with the fields needed to choose one."},"CatalogRow":{"properties":{"tool_name":{"type":"string","title":"Tool Name"},"provider":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Provider"},"provider_docs":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Provider Docs","description":"The third party's own documentation, where they publish one."},"reference_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reference Url","description":"This tool's page in the public documentation. Resolves for a signed-out reader."},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"description":{"type":"string","title":"Description","default":""},"tags":{"items":{"type":"string"},"type":"array","title":"Tags"},"estimated_billed_usd":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Estimated Billed Usd"},"cost_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cost Source"}},"type":"object","required":["tool_name"],"title":"CatalogRow","description":"One tool as the catalog renders it: what it does, who runs it, what it costs."},"CreateMCPKeyRequest":{"properties":{"name":{"type":"string","maxLength":120,"title":"Name","default":""},"allowed_categories":{"items":{"type":"string"},"type":"array","title":"Allowed Categories"},"denied_categories":{"items":{"type":"string"},"type":"array","title":"Denied Categories"},"daily_spend_limit_usd":{"anyOf":[{"type":"number","minimum":0.0},{"type":"null"}],"title":"Daily Spend Limit Usd"},"total_spend_limit_usd":{"anyOf":[{"type":"number","minimum":0.0},{"type":"null"}],"title":"Total Spend Limit Usd"},"ip_allowlist":{"items":{"type":"string"},"type":"array","title":"Ip Allowlist"}},"type":"object","title":"CreateMCPKeyRequest","description":"POST /api/mcp/keys body."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"InvocationsResponse":{"properties":{"invocations":{"items":{"$ref":"#/components/schemas/ToolInvocation"},"type":"array","title":"Invocations"},"count":{"type":"integer","title":"Count","description":"Number of rows in this response, not the lifetime total."}},"type":"object","required":["invocations","count"],"title":"InvocationsResponse","description":"Recent metered calls, newest first."},"PricingResponse":{"properties":{"tools":{"items":{"$ref":"#/components/schemas/PricingRow"},"type":"array","title":"Tools"},"count":{"type":"integer","title":"Count","description":"Rows returned, after the `limit` was applied."},"total_tools":{"type":"integer","title":"Total Tools","description":"Rows available before the limit."},"price_note":{"type":"string","title":"Price Note","description":"What the price figure means to a caller."}},"type":"object","required":["tools","count","total_tools","price_note"],"title":"PricingResponse","description":"The public price list. Takes no credential, and publishes no margin."},"PricingRow":{"properties":{"tool_name":{"type":"string","title":"Tool Name"},"provider":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Provider"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"estimated_billed_usd":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Estimated Billed Usd","description":"What a representative call debits from your wallet. Computed at runtime; the actual debit follows the live provider cost."},"cost_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cost Source","description":"How the figure was derived — a published rate card, or a measurement."}},"type":"object","required":["tool_name"],"title":"PricingRow","description":"What one tool costs the caller. Never what it costs us."},"RevokedResponse":{"properties":{"success":{"type":"boolean","title":"Success"}},"type":"object","required":["success"],"title":"RevokedResponse","description":"Revocation is immediate: the next call with this key fails authentication."},"ToolHealth":{"properties":{"total":{"type":"integer","title":"Total","description":"Calls inside the window, across all users."},"success":{"type":"integer","title":"Success","description":"How many of them succeeded."},"success_rate":{"type":"number","title":"Success Rate","description":"Percentage, 0-100."},"last_status":{"type":"string","title":"Last Status","description":"Outcome of the most recent call: `ok` or `error`."},"last_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last At","description":"ISO-8601 time of that call."}},"type":"object","required":["total","success","success_rate","last_status"],"title":"ToolHealth","description":"Aggregate outcome for one tool. No per-user data, by construction."},"ToolHealthResponse":{"properties":{"window_minutes":{"type":"integer","title":"Window Minutes"},"tools":{"additionalProperties":{"$ref":"#/components/schemas/ToolHealth"},"type":"object","title":"Tools","description":"Keyed by tool name. Absent means idle, which the UI reads as healthy."}},"type":"object","required":["window_minutes","tools"],"title":"ToolHealthResponse","description":"Platform health per tool. A tool absent from `tools` had no calls in the window."},"ToolInvocation":{"properties":{"tool_name":{"type":"string","title":"Tool Name","description":"Registry name of the tool that was called."},"provider_cost_usd":{"type":"number","title":"Provider Cost Usd","description":"What the upstream provider charged for this call."},"billed_usd":{"type":"number","title":"Billed Usd","description":"What this call actually debited from the wallet."},"markup_usd":{"type":"number","title":"Markup Usd","description":"The difference between the two figures above."},"success":{"type":"boolean","title":"Success","description":"Whether the provider returned a usable result."},"latency_ms":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Latency Ms","description":"Round-trip time in milliseconds."},"api_key_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api Key Id","description":"Which API key made the call."},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At","description":"ISO-8601 timestamp of the call."}},"type":"object","required":["tool_name","provider_cost_usd","billed_usd","markup_usd","success"],"title":"ToolInvocation","description":"One metered tool call belonging to the authenticated caller.\n\n`provider_cost_usd` and `markup_usd` are named here on purpose: this is the\ncaller's own transaction, and a wallet they cannot reconcile is a wallet they\ncannot trust. DOC-12 governs the *unauthenticated* surfaces, where neither the\nfigures nor a formula for them appear."},"UsageByTool":{"properties":{"tool_name":{"type":"string","title":"Tool Name"},"calls":{"type":"integer","title":"Calls"},"billed_usd":{"type":"number","title":"Billed Usd"}},"type":"object","required":["tool_name","calls","billed_usd"],"title":"UsageByTool","description":"Spend rolled up per tool over the requested window."},"UsageSummaryResponse":{"properties":{"subscription_balance_usd":{"type":"number","title":"Subscription Balance Usd","description":"Plan credit for the current period. Burns at period end, spent first."},"extra_balance_usd":{"type":"number","title":"Extra Balance Usd","description":"Top-up credit. Never expires, spent after the subscription pool."},"total_available_usd":{"type":"number","title":"Total Available Usd","description":"What the next call can draw on: the two pools added together."},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At","description":"ISO-8601 timestamp of the last balance change, or null if never moved."},"days":{"type":"integer","title":"Days","description":"Length of the window this summary covers, in days."},"invocation_count":{"type":"integer","title":"Invocation Count","description":"Metered calls inside the window."},"total_billed_usd":{"type":"number","title":"Total Billed Usd","description":"Total debited from the wallet inside the window."},"total_provider_usd":{"type":"number","title":"Total Provider Usd","description":"Total upstream provider cost inside the window."},"by_tool":{"items":{"$ref":"#/components/schemas/UsageByTool"},"type":"array","title":"By Tool","description":"Top 20 tools by spend, highest first."}},"type":"object","required":["subscription_balance_usd","extra_balance_usd","total_available_usd","days","invocation_count","total_billed_usd","total_provider_usd","by_tool"],"title":"UsageSummaryResponse","description":"Spend over a window, with the current balances alongside it.\n\nInherits `WalletBalances` because the handler spreads the wallet into its reply\n(`persistence/wallet.py:2405`) — modelling it any other way would make the\nenforced filter drop the four balance fields the dashboard reads."},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"WalletBalances":{"properties":{"subscription_balance_usd":{"type":"number","title":"Subscription Balance Usd","description":"Plan credit for the current period. Burns at period end, spent first."},"extra_balance_usd":{"type":"number","title":"Extra Balance Usd","description":"Top-up credit. Never expires, spent after the subscription pool."},"total_available_usd":{"type":"number","title":"Total Available Usd","description":"What the next call can draw on: the two pools added together."},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At","description":"ISO-8601 timestamp of the last balance change, or null if never moved."}},"type":"object","required":["subscription_balance_usd","extra_balance_usd","total_available_usd"],"title":"WalletBalances","description":"The two pools a caller spends from, rounded to whole cents at this boundary.\n\nSub-cent precision is deliberate *inside* the store (SSH-305) and deliberately\nabsent here — `web/server.py::_rounded_wallet_fields` rounds on the way out so a\ndisplay value never carries a float tail."}}}}