Conventions
These hold on every endpoint, so they are described once here rather than repeated on each.
Pagination
Collections are cursor paginated, not offset paginated. An offset is unstable on a catalogue that changes while you walk it: insert a record during a scan and offset paging silently repeats one; delete one and it silently skips one. A cursor points at a position in a stable ordering, so neither happens.
# First page
GET /v1/api/properties?city=Berlin&limit=100
# Every page after it
GET /v1/api/properties?city=Berlin&limit=100&cursor=eyJpZCI6…Stop when meta.has_more is false. Do not stop on a short page, and do not
compute a page count: there is no total, deliberately, because counting a large
filtered set is expensive and the answer is stale the moment it is returned.
Every client has an iterator that does this for you. See paging.
Treat a cursor as opaque. It encodes sort position and filter state, so it is only valid against the same query, and decoding one to build your own is not supported.
Keeping a mirror in sync
Do not re-read a market on a timer. Page it once, store the newest updated_at
you saw, then ask only for what changed since:
GET /v1/api/properties?city=Berlin&updated_since=2026-08-11T04:00:00ZOverlap the window by a few minutes rather than using the exact timestamp of your last run. Records are written concurrently, so a strict boundary can miss one committed a moment after your cutoff. Handling the same record twice is harmless if your write is an upsert; missing one is not.
Webhooks remove the need for this loop entirely.
Filtering and sorting
Filters combine with AND. Repeating a parameter forms an OR within that
parameter, so ?property_type=apartment&property_type=studio means either type.
Range filters use min_ and max_ prefixes and are inclusive.
Sort with a field name, prefixed by a minus for descending: ?sort=-price.
Ordering always breaks ties on id, so paging is deterministic even when many
records share a price. Search results are ranked by relevance and ignore sort.
Sparse fields and expansion
Two parameters shape the payload in opposite directions. fields narrows a
record to what you asked for, and expand inlines a related resource that is
otherwise a separate call. Used together they usually collapse an N+1 into one
request:
GET /v1/api/properties?fields=id,price,living_area&expand=imagesIdempotency
Send an Idempotency-Key on every POST that creates something. A network
timeout tells you nothing about whether the server acted, and retrying without a
key is how duplicate listings appear.
curl -sS -X POST "https://api.skautik.com/v1/api/properties" \
-H "Authorization: Bearer $SKAUTIK_API_KEY" \
-H "Idempotency-Key: 6f1c2a7e-4d90-4a1b-9f33-0c2f5b8e77aa" \
-H "Content-Type: application/json" \
-d @property.jsonReplaying a key returns the original response rather than creating a second
record. Reusing one with a different body is a mistake and returns 409. Keys
are remembered for 24 hours. Generate a UUID per logical creation, not per
attempt.
Conditional requests
Reads return an ETag. Send it back as If-None-Match and an unchanged
resource answers 304 with no body, which costs you no quota.
On a PATCH, send the ETag as If-Match instead. If the record changed since you
read it, the write is rejected with 412 rather than silently discarding the
other edit. Re-read, reapply, retry.
Data formats
| Kind | Format |
|---|---|
| Timestamps | RFC 3339, always UTC, always with the Z suffix. Never a local time and never an epoch integer. |
| Money | Integer minor units with a separate ISO 4217 currency. 42900000 with EUR is 429,000.00. Floats are never used for money. |
| Areas | Square metres as a number. No conversion is applied; the market record states its own area unit convention. |
| Identifiers | Opaque prefixed strings such as prop_ and whk_. Do not parse them; the prefix is for recognition in logs, not for routing. |
| Absent values | Explicit null, not an omitted key or an empty string. Null means the source never supplied it, which is different from zero. |
Request identifiers
Every response carries an X-Request-Id header, repeated in the body of any
error. Log it. Quoting one lets support find the exact request rather than
asking you to reproduce the problem.