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.
| 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 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.
| 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 |
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.
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."
}