Muutto365 — Agent Interface

This page is the machine-native entry point for Muutto365, an insured moving company operating in Jyväskylä and across Finland. From here you can discover what the service can do, read the endpoint contract, and start making calls. No JavaScript is required to use any endpoint on this page.

Human website: https://muutto365.fi

Every page here also has a markdown representation: send Accept: text/markdown, or fetch /index.md directly.

Choose an interface

Nothing in the public customer journey requires an account. If a person could do it anonymously in a browser, so can you — do not register an MCP client just to price a move.

You areUseAuthCovers
A browser agent on muutto365.fi WebMCP in-page tools none Moving (/, /varaa-muutto/) and item removal (/poistopalvelu/)
A server, script or backend agent REST API none Moving and item removal
An MCP client with no browser session MCP server OAuth 2.1 + PKCE Moving only

WebMCP (in-page tools)

https://muutto365.fi/varaa-muutto/ — the booking form — registers WebMCP tools covering the whole public moving journey (item removal has its own page and tools, below; the homepage registers the discovery tools only). They drive the page's own controllers, so the visible form stays in step with what you do: the person at the browser can see and correct anything you set, and their edits are visible to your next call. Prefer this over DOM or screenshot guessing.

ToolDoes
get_servicesServices offered, with ids and option vocabularies
get_service_detailsWhat a quote needs for one service
get_service_areaWhere we operate and what actually limits a booking
get_policiesCancellation, liability, payment, prohibited items, privacy
calculate_quotePrice a job through the production pricing engine
explain_quoteThe cost lines behind the current quote and what drives them
check_availabilityArrival windows still free on a date
create_booking_draftStart a draft for a service
update_booking_draftChange draft fields through the form's validation
review_bookingMachine-readable summary of what would be submitted
submit_bookingHand off to agreement and card capture

Every tool returns {"ok": true, …} or {"ok": false, "error": {"code", "message", …}}. A validation error names the field; a quote failure distinguishes an unresolved address from a server fault. You never have to infer an outcome from a toast or a modal.

submit_booking does not create the booking

Creating one requires a card on file — a Stripe SetupIntent, stored and not charged. That is an anti-fraud control on an otherwise anonymous flow, the card fields live in a cross-origin Stripe iframe the page cannot fill, and being an agent does not waive it. 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 and booking_id: null. It will not report a booking 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.

ToolDoes
get_removal_serviceSize buckets with live VAT-inclusive base prices, the Yhteiskuljetusetu benefit, what cannot be collected, scheduling rules
calculate_removal_quotePrice a removal: breakdown, detected items, restricted
check_removal_availabilityArrival windows free on a date for this job, and whether it earns Yhteiskuljetusetu
create_removal_draftCreate or update the draft in the visible form
review_removal_bookingExactly what would be submitted, and what is still missing
submit_removal_bookingOpens the agreement for the visitor; never books by itself

No card is taken for item removal. submit_removal_booking stops because the consents and the final choice — Varaa nyt, Ota tarjous talteen or Jätä soittopyyntö — are the visitor's. Biological waste, hazardous chemicals, asbestos, medical waste, firearms, ammunition and explosives cannot be booked online; a call request still can.

Yhteiskuljetusetu: a removal booked onto a date Muutto365 is already collecting on costs 5 € less (VAT included). The server decides it from the chosen date alone and applies it once per booking; calculate_removal_quote returns it as price.discount_eur, already subtracted from total_eur. It never makes an unavailable date or arrival window bookable, and does not apply to moves.

Cancellation

The cancellation link in a confirmation email opens https://muutto365.fi/cancel.html?token=…, which registers get_cancellation_context, cancel_booking and submit_cancellation_feedback, scoped to that one booking by the token already in the URL. No tool anywhere accepts a cancellation token as an argument.

Discovery documents

Served from the same files on both muutto365.fi and agent.muutto365.fi, so discovery completes from either entry point.

PathContents
/.well-known/api-catalogRFC 9727 catalog of every interface
/.well-known/mcp/server-card.jsonMCP server card
/.well-known/agent-skills/index.jsonCapability index
/.well-known/oauth-protected-resourceRFC 9728 protected resource metadata
/.well-known/oauth-authorization-serverRFC 8414 authorization server metadata
/openapi.jsonOpenAPI 3.1 description of the REST API
/auth.mdWhat needs authentication, and what does not
/llms.txtOverview and workflow

Note that muutto365.fi is not itself an authorization server. It publishes the OAuth documents so an agent starting at the apex domain can find the real one; both name https://mcp.muutto365.fi as the issuer, and that is the value to use.

Recommended workflow

Follow this sequence to price and book a move via REST. MCP agents can skip to the MCP section and use the tools directly.

  1. Geocode both addresses — call GET /api/geocode for the pickup and drop-off addresses. Store the returned lat and lon for both. This step can be skipped if you already have coordinates, or if you are willing to let the backend geocode during the price call (slower).
  2. Estimate price and duration — call POST /api/price with move details and the coordinates from step 1. The response contains quoted_price_eur, estimated_move_time (hours), and an itemized breakdown.
  3. Find available timeslots — call GET /api/timeslots with the target date and the estimated_move_time from step 2. Returns the subset of the four daily windows that are still open.
  4. Preview the moving agreement — call POST /api/bookings/agreement/preview with the customer, move, window, and price summary. Present the returned PDF and retain its X-Idempotency-Key response header.
  5. Record explicit acceptance — after the customer accepts the displayed agreement, call POST /api/bookings/agreement/confirm with the idempotency key and "accepted": true. Previewing alone is not acceptance.
  6. Create the booking — call POST /api/booking with all move details, the chosen timeslot, the customer's contact information, and the accepted agreement_idempotency_key. Returns a booking_id. The backend re-runs pricing server-side.
  7. Deliver the cancel token to the customer — the booking confirmation email includes a cancellation link. If you need to cancel programmatically, use POST /api/cancel with the token from the confirmation email.

MCP interface

If your agent framework supports the Model Context Protocol, connect to the MCP endpoint instead of calling REST directly. The MCP server exposes a moving-only booking workflow and handles auth through the Origo OAuth 2.1 provider. Public dynamic client registration and PKCE require no pre-provisioned credentials. Item-removal (poistopalvelu) booking tools are not exposed over MCP; use the WebMCP tools on /poistopalvelu/ or the REST API.

Connection details

Muutto365 uses the origo package (source) for OAuth on the MCP server.

Endpoint
https://mcp.muutto365.fi/mcp
Transport
Streamable HTTP; production compatibility version MCP 2025-11-25. Session-bearing: initialize returns an Mcp-Session-Id and you must send it back on subsequent requests.
Authorization server discovery
https://mcp.muutto365.fi/.well-known/oauth-authorization-server
Protected resource metadata
https://mcp.muutto365.fi/.well-known/oauth-protected-resource
Path-specific protected resource metadata
https://mcp.muutto365.fi/.well-known/oauth-protected-resource/mcp
Client registration
POST https://mcp.muutto365.fi/register — public, no prior approval required
Token endpoint
POST https://mcp.muutto365.fi/token

Auth flow summary

  1. Fetch the authorization server metadata to confirm endpoints.
  2. Register a public client: POST /register with {"redirect_uris":["your-callback-uri"],"token_endpoint_auth_method":"none"}. Receive a client_id; no client secret is issued.
  3. Complete the authorization code flow with PKCE and resource https://mcp.muutto365.fi/mcp to obtain an access token.
  4. Include the token as Authorization: Bearer <token> on all requests to /mcp.

Available tools

get_moving_quote
Resolve the full pickup and drop-off addresses and return the authoritative moving price and duration used by the agreement and booking tools. Use fallback_distance_km only if routing is temporarily unavailable.
get_timeslots
Return available arrival windows for a date. Required: move_date (ISO 8601), estimated_duration (hours, from get_moving_quote). Returns available_windows as a list of window keys.
preview_booking_agreement
Re-price the exact move and create a non-accepted agreement preview. Returns the agreement key, a base64-encoded PDF, and a summary. Present these to the customer.
accept_booking_agreement
Record explicit customer acceptance. Call only after the customer accepts the preview, passing the agreement key, matching customer email, and accepted: true.
create_booking
Create a confirmed moving booking. Requires the same details that were previewed, a chosen timeslot, and the accepted agreement_idempotency_key. Returns booking_id; confirmation and cancellation details are delivered to the customer by email.
get_price (legacy compatibility)
Distance-based estimator retained for existing clients. New clients should use get_moving_quote so routing, rush-hour, and deadhead inputs match booking.

REST API

Base URL: https://api.muutto365.fi

All endpoints are public. No API key or authentication header is required.

Item removal has its own endpoints under /api/poistopalvelu/ (pricing, quote, agreement preview, booking, save-for-later, call request); they are described in openapi.json.

All responses are JSON. POST /api/price accepts application/x-www-form-urlencoded; POST /api/booking and POST /api/cancel accept application/json.

GET /api/geocode — Resolve a Finnish address to coordinates

Calls the self-hosted Nominatim instance. Results are cached for 24 hours. Pass the geocode-safe street (name + house number, no apartment suffix) as street; apartment suffixes like "A 12" are handled automatically if you include them — they are stripped before the geocoding call.

Parameters

street (required, string)
Street name and house number. Example: Kauppakatu 15 or Kauppakatu 15 A 12
city (required, string)
City name. Example: Jyväskylä
postcode (required, string)
Finnish 5-digit postal code. Example: 40100

Example

curl "https://api.muutto365.fi/api/geocode?street=Kauppakatu%2015&city=Jyv%C3%A4skyl%C3%A4&postcode=40100"

Response

{
  "lat": 62.2415,
  "lon": 25.7209
}

POST /api/price — Estimate move price and duration

Content-Type: application/x-www-form-urlencoded. If you supply all six address fields (from_street, from_city, from_postal_code, to_street, to_city, to_postal_code), the backend resolves road distance via OSRM and ignores trip_length_km_1way. Pass pre-resolved coordinates (from_lat, from_lon, to_lat, to_lon) alongside address fields to skip re-geocoding.

Parameters

rooms (integer, default 0)
Number of rooms being moved.
square_meters (integer, default 0)
Apartment size in square metres.
item_density (string, default "standard")
How full the apartment is. One of: minimalist, standard, large, well equipped
trip_length_km_1way (float, default 0.0)
One-way road distance in kilometres. Used only when address fields are absent.
from_floor (integer, default 0)
Pickup floor number. Ground floor = 0.
from_postal_code (string, default "")
Pickup Finnish 5-digit postal code.
to_floor (integer, default 0)
Drop-off floor number. Ground floor = 0.
to_postal_code (string, default "")
Drop-off Finnish 5-digit postal code.
elevator_available (string "true"/"false", default "false")
Whether an elevator is available at the pickup address.
carrying_distance_m (integer, default 0)
Distance in metres the crew must carry items from the door to the vehicle.
items_need_packing (string "true"/"false", default "false")
Whether packing service is requested.
is_student (string "true"/"false", default "false")
Apply student discount.
from_street, from_city, to_street, to_city (strings)
Address fields for automatic OSRM distance resolution.
from_lat, from_lon, to_lat, to_lon (floats, optional)
Pre-resolved coordinates. When all four are present, Nominatim geocoding is skipped.

Example

curl -X POST https://api.muutto365.fi/api/price \
  -d "rooms=3" \
  -d "square_meters=65" \
  -d "item_density=standard" \
  -d "from_floor=2" \
  -d "from_postal_code=40100" \
  -d "to_floor=0" \
  -d "to_postal_code=40200" \
  -d "elevator_available=false" \
  -d "carrying_distance_m=20" \
  -d "items_need_packing=false" \
  -d "is_student=false" \
  -d "from_street=Kauppakatu+15" \
  -d "from_city=Jyv%C3%A4skyl%C3%A4" \
  -d "to_street=Vapaudenkatu+38" \
  -d "to_city=Jyv%C3%A4skyl%C3%A4" \
  -d "from_lat=62.2415" \
  -d "from_lon=25.7209" \
  -d "to_lat=62.2388" \
  -d "to_lon=25.7301"

Response

{
  "quoted_price_eur": 245.00,
  "estimated_move_time": 3.5,
  "price_itemized": {
    "base": 80.00,
    "drive": 12.00,
    "volume": 55.00,
    "carry": 8.00,
    "packing": 0.00,
    "elevator": 0.00,
    "subtotal": 155.00,
    "discount_abs": 0.00,
    "alv_abs": 39.53,
    "final_price": 245.00
  },
  "time_itemized": {
    "base": 1.5,
    "volume": 1.2,
    "carry": 0.3,
    "drive": 0.5,
    "total": 3.5
  }
}

All prices are in EUR and include Finnish VAT at 25.5%.

GET /api/timeslots — Available arrival windows

Returns the subset of the four daily arrival windows that are still available for the given date and estimated move duration. Call this after POST /api/price to present real-time availability.

Parameters

move_date (required, ISO 8601 date)
The requested move date. Example: 2026-06-15
estimated_duration (float, default 2.0, range 0.5–24.0)
Estimated move duration in hours, from estimated_move_time in the price response.

Example

curl "https://api.muutto365.fi/api/timeslots?move_date=2026-06-15&estimated_duration=3.5"

Response

{
  "move_date": "2026-06-15",
  "available_windows": ["08-09", "12-13", "16-17"]
}

An empty available_windows list means no windows are open on that date. Try a different date.

If rejection_reason: "overtime" is present, the move duration exceeds the absolute maximum (15 hours) and cannot be booked on any date. A duration between 12 and 15 hours is not itself an overtime rejection — see "Scheduling rules" below for what it takes to still get a window.

POST /api/nlp — Detect special-handling items in free text

Content-Type: application/json. Runs Finnish text through a three-stage pipeline: Voikko morphological normalisation, dictionary matching, and multilingual-e5-large embedding similarity. Use this before creating a booking to check whether the customer's item description contains anything that requires special equipment or manual contact. Items with blocking: true cannot be booked through the standard flow — the customer must contact Muutto365 directly.

Request body

text (string, required, max 5000 chars)
Free-text description of items or move details. Finnish input performs best. Control characters are stripped server-side.
context (string, required)
Source of the text. One of: heavy_items — from the explicit heavy-items field, move_description — from the general item inventory field, lisatiedot — from the additional notes field. Used for analytics only; does not affect detection logic.

Example

curl -X POST https://api.muutto365.fi/api/nlp \
  -H "Content-Type: application/json" \
  -d '{"text": "Meillä on flyygeli ja iso akvaario", "context": "move_description"}'

Response

{
  "detected_items": [
    { "key": "grand_piano", "label_fi": "Flyygeli",    "confidence": 0.97, "blocking": true  },
    { "key": "aquarium",    "label_fi": "Iso akvaario","confidence": 0.94, "blocking": true  }
  ]
}

An empty detected_items array means no recognised special-handling items were found. confidence is the cosine similarity score (0–1); dictionary hits always return 0.98. Items are sorted: blocking first, then by confidence descending.

If the NLP service is temporarily unavailable the endpoint returns {"detected_items": []} rather than an error, so callers do not need special handling for a 503 from this endpoint.

POST /api/booking — Create a booking

Content-Type: application/json. The backend re-calculates the price server-side using the same engine as /api/price. The quoted price in the request body is not trusted. Returns HTTP 201 on success.

Request body

customer_name (string, required, max 200 chars)
Full name of the customer.
phone (string, required, max 50 chars)
Finnish phone number. Accepted formats: +358401234567, 0401234567, 040 123 4567
email (string, required)
Customer email address. Used to send booking confirmation and cancellation link.
from_address (string, required, max 500 chars)
Full pickup street address including apartment if applicable. Example: Kauppakatu 15 A 12
from_city (string, required)
Pickup city.
from_postal_code (string, required)
Pickup Finnish 5-digit postal code.
to_address (string, required, max 500 chars)
Full drop-off street address including apartment if applicable.
to_city (string, required)
Drop-off city.
to_postal_code (string, required)
Drop-off Finnish 5-digit postal code.
move_date (ISO 8601 date, required)
Must be today or in the future.
timeslot (string, required)
One of: 08-09, 12-13, 16-17, 20-21. Must be in available_windows from /api/timeslots.
trip_length_km_1way (decimal, required)
One-way road distance in kilometres. Use the value resolved by the price call.
rooms (integer, required, 1–20)
square_meters (integer, optional)
item_density (string, optional)
One of: minimalist, standard, large, well equipped
from_floor (integer, required)
to_floor (integer, required)
from_elevator_available (boolean, required)
Whether the pickup building has a usable elevator.
to_elevator_available (boolean, required)
Whether the drop-off building has a usable elevator.
carrying_distance_m (integer, optional)
items_need_packing (boolean, required)
vehicle_type (string, required)
One of: van, van_trailer
agreement_idempotency_key (string, required)
Key returned by agreement preview and explicitly accepted through the confirmation endpoint.
is_student (boolean, default false)
extra_notes (string, optional, max 5000 chars)
Free-text operational notes for the crew (access restrictions, narrow stairs, parking, etc.).
move_description (string, optional, max 3000 chars)
Free-text inventory of items being moved. Processed by the NLP pipeline on the booking form — pass it here so it is stored with the booking. Check POST /api/nlp first if the description may contain blocking items.
heavy_items_description (string, optional, max 2000 chars)
Explicit description of heavy or special-handling items. Also NLP-processed. Omit if the customer did not flag any heavy items.

Example

curl -X POST https://api.muutto365.fi/api/booking \
  -H "Content-Type: application/json" \
  -d '{
    "customer_name": "Matti Meikäläinen",
    "phone": "0401234567",
    "email": "[email protected]",
    "from_address": "Kauppakatu 15 A 12",
    "from_city": "Jyväskylä",
    "from_postal_code": "40100",
    "to_address": "Vapaudenkatu 38",
    "to_city": "Jyväskylä",
    "to_postal_code": "40200",
	    "move_date": "2026-09-15",
    "timeslot": "08-09",
    "trip_length_km_1way": 2.4,
    "rooms": 3,
    "square_meters": 65,
    "item_density": "standard",
    "from_floor": 2,
    "to_floor": 0,
	    "from_elevator_available": false,
	    "to_elevator_available": false,
    "carrying_distance_m": 20,
    "items_need_packing": false,
    "vehicle_type": "van",
	    "is_student": false,
	    "agreement_idempotency_key": "key-returned-by-agreement-preview"
  }'

Response (HTTP 201)

{
  "status": "success",
  "message": "Booking received securely.",
  "booking_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}

The customer receives a confirmation email with a cancellation link. The booking_id is a UUID.

Conflict response (HTTP 409)

{
  "detail": "Pahoittelut! Tämä aika varattiin juuri. Valitse toinen aika."
}

The timeslot was taken between your availability check and booking attempt. Re-fetch timeslots and retry with a different window.

POST /api/cancel — Cancel a booking

Content-Type: application/json. Requires the cancel_token from the confirmation email sent to the customer. The backend does not expose the token via any query endpoint — it must be obtained from the email.

Request body

token (string, required)
The cancellation token from the customer's confirmation email.

Example

curl -X POST https://api.muutto365.fi/api/cancel \
  -H "Content-Type: application/json" \
  -d '{"token": "your-cancel-token-here"}'

Response

{
  "status": "success",
  "message": "Varauksesi on peruutettu onnistuneesti."
}

Constraints and valid values

Dates and times

Arrival windows

KeyArrival rangeLatest arrival
08-0908:00 – 09:0009:00
12-1312:00 – 13:0013:00
16-1716:00 – 17:0017:00
20-2120:00 – 21:0021:00

Scheduling rules

There is no daily crew-hours cap and no rule that a long job blocks the rest of its day on its own — only actual overlap with another booked block does.

Pricing inputs

item_density
  • minimalist — sparse furnishings, few large items
  • standard — typical furnished apartment
  • large — well-furnished, many heavy items
  • well equipped — fully furnished, lots of items
vehicle_type
  • van — standard moving van
  • van_trailer — van with trailer, for larger moves

Address format

Finnish addresses follow the pattern <street name> <house number> [<staircase> <apartment>]. The backend automatically splits apartment information from the geocoding-safe part. You may include the full address including apartment in from_address and to_address — it will be stored in full and the apartment suffix will be parsed separately.

VAT

All prices are in EUR and include Finnish VAT at 25.5%.

Rate limits

EndpointLimitWindow
POST /api/price120per minute
GET /api/timeslots30per minute
GET /api/geocode20per minute
POST /api/nlp30per minute
POST /api/cancel10per hour
POST /api/booking5per hour

Rate limits are per IP address. Exceeding a limit returns HTTP 429.

Response codes

200 OK
Request succeeded. Response body contains the result.
201 Created
Booking created successfully.
404 Not Found
Address could not be geocoded, or cancellation token is invalid.
409 Conflict
Timeslot was taken between availability check and booking. Re-fetch timeslots and retry.
422 Unprocessable Entity
Request body failed validation. Response contains a detail array describing each invalid field.
429 Too Many Requests
Rate limit exceeded. Back off and retry.
500 Internal Server Error
Unexpected server error. Safe to retry with exponential backoff.
503 Service Unavailable
Geocoding or routing service temporarily unreachable. Retry after a short delay.