Repository navigation
Comp Composite and clone lineage
Fastighetsbenchmark models real estate as a small object graph. Understanding it unlocks the whole product. This section gives the mental model, a diagram, a fully worked end-to-end example, and the ingestion (create) flow.
Everything in Fastighetsbenchmark is a Comp (a comparable object) with a natural key sedisId. There are a handful of business types, each exposed as its own resource (Endpoints reference):
-
Fastighet (
/property-units) — a real property unit you own. -
Samling (
/samlingar) — your own collection. A container that exposes its children ascontainingCompSedisIds. You can create, replace, and delete these (the full lifecycle — see Ingestion (creating data)). -
Jämförelseobjekt (
/jamforelseobjekt) — a comparison zone you created by cloning a Sedis-owned SedisObjekt aggregate. (Swedish term retained — there is no clean English equivalent; think "comparison object".) -
reference-zones (
/reference-zones) — Sedis-curated external benchmark zones (Nils Holgersson, etc.). Shared reference data.
Note: The SedisObjekt source is not a fetchable resource. A SedisObjekt is a Sedis-owned, anonymized aggregate collection that you clone into a Jämförelseobjekt. You never own a SedisObjekt and never fetch one directly — you see only your clones of it. The relationship between your clone, the source aggregate's member property-units, and your own units is exposed through the bidirectional clone lineage below, not by reading the SedisObjekt itself.
Your Samling collections expose their children as containingCompSedisIds — an array of IDs, not nested objects. You follow up with ?sedisIdIn=... (comma-separated) to resolve them (this keeps payloads bounded — see Query conventions).
Clone membership is exposed as two opt-in (?fields=), tenant-filtered arrays of { sedisId, name } references — you only ever see your own rows in either direction, even when many customers clone the same shared SedisObjekt:
-
belongsToJamforelseobjekt(on a property-unit, or any Comp) — the reverse hop: the clones of mine whose source SedisObjekt contains this unit. Opt-in via?fields=…,belongsToJamforelseobjekt. This is the field-form reverse of the?belongsToJamforelseobjektSedisId=filter on/property-units. -
containsPropertyUnits(on a Jämförelseobjekt only) — the forward hop: my property-units contained in this clone's source SedisObjekt. Opt-in via?fields=…,containsPropertyUnits. Populated only on clone (Jämförelseobjekt) rows; absent on reference-zones.
The time-series values (the actual benchmarking numbers) live in CompDatum, fetched separately via /comp-data — they are never inlined into a Comp, because a single Comp can carry thousands of data points (see CompDatum below).
SedisObjekt (Sedis-owned aggregate; NOT a fetchable resource)
sedisId = SO-STHLM
│
contains → [ PU-1, PU-2, PU-3, ... ] (members; tenant-filtered to YOURS)
│ │
cloned by │ │ members are
you │ ▼
▼ PropertyUnit (Fastighet, yours)
Jämförelseobjekt sedisId = PU-1
(your comparison zone) │
sedisId = JO-A └── CompDatum time-series
(via /comp-data)
Forward lineage: JO-A's containsPropertyUnits: [{ sedisId: "PU-1" }, ...]
Reverse lineage: PU-1's belongsToJamforelseobjekt: [{ sedisId: "JO-A" }]
(both opt-in, tenant-filtered — you see only YOUR rows)
graph TD
SO["SedisObjekt SO-STHLM<br/>(Sedis aggregate; not fetchable)"]
PU1["PropertyUnit PU-1 (yours)"]
PU2["PropertyUnit PU-2 (yours)"]
JO["Jämförelseobjekt JO-A (yours)<br/>clone of SO-STHLM"]
CD["CompDatum time-series<br/>(via /comp-data)"]
SO -->|contains members| PU1
SO -->|contains members| PU2
SO -.->|cloned by you| JO
PU1 --> CD
JO -.->|containsPropertyUnits<br/>forward, tenant-filtered| PU1
PU1 -.->|belongsToJamforelseobjekt<br/>reverse, tenant-filtered| JO
Goal: "Show all my office property units within the Stockholm comparison zone, with Total return for the last 10 years, as a table + graph." Four calls:
1 — Find the comparison zone by name:
curl -H "X-Api-Key: $SEDIS_API_KEY" \
"https://api.sedis.se/v2/fastighetsbenchmark/jamforelseobjekt?nameContains=Stockholm"
# → data[0].sedisId = "JO-A"2 — Find your office property units in that zone (reverse-lineage filter + property-type filter):
curl -H "X-Api-Key: $SEDIS_API_KEY" \
"https://api.sedis.se/v2/fastighetsbenchmark/property-units?belongsToJamforelseobjektSedisId=JO-A&propertyType=1"
# → data[*].sedisId = "PU-1", "PU-2", ...Note:
?propertyType=takes the property-type id (an int), not a name. Each property unit's response carriespropertyType: { id, name }; discover the id you want from the{ id, name }pairs present in your data, then filter on that id. You can also filter by the type name with?propertyTypeName=(a case-insensitive substring against the stored Swedish type name — see the caveat in Language and localization).
3 — Discover the parameter code for "Total return":
curl -H "X-Api-Key: $SEDIS_API_KEY" \
"https://api.sedis.se/v2/fastighetsbenchmark/parameters?nameContains=Total%20return"
# → data[0].code = "NX71"4 — Pull the time-series (multi-value ?sedisIdIn=, date range):
curl -H "X-Api-Key: $SEDIS_API_KEY" \
"https://api.sedis.se/v2/fastighetsbenchmark/comp-data?sedisIdIn=PU-1,PU-2¶meterCode=NX71&fromDate=2016-01-01&toDate=2026-12-31"You now have everything to render the table + graph.
GET /fastighetsbenchmark/comp-data returns a flat, self-describing shape. Each row carries a dataType telling you which value member to read:
{
"data": [
{ "sedisId": "PU-1", "parameterCode": "NX71", "valueDate": "2025-12-31", "dataType": "decimal", "figure": 7.42 },
{ "sedisId": "PU-1", "parameterCode": "BJ0", "valueDate": "2025-12-31", "dataType": "boolean", "figure": 1, "boolean": true },
{ "sedisId": "PU-1", "parameterCode": "EB0", "valueDate": "2025-12-31", "dataType": "enum", "figure": 1, "enum": { "value": 1, "name": "Kontor", "enumType": "PropertyType" } },
{ "sedisId": "PU-1", "parameterCode": "DA0", "valueDate": "2025-12-31", "dataType": "date", "date": "2025-11-03" }
],
"page": 1, "pageSize": 50, "totalCount": 4, "totalPages": 1
}Note — the value is self-describing via
dataType(read this carefully). A parameter's datatype is decided by its code's first letter (B= boolean,D= date,E= enum, anything else = a numeric decimal). Each CompDatum row setsdataTypeaccordingly and populates exactly the matching member:
dataType: "decimal"→ readfigure(a decimal). The common case.dataType: "boolean"→ readboolean(true/false).figurealso carries the raw1/0for transparency.dataType: "enum"→ readenum={ value, name, enumType }:valueis the raw int,namethe resolved label,enumTypewhich domain it belongs to (e.g.PropertyType,CompClassification).figurealso carries the raw int. Discover the full set of legal values for an enum parameter viaGET /parameters/{code}→enumValues(see below).dataType: "date"→ readdate(an ISOyyyy-MM-ddstring — e.g. a transaction date).figureis absent.Null members are omitted from the JSON, so only the member that matches
dataTypeappears (plus the rawfigurefor boolean/enum). Notedateis notvalueDate:valueDateis the period the value applies to (part of the natural key);dateis the date-typed value itself forD-prefixed parameters.
Note — a row is not necessarily a year.
valueDateis the end of a quarter (31 Mar, 30 Jun, 30 Sep or 31 Dec), so a property unit can have several value dates per year once quarterly and half-yearly data is released; until then only 31 December is returned. Do not assume one row per year — group or filter onvalueDate(?fromDate=/?toDate=). On a date other than 31 December, flows (rent, costs, net operating income) cover the 12 months ending onvalueDate, and stocks (market value, area) are as of that date, so a 30 June value compares directly with a 31 December one. Sedis objects are calculated on 31 December only.
For any enum (E-prefixed) parameter, GET /parameters/{code} lists the full legal-value domain in enumValues. This is how you map property-type ids to names, or list every valid classification:
curl -H "X-Api-Key: $SEDIS_API_KEY" \
"https://api.sedis.se/v2/fastighetsbenchmark/parameters/EB0"
# → { "code": "EB0", "name": "Property type", "dataType": "enum",
# "enumValues": [ { "value": 1, "name": "Kontor" }, { "value": 2, "name": "Bostad" }, ... ] }Non-enum parameters carry dataType (decimal/boolean/date) and omit enumValues.
Use ?count=false on /comp-data for fast bulk sync (Query conventions).
The write endpoints let you push data into Fastighetsbenchmark. The POST endpoints require a mandatory Idempotency-Key header. A Samling additionally supports PUT (replace) and DELETE — the full lifecycle — and those two are idempotent by HTTP semantics, so they do not require the header.
curl -X POST -H "X-Api-Key: $SEDIS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 5f9c1e2a-7b3d-4a1f-9c0e-2b6d4a8f1c33" \
"https://api.sedis.se/v2/fastighetsbenchmark/samlingar" \
-d '{ "name": "My Stockholm offices", "containingCompSedisIds": ["PU-1","PU-2"] }'
# → 201 Created, body is the created Samling (same shape as GET /samlingar/{id})- The owning customer is stamped from your API key — there is no owner field in the body, so you cannot create data for another tenant (Multi-tenant isolation).
- Creating a Samling triggers Sedis's recalculation cascade so its derived values compute. The
201returns synchronously; the recalc runs asynchronously in the background (Data freshness). - A name collision within your tenant returns
409 Conflict(Errors (RFC 7807)).
curl -X PUT -H "X-Api-Key: $SEDIS_API_KEY" \
-H "Content-Type: application/json" \
"https://api.sedis.se/v2/fastighetsbenchmark/samlingar/SAM-abc123" \
-d '{ "name": "My Stockholm offices (2026)", "containingCompSedisIds": ["PU-1","PU-3"] }'
# → 200 OK, body is the updated Samling (same shape as GET /samlingar/{id})-
Full replace, not a patch. The
nameand thecontainingCompSedisIdsyou send become the collection's entire state — members not listed are removed. - Because the member set changed, the update re-triggers the recalculation cascade so the collection's derived values recompute (Data freshness).
- Tenant-scoped: a
{sedisId}that is not your own Samling returns404 Not Found(never403— Multi-tenant isolation). - The owner is taken from your API key, never the body — you cannot move a Samling to another tenant.
-
PUTis idempotent; it does not require anIdempotency-Keyheader.
curl -X DELETE -H "X-Api-Key: $SEDIS_API_KEY" \
"https://api.sedis.se/v2/fastighetsbenchmark/samlingar/SAM-abc123"
# → 204 No Content- Deletes the Samling and cascades its data/tags cleanup. The member property-units themselves are not deleted — only the collection and its membership.
- Tenant-scoped: deleting a
{sedisId}that is not your own returns404 Not Found. -
DELETEis idempotent; a second delete of the same id returns404. NoIdempotency-Keyheader is required.
Property units are created with POST property-units and updated with PUT — an upload of values (POST comp-uploads) never creates one. The full guide, with the typed values, the strict 422 contract and locations, is Writing data.
-
Mandatory on every v2 POST. Missing/blank (or longer than 255 characters) →
400withinvalidFields: ["Idempotency-Key"]. - Use a fresh UUID per logical operation.
-
Replaying the same key (e.g. after a network timeout) returns the original response without re-executing — no duplicate Samling, no double ingest. Replays carry an
Idempotency-Replayed: trueheader. - The replay window is 24 hours, scoped to your tenant + key.
- A retry with the same key while the first request is still running →
409; retry after it finishes to get the replay. - The same key with a different request (another method, path or body) →
422with typehttps://api.sedis.se/problems/idempotency-key-reused— never the other request's response. Whitespace and member order in the JSON body do not count as different. - Keys are kept by Sedis across restarts and server instances (since 2.3.0), so a retry is safe even if it lands on another server.
- If the first request fails with a server error, nothing is kept — the retry runs again.