Stanovina.me · REST API v1
API Documentation
Manage your own listings and profile programmatically. The API is a first-class counterpart to the dashboard: everything it validates, the dashboard validates — including required fields, photo rules and moderation.
Overview
A machine-readable description of everything below is served at /openapi.json and linked from every page as rel="service-desc". The endpoints the website's own pages call are not part of this contract: they authenticate with a session cookie, change whenever the interface does, and are not supported for integrations.
Base URL: https://stanovina.me/api/v1. All requests and responses are JSON (photo upload also accepts multipart/form-data). The API operates strictly on your own resources: your profile and the listings you own. Agencies and paid promotions are not accessible through the API.
curl https://stanovina.me/api/v1/me \ -H "Authorization: Bearer stv_YOUR_KEY"
Using an AI assistant? Skip the HTTP calls and connect it to the MCP server.
AI agents (MCP)
The same API is available as a Model Context Protocol server, so an AI assistant — Claude, Cursor, ChatGPT/Codex, your own agent — can create, edit and publish your listings in conversation. There is nothing to install: it is one URL and the API key you already use.
URL: https://stanovina.me/mcp Header: Authorization: Bearer stv_YOUR_KEY Protocol: MCP Streamable HTTP (stateless, JSON responses)
Create a key as described under Authentication. Give it only the scopes the assistant needs: a key with listings:read lets it look but not change anything. The server does nothing a REST call with the same key could not do — scopes, rate limits, validation and moderation are the same.
Public search server (no key)
A second, read-only MCP server lets an assistant search the live listings for a user. It needs no key and no account. It has four tools: search_listings, get_listing, find_places and get_market_summary. It never returns seller names, phone numbers or email addresses. The limit is 60 tool calls per minute per IP address.
URL: https://stanovina.me/mcp/search Auth: none Protocol: MCP Streamable HTTP (stateless, JSON responses) Card: https://stanovina.me/.well-known/mcp/server-card.json
Connect
Claude Code
claude mcp add --transport http stanovina https://stanovina.me/mcp \ --header "Authorization: Bearer stv_YOUR_KEY"
Cursor and other mcp.json clients
{
"mcpServers": {
"stanovina": {
"url": "https://stanovina.me/mcp",
"headers": { "Authorization": "Bearer stv_YOUR_KEY" }
}
}
}Devin Desktop (formerly Windsurf)
File ~/.codeium/mcp_config.json (older Windsurf installs: ~/.codeium/windsurf/mcp_config.json). This client calls the URL key serverUrl, not url.
{
"mcpServers": {
"stanovina": {
"serverUrl": "https://stanovina.me/mcp",
"headers": { "Authorization": "Bearer stv_YOUR_KEY" }
}
}
}Claude Desktop
File claude_desktop_config.json. Goes through the mcp-remote bridge, which needs Node.js.
{
"mcpServers": {
"stanovina": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://stanovina.me/mcp",
"--header", "Authorization:${STANOVINA_AUTH}"],
"env": { "STANOVINA_AUTH": "Bearer stv_YOUR_KEY" }
}
}
}Codex
File ~/.codex/config.toml. Put the key in the STANOVINA_API_KEY environment variable.
[mcp_servers.stanovina] url = "https://stanovina.me/mcp" bearer_token_env_var = "STANOVINA_API_KEY"
Check without a client (curl)
curl -X POST https://stanovina.me/mcp \
-H "Authorization: Bearer stv_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'Clients that only accept OAuth sign-in (for example the “custom connector” form in the Claude and ChatGPT web apps) cannot send a bearer header and are not supported yet. Use one of the clients above.
What to ask
Create a listing on Stanovina: 2-bedroom apartment for sale in Budva, 62 m², €185,000, description and photos from the files in this folder. Save it as a draft and show me before you publish.
The server tells the assistant the order of operations when it connects, so you do not need to explain the API. Photos are attached from public https URLs; an assistant cannot upload files through MCP, so host the images first (or use the multipart upload over REST). The assistant cannot publish what the account cannot: a verified phone number and at least one photo are still required.
Tools
| Tool | REST equivalent | Scope |
|---|---|---|
get_enums | GET /api/v1/reference/enums | any |
list_countries | GET /api/v1/reference/countries | any |
list_municipalities | GET /api/v1/reference/municipalities | any |
list_cities | GET /api/v1/reference/cities | any |
list_districts | GET /api/v1/reference/districts | any |
get_profile | GET /api/v1/me | profile:read |
update_profile | PATCH /api/v1/me | profile:write |
list_listings | GET /api/v1/listings | listings:read |
get_listing | GET /api/v1/listings/{id} | listings:read |
create_listing | POST /api/v1/listings | listings:write |
update_listing | PATCH /api/v1/listings/{id} | listings:write |
delete_draft_listing | DELETE /api/v1/listings/{id} | listings:write |
publish_listing | POST /api/v1/listings/{id}/publish | listings:write |
set_listing_status | POST /api/v1/listings/{id}/status | listings:write |
list_listing_photos | GET /api/v1/listings/{id}/images | listings:read |
add_listing_photos | POST /api/v1/listings/{id}/images | listings:write |
reorder_listing_photos | PATCH /api/v1/listings/{id}/images | listings:write |
delete_listing_photo | DELETE /api/v1/listings/{id}/images/{imageId} | listings:write |
A failed call comes back as a tool result marked as an error, with the usual error body. A wrong or revoked key is refused when the client connects, and counts toward the same 1-hour IP ban as REST requests.
Authentication
Every request must carry an API key in the Authorization header:Bearer stv_.... Keys are created in your dashboard → API keys section. API access is enabled per account (or per agency) by our team on request — if you don't have it yet, email [email protected] with a short description of your integration.
- The full key is shown once, at creation. Only its hash is stored.
- Keys act with the identity and permissions of the account that created them.
- Agency keys. A key created by a member of an agency belongs to the agency. Every member sees the same keys in the dashboard and can revoke any of them, and a key keeps working after the member who created it leaves. It reaches all of the agency's listings, not only the creator's own. A listing created with it is filed under the agency and owned by the member who made the key — or by the agency owner once that member has left. API access for an agency key is switched on for the agency as a whole. Users who are not in an agency keep personal keys, which reach only their own listings.
GET /api/v1/mereturns ascopefield that tells the two apart. - Revoking a key (dashboard) is immediate and permanent.
- If API access is switched off for the account, existing keys stop working (HTTP 403, code
api_access_disabled) but are not deleted; they resume if access is re-enabled.
Retry-After header. Configure your key once and this will never affect you.Scopes
Each key carries a set of scopes chosen at creation. A write scope implies the matching read scope. A request outside the key's scopes answers 403 insufficient_scope.
| Scope | Grants |
|---|---|
profile:read | Read your profile |
profile:write | Update your profile (includes read) |
listings:read | List and read your listings and photos |
listings:write | Create/edit listings, manage photos, publish, change status (includes read) |
The reference-data endpoints accept any valid key regardless of scopes.
Rate limits
| Requests | Budget (per key) |
|---|---|
| Reads (GET) | 120 / minute |
| Writes (POST/PATCH/DELETE) | 30 / minute |
| Photo attachment (upload or URL fetch) | 10 / minute |
Exceeding a budget answers 429 rate_limited with a Retry-After header (seconds).
Errors
Every error has one shape. field appears on validation errors and names the offending field; retryAfter (seconds) appears on 429s.
{
"error": {
"code": "validation_error",
"message": "price must be a finite number greater than zero",
"field": "price"
}
}| HTTP | Code | Meaning |
|---|---|---|
| 401 | unauthorized | Missing or malformed Authorization header |
| 401 | invalid_key | Key not recognized (counts toward the IP ban) |
| 401 | key_revoked | Key was revoked in the dashboard |
| 403 | api_access_disabled | API access is switched off for this account |
| 403 | account_banned | The account is banned |
| 403 | insufficient_scope | The key lacks the required scope |
| 403 | phone_not_verified | Publishing requires a verified phone number |
| 404 | not_found | No such resource owned by you |
| 400 | validation_error | A field is missing or invalid (see field) |
| 400 | invalid_status_transition | The requested status change is not allowed from the current status |
| 403 | agency_key_profile_readonly | Agency key whose creator has left: GET /api/v1/me returns only the scope and PATCH /api/v1/me is refused |
| 409 | managed_by_feed | The listing is managed by an XML feed; it can be read but not changed through the API |
| 409 | conflict | e.g. the phone number is verified by another account |
| 429 | rate_limited | Rate limit or IP ban; see Retry-After |
| 500 | internal_error | Our side; retry later |
Listing lifecycle
A listing is created as a DRAFT — invisible to everyone but you. When you publish it, it either goes live immediately (ACTIVE, for accounts marked trusted) or enters review (PENDING_MODERATION) exactly as on the website. Publishing requires at least one photo and a verified phone number on the account — the number is displayed publicly on the listing.
DRAFT ──publish──▶ PENDING_MODERATION ──approved──▶ ACTIVE
│ │
└──rejected──▶ REJECTED ──publish─┘ (resubmit)
ACTIVE ⇄ SOLD / RENTED (owner, via /status)- Editing a
DRAFTkeeps it a draft — publishing is always explicit. - Editing a live listing keeps it live; editing one still in review re-queues it (unless the account is trusted).
- If a listing is rejected,
moderationNotecarries the reason; fix and publish again. - Only
DRAFTlistings can be deleted via the API.
Profile
/api/v1/meprofile:readReturns your profile.
/api/v1/meprofile:writePartial update. Accepted fields: firstName, lastName, phone, userType (INDIVIDUAL | COMPANY), companyName, companyTaxId, instagram, facebook, telegram, hasWhatsapp, hasViber, agentLicense.
Changing phone (international format, e.g. +38267123456) resets its verified status unless your account is linked to an OAuth provider; verify the new number in the dashboard. A number already verified by another account answers 409.
curl -X PATCH https://stanovina.me/api/v1/me \
-H "Authorization: Bearer stv_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"firstName": "Ana", "hasWhatsapp": true}'Listings
/api/v1/listingslistings:readYour listings, newest first. Query: ?status= (any listing status), ?page=, ?limit= (max 100).
/api/v1/listingslistings:writeCreate a listing (as DRAFT). Required fields — the same set the website form requires:
title,description— primary language (Montenegrin);titleEn/titleRu/descriptionEn/descriptionRuoptionalpropertyType,listingType— see enumscountryId— from/api/v1/reference/countriesprice,area— positive numberspriceOnRequest— optional boolean. Set it totruefor a listing whose asking price is not public yet:pricebecomes unnecessary and is stored as0. Such listings show “Cijena na upit” instead of a price, are skipped by thepriceFrom/priceTofilters, sort last underpriceAsc/priceDesc, and are left out of the price statistics. APATCHsetting it back tofalsemust send a realpricein the same request.
Optional: cityId, districtId (must belong to the city), currency, rooms, bedrooms, bathrooms, floor, totalFloors, yearBuilt, landArea, furnishing, latitude/longitude, address, distanceToSea, rental fields (deposit, utilitiesIncluded, utilitiesPrice, availableFrom, petsAllowed, annualRent) and amenity flags (hasParking, hasPool, hasBalcony, …). A listing without cityId won't appear on city/municipality pages — set it whenever you can.
New builds: buildStage is "NEW_BUILD", "UNDER_CONSTRUCTION" or null (existing building). For UNDER_CONSTRUCTION you may add the expected completionYear (from last year to six years ahead of the current year) and completionQuarter (1–4). Any other stage stores both as null. In a PATCH, send the completion fields together with buildStage. Listings returned by the API carry all three fields.
Shortcuts: "imageUrls": [...] attaches photos from URLs in the same call (first URL becomes the main photo); "publish": true submits immediately after creation. If the publish step fails (no photos, unverified phone), the response still contains the created draft plus a publishError.
curl -X POST https://stanovina.me/api/v1/listings \
-H "Authorization: Bearer stv_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Dvosoban stan, Budva",
"description": "Stan od 55 m2 na 200 m od mora...",
"propertyType": "APARTMENT",
"listingType": "SALE",
"countryId": "COUNTRY_ID",
"cityId": "CITY_ID",
"price": 120000,
"area": 55,
"rooms": 2,
"buildStage": "UNDER_CONSTRUCTION",
"completionYear": 2027,
"completionQuarter": 2,
"hasBalcony": true,
"imageUrls": ["https://example.com/photo1.jpg"],
"publish": true
}'/api/v1/listings/{id}listings:readOne of your listings, photos and location included.
/api/v1/listings/{id}listings:writePartial edit — send only what changes. Required fields cannot be blanked. Drafts stay drafts; see lifecycle for how edits affect live and in-review listings.
/api/v1/listings/{id}/publishlistings:writeSubmit a DRAFT, or resubmit a REJECTED listing. Validates ≥1 photo and a verified phone. Response: {"status": "ACTIVE"} or {"status": "PENDING_MODERATION"}.
/api/v1/listings/{id}/statuslistings:writeOwner transitions: body {"status": "SOLD"}. Allowed: ACTIVE→SOLD, ACTIVE→RENTED, SOLD|RENTED→ACTIVE, REJECTED→PENDING_MODERATION.
/api/v1/listings/{id}listings:writeDeletes a DRAFT and its photos. Non-drafts cannot be deleted.
Photos
A listing holds up to 20 photos, each up to 10 MB, in image/jpeg, image/png, image/webp or image/avif. Photos are ordered; the photo at order 0 is the main photo — it fronts the listing everywhere. At least one photo is required to publish.
/api/v1/listings/{id}/imageslistings:readPhotos in display order. Each carries isMain (true at order 0).
/api/v1/listings/{id}/imageslistings:writeAttach photos, appended after the existing ones. Two request shapes:
Multipart upload
curl -X POST https://stanovina.me/api/v1/listings/LISTING_ID/images \ -H "Authorization: Bearer stv_YOUR_KEY" \ -F "[email protected]" \ -F "[email protected]"
By URL (server-side fetch)
curl -X POST https://stanovina.me/api/v1/listings/LISTING_ID/images \
-H "Authorization: Bearer stv_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"urls": ["https://example.com/a.jpg", "https://example.com/b.jpg"]}'URLs must be https:// and publicly reachable (private and internal addresses are refused). Per-URL failures don't fail the batch — they are returned in skipped with a reason.
/api/v1/listings/{id}/imageslistings:writeReorder. Either {"mainImageId": "IMAGE_ID"} — moves that photo to the front — or {"order": ["id1", "id2", ...]} listing every photo id exactly once.
/api/v1/listings/{id}/images/{imageId}listings:writeRemoves one photo; remaining photos close ranks (orders re-packed from 0), so deleting the main photo promotes the next one.
Reference data
Location ids for listing payloads. Any valid key may call these. Hierarchy: country → municipality → city → district; cityId and districtId are optional on a listing, countryId is required.
| Endpoint | Returns |
|---|---|
GET /api/v1/reference/countries | Countries with ids, codes, currency |
GET /api/v1/reference/municipalities?country=me | Municipalities, optionally by country code/slug |
GET /api/v1/reference/cities?municipality=budva | Cities, by municipality slug or ?country= |
GET /api/v1/reference/districts?city=CITY_ID | Districts of one city |
GET /api/v1/reference/enums | All enum values the API accepts |
Enums
propertyType
APARTMENT · HOUSE · VILLA · STUDIO · PENTHOUSE · LAND · COMMERCIAL · OFFICE · GARAGE · HOTEL
listingType
SALE · LONG_TERM_RENT · SHORT_TERM_RENT · DAILY_RENT
furnishing
FURNISHED · SEMI_FURNISHED · UNFURNISHED
buildStage
NEW_BUILD · UNDER_CONSTRUCTION
status
DRAFT · PENDING_MODERATION · ACTIVE · REJECTED · INACTIVE · SOLD · RENTED · ARCHIVED