# Muutto365 — agent documentation

Machine-native entry point for <https://muutto365.fi>, a Finnish moving company
based in Jyväskylä. This is the markdown representation of
<https://agent.muutto365.fi/>.

Muutto365 prices and books jobs automatically. There is no quote request and no
callback: an agent can go from an address pair to a confirmed arrival window
without a human in the loop on our side.

- All prices are EUR, **VAT included** (25.5%).
- All dates are ISO 8601. All times are **Europe/Helsinki**.
- Pricing and scheduling are computed server-side. A price supplied by a client
  is never trusted — `/api/booking` re-prices the job before writing it.

## Pick an interface

| Interface | Use when | Auth | Endpoint |
| --- | --- | --- | --- |
| **WebMCP** | You are a browser agent on muutto365.fi | none | in-page tools |
| **REST** | You are a server, script or backend agent | none | `https://api.muutto365.fi` |
| **MCP** | You are an MCP client with no browser session | OAuth 2.1 + PKCE | `https://mcp.muutto365.fi/mcp` |

If a person could do it anonymously in a browser, you do not need an account to
do it either. Do not register an MCP client just to price a move.

## WebMCP (in-page tools)

`https://muutto365.fi/varaa-muutto/` — the booking form — registers a WebMCP
tool surface covering the whole public moving journey (item removal has its
own page — see below). The homepage registers the discovery tools only. The tools
drive the page's own controllers, so the visible form stays in step with what
you do — a person watching can see and correct every value you set, and any
edit they make is visible to your next call.

| Tool | Does |
| --- | --- |
| `get_services` | Services offered, with ids and option vocabularies |
| `get_service_details` | What a quote needs for one service |
| `get_service_area` | Where we operate and what actually limits a booking |
| `get_policies` | Cancellation, liability, payment, prohibited items, privacy |
| `calculate_quote` | Price a job through the production pricing engine |
| `explain_quote` | The cost lines behind the current quote and what drives them |
| `check_availability` | Arrival windows still free on a date |
| `create_booking_draft` | Start a draft for a service |
| `update_booking_draft` | Change draft fields through the form's validation |
| `review_booking` | Machine-readable summary of what would be submitted |
| `submit_booking` | Hand off to agreement + card capture |

Every tool returns `{ ok: true, … }` or
`{ ok: false, error: { code, message, … } }`. Failures are structured: a
validation error names the field, a quote failure distinguishes an unresolved
address from a server fault, and no tool asks you to infer an outcome from a
toast or a modal.

**`submit_booking` does not create the booking.** Creating one requires a card
on file (stored via Stripe, not charged) — an anti-fraud control on an
otherwise anonymous flow. The card fields live in a cross-origin Stripe iframe
that the page cannot fill, and being an agent does not waive the control. The
tool advances the visitor to the same agreement-and-card step the human submit
button reaches, and returns
`status: "requires_human_confirmation"` with `submitted: false`. It never
returns a booking id it did not get.

### Item removal: `/poistopalvelu/`

`https://muutto365.fi/poistopalvelu/` is item removal's own page, and it
registers its own tools. (On the homepage, `get_services` lists the service as
a pointer to it; the homepage tools cannot draft it.)

| Tool | Does |
| --- | --- |
| `get_removal_service` | Size buckets with live VAT-inclusive base prices, the Yhteiskuljetusetu benefit, what cannot be collected, scheduling rules |
| `calculate_removal_quote` | Price a removal: breakdown, detected items, `restricted` |
| `check_removal_availability` | Arrival windows free on a date for this job, and whether it earns Yhteiskuljetusetu |
| `create_removal_draft` | Create or update the draft in the visible form |
| `review_removal_booking` | Exactly what would be submitted, and what is still missing |
| `submit_removal_booking` | Opens the agreement for the visitor — never books by itself |

The visitor ticks the consents and chooses Varaa nyt, Ota tarjous talteen or
Jätä soittopyyntö. Biological waste, hazardous chemicals, asbestos, medical
waste, firearms, ammunition and explosives cannot be booked online (a call
request still can). No card is taken for item removal.

**Yhteiskuljetusetu**: a removal booked onto a date Muutto365 is already
collecting on costs 5 € less (VAT included). The server decides it from the
calendar date alone and applies it once per booking, however many other
collections that date holds; `calculate_removal_quote` returns it as
`price.discount_eur`, already subtracted from `total_eur`, and
`check_removal_availability` says whether a given date earns it. It never
makes an unavailable date or arrival window bookable, and does not apply to
moves.

## REST API

Base URL `https://api.muutto365.fi`. All endpoints public, no API key.
Full description: [`/openapi.json`](https://muutto365.fi/openapi.json).

| Endpoint | Purpose | Limit |
| --- | --- | --- |
| `GET /api/geocode` | Resolve a Finnish address to lat/lon | 20/min |
| `POST /api/price` | Estimate move price and duration (form-encoded) | 120/min |
| `GET /api/poistopalvelu/pricing` | Item-removal size buckets with VAT-inclusive base prices | — |
| `POST /api/poistopalvelu/quote` | Price an item removal (JSON): breakdown, detected items, `restricted` | 60/min |
| `POST /api/poistopalvelu/booking` | Book an item removal; also `/agreement/preview`, `/save-for-later`, `/call-requested` | — |
| `GET /api/timeslots` | Arrival windows available for a date | 30/min |
| `POST /api/nlp` | Detect special-handling items in Finnish text | 30/min |
| `POST /api/bookings/agreement/preview` | Render the agreement, returns `X-Idempotency-Key` | — |
| `POST /api/bookings/agreement/confirm` | Record acceptance of that agreement | — |
| `POST /api/booking` | Create a booking | 5/hour |
| `POST /api/cancel` | Cancel using the emailed token | 10/hour |

### Workflow

1. `GET /api/geocode` for both addresses → keep `lat`/`lon`.
2. `POST /api/price` with the job details and those coordinates → `price_itemized.final_price` and `time_itemized.total`.
3. `GET /api/timeslots` with `move_date` and that duration → `available_windows`.
4. Optional: `POST /api/nlp` on the customer's item description. Anything with `blocking: true` cannot go through the standard flow.
5. `POST /api/bookings/agreement/preview` → show the PDF, retain `X-Idempotency-Key`.
6. After the customer actually accepts, `POST /api/bookings/agreement/confirm`.
7. `POST /api/booking` with the chosen window and `agreement_idempotency_key`.

`/api/price` is form-encoded rather than JSON on purpose: a simple request
skips the CORS preflight round-trip from a browser.

## MCP server

`https://mcp.muutto365.fi/mcp` — Streamable HTTP, protocol version `2025-11-25`.
Session-bearing: `initialize` returns an `Mcp-Session-Id`, and you must send it
back on subsequent requests. Server card:
[`/.well-known/mcp/server-card.json`](https://muutto365.fi/.well-known/mcp/server-card.json).

Tools: `get_price`, `get_timeslots`, `create_booking`.

Scope: **moving only.** Item removal is not exposed here; use the WebMCP tools
on `/poistopalvelu/` or REST for it.

OAuth 2.1 via [Origo](https://github.com/ieepirzy/origo), with open dynamic
client registration — register with `token_endpoint_auth_method: "none"` for a
secretless PKCE client. Details in [`/auth.md`](https://muutto365.fi/auth.md).

## Constraints that will bite you

- **Arrival windows:** `08-09`, `12-13`, `16-17`, `20-21`. The window is when
  the crew arrives, not how long the job takes.
- **Booking horizon:** `move_date` must be today or at most 90 days ahead.
- **Duration cutoff:** a job estimated over 12 hours cannot be booked
  automatically. Contact Muutto365 instead.
- **Empty availability is a real answer**, not an error: the day is full, or
  what is left of it no longer fits this job.
- **409 on booking** means the window was taken while your request was in
  flight. Re-fetch timeslots and pick again.
- **403 on booking** means no card is on file for the agreement.
- **Vehicle type** is chosen by Muutto365, not the customer.
- **Service area:** no fixed boundary. Distance is priced, not gated — but both
  addresses must geocode.

### Vocabularies

- `item_density`: `minimalist`, `standard`, `large`, `well equipped`
- `housing_type`: `omakotitalo`, `paritalo`, `rivitalo`, `huoneisto`, `muu`
- `elevator_size`: `pieni`, `keskikokoinen`, `iso`
- `size_bucket` (item removal): `small`, `medium`, `large`, `extra_large` — `extra_large` also needs `square_meters` (1–1000) and `rooms` (1–50)
- `vehicle_type`: `van`, `van_trailer`

## Discovery

| Document | Path |
| --- | --- |
| Interface catalog (RFC 9727) | `/.well-known/api-catalog` |
| MCP server card | `/.well-known/mcp/server-card.json` |
| Capability index | `/.well-known/agent-skills/index.json` |
| Protected resource metadata (RFC 9728) | `/.well-known/oauth-protected-resource` |
| Authorization server metadata (RFC 8414) | `/.well-known/oauth-authorization-server` |
| OpenAPI 3.1 | `/openapi.json` |
| Authentication guide | `/auth.md` |
| Overview | `/llms.txt` |

Served on both `muutto365.fi` and `agent.muutto365.fi`, from the same files.

Any page here returns markdown instead of HTML if you ask for it:
`Accept: text/markdown`.

## Legal and contact

- Terms of service: <https://muutto365.fi/TOS.md>
- Privacy policy: <https://muutto365.fi/PP.md>
- Email: asiakaspalvelu@muutto365.fi
- Phone: +358 45 176 6384

If something in this document is wrong or out of date, that is a bug — please
report it to the address above.
