Docs
API guides

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)
WhatImages and PDFsVideo and virtual-tour links (YouTube, Vimeo, Matterport)
StoredDownloaded by Resi to its own storageURL only, never downloaded
OwnershipAccount-level library asset, attached to any number of parentsBelongs to exactly one parent
EditingA metadata edit applies everywhere the file is attachedLocal 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

FieldRules
kindRequired. file or embed
urlRequired. Files: max 2048 characters. Embeds: max 255 characters
attachable_typeRequired. See the allowlist below
attachable_idRequired. A UUID that exists in your account
tagsOptional array of strings
sort_orderOptional integer, 0 or more. Defaults to 1 for files and 0 for embeds

kind: file only

FieldRules
media_typeRequired. The slot the file occupies
captionOptional, max 250
alt_textOptional, max 250
reference_idOptional, max 255. Your external id; used for deduplication

kind: embed only

FieldRules
embed_typeRequired. video, virtual_tour or other
providerOptional. Inferred from the URL
titleOptional, max 255
settingsOptional 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

TargetFilesEmbeds
property, unit, floor_plan, gallery, amenity, content_block, content_itemYesYes
building, announcement, neighborhood_placeYesNo

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." }
  ]
}
statusMeaning
201Imported. data is what POST /api/v2/media would have returned
403, 404Not permitted: you may not import media, or may not write to that target
422Invalid item (including a target not found in your account), or the file could not be fetched. message and errors say why
503Not 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

PATCH /api/v2/media/{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:

KindAccepts
Filecaption, alt_text, tags
Embedtitle, 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_order there are updated.
  • The existing caption, alt_text and tags are 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_id match whose url differs 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. PATCH on the same path with a sort_order re-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-folders lists and creates the library's folders. They do not nest: each is account-wide or belongs to one property. File a file with folder_id on PATCH /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/media allows 60 requests a minute and POST /api/v2/media/batch 10 a minute, both within the general 120 a minute. Each call triggers downloads, so serialize bulk imports and back off on 429.
  • 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_order explicitly. Display order follows it, not the order you imported in.

Last updated on

On this page