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.
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 |
| 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.
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
}'/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
status
DRAFT · PENDING_MODERATION · ACTIVE · REJECTED · INACTIVE · SOLD · RENTED · ARCHIVED