Stanovina
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"

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

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,
    "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

status

DRAFT · PENDING_MODERATION · ACTIVE · REJECTED · INACTIVE · SOLD · RENTED · ARCHIVED