Stanovina

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

ToolREST equivalentScope
get_enumsGET /api/v1/reference/enumsany
list_countriesGET /api/v1/reference/countriesany
list_municipalitiesGET /api/v1/reference/municipalitiesany
list_citiesGET /api/v1/reference/citiesany
list_districtsGET /api/v1/reference/districtsany
get_profileGET /api/v1/meprofile:read
update_profilePATCH /api/v1/meprofile:write
list_listingsGET /api/v1/listingslistings:read
get_listingGET /api/v1/listings/{id}listings:read
create_listingPOST /api/v1/listingslistings:write
update_listingPATCH /api/v1/listings/{id}listings:write
delete_draft_listingDELETE /api/v1/listings/{id}listings:write
publish_listingPOST /api/v1/listings/{id}/publishlistings:write
set_listing_statusPOST /api/v1/listings/{id}/statuslistings:write
list_listing_photosGET /api/v1/listings/{id}/imageslistings:read
add_listing_photosPOST /api/v1/listings/{id}/imageslistings:write
reorder_listing_photosPATCH /api/v1/listings/{id}/imageslistings:write
delete_listing_photoDELETE /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/me returns a scope field 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.
Brute-force lockout. Three requests with an invalid key from one IP address ban that IP from the entire API for 1 hour — including requests that would have carried a valid key. The response is HTTP 429 with a 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.

ScopeGrants
profile:readRead your profile
profile:writeUpdate your profile (includes read)
listings:readList and read your listings and photos
listings:writeCreate/edit listings, manage photos, publish, change status (includes read)

The reference-data endpoints accept any valid key regardless of scopes.

Rate limits

RequestsBudget (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"
  }
}
HTTPCodeMeaning
401unauthorizedMissing or malformed Authorization header
401invalid_keyKey not recognized (counts toward the IP ban)
401key_revokedKey was revoked in the dashboard
403api_access_disabledAPI access is switched off for this account
403account_bannedThe account is banned
403insufficient_scopeThe key lacks the required scope
403phone_not_verifiedPublishing requires a verified phone number
404not_foundNo such resource owned by you
400validation_errorA field is missing or invalid (see field)
400invalid_status_transitionThe requested status change is not allowed from the current status
403agency_key_profile_readonlyAgency key whose creator has left: GET /api/v1/me returns only the scope and PATCH /api/v1/me is refused
409managed_by_feedThe listing is managed by an XML feed; it can be read but not changed through the API
409conflicte.g. the phone number is verified by another account
429rate_limitedRate limit or IP ban; see Retry-After
500internal_errorOur 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 DRAFT keeps 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, moderationNote carries the reason; fix and publish again.
  • Only DRAFT listings can be deleted via the API.

Profile

GET/api/v1/meprofile:read

Returns your profile.

PATCH/api/v1/meprofile:write

Partial 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

GET/api/v1/listingslistings:read

Your listings, newest first. Query: ?status= (any listing status), ?page=, ?limit= (max 100).

POST/api/v1/listingslistings:write

Create a listing (as DRAFT). Required fields — the same set the website form requires:

  • title, description — primary language (Montenegrin); titleEn/titleRu/descriptionEn/descriptionRu optional
  • propertyType, listingType — see enums
  • countryId — from /api/v1/reference/countries
  • price, area — positive numbers
  • priceOnRequest — optional boolean. Set it to true for a listing whose asking price is not public yet: price becomes unnecessary and is stored as 0. Such listings show “Cijena na upit” instead of a price, are skipped by the priceFrom/priceTo filters, sort last under priceAsc/priceDesc, and are left out of the price statistics. A PATCH setting it back to false must send a real price in 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
  }'
GET/api/v1/listings/{id}listings:read

One of your listings, photos and location included.

PATCH/api/v1/listings/{id}listings:write

Partial edit — send only what changes. Required fields cannot be blanked. Drafts stay drafts; see lifecycle for how edits affect live and in-review listings.

POST/api/v1/listings/{id}/publishlistings:write

Submit a DRAFT, or resubmit a REJECTED listing. Validates ≥1 photo and a verified phone. Response: {"status": "ACTIVE"} or {"status": "PENDING_MODERATION"}.

POST/api/v1/listings/{id}/statuslistings:write

Owner transitions: body {"status": "SOLD"}. Allowed: ACTIVE→SOLD, ACTIVE→RENTED, SOLD|RENTED→ACTIVE, REJECTED→PENDING_MODERATION.

DELETE/api/v1/listings/{id}listings:write

Deletes 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.

GET/api/v1/listings/{id}/imageslistings:read

Photos in display order. Each carries isMain (true at order 0).

POST/api/v1/listings/{id}/imageslistings:write

Attach 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.

PATCH/api/v1/listings/{id}/imageslistings:write

Reorder. Either {"mainImageId": "IMAGE_ID"} — moves that photo to the front — or {"order": ["id1", "id2", ...]} listing every photo id exactly once.

DELETE/api/v1/listings/{id}/images/{imageId}listings:write

Removes 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.

EndpointReturns
GET /api/v1/reference/countriesCountries with ids, codes, currency
GET /api/v1/reference/municipalities?country=meMunicipalities, optionally by country code/slug
GET /api/v1/reference/cities?municipality=budvaCities, by municipality slug or ?country=
GET /api/v1/reference/districts?city=CITY_IDDistricts of one city
GET /api/v1/reference/enumsAll 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