Developer Documentation
Platform Overview
Authentication
API Services
Overview Accounts Accounts: Associations Accounts: Metadata Accounts: Profile Appstore: Users Broker Distributions Broker Tours Consumers Consumers: Linked Agents Contacts Contacts: Activity Contacts: Export Contacts: Portal Accounts Contacts: Tags Developers: Authorizations Developers: Billing Summary Developers: Change History Developers: Domains Developers: Identities Developers: Keys Developers: Roles Developers: Syndications Developers: Templates Developers: Usage Detail Developers: Usage Summary Devices Flexmls: Email Links Flexmls: Listing Meta Field List Translations Flexmls: Listing Meta Origins Flexmls: Listing Meta Translations Flexmls: Listing Reports Flexmls: Mapping Layers Flexmls: Mapping Shapegen IDX IDX Links Incomplete Listings Incomplete Listings: Documents Incomplete Listings: Documents Metadata Incomplete Listings: Document Uploads Incomplete Listings: Floor Plans Incomplete Listings: Floor Plans Metadata Incomplete Listings: Floor Plan Uploads Incomplete Listings: Photos Incomplete Listings: Photos Metadata Incomplete Listings: Photo Uploads Incomplete Listings: Required Documents Incomplete Listings: Rooms Incomplete Listings: Tickets Incomplete Listings: Units Incomplete Listings: Videos Incomplete Listings: Videos Metadata Incomplete Listings: Virtual Tours Incomplete Listings: Virtual Tours Metadata Listing Carts Listing Carts: Portal/VOW Carts Listings Listings: Clusters Listings: Documents Listings: Documents Metadata Listings: Document Uploads Listings: Floor Plans Listings: Floor Plans Metadata Listings: Floor Plan Uploads Listings: Historical Listings: History Listings: Hot Sheet Parameters Listings: Notes Listings: Open Houses Listings: Photos Listings: Photos Metadata Listings: Photo Uploads Listings: Rental Calendar Listings: Required Documents Listings: Rooms Listings: Rules Listings: Search Parameters Listings: Tickets Listings: Tour of Homes Listings: Units Listings: Validation Listings: Videos Listings: Videos Metadata Listings: Virtual Tours Listings: Virtual Tours Metadata Listing Meta: Custom Field Groups Listing Meta: Custom Fields Listing Meta: Field Order Listing Meta: Field Relations Listing Meta: Property Types Listing Meta: Rooms Listing Meta: Standard Fields Listing Meta: Units Market Statistics News Feed: Groups Notifications Open Houses Overlays Overlays: Geometries Portals Portals: Listing Categories Portals: Metadata Preferences Registered Listings Saved Searches Saved Searches: Provided Saved Searches: Restrictions Saved Searches: Tags Search Templates: Quick Searches Search Templates: Sorts Search Templates: Views Shared Links System Info System Info: Languages System Info: Search Templates
Webhooks
Supporting Documentation
Examples
RESO Web API
RETS
Terms of Use

RESO Web API Media Add/Edit

Use Version 3 nested Media endpoints to manage media on incomplete or published Properties. Read Add/Edit access requirements first: writes require private with the listing_update client keyrole, or private_full without operation-specific keyrole requirements. Existing listing and resource permissions still apply. The user also needs media access to the listing.

  1. Operations and categories
  2. Videos and virtual tours
  3. Create and upload file-backed Media
  4. Status, completion, and recovery
  5. Update metadata and delete Media
 

Operations and categories

POST to /Version/3/Reso/OData/Property('ListingKey')/Media. PATCH or DELETE an entity at /Version/3/Reso/OData/Property('ListingKey')/Media('MediaKey'). Use the assigned API host, an authorized access token, RESO-Dictionary-Version: 2.0, and Content-Type: application/json for POST/PATCH. PUT and top-level Media writes are not supported.

MediaCategoryCreationAfter creation
VideoJSON embed markupPATCH metadata / markup; DELETE
Branded Virtual Tour, Unbranded Virtual TourJSON URLPATCH metadata / URL; DELETE
Photo, Document, Floor PlanJSON upload reservation, then binary POSTPATCH finalized metadata; DELETE pending or finalized Media

The JSON endpoints use the shared response preferences and OData validation errors. Unsupported categories and empty metadata PATCH requests fail validation.

 

Videos require ObjectHTML. Optional writable fields are ShortDescription, LongDescription, and Permission. New videos are unbranded; ShortDescription defaults to Video. MediaURL is read-only for video writes.

POST /Version/3/Reso/OData/Property('ListingKey')/Media
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
RESO-Dictionary-Version: 2.0
Prefer: return=representation

{
  "MediaCategory":"Video",
  "ObjectHTML":"<iframe src=\"https://example.com/video\"></iframe>",
  "ShortDescription":"Video"
}

Virtual tours require MediaURL; ShortDescription and Permission are also writable. ObjectHTML and LongDescription are ignored with warnings for tours.

{
  "MediaCategory":"Unbranded Virtual Tour",
  "MediaURL":"https://example.com/tour"
}

Use this JSON body with the same POST endpoint and headers as the video example. Both creations return HTTP 201 with a representation by default. Save its MediaKey and edit link. PATCH the entity URL with only the writable fields to change. A virtual tour can switch between branded and unbranded categories, but this may produce a new MediaKey; use the key and URL returned by the update. Other cross-category changes are rejected. Permission values must be valid for the category and MLS.

 

Create and upload file-backed Media

Photo, Document, and Floor Plan creation uses two requests: a JSON POST to reserve Media, then a raw binary POST to the returned upload URL. The binary service is implemented separately from the OData API. It uses a capability URL, not the Spark ticket service's request envelope or an OData $value stream.

1. Reserve the Media and retrieve its upload URL

POST /Version/3/Reso/OData/Property('ListingKey')/Media
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
RESO-Dictionary-Version: 2.0
Prefer: return=representation

{
  "MediaCategory":"Photo",
  "MediaType":"image/jpeg",
  "ShortDescription":"Front exterior",
  "LongDescription":"Front of the property",
  "Order":1
}

Use Document with application/pdf for a PDF, or Floor Plan with an accepted image type for a floor plan. MediaCategory and MediaType are required. ShortDescription is optional for Photo and Floor Plan. LongDescription is accepted at reservation only for Photo; it is ignored with a warning for Document and Floor Plan. Set other finalized metadata after upload.

Document reservations must include a nonblank ShortDescription for the subsequent binary upload to succeed. The reservation API may accept a Document without a name, but the upload servlet rejects it with HTTP 400 after claiming the ticket. That ticket cannot be reused; create a new reservation with a valid Document name. Some MLSs also require Document names to be unique within a listing. For a PDF Document, use the same POST endpoint and headers with this body:

{
  "MediaCategory":"Document",
  "MediaType":"application/pdf",
  "ShortDescription":"Property brochure",
  "Order":1
}

Order is optional for Photo and Document reservations and is used during finalization. An Order supplied during Floor Plan reservation is not preserved during finalization; set the desired Floor Plan order through a metadata PATCH after finalization.

Permission supplied during RESO Media reservation is ignored. Finalized Documents are initially created with public confidentiality. A later metadata PATCH can change Permission, subject to MLS rules, but does not eliminate the interval of public confidentiality. Other listing and MLS access restrictions still apply; public confidentiality does not mean anonymous access.

Do not use this RESO upload workflow for Documents that require restricted confidentiality from creation. Use the separate Spark Document Upload service, which supports confidentiality at creation.

Accepted image types are image/bmp, image/jpeg, image/tiff, image/png, image/gif, image/x-portable-graymap, image/x-portable-bitmap, image/x-portable-pixmap, and image/heic. Photo and Floor Plan require an image. Documents accept PDF and images when the MLS enables document_upload_pdf (enabled by default); when disabled, only application/pdf is accepted. The API normalizes MediaType by trimming whitespace and lowercasing it.

The reservation response is HTTP 201 and includes MediaKey, MediaStatus: Incomplete, and @FBS.MediaUploadUrl. Save the pending key, Location, and upload URL. With Prefer: return=minimal, the response is 204 without a body; GET its Location to obtain the annotation. Pending rows appear in individual GET responses, not the Media collection. Retrieve the pending entity without $select when you need the upload annotation; field selection omits it.

The following is an excerpt of the reservation body. Keys, API host, MLS segment, and ticket are illustrative placeholders; use the values in your actual response:

{
  "@odata.id":"https://API_HOST/Version/3/Reso/OData/Property('ListingKey')/Media('PendingMediaKey')",
  "@odata.editLink":"https://API_HOST/Version/3/Reso/OData/Property('ListingKey')/Media('PendingMediaKey')",
  "@FBS.MediaUploadUrl":"https://UPLOAD_HOST/flexmls-servlets/servlets/ODataMediaUpload/MLS_NAME?ticketId=TICKET",
  "MediaKey":"PendingMediaKey",
  "MediaStatus":"Incomplete"
}

2. POST the file bytes to the returned URL

Follow @FBS.MediaUploadUrl exactly, including its MLS path and ticketId query parameter. Treat the URL as a credential: it authorizes a single upload, expires one hour after reservation, and should not be shared. The upload request uses that capability rather than an API bearer token. Do not construct the URL from an API hostname.

Send the raw file bytes with POST, not multipart form data or JSON. Content-Type must match the reserved MediaType and the file must be valid for the category. For the JPEG reservation above, put the returned URL in MEDIA_UPLOAD_URL and run:

curl --request POST \
  --header 'Content-Type: image/jpeg' \
  --data-binary @front-exterior.jpg \
  "$MEDIA_UPLOAD_URL"

The upload servlet claims the ticket and enters Processing before validating the upload. It validates the content type, file contents, and configured size limit, creates the finalized Media, and commits before returning a successful HTTP 204 with an empty body. Processing may be observed while the upload request is executing. Missing/mismatched content types or invalid files can reject the upload. Do not send a second upload to the same ticket. The upload service has its own error envelope rather than the OData validation envelope: verified invalid-upload responses include HTTP 400 with error code 1053, and an unknown ticket returns 404. Do not depend on exact error message wording.

Upload-service errors use D.Success, D.Code, and D.Message, rather than the RESO OData validation error envelope. For example, an expired ticket returns HTTP 400 with this body:

{
  "D":{
    "Success":false,
    "Code":"1053",
    "Message":"A parameter was provided but does not adhere to constraints (Ticket has expired)"
  }
}

Other HTTP 400 responses can have different codes and messages. Inspect both the HTTP status and response body; do not depend on exact message wording.

3. Check completion and retrieve finalized Media

GET the saved pending entity URL with your API bearer token. Inspect MediaStatus and MediaStatusDescription until completion or rejection. Do not treat a successful reservation as a completed upload. The finalized item has its own MediaKey; retrieve the listing's Media collection to obtain the resulting resource and its URL, then use that finalized key for metadata updates or deletion. Collection reads can lag completion because listing-read propagation is asynchronous; allow for that delay instead of assuming immediate visibility. Do not assume the pending key is the finalized key.

GET /Version/3/Reso/OData/Property('ListingKey')/Media('PendingMediaKey')
Authorization: Bearer ACCESS_TOKEN
RESO-Dictionary-Version: 2.0

Once complete, retrieve GET /Version/3/Reso/OData/Property('ListingKey')/Media with the same headers, following pagination to retrieve all results. Neither the upload response nor the pending status representation supplies the finalized MediaKey or a link to it. When uploads are serialized, comparing collection keys before reservation and after completion is one possible way to identify the new item. Concurrent uploads or other listing changes can make this comparison ambiguous; there is no guaranteed correlation mechanism. There is no documented completion callback or fixed polling interval. Upload size limits and processing timing depend on the upload service configuration; confirm applicable limits with your provider.

 

Status, completion, and recovery

MediaStatusMeaningClient action
IncompleteAwaiting the binary; upload annotation is available.Confirm ticket validity before considering another upload after an uncertain outcome; upload before expiration.
ProcessingTicket claimed; upload validation and finalization are underway during the upload request. Upload annotation is omitted.Continue checking; do not immediately retry the upload.
CompleteFinalized Media exists. Upload annotation is omitted.Retrieve the resulting Media resource.
RejectedTerminal validation, processing, or expiration failure. Upload annotation is omitted.Read MediaStatusDescription; delete and create a new reservation.

These status fields and @FBS.MediaUploadUrl are Spark extensions. Short-lived transitions may not be observed between reads. Expired unused tickets and stalled processing are marked Rejected by background cleanup; do not depend on an exact cleanup time. Rejection does not automatically remove the pending row. PATCH on a pending key returns 404; there is no reset or re-upload operation. To recover from rejection or expiration, correct the cause, DELETE the pending entity, and POST a new reservation for a fresh ticket.

HTTP failure alone does not establish whether a ticket remains usable. If a binary request's outcome is uncertain, such as a network timeout after sending bytes, read the pending status and follow the actions above before taking further action. Claimed tickets generally cannot be reused after failure; even HTTP 500 can consume a ticket. A malformed request may leave the ticket unused, while content validation failures can consume it and mark it Rejected. Do not blindly resend bytes after a timeout, even if an upload-service error message suggests retrying. A Complete ticket cannot be reused, and a Rejected ticket requires a new reservation and upload ticket.

 

Update metadata and delete Media

Once file-backed Media is finalized, PATCH its entity URL with the JSON fields to change:

CategoryWritable finalized metadata
PhotoShortDescription, LongDescription, Permission, Order, PreferredPhotoYN
DocumentShortDescription, Permission, Order
Floor PlanLongDescription, Permission, Order

For example, update a Photo caption:

PATCH /Version/3/Reso/OData/Property('ListingKey')/Media('MediaKey')
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
RESO-Dictionary-Version: 2.0

{"LongDescription":"Updated photo caption"}

Change the Photo order in a separate request. Photo and Floor Plan Order updates must be submitted alone, without other metadata fields:

PATCH /Version/3/Reso/OData/Property('ListingKey')/Media('MediaKey')
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
RESO-Dictionary-Version: 2.0

{"Order":2}

These examples apply to a Photo. For a conditional Media PATCH or DELETE, copy @odata.etag from a successful POST/PATCH representation of the same finalized Media resource into If-Match unchanged, when that annotation is available. This is a response-body annotation, not an HTTP ETag header; Media GET does not provide Property's GET-to-write token guarantees. If-Match is optional; a nonmatching token returns HTTP 412 without applying the mutation.

Photo privacy is subject to MLS rules; making a Photo primary with PreferredPhotoYN forces public privacy. Branded Virtual Tours require MLS support; retain the key returned by any update that changes tour branding, as described above.

File bytes are not writable through PATCH. Replacing a file requires deleting the old Media and creating/uploading a new resource, subject to the deletion restrictions below. These restrictions can prevent delete-and-recreate replacement.

DELETE the entity URL with the access token and Dictionary 2.0 header; success is HTTP 204 with no body. This works for finalized Media and pending rows in any status. Deleting a pending row invalidates its upload ticket; deleting finalized uploaded Media also cleans up its completed pending record. Unknown keys return 404. Keep the finalized key distinct from the pending key when deleting the actual file.

A primary Photo cannot be deleted individually while other Photos remain; make another Photo primary first. Deleting the last Photo may be prohibited on published listings when MLS Photo requirements apply. Required Documents on published listings may also be protected against renaming and deletion. Applicable requirements vary by MLS and listing.