Format de réponse
JSON:API par défaut, JSON « à plat » sur demande — et comment lire relationships / included
L'API renvoie chaque réponse dans l'un de deux formats, choisi par l'en-tête Accept de la requête :
Accept envoyé | Format renvoyé | Content-Type de la réponse |
|---|---|---|
aucun, */* ou application/* | JSON:API (défaut) | application/vnd.api+json |
application/json | JSON « à plat » | application/json |
application/vnd.api+json | JSON:API | application/vnd.api+json |
| tout autre type | erreur 406 | application/problem+json |
Les deux formats portent les mêmes données ; seule l'organisation change. Les erreurs, elles, sont toujours en application/problem+json (voir Codes d'erreur).
Si vous ne précisez rien, vous recevez du JSON:API. La plupart des clients HTTP (curl, fetch, Postman) envoient Accept: */* par défaut, ce qui revient au même.
JSON:API — data, relationships, included
Dans le format JSON:API, chaque objet n'est décrit qu'une seule fois dans la réponse :
datacontient la ressource principale : sontype, sonid, ses champs dansattributes, et ses liens vers d'autres ressources dansrelationships.relationshipsne contient que des références (type+id), jamais les objets complets.includedest le tableau, au premier niveau du document, qui contient les objets complets référencés.
Exemple sur GET /properties/{id} :
{
"data": {
"type": "Property",
"id": "019fe5b2-001f-78c9-8ed3-968ddffbb712",
"attributes": {
"propertyType": "FLAT",
"transaction": { "type": "SELL", "saleType": "STANDARD" },
"area": { "displayed": 54, "unit": "SQM" },
"unit": { "rooms": 3, "bedrooms": 2 }
},
"relationships": {
"listings": {
"data": [
{ "type": "Listing", "id": "019fe5b1-c448-7b00-a993-feacc6966090" },
{ "type": "Listing", "id": "019feed7-d188-7c96-be53-9b85ed93df85" }
]
},
"publishers": {
"data": [
{ "type": "Publisher", "id": "019eff3c-28d9-7c10-8a44-9f2f00329980" }
]
},
"propertyPublishers": {
"data": [
{ "type": "PropertyPublisher", "id": "5b1f7c2e-8a41-5d3b-9c6e-2f4a1b7d9e01" }
]
}
},
"links": { "self": "/properties/019fe5b2-001f-78c9-8ed3-968ddffbb712" }
},
"links": { "self": "/properties/019fe5b2-001f-78c9-8ed3-968ddffbb712" },
"included": [
{
"type": "Listing",
"id": "019fe5b1-c448-7b00-a993-feacc6966090",
"attributes": {
"url": "https://www.example-realestate.com/listings/019fe5b1",
"publishedAt": "2026-04-15T09:00:00+00:00",
"lastSeenAt": "2026-05-12T08:30:00+00:00",
"createdAt": "2026-04-15T09:05:00+00:00",
"updatedAt": "2026-05-12T08:30:00+00:00"
},
"relationships": {
"publisher": { "data": { "type": "Publisher", "id": "019eff3c-28d9-7c10-8a44-9f2f00329980" } },
"source": { "data": { "type": "Source", "id": "019dcfc7-3f71-7d6f-a0e6-89d8e1f9844b" } }
}
},
{
"type": "Publisher",
"id": "019eff3c-28d9-7c10-8a44-9f2f00329980",
"attributes": {
"publisherType": "REAL_ESTATE_AGENCY",
"agencyName": "Foncia Paris 18",
"website": "https://www.foncia-paris18.example.com"
},
"links": { "self": "/publishers/019eff3c-28d9-7c10-8a44-9f2f00329980" }
},
{
"type": "PropertyPublisher",
"id": "5b1f7c2e-8a41-5d3b-9c6e-2f4a1b7d9e01",
"attributes": {
"mandate": "EXCLUSIVE",
"reference": "REF-AB42",
"contact": { "email": "contact@foncia-paris18.example.com", "phone": "+33123456789" }
},
"relationships": {
"property": { "data": { "type": "Property", "id": "019fe5b2-001f-78c9-8ed3-968ddffbb712" } },
"publisher": { "data": { "type": "Publisher", "id": "019eff3c-28d9-7c10-8a44-9f2f00329980" } }
}
},
{
"type": "Source",
"id": "019dcfc7-3f71-7d6f-a0e6-89d8e1f9844b",
"attributes": { "slug": "realty-portal" },
"links": { "self": "/sources/019dcfc7-3f71-7d6f-a0e6-89d8e1f9844b" }
}
]
}Le bloc relationships.listings de la property référence ses deux annonces ; les annonces elles-mêmes sont dans included, avec leur type: "Listing". Chaque Listing référence à son tour son Publisher (annonceur) et sa Source (portail), eux aussi présents dans included.
Mandat, référence et contact de l'annonceur
Un même annonceur peut apparaître sur plusieurs biens d'une page, mais il n'est décrit qu'une fois dans included. Ce qu'il déclare sur un bien précis — mandate (son mandat), reference (la référence interne de l'agence) et contact — ne peut donc pas vivre sur le Publisher : ces champs sont portés par une ressource PropertyPublisher, une par couple bien / annonceur.
- La property la référence dans
relationships.propertyPublishers, avec autant d'entrées que dansrelationships.publishers. - Chaque
PropertyPublisherpointe vers son bien (relationships.property) et son annonceur (relationships.publisher). - Un annonceur présent sur deux biens de la page donne un seul
Publisheret deuxPropertyPublisher, chacun avec les valeurs de son bien. - Son
idest stable pour un même couple bien / annonceur, mais aucune route ne le sert : il n'a pas delinks.self.
Les valeurs sont les mêmes que dans publishers[] du JSON à plat.
Résoudre une référence
Pour retrouver un objet référencé : prenez son type et son id dans relationships, puis cherchez l'entrée de même type et même id dans included. Le couple (type, id) est unique dans tout le document.
const doc = await res.json(); // JSON:API
const byKey = new Map(
(doc.included ?? []).map((r) => [`${r.type}:${r.id}`, r]),
);
// Une référence vaut null quand l'objet n'est pas exposé (annonce sans annonceur ou source connus).
const resolve = (ref) => (ref ? byKey.get(`${ref.type}:${ref.id}`) : undefined);
const property = doc.data;
const listings = property.relationships.listings.data.map(resolve);
const publishers = property.relationships.publishers.data.map(resolve);
// Chaque annonce pointe vers son annonceur et son portail
for (const listing of listings) {
const publisher = resolve(listing.relationships.publisher.data);
const source = resolve(listing.relationships.source.data);
console.log(listing.attributes.url, publisher?.attributes.agencyName, source?.attributes.slug);
}Types de ressources
type | Ressource | Où on la trouve |
|---|---|---|
Property | Le bien dédoublonné — vue consolidée dans attributes | data |
Listing | Une annonce source, avec ses valeurs propres (URL, dates, écarts par rapport au bien) | included, référencée par relationships.listings |
Publisher | Un annonceur distinct (agence, particulier…) | included, référencée par relationships.publishers et par Listing.relationships.publisher |
PropertyPublisher | Ce qu'un annonceur déclare sur un bien : mandate, reference, contact | included, référencée par relationships.propertyPublishers |
Source | Le portail d'où provient l'annonce | included, référencée par Listing.relationships.source |
Pourquoi plusieurs Listing et plusieurs Publisher sur un même bien : voir Property — agrégation et déduplication.
Collections : data[] et meta
Sur POST /properties, data est un tableau de ressources Property, et la pagination est portée par meta :
{
"data": [ { "type": "Property", "id": "…", "attributes": { "…": "…" }, "relationships": { "…": "…" } } ],
"meta": {
"totalItems": 4231,
"hasNextPage": true,
"cursor": "eyJzb3J0IjpbMTc3ODg0NDY0MzAwMCwiMDE5Z…",
"pagination": { "type": "CURSOR", "size": 50, "nextRequestBody": { "…": "…" } }
},
"links": { "self": "/properties" },
"included": [ /* Listing, Publisher, PropertyPublisher, Source de toutes les properties de la page */ ]
}included est partagé par toute la page : un même Publisher publié sur plusieurs biens n'y figure qu'une fois. Voir Pagination.
JSON « à plat » — Accept: application/json
Envoyez Accept: application/json pour recevoir les objets complets imbriqués dans chaque bien, sans relationships ni included :
curl "https://api-v2.stream.estate/properties/019fe5b2-001f-78c9-8ed3-968ddffbb712" \
-H "X-API-KEY: <votre_clé>" \
-H "Accept: application/json"{
"id": "019fe5b2-001f-78c9-8ed3-968ddffbb712",
"property": {
"propertyType": "FLAT",
"transaction": { "type": "SELL", "saleType": "STANDARD" },
"area": { "displayed": 54, "unit": "SQM" },
"unit": { "rooms": 3, "bedrooms": 2 }
},
"publishers": [
{
"id": "019eff3c-28d9-7c10-8a44-9f2f00329980",
"publisherType": "REAL_ESTATE_AGENCY",
"agencyName": "Foncia Paris 18",
"website": "https://www.foncia-paris18.example.com"
}
],
"listings": [
{
"id": "019fe5b1-c448-7b00-a993-feacc6966090",
"url": "https://www.example-realestate.com/listings/019fe5b1",
"source": { "id": "019dcfc7-3f71-7d6f-a0e6-89d8e1f9844b", "slug": "realty-portal" },
"publishedAt": "2026-04-15T09:00:00+00:00",
"lastSeenAt": "2026-05-12T08:30:00+00:00",
"createdAt": "2026-04-15T09:05:00+00:00",
"updatedAt": "2026-05-12T08:30:00+00:00"
}
]
}C'est le format décrit dans Property — schéma.
Sur POST /properties, le format à plat renvoie un tableau nu de biens ([ { "id": …, "property": …, … } ]), sans totalItems, hasNextPage ni cursor. Pour paginer une recherche — en particulier en mode CURSOR, qui exige de renvoyer le curseur reçu — utilisez le format JSON:API (défaut) et lisez meta.
Correspondance entre les deux formats
| JSON:API | JSON à plat |
|---|---|
data.id | id |
data.attributes | property |
data.relationships.publishers.data[] → included (type: "Publisher") | publishers[] |
data.relationships.propertyPublishers.data[] → included (type: "PropertyPublisher") | publishers[].mandate, publishers[].reference, publishers[].contact |
data.relationships.listings.data[] → included (type: "Listing") | listings[] |
Listing.relationships.source.data → included (type: "Source") | listings[].source |
Listing.relationships.publisher.data → included (type: "Publisher") | l'entrée correspondante de publishers[] |
data[] + meta.totalItems / meta.hasNextPage / meta.cursor (recherche) | tableau nu, sans métadonnées de pagination |
Voir aussi
- Property — pourquoi un bien regroupe plusieurs annonces et plusieurs annonceurs
- Pagination —
meta.totalItems,meta.hasNextPage,meta.cursor - Codes d'erreur — format
problem+json