# Nettipoika agent quick start (HTTP API v1)

- Canonical installation discovery: `https://studio-dev.nettipoika.fi/llms.txt`
- Product discovery index: `https://nettipoika.fi/llms.txt`
- Release: `main-20261029-eb57326ca804`

## Connect safely

1. Read `GET https://studio-dev.nettipoika.fi/api/v1/capabilities` and `GET https://studio-dev.nettipoika.fi/openapi.json`.
2. Use the customer dashboard's copyable agent instructions for the intended site. Those instructions identify the exact hostname and `site_id` without containing a bearer token.
3. Generate independent random poll and continuation verifiers locally. Send only their SHA-256 challenges when creating the access request described by `https://studio-dev.nettipoika.fi/payment/openapi.json`.
4. Show the human only the returned approval URL. Wait for approval of the exact site and scopes, then poll at the response's instructed interval.
5. Wait for `access.state=installed`, then follow [OAuth setup](https://studio-dev.nettipoika.fi/agents/v1/oauth-setup.md) with the discovered registered `client_id` and the exact approved scope set. Stop on OAuth errors; never accept a broader fallback. Connect an OAuth-capable MCP client to `https://studio-dev.nettipoika.fi/mcp`. Discover authorization through `https://studio-dev.nettipoika.fi/.well-known/oauth-protected-resource/mcp`.
6. Let the OAuth client renew the maximum-15-minute access token using its protected rotating refresh credential. If renewal fails or the connection is revoked, stop and request fresh human authorization.

Never ask the human to paste credentials into chat. Keep bearer tokens, refresh credentials, private keys and verifiers in protected client storage and send access tokens only in the `Authorization` header.

## Operate the authorized websites

A connection reaches one website, or every website of the account when the
person approved that (`account:sites`). `GET /api/v1/sites` (`list_sites`)
returns the websites this connection may manage. With several websites, pass
the `site_id` of the website the person means on every site call; a call
without it returns `site_required` with the candidates, so ask the person
instead of guessing. If the selection question is skipped or unanswered,
wait for an explicit website before editing. Every site response and tool result names the website.

An account-wide connection can create a website with `POST /api/v1/sites`
(`create_site`) and an `Idempotency-Key`; poll the returned provisioning
operation, then edit the new `site_id`. Websites share the account's CPU,
memory and storage (`GET /api/v1/account/usage`, `get_account_usage`); a
refusal returns `account_limit` with a `reason`. To remove a website, call
`POST /api/v1/sites/{site_id}/deletion-requests` (`request_site_deletion`)
with an optional one-sentence `reason` the owner sees (at most 280
characters, no personal data or secrets; longer is `reason_too_long`),
show the person the returned `confirm_url` and end your turn: only the owner
can confirm on the dashboard. A deleted website's data still counts against
the account's storage (`retained_disk_bytes`) until it is purged after 30 days
or the owner deletes it permanently on the dashboard. Each website also needs
one of the account's database connections, so `pool.database_connections`
bounds how many websites the account can have.

The HTTP API is canonical; MCP exposes the same bounded capabilities. Use the returned site hostname instead of constructing one from the product origin. Reads require the documented scope. Every mutation requires an `Idempotency-Key`, and revision-aware file changes also require the current base revision and ETags.

Long-running work returns `202 Accepted` and a pollable operation. A candidate is not ready until its health check and public HTTPS verification succeed. Code rollback selects a retained compatible release and does not roll database contents back. Human approval remains mandatory for payment, sensitive scope grants, recovery and destructive database actions.

Serve fonts, scripts and styles from the website itself. Do not add
third-party fonts or CDNs, analytics, session replay or other tracking unless
the person asks for it. If they do, tell them that visitors' data then goes
to that third party and that their own privacy notice, and consent for
anything not strictly necessary, are their responsibility.

Use a conforming OAuth-capable MCP client; customers do not select protocol
versions. The same endpoint supports `2026-07-28`,
`2025-11-25` and `2025-06-18` with the same tools and authorization.

Raw clients using `2026-07-28` send stateless requests:
include `io.modelcontextprotocol/protocolVersion` and
`io.modelcontextprotocol/clientCapabilities` in `params._meta`, and send matching
`MCP-Protocol-Version` and `Mcp-Method` headers. For `tools/call`, also send
`Mcp-Name` matching `params.name`. `server/discover` optionally supplies supported
versions, capabilities and instructions; initialization is not required. Legacy
clients use `initialize`, then send the negotiated `MCP-Protocol-Version` on later
requests. Authentication and scope failures require the normal authorization
flow, never a broader scope or protocol fallback.

For a small change, call `GET /api/v1/sites/{site_id}/edit-context` (or
`get_edit_context`) to read the served source. Then send one
`POST /api/v1/sites/{site_id}/updates` (or `update_site`) with its
`base_source_commit`, a new idempotency key, and a targeted text replacement,
new file, or ETag-bound full-file write. Replacement text must occur exactly
once. The response has a `change_id` and waits briefly for public readiness;
poll only if the operation remains pending. Static and Django use verified
live saving; Go builds an immutable candidate.

To show the person a change before it is public, save it to the site's draft:
`POST /api/v1/sites/{site_id}/draft/updates` (or `update_site` with
`publish: false`) takes the same body and never changes the public site.
`GET /api/v1/sites/{site_id}/draft` (or `get_preview`) returns the draft
revision and an owner-only `preview.url`; only the signed-in owner can open it,
and it uses the site's live database. Publish exactly the revision the person
saw with `POST /api/v1/sites/{site_id}/draft/publish` (or `publish_site`) when
they ask, or discard it. While a draft has changes, a publish-now edit gets
`draft_open`.

For an image or document, upload original bytes through
`POST /api/v1/sites/{site_id}/assets` with `X-Asset-Name` and an
`Idempotency-Key`. Send `X-Asset-SHA256` when the original file's checksum is
available; a mismatch is rejected before storage. HTTP accepts at most 25 MiB
per file. For MCP `upload_asset`, compute `expected_sha256` from the original
attachment and send it with at most 1 MiB of base64; a mismatch is rejected.
The response contains a SHA-256 checksum
and an `/assets/{asset_id}/{name}` path. Include the asset ID in `asset_ids`
and the path in the update's new source text. The public path becomes available
only after that update succeeds. Do not assume a chat attachment reaches a
connector tool unless that exact client supplies its original bytes.

## Use a secret such as an API key

A Go or Django website can use secrets, such as a payment or map service key.
The owner sets each value in the dashboard; you never set or read one. Code
reads a set secret from the environment variable with the secret's name, or
from the file `/run/secrets/<NAME>`. Never print, log, commit or return a
value, and never put one in a page.

To get a new key, call `POST /api/v1/sites/{site_id}/secret-requests` (or
`request_site_secret`) with `name` and a short `description`, and give the
person the returned `dashboard_url`. Never ask them to paste a key into the
conversation. `GET /api/v1/sites/{site_id}/secrets` (or `list_site_secrets`)
shows each name with `state` (`set` or `empty`), `length` and `updated_at`;
no value or hash is ever returned. Saving a value restarts the website with
a health check; the previous values stay in use if the restart fails. A
draft preview uses the same values as the published website.

Names match `^[A-Z][A-Z0-9_]{0,63}$`. `NETTIPOIKA_*`, `PG*`, `PORT`,
`DATABASE_URL`, `DJANGO_SECRET_KEY` and `DJANGO_SECRET_KEY_FILE` are reserved
(`secret_name_reserved`). At most 50 secrets per website and 4 KiB per value
(`secret_limit_exceeded`). A static website has no secrets
(`secrets_unsupported_profile`). Builds and `run_build_command` never see
secrets. Exact values of 8 bytes or more may be replaced with
`[secret:NAME]` in output Nettipoika shows, but shorter, encoded or partial
values are not caught: the only protection is that your code never reveals
a value.

## Report a Nettipoika problem

When a Nettipoika call keeps failing and its error detail does not tell you
what to do, search first: `GET /api/v1/problem-reports` (or
`search_problem_reports`) with the problem `code`, the `tool` and a few words.
It shows this account's reports and published known issues. If one matches,
add your case with `POST /api/v1/problem-reports` (or `report_problem`) and
its reference as `join_report_id`; otherwise file a new report with `title`,
`steps`, `expected`, `actual`, `code`, `tool` and the `request_id` values of
the failed responses. Use a new `Idempotency-Key` per report and tell the
person the returned `reference`. Later, `GET /api/v1/problem-reports/{reference}`
(or `get_problem_report`) shows its `state` (`open`, `triaged`, `accepted`,
`rejected`, `duplicate`, `closed`) with a plain `state_detail`, and the
operator's public `known_issue` summary when one is published. The owner sees
the same reports on the dashboard under Tuki ja ongelmat. Do not report the website's own bugs,
account limits or errors whose detail explains the fix.

Never put personal data, conversation text, email addresses, passwords or
tokens in a report. Known secret shapes are replaced before storage, but only
as a safety net. The server adds the release, runtime profile and the
account's recent request IDs. Nettipoika staff read the report in Nettipoika;
the issue tracker receives only the reference and technical context. Limits:
title 200 characters, each text field 4 KiB, `excerpt` 8 KiB, 20
`request_ids`, 10 new reports an hour (`report_too_large`,
`report_rate_limited` with `Retry-After`).
