Media
Import photos, floor plans, PDFs, videos, and virtual tours by URL and attach them to Resi records.
Resi media covers two different things behind one endpoint, and the difference matters more than it first appears.
Files (kind: file) | Embeds (kind: embed) | |
|---|---|---|
| What | Images and PDFs | Video and virtual-tour links (YouTube, Vimeo, Matterport) |
| Stored | Downloaded by Resi to its own storage | URL only, never downloaded |
| Ownership | Account-level library asset, attached to any number of parents | Belongs to exactly one parent |
| Editing | A metadata edit applies everywhere the file is attached | Local to its one placement |
The file model is the one to internalize: a photo is a library asset with placements, not a property of a unit. Change its caption and every listing showing that photo changes.
Importing media
POST /api/v2/media imports media by URL: Resi fetches the file from a URL you supply. There is no multipart upload. A target is required at creation: you cannot create an unattached asset.
Import a photo onto a unit
curl -X POST https://v2.getresi.com/api/v2/media \
-H "Authorization: Bearer $RESI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"kind": "file",
"url": "https://cdn.example.com/listings/unit-101-living.jpg",
"attachable_type": "unit",
"attachable_id": "019dd605-61bc-71bf-acf9-1f1dd97ac1ca",
"media_type": "image",
"caption": "Living room",
"alt_text": "Open-plan living room with hardwood floors and a large south-facing window",
"reference_id": "PMS-IMG-55",
"sort_order": 1
}'It returns 201 with the asset and every placement it now has:
{
"data": {
"kind": "file",
"media": {
"id": "019dd606-dff6-71ad-8619-312ce23cfa01",
"url": "https://dam.getresi.co/18349/unit-101-living.jpg",
"thumb_url": "https://dam.getresi.co/18349/conversions/unit-101-living-thumb.jpg",
"full_url": "https://dam.getresi.co/18349/conversions/unit-101-living-full.jpg",
"caption": "Living room",
"alt_text": "Open-plan living room with hardwood floors and a large south-facing window",
"tags": [],
"reference_url": "https://cdn.example.com/listings/unit-101-living.jpg",
"reference_id": "PMS-IMG-55"
},
"attachments": [
{
"attachable_type": "unit",
"attachable_id": "019dd605-61bc-71bf-acf9-1f1dd97ac1ca",
"slot": "image",
"sort_order": 1
}
]
}
}If Resi cannot fetch the file, the response is 422 with the message "The media could not be retrieved from the provided URL."
Attach a virtual tour to a property
curl -X POST https://v2.getresi.com/api/v2/media \
-H "Authorization: Bearer $RESI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"kind": "embed",
"url": "https://my.matterport.com/show/?m=abc123",
"attachable_type": "property",
"attachable_id": "019dd604-8468-7388-9a52-4c31a2e1209a",
"embed_type": "virtual_tour",
"title": "Clubhouse & amenity tour"
}'provider (matterport, youtube, vimeo or other) is inferred from the URL when omitted. Supply it explicitly only if the URL does not identify the provider.
Fields
Shared
| Field | Rules |
|---|---|
kind | Required. file or embed |
url | Required. Files: max 2048 characters. Embeds: max 255 characters |
attachable_type | Required. See the allowlist below |
attachable_id | Required. A UUID that exists in your account |
tags | Optional array of strings |
sort_order | Optional integer, 0 or more. Defaults to 1 for files and 0 for embeds |
kind: file only
| Field | Rules |
|---|---|
media_type | Required. The slot the file occupies |
caption | Optional, max 250 |
alt_text | Optional, max 250 |
reference_id | Optional, max 255. Your external id; used for deduplication |
kind: embed only
| Field | Rules |
|---|---|
embed_type | Required. video, virtual_tour or other |
provider | Optional. Inferred from the URL |
title | Optional, max 255 |
settings | Optional object, provider-specific |
Fields are strictly partitioned. Sending caption, alt_text, media_type or reference_id on an embed, or embed_type, provider, title or settings on a file, is rejected with 422 rather than silently ignored. This is deliberate: it catches the mistake at the boundary instead of producing media with quietly missing metadata.
Note also that embeds have no alt_text; title carries the description.
media_type: the slot a file occupies
image · background_image · video · panorama · virtual_tour · pdf · property_image · floor_plan_2d · floor_plan_3d · fallback_image_thumb · fallback_virtual_tour_thumb · fallback_video_thumb
The slot determines where the file surfaces in the payload and on a Resi-rendered site. property_image is the hero/card image for a property; image is a gallery photo; floor_plan_2d is the layout diagram on a floor plan record.
attachable_type: valid targets
| Target | Files | Embeds |
|---|---|---|
property, unit, floor_plan, gallery, amenity, content_block, content_item | Yes | Yes |
building, announcement, neighborhood_place | Yes | No |
Always the friendly name shown here. An embed on a target that cannot hold one returns 422 with a message naming the resource and kind.
Importing in bulk
POST /api/v2/media/batch takes up to 100 items. Each item is exactly a POST /api/v2/media body:
curl -X POST https://v2.getresi.com/api/v2/media/batch \
-H "Authorization: Bearer $RESI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"items": [
{ "kind": "file", "url": "https://cdn.example.com/a.jpg", "attachable_type": "unit", "attachable_id": "019dd605-61bc-71bf-acf9-1f1dd97ac1ca", "media_type": "image", "reference_id": "PMS-IMG-56" },
{ "kind": "file", "url": "https://cdn.example.com/b.jpg", "attachable_type": "unit", "attachable_id": "019dd605-61bc-71bf-acf9-1f1dd97ac1ca", "media_type": "image", "reference_id": "PMS-IMG-57" }
]
}'Every item is validated, authorized and deduplicated on its own, in order. One failing item does not stop or roll back the others, so the response is 200 whenever the envelope is valid. Read each result's status:
{
"data": [
{ "index": 0, "status": 201, "data": { "kind": "file", "media": { "id": "019dd606-…" }, "attachments": [] } },
{ "index": 1, "status": 503, "message": "Not attempted: the batch ran out of time. Send this item again." }
]
}status | Meaning |
|---|---|
201 | Imported. data is what POST /api/v2/media would have returned |
403, 404 | Not permitted: you may not import media, or may not write to that target |
422 | Invalid item (including a target not found in your account), or the file could not be fetched. message and errors say why |
503 | Not attempted: the batch ran out of time. Send it again |
Files download while the request runs. After 40 seconds the batch cuts short any download still under way (that item fails with 422) and returns the items it did not start as 503. A batch of many new files may therefore need to be sent in parts: resend only the indexes that came back 503. Files may come from at most 25 different hosts per batch; an item from a further host fails with 422.
Reading media
GET /api/v2/media requires kind: files and embeds live in different stores and are not paginated together.
curl "https://v2.getresi.com/api/v2/media?kind=file&tag=exterior&per_page=100" \
-H "Authorization: Bearer $RESI_TOKEN"To show both, make two calls and merge client-side. To list what is on one record, pass attachable_type and attachable_id together. GET /api/v2/media/{media} fetches one item of either kind.
GET /api/v2/media returns the account's entire media library (uploads in the Resi app, PMS imports and prior API calls alike), not just media your integration created. Filter by tag, or tag everything you create so you can find your own work later.
Media also appears inline on parent resources, which is usually the more convenient read. A property carries:
{
"property_images": [
{
"id": "019dd606-dff6-71ad-8619-312ce23cfa01",
"url": "https://dam.getresi.co/18349/DJI_0316.jpg",
"thumb_url": "https://dam.getresi.co/18349/conversions/DJI_0316-thumb.jpg",
"full_url": "https://dam.getresi.co/18349/conversions/DJI_0316-full.jpg",
"caption": "",
"alt_text": "an aerial view of an apartment building with a green lawn and fence",
"tags": [],
"reference_url": "https://source-system.example.com/original.jpg",
"reference_id": null
}
],
"images": [],
"background_image": null,
"videos": [],
"virtual_tours": []
}url is the original. thumb_url (a 600 × 600 crop) is generated during the import. full_url (2000 pixels wide) is generated in the background after it, so it may 404 briefly on a freshly imported file; fall back to url. reference_url is the source URL the asset was imported from.
Updating media
curl -X PATCH "https://v2.getresi.com/api/v2/media/$MEDIA_ID" \
-H "Authorization: Bearer $RESI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"caption": "Living room, renovated 2026", "tags": ["interior", "renovated"]}'What you can change depends on the kind:
| Kind | Accepts |
|---|---|
| File | caption, alt_text, tags |
| Embed | title, url, embed_type, provider, settings, sort_order, tags |
Any other field is a 422.
Updating a file is global. A PATCH to a file changes caption, alt_text and tags on the library asset, and therefore on every parent it is attached to. There is no per-placement metadata. If two properties need different captions on the same photo, they need two assets.
Updating an embed is local to its single parent.
A file's slot and sort_order belong to each placement, not to the asset, so PATCH cannot change them. To move or re-order a file on one parent, import it onto that parent again with the new media_type and sort_order (see below).
Deduplication
Imports are idempotent, so they are safe to retry.
Files deduplicate within the account on the source url first, then on reference_id:
- A match reuses the existing asset instead of downloading again, and attaches it to the parent you named. If it was already on that parent, its slot and
sort_orderthere are updated. - The existing
caption,alt_textandtagsare left as they are. An import will not overwrite a caption someone wrote in the Resi app. To change metadata,PATCH; do not re-import and expect an update. - A
reference_idmatch whoseurldiffers replaces the stored file with a fresh download from the new URL.
Give each file a stable reference_id, such as the source system's image id, so a changed CDN URL still lands on the same asset.
Embeds deduplicate on parent + embed_type + url. A repeat updates that embed's title, provider, settings, tags and sort_order rather than adding a copy, and a field you leave out is cleared. Send the complete embed every time.
Scope of the media API
The media API covers import, read, update, re-order, removal and folders. A few things to plan around:
- Import is URL-based, so a source file must be reachable at a public URL at the moment of import.
- File metadata is asset-level, so a caption or alt-text edit applies to every placement of that asset.
- Take a file off one record without deleting it with
DELETE /api/v2/media/{media}/attachments/{attachable_type}/{attachable_id}. It stays in the library and on every other record.PATCHon the same path with asort_orderre-orders it on that record, for example to change a gallery's first photo. DELETE /api/v2/media/{media}deletes a file everywhere: from the library and every record it is on. For an embed, which belongs to one record, it deletes the embed.- Folders.
/api/v2/media-folderslists and creates the library's folders. They do not nest: each is account-wide or belongs to one property. File a file withfolder_idonPATCH /api/v2/media/{media}, in an account-wide folder or one of its own property's.
Practical guidance
- Mind the import limits.
POST /api/v2/mediaallows 60 requests a minute andPOST /api/v2/media/batch10 a minute, both within the general 120 a minute. Each call triggers downloads, so serialize bulk imports and back off on429. - Host source files somewhere stable and fast. Resi fetches while your request waits; a slow or flaky origin turns into timeouts on your side.
- Write real
alt_text. It is the accessibility text on client sites, and it is a differentiator when a client evaluates your integration."image1.jpg"is not alt text. - Tag on import, for example
["vendor:acme", "batch:2026-08"], so your assets are findable later. There is no "created by this integration" filter. - Set
sort_orderexplicitly. Display order follows it, not the order you imported in.
Last updated on