A described API, and a refusal you can read.
The dashboard is a client of this API and nothing more — there is no second, private surface behind it. Every route is described by an OpenAPI 3.1 document served from the API itself, so a client can be generated rather than transcribed.
curl -X POST "$NETWEFT_API/mcp" \
-H "Authorization: Bearer nw_<prefix>_<secret>" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
The description is served, not published
GET /openapi.json
returns an OpenAPI 3.1 document generated from the route definitions
themselves rather than written beside them. A route whose request or
response shape changes changes the document in the same deploy,
because the document is the route's own schema — there is no second
copy to fall behind.
Two ways to authenticate
The dashboard authenticates with a session cookie, created by
signing in with Google, GitHub or an email address and a password.
The MCP server at
POST /mcp
authenticates with an API key: a bearer token, narrowed to some of
6
scopes, looked up by its public prefix and compared in constant time
against a stored hash. The other routes take only the session
cookie.
A key is displayed once and never again. It can be given an expiry when it is created, and it can be revoked — which leaves the row in place for the audit log rather than deleting the evidence that it existed.
Money is refused, not invoiced
A paid operation reserves credits before the vendor is reached. A workspace that cannot cover it gets a 402 with the shortfall in the body, before anything is bought — so a client can say by how much it is short instead of discovering an overage later. There is no overage, because there is nothing to overrun.
{
"code": "insufficient_credits",
"error": "This workspace cannot cover the call.",
"available": 1240,
"required": 2000
}
Failures have names.
A failing response carries a human sentence in
error
and a stable machine code in
code. The sentence is free to change; the code is the contract, and each one
has exactly one HTTP status so a name never means two things.
- insufficient_credits HTTP 402
- The workspace cannot cover the call. Answered before the vendor is reached, so nothing was bought and nothing was charged.
availableandrequiredcome with it, so a client can say by how much. - rate_limited HTTP 429
- The fixed window is full. A
Retry-Afterheader comes with it, so a client library backs off on a number rather than on a guess. - insufficient_role HTTP 403
- The caller is in the workspace but not with a role that may do this. Distinct from not being a member at all: the fix is a role change, not an invitation.
- background_unavailable HTTP 503
- The background service that runs audits and scheduled checks is unreachable. Nothing was charged and nothing was lost — the run simply cannot start yet.
Rate limits
Every policy is a fixed window. A refused request carries
Retry-After, so a client backs off on a number rather than on a guess.
- Spending endpoints per user — Keyed per user rather than per workspace, so a team of ten is not throttled by one member's script.
- 60 per 60s
- API key requests per key — Applied at the MCP endpoint against the key's public prefix.
- 120 per 60s
- Trial workspaces per workspace — Tighter while a workspace is on trial, and back to the normal policy the moment it is not.
- 10 per 60s
The dashboard is a client. So is whatever you write.
The API key is created in the dashboard, shown once, and scoped before it is shown. Everything it can reach is listed on the MCP page.
There is no free tier. Three plans from $50 a month, each opening with a free trial.