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.
- Operations and categories
- Videos and virtual tours
- Create and upload file-backed Media
- Status, completion, and recovery
- 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.
| MediaCategory | Creation | After creation |
|---|---|---|
Video | JSON embed markup | PATCH metadata / markup; DELETE |
Branded Virtual Tour, Unbranded Virtual Tour | JSON URL | PATCH metadata / URL; DELETE |
Photo, Document, Floor Plan | JSON upload reservation, then binary POST | PATCH 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 and virtual tours
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
| MediaStatus | Meaning | Client action |
|---|---|---|
| Incomplete | Awaiting the binary; upload annotation is available. | Confirm ticket validity before considering another upload after an uncertain outcome; upload before expiration. |
| Processing | Ticket claimed; upload validation and finalization are underway during the upload request. Upload annotation is omitted. | Continue checking; do not immediately retry the upload. |
| Complete | Finalized Media exists. Upload annotation is omitted. | Retrieve the resulting Media resource. |
| Rejected | Terminal 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:
| Category | Writable finalized metadata |
|---|---|
| Photo | ShortDescription, LongDescription, Permission, Order, PreferredPhotoYN |
| Document | ShortDescription, Permission, Order |
| Floor Plan | LongDescription, 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.