{"openapi":"3.1.0","info":{"title":"stroomprijzenapi","version":"1.0.0","summary":"Dutch day-ahead electricity prices, with energy tax and VAT applied.","description":"Dutch day-ahead market prices with energy tax, historical ODE and VAT.\nSupplier markup, grid fees, fixed costs and the annual tax rebate are excluded.\nTax boundaries follow Dutch local time rather than UTC.\n\nEvery price point carries the raw price *and* the full breakdown, so you can\nrecompute the result instead of having to trust it:\n\n    allInEurKwh = (rawEurMwh / 1000 + energyTaxEurKwh + odeEurKwh) * (1 + vatRate)\n\nThe rates behind those fields are published at `/api/v1/tax-rates`.\n\n### Which field do you want?\n\nUse `allInEurKwh` for the market price including energy tax, ODE and VAT.\nIt excludes supplier markup, grid fees and fixed costs.\n\n`rawEurKwh` is the market price before taxes; the difference varies with the\nmarket price and tax period. `rawEurMwh` uses EUR/MWh: divide by 1,000 to get EUR/kWh.\nRaw hourly observations start on 2015-01-05, but the public price calculation\nsupports the supplied tax years from 2019. Pre-2019 raw data has no separate public endpoint.\n\n### Time zones\n\nAll instants are UTC. A \"day\" is a **Dutch calendar day**, not a UTC day.\n\nThat distinction bites. Midnight UTC is 02:00 in Amsterdam during summer time, so\na window of `00:00Z` to `23:59Z` is shifted by two hours against the Dutch day —\none hour in winter. Ask for `?date=2026-08-14` and let this API work out the\nboundaries; that is exactly why it takes a date instead of a timestamp range.\n\nOn the clock-change days in March and October a day has 23 or 25 intervals. Do not\nassume 24, and do not line two series up by array position — match on `momentUtc`.\n\n### Freshness\n\nTomorrow's prices appear at SMARD around 14:00 CE(S)T. Until then a request for\ntomorrow returns an empty `points` array rather than an error. Responses carry\n`Cache-Control: private, no-store` so every data request validates the key.\nCache useful data in your own application and retry empty days later.\n\n### Verifying our arithmetic\n\nThe tax layer is the part of this API you should not have to take on faith, so\nthe numbers to check it with are published: `/api/v1/test-vectors` returns a\nworked amount for every tax year since 2019, plus the boundary cases — the VAT\ncut of 1 July 2022, the year change where the rate flips at Dutch midnight, and\nthe clock-change days.\n\nEach vector carries the inputs, the expected breakdown, and the sum written out\nin full so you can check it by hand. They were derived from the official\nBelastingdienst tables with exact decimal arithmetic rather than by running this\ncode, and our own test suite asserts against the same file — so if a rate here is\nwrong, both the vectors and our tests are wrong in the same visible way.\n\n### Home Assistant\n\nThere is a page with copy-paste sensors, automations and charts at\n[/docs/home-assistant](/docs/home-assistant).\n\n### For assistants\n\nThere is a hosted MCP server at `/mcp`, Streamable HTTP, so a model can read these\nprices without anyone writing an integration. Besides the data it exposes\n`find_cheapest_window`, which answers the question people actually ask — when to\nrun the washing machine or charge the car — instead of returning a series to sort\nthrough.\n\n### API keys\n\nA valid API key is required for the data endpoints and `/mcp`.\nCreate and manage keys at `/api-key` after verifying your email.\nExisting keys remain valid and are linked to their verified email owner.\nFree keys have 10,000 requests per Dutch calendar day across REST and MCP.\nSend `Authorization: Bearer <key>`, `X-API-Key: <key>` or `?apiKey=<key>`.\nMissing, unknown and revoked keys return 401. Validation outages return 503.\nAuthenticated data responses are private and not shared-cacheable.\nHealth, status and API discovery documents remain public.\n\n### Comparing against other sources\n\nTwo sources are commonly used to cross-check Dutch prices, and both need care.\n\n**Energy-Charts** (Fraunhofer ISE) takes its NL prices from the same SMARD feed we\ndo, so it validates ingestion rather than arithmetic. Its `unix_seconds` field is\nin **seconds**, not milliseconds.\n\n**EnergyZero**'s `/v1/energyprices` returns the *raw market price*, despite the\n`inclBtw` parameter suggesting otherwise. Its `price` matches our `rawEurKwh`, not\nour `allInEurKwh`. With `inclBtw` omitted the default is `false`; with `true` you\nget the same raw price times 1.21, rounded to whole cents — still no energy tax.\nA residual of about 5e-6 EUR/kWh is expected and is not an error on either side:\nEnergyZero serves the unrounded hourly mean while SMARD rounds to 0.01 EUR/MWh.","license":{"name":"Source data: CC BY 4.0 (Bundesnetzagentur | SMARD.de)","url":"https://creativecommons.org/licenses/by/4.0/"},"x-logo":{"url":"/logo.webp","altText":"stroomprijzenapi"}},"servers":[{"url":"/","description":"This deployment"}],"security":[],"tags":[{"name":"prices","description":"Day-ahead prices with tax applied."},{"name":"reference","description":"The rates and coverage behind the numbers."},{"name":"keys","description":"API keys and their usage."},{"name":"mcp","description":"The same data over the Model Context Protocol."},{"name":"operations","description":"Scheduled ingest and administration. Not for public use."}],"paths":{"/api/v1/prices":{"get":{"tags":["prices"],"operationId":"getPrices","security":[{"apiKey":[]},{"apiKeyHeader":[]}],"summary":"Prices for one Dutch calendar day","description":"Returns every interval of the requested day. When `date` is omitted the answer is tomorrow once its prices have been published, and today otherwise — which is what a visitor usually wants to see.","parameters":[{"name":"date","in":"query","required":false,"description":"Dutch calendar day, YYYY-MM-DD. Defaults to tomorrow when available, else today. Stored raw observations start 2015-01-05; price calculations require a supported tax year (from 2019).","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"examples":{"tomorrow":{"value":"2026-08-15"}}},{"name":"resolution","in":"query","required":false,"description":"Interval length. Quarter-hourly data exists from 1 October 2025, when the day-ahead market moved to quarter-hourly trading. An hourly price is the mean of its four quarter-hours.","schema":{"type":"string","enum":["hour","quarterhour"],"default":"hour"}}],"responses":{"200":{"description":"The day. `points` is empty when the prices have not been published yet; that is not an error.","content":{"application/json":{"schema":{"type":"object","required":["date","resolution","count","points"],"properties":{"date":{"type":"string","examples":["2026-08-15"]},"resolution":{"type":"string","enum":["hour","quarterhour"]},"timeZone":{"type":"string","examples":["Europe/Amsterdam"]},"source":{"type":"string","examples":["smard"]},"license":{"type":"string","examples":["CC-BY-4.0"]},"attribution":{"type":"string","examples":["Bundesnetzagentur | SMARD.de"]},"units":{"type":"object","properties":{"raw":{"type":"string","examples":["EUR/MWh"]},"retail":{"type":"string","examples":["EUR/kWh"]}}},"publishedAt":{"type":["string","null"],"format":"date-time","description":"SMARD's own publication instant for this data.","examples":["2026-08-14T12:05:11.416Z"]},"count":{"type":"integer","examples":[24]},"summary":{"type":["object","null"],"properties":{"minEurKwh":{"type":"number","examples":[0.118169]},"maxEurKwh":{"type":"number","examples":[0.334686]},"averageEurKwh":{"type":"number","examples":[0.259526]}}},"points":{"type":"array","items":{"type":"object","required":["momentUtc","localTime","rawEurMwh","rawEurKwh","energyTaxEurKwh","odeEurKwh","vatRate","exclVatEurKwh","allInEurKwh"],"properties":{"momentUtc":{"type":"string","format":"date-time","description":"Start of the interval, always UTC.","examples":["2026-08-14T22:00:00.000Z"]},"localTime":{"type":"string","description":"The same instant in Europe/Amsterdam, with offset.","examples":["2026-08-15T00:00:00+02:00"]},"rawEurMwh":{"type":"number","description":"Day-ahead market price as published by SMARD. Can be negative.","examples":[169.45]},"rawEurKwh":{"type":"number","description":"rawEurMwh / 1000.","examples":[0.16945]},"energyTaxEurKwh":{"type":"number","description":"Energy tax, first bracket, for the calendar year of this instant in Dutch local time.","examples":[0.09161]},"odeEurKwh":{"type":"number","description":"Sustainable energy surcharge. Zero from 2023, when it was folded into the energy tax.","examples":[0]},"vatRate":{"type":"number","description":"VAT rate for this instant. 0.09 between 1 July and 31 December 2022, 0.21 otherwise.","examples":[0.21]},"exclVatEurKwh":{"type":"number","description":"rawEurKwh + energyTaxEurKwh + odeEurKwh.","examples":[0.26106]},"allInEurKwh":{"type":"number","description":"exclVatEurKwh * (1 + vatRate). Rounded to six decimals for presentation only.","examples":[0.315883]}}}}}}}}},"400":{"description":"Unparseable date, a date outside the covered range, or an unknown resolution.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","examples":[400]},"statusMessage":{"type":"string","examples":["resolution must be one of hour, quarterhour"]}}}}}},"401":{"description":"A valid API key is required; the key is missing, unknown or revoked."},"500":{"description":"The database could not be reached.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","examples":[400]},"statusMessage":{"type":"string","examples":["resolution must be one of hour, quarterhour"]}}}}}},"503":{"description":"API key validation or usage storage is temporarily unavailable."}}}},"/api/v1/prices/now":{"get":{"tags":["prices"],"operationId":"getCurrentPrice","security":[{"apiKey":[]},{"apiKeyHeader":[]}],"summary":"The interval containing the current moment","parameters":[{"name":"resolution","in":"query","required":false,"description":"Interval length. Quarter-hourly data exists from 1 October 2025, when the day-ahead market moved to quarter-hourly trading. An hourly price is the mean of its four quarter-hours.","schema":{"type":"string","enum":["hour","quarterhour"],"default":"hour"}}],"responses":{"200":{"description":"The interval we are in right now.","content":{"application/json":{"schema":{"type":"object","properties":{"resolution":{"type":"string","enum":["hour","quarterhour"]},"timeZone":{"type":"string","examples":["Europe/Amsterdam"]},"source":{"type":"string","examples":["smard"]},"license":{"type":"string","examples":["CC-BY-4.0"]},"attribution":{"type":"string","examples":["Bundesnetzagentur | SMARD.de"]},"units":{"type":"object","properties":{"raw":{"type":"string","examples":["EUR/MWh"]},"retail":{"type":"string","examples":["EUR/kWh"]}}},"publishedAt":{"type":["string","null"],"format":"date-time"},"point":{"type":"object","required":["momentUtc","localTime","rawEurMwh","rawEurKwh","energyTaxEurKwh","odeEurKwh","vatRate","exclVatEurKwh","allInEurKwh"],"properties":{"momentUtc":{"type":"string","format":"date-time","description":"Start of the interval, always UTC.","examples":["2026-08-14T22:00:00.000Z"]},"localTime":{"type":"string","description":"The same instant in Europe/Amsterdam, with offset.","examples":["2026-08-15T00:00:00+02:00"]},"rawEurMwh":{"type":"number","description":"Day-ahead market price as published by SMARD. Can be negative.","examples":[169.45]},"rawEurKwh":{"type":"number","description":"rawEurMwh / 1000.","examples":[0.16945]},"energyTaxEurKwh":{"type":"number","description":"Energy tax, first bracket, for the calendar year of this instant in Dutch local time.","examples":[0.09161]},"odeEurKwh":{"type":"number","description":"Sustainable energy surcharge. Zero from 2023, when it was folded into the energy tax.","examples":[0]},"vatRate":{"type":"number","description":"VAT rate for this instant. 0.09 between 1 July and 31 December 2022, 0.21 otherwise.","examples":[0.21]},"exclVatEurKwh":{"type":"number","description":"rawEurKwh + energyTaxEurKwh + odeEurKwh.","examples":[0.26106]},"allInEurKwh":{"type":"number","description":"exclVatEurKwh * (1 + vatRate). Rounded to six decimals for presentation only.","examples":[0.315883]}}}}}}}},"401":{"description":"A valid API key is required; the key is missing, unknown or revoked."},"404":{"description":"No price stored for this moment.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","examples":[400]},"statusMessage":{"type":"string","examples":["resolution must be one of hour, quarterhour"]}}}}}},"500":{"description":"The database could not be reached.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","examples":[400]},"statusMessage":{"type":"string","examples":["resolution must be one of hour, quarterhour"]}}}}}},"503":{"description":"API key validation or usage storage is temporarily unavailable."}}}},"/api/v1/tax-rates":{"get":{"tags":["reference"],"operationId":"getTaxRates","security":[{"apiKey":[]},{"apiKeyHeader":[]}],"summary":"The energy tax, ODE and VAT rates used in the calculation","description":"Published so anyone can recompute our all-in price from the raw price. Sourced from the Belastingdienst; see docs/DATA-SOURCES.md in the repository for the exact tables and the yearly update procedure. All amounts exclude VAT.","responses":{"200":{"description":"Rates per year, plus the VAT periods.","content":{"application/json":{"schema":{"type":"object","properties":{"timeZone":{"type":"string","examples":["Europe/Amsterdam"]},"unit":{"type":"string","examples":["EUR/kWh excluding VAT"]},"note":{"type":"string"},"formula":{"type":"string","examples":["allIn = (eurMwh / 1000 + energyTax + ode) * (1 + vat)"]},"years":{"type":"array","items":{"type":"object","properties":{"year":{"type":"integer","examples":[2026]},"validFrom":{"type":"string","examples":["2026-01-01"]},"validUntil":{"type":"string","examples":["2027-01-01"]},"energyTax":{"type":"number","examples":[0.09161]},"ode":{"type":"number","examples":[0]},"taxRebateEurYear":{"type":"number","description":"Per year per connection. Deliberately NOT part of the hourly price.","examples":[519.8]}}}},"vatPeriods":{"type":"array","items":{"type":"object","properties":{"validFrom":{"type":"string","examples":["2022-07-01"]},"validUntil":{"type":["string","null"],"examples":["2023-01-01"]},"rate":{"type":"number","examples":[0.09]}}}}}}}}},"401":{"description":"A valid API key is required; the key is missing, unknown or revoked."},"503":{"description":"API key validation or usage storage is temporarily unavailable."}}}},"/api/v1/test-vectors":{"get":{"tags":["reference"],"operationId":"getTestVectors","security":[{"apiKey":[]},{"apiKeyHeader":[]}],"summary":"Canonical vectors for verifying the calculation","description":"A worked amount for every tax year plus the boundary cases. Computed from the official Belastingdienst tables with exact decimal arithmetic, not by running this code. Compare against `expected.allInEurKwh` within `tolerance`.","responses":{"200":{"description":"The vectors, with the formula and the tolerance to compare at.","content":{"application/json":{"schema":{"type":"object","properties":{"description":{"type":"string"},"purpose":{"type":"string"},"formula":{"type":"string","examples":["allInEurKwh = (rawEurMwh / 1000 + energyTaxEurKwh + odeEurKwh) * (1 + vatRate)"]},"tolerance":{"type":"number","examples":[1e-9]},"verified":{"type":"string","examples":["2026-08-14"]},"vectors":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","examples":["2026-first-instant"]},"momentUtc":{"type":"string","format":"date-time"},"localTime":{"type":"string"},"taxYear":{"type":"integer","examples":[2026]},"rawEurMwh":{"type":"number","examples":[100]},"expected":{"type":"object","properties":{"energyTaxEurKwh":{"type":"number"},"odeEurKwh":{"type":"number"},"vatRate":{"type":"number"},"exclVatEurKwh":{"type":"number"},"allInEurKwh":{"type":"number"}}},"arithmetic":{"type":"string","description":"The same sum written out, so it can be checked by hand.","examples":["(100.0 / 1000 + 0.09161 + 0) * (1 + 0.21) = 0.2318481"]},"note":{"type":"string"}}}}}}}}},"401":{"description":"A valid API key is required; the key is missing, unknown or revoked."},"503":{"description":"API key validation or usage storage is temporarily unavailable."}}}},"/api/v1/keys":{"post":{"tags":["keys"],"operationId":"createApiKey","security":[{"accountSession":[]}],"summary":"Mint an API key","description":"Requires a verified Better Auth session cookie and a same-origin request. Create an account or sign in at /api-key. The email is taken from the session. The key is returned once and only its hash is stored, so it cannot be recovered — only replaced. One address may hold a small number of live keys at a time.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email","description":"Ignored; the verified session email is used instead.","examples":["you@example.com"]},"label":{"type":"string","maxLength":80,"description":"What this key is for, so you recognise it later.","examples":["home assistant"]}}}}}},"responses":{"201":{"description":"The key. This is the only time it exists in readable form.","content":{"application/json":{"schema":{"type":"object","properties":{"key":{"type":"string","examples":["sp_live_…"]},"warning":{"type":"string"},"keyPrefix":{"type":"string","examples":["sp_live_0U09tM"]},"tier":{"type":"string","enum":["free","commercial"]},"dailyLimit":{"type":"integer","examples":[10000]},"createdAt":{"type":"string","format":"date-time"}}}}}},"400":{"description":"Invalid request."},"401":{"description":"Verified account session required."},"403":{"description":"Same-origin request required."},"409":{"description":"This address already holds the maximum number of live keys."}}}},"/api/v1/keys/me":{"get":{"tags":["keys"],"operationId":"getKeyUsage","summary":"What the calling key has used","description":"Requires the key itself. Counts are origin-side and therefore a lower bound on what you called.","security":[{"apiKey":[]},{"apiKeyHeader":[]}],"responses":{"200":{"description":"The key and its usage over the last 30 days.","content":{"application/json":{"schema":{"type":"object","properties":{"keyPrefix":{"type":"string"},"label":{"type":["string","null"]},"tier":{"type":"string","enum":["free","commercial"]},"dailyLimit":{"type":"integer"},"counting":{"type":"string"},"requestsLast30Days":{"type":"integer"},"byDay":{"type":"array","items":{"type":"object","properties":{"day":{"type":"string"},"endpoint":{"type":"string"},"requests":{"type":"integer"}}}}}}}}},"401":{"description":"No key supplied, or it is unknown or revoked."}}}},"/api/v1/status":{"get":{"tags":["reference"],"operationId":"getStatus","summary":"Coverage and last ingest","description":"Answers whether the data is current. A `lastIngest.status` of `unchanged` is healthy: it means SMARD had nothing new. `error` means the run failed and `error` carries the reason.","responses":{"200":{"description":"Coverage per resolution and the most recent ingest run.","content":{"application/json":{"schema":{"type":"object","properties":{"coverage":{"type":"array","items":{"type":"object","properties":{"resolution":{"type":"string","enum":["hour","quarterhour"]},"points":{"type":"integer","examples":[101783]},"firstMomentUtc":{"type":["string","null"],"format":"date-time"},"lastMomentUtc":{"type":["string","null"],"format":"date-time"}}}},"lastIngest":{"type":["object","null"],"properties":{"startedAt":{"type":"string","format":"date-time"},"resolution":{"type":"string"},"status":{"type":"string","enum":["ok","unchanged","error"]},"weeksFetched":{"type":"integer"},"pointsInserted":{"type":"integer"},"nullsSkipped":{"type":"integer","description":"Intervals SMARD had not published yet. Skipped, never stored as 0."},"smardPublishedAt":{"type":["string","null"],"format":"date-time"},"durationMs":{"type":["integer","null"]},"error":{"type":["string","null"]}}}}}}}},"500":{"description":"The database could not be reached.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","examples":[400]},"statusMessage":{"type":"string","examples":["resolution must be one of hour, quarterhour"]}}}}}}}}},"/health":{"get":{"tags":["reference"],"operationId":"getHealth","summary":"Liveness probe","description":"Deliberately does not touch the database, so it stays green during a database outage.","responses":{"200":{"description":"The service is up.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","examples":[true]},"service":{"type":"string","examples":["stroomprijzenapi"]}}}}}}}}},"/mcp":{"post":{"tags":["mcp"],"operationId":"mcp","summary":"Model Context Protocol endpoint","description":"A hosted MCP server over Streamable HTTP, so an assistant can read these prices\ndirectly. Point any MCP client at `https://stroomprijzenapi.nl/mcp`.\n\nThe body is JSON-RPC 2.0, not a REST payload — this operation is here so the\nendpoint is discoverable, not so a client can be generated from it. Use an MCP\nclient library; it will handle `initialize`, `tools/list` and `tools/call`.\n\nSix tools: `get_prices`, `get_current_price`, `find_cheapest_window`,\n`get_tax_rates`, `get_data_coverage` and `get_api_usage`. All read-only, and the first\nfive return the same audited calculation as the REST endpoints. `get_api_usage`\nanswers about the calling key instead: what is left of its allowance today and\nwhen it resets, so a refused call can be explained rather than retried. The\nallowance is one number per key per day, spent across every endpoint together.\n\nThe server is stateless: no session id is issued, every POST stands on its own,\nand `GET /mcp` answers 405 because there is no server-to-client stream to open.\nA request must accept both `application/json` and `text/event-stream`, which the\nprotocol requires and every MCP client sends.\n\nAn API key is required here, the only place in this API where one is. A single\nconversation is an `initialize`, a `tools/list` and a call per question, none of\nit cacheable, so an assistant makes far more requests than a thermostat does,\nA valid API key is required, just as on the REST data endpoints.\nAdd it to the server URL as `?apiKey=<key>`, the one form every client can manage,\nor send it as `Authorization: Bearer <key>`. Without one the answer is 401 with a\nJSON-RPC error saying where to get a key. It counts against the same daily\nallowance as the data endpoints.\n\nEvery tool result carries a `source` block naming Stroomwekker.nl, and the server\ninstructions ask the assistant to cite it. `source.page` is the page for the day\nthe answer is about — `/stroomprijs/vandaag`, `/stroomprijs/morgen` or\n`/stroomprijs/<date>` — so the link goes where the numbers are, not to the front\ndoor. The underlying market data stays credited to Bundesnetzagentur | SMARD.de\nunder CC-BY-4.0.","security":[{"apiKey":[]},{"apiKeyHeader":[]}],"requestBody":{"required":true,"description":"A JSON-RPC 2.0 request, or a batch of them.","content":{"application/json":{"schema":{"type":"object","required":["jsonrpc","method"],"properties":{"jsonrpc":{"type":"string","enum":["2.0"]},"id":{"type":["string","integer","null"]},"method":{"type":"string","examples":["tools/list"]},"params":{"type":"object"}}}}}},"responses":{"200":{"description":"The JSON-RPC response.","content":{"application/json":{"schema":{"type":"object"}}}},"202":{"description":"The body held only notifications or responses, so there is nothing to answer."},"400":{"description":"Unparseable JSON, or not a valid JSON-RPC message."},"401":{"description":"No API key, or one that is unknown or revoked. The body is a JSON-RPC error naming POST /api/v1/keys.","content":{"application/json":{"schema":{"type":"object"}}}},"406":{"description":"The Accept header does not list both application/json and text/event-stream."},"415":{"description":"Content-Type is not application/json."},"429":{"description":"The daily allowance is spent. Resets at midnight Europe/Amsterdam.","content":{"application/json":{"schema":{"type":"object","properties":{"statusCode":{"type":"integer","examples":[400]},"statusMessage":{"type":"string","examples":["resolution must be one of hour, quarterhour"]}}}}}}}}},"/.well-known/mcp/server-card.json":{"get":{"tags":["mcp"],"operationId":"getMcpServerCard","summary":"What the MCP server is, before you connect to it","description":"A discovery document for `/mcp`: the endpoint, the transport, the tools with\ntheir descriptions and argument schemas, and the fact that a key is required.\n\nIt exists because `/mcp` cannot answer any of that without a key, and a client\nthat has only a domain name has nowhere else to look. This document itself is\nopen and cacheable — refusing to say what the server offers to anyone who has\nnot already signed up would defeat the point of publishing it.\n\nGenerated from the same tool definitions the MCP server registers, so it cannot\ndescribe a tool that does not exist or miss one that does. The test suite\ncompares it field for field against a real `tools/list` response.","responses":{"200":{"description":"The server card.","content":{"application/json":{"schema":{"type":"object","required":["serverInfo","endpoint","authentication","tools"],"properties":{"serverInfo":{"type":"object","properties":{"name":{"type":"string","examples":["stroomprijzenapi"]},"title":{"type":"string","examples":["Stroomwekker Stroomprijzen"]},"version":{"type":"string","examples":["1.0.0"]},"websiteUrl":{"type":"string","examples":["https://www.stroomwekker.nl"]},"documentationUrl":{"type":"string","examples":["https://stroomprijzenapi.nl/docs"]}}},"endpoint":{"type":"string","examples":["https://stroomprijzenapi.nl/mcp"]},"transport":{"type":"string","examples":["streamable-http"]},"protocolVersions":{"type":"array","description":"The MCP protocol revisions this deployment can negotiate.","items":{"type":"string","examples":["2025-11-25"]}},"instructions":{"type":"string","description":"The same text the server sends a model at the handshake."},"authentication":{"type":"object","properties":{"required":{"type":"boolean","examples":[true]},"schemes":{"type":"array","items":{"type":"string","examples":["apiKey"]}},"header":{"type":"string","examples":["Authorization: Bearer <key>"]},"queryParameter":{"type":"string","examples":["apiKey"]},"keysUrl":{"type":"string","examples":["https://stroomprijzenapi.nl/api/v1/keys"]},"description":{"type":"string"}}},"tools":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","examples":["find_cheapest_window"]},"title":{"type":"string","examples":["Cheapest stretch of hours"]},"description":{"type":"string"},"inputSchema":{"type":"object","description":"JSON Schema for the tool arguments, identical to the one `tools/list` serves."},"annotations":{"type":"object","description":"MCP tool hints. Every tool here is read-only."}}}},"resources":{"type":"array","description":"Empty: this server exposes tools only.","items":{"type":"object"}},"prompts":{"type":"array","description":"Empty: this server exposes tools only.","items":{"type":"object"}},"attribution":{"type":"object","properties":{"source":{"type":"string","examples":["Stroomwekker.nl"]},"url":{"type":"string","examples":["https://www.stroomwekker.nl"]}}}}}}}}}}},"/api/v1/admin/dashboard":{"get":{"tags":["operations"],"operationId":"getAdminDashboard","summary":"Everything the dashboard shows, in one request","description":"Totals, keys minted per day, acquisition sources, traffic per endpoint and per day. Requires the verified account session whose user ID matches ADMIN_USER_ID. Not for public use.","security":[{"accountSession":[]}],"responses":{"200":{"description":"Aggregates for the admin dashboard."},"401":{"description":"A verified account session is required."},"403":{"description":"The signed-in account is not the configured admin."}}}},"/api/v1/admin/key":{"get":{"tags":["operations"],"operationId":"getAdminKey","summary":"One key and what it calls","description":"The per-endpoint breakdown behind a single key, busiest first, plus its traffic per day and where the signup came from. Requires the verified account session whose user ID matches ADMIN_USER_ID. Not for public use.","security":[{"accountSession":[]}],"parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"integer","minimum":1},"description":"The key id from the dashboard listing."},{"name":"days","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":365,"default":30},"description":"How far back the usage is summed. The key itself is returned whatever the window, so a key with no recent traffic still answers."}],"responses":{"200":{"description":"The key, its endpoints ranked by volume, and its traffic per day."},"400":{"description":"Missing or malformed id, or a days value outside 1–365."},"401":{"description":"A verified account session is required."},"403":{"description":"The signed-in account is not the configured admin."},"404":{"description":"No key with that id."}}}},"/api/v1/admin/usage":{"get":{"tags":["operations"],"operationId":"getAdminUsage","summary":"Usage across every key","description":"Requires the verified account session whose user ID matches ADMIN_USER_ID. Not for public use.","security":[{"accountSession":[]}],"responses":{"200":{"description":"Every key with its recent volume, plus anonymous traffic."},"401":{"description":"A verified account session is required."},"403":{"description":"The signed-in account is not the configured admin."}}}},"/cron/compact":{"get":{"tags":["operations"],"operationId":"runCompaction","summary":"Fold old anonymous usage rows together","description":"Triggered by the Cloudflare Cron Trigger once a day, which runs the `compact` Nitro task. Anonymous usage is counted per origin, which is what keeps the allowance fair and what makes the table grow with visitors; this sums rows older than the cutoff into one row per day per endpoint. Totals are preserved exactly, so nothing the dashboard shows changes — only the split by origin is given up, and only beyond the cutoff, which sits far outside the thirty days that split is read over. Keyed rows are never touched.","security":[{"cronSecret":[]}],"parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","minimum":31,"maximum":3650,"default":90},"description":"Age in days past which anonymous rows are folded. Below 31 is refused: that is inside the window the dashboard reads origins over."}],"responses":{"200":{"description":"How many rows were folded, into how many, and the requests they hold."},"401":{"description":"Missing or wrong bearer token."}}}},"/cron/ingest":{"get":{"tags":["operations"],"operationId":"runIngest","summary":"Fetch from SMARD and store","description":"Triggered on schedule by the Cloudflare Cron Trigger (the `ingest` Nitro task), or by hand via this endpoint with `Authorization: Bearer $CRON_SECRET`. Idempotent: it compares SMARD's publication instant with what is already stored and writes only points whose value changed, so most runs touch the database not at all.","security":[{"cronSecret":[]}],"parameters":[{"name":"resolution","in":"query","required":false,"description":"Limit to one resolution. Default is both.","schema":{"type":"string","enum":["hour","quarterhour"],"default":"hour"}},{"name":"weeks","in":"query","required":false,"description":"How many weeks back to cover. The look-ahead week is always added.","schema":{"type":"integer","minimum":1,"maximum":52,"default":2}}],"responses":{"200":{"description":"Every requested resolution ran."},"401":{"description":"Missing or wrong bearer token, or no CRON_SECRET configured."},"500":{"description":"At least one resolution failed; see `results[].error`."}}}}},"components":{"securitySchemes":{"accountSession":{"type":"apiKey","in":"cookie","name":"__Secure-better-auth.session_token","description":"Better Auth session after verified mailbox login at /api-key. Local HTTP development uses better-auth.session_token. Issuance also requires a same-origin request."},"cronSecret":{"type":"http","scheme":"bearer","description":"The value of the CRON_SECRET environment variable."},"apiKey":{"type":"http","scheme":"bearer","description":"Create an API key at /api-key after verifying your email address. Required on all data endpoints, /api/v1/keys/me and /mcp. Unknown or revoked keys return 401; authentication failures return 503."},"apiKeyHeader":{"type":"apiKey","in":"header","name":"X-API-Key","description":"The same key as the bearer scheme, sent as a bare header — paste the key and nothing else. Equivalent everywhere the bearer token is accepted. Also accepted as ?apiKey=, which keeps a credential in the URL and is only worth it for a client that cannot set headers at all."}}}}