Both land in the same place and are processed the same way. Neither lets you write to a
Maat storage bucket of your choosing, and neither accepts an object key.
URL ingestion, on the push
Sendphotos: [{ url, position }] with a property. We fetch each one over https, from a
public host, with no redirects, and put it through the same pipeline a photo uploaded in
the Maat apps goes through. A URL that has not changed since your last push is not
re-fetched, so a nightly full sync costs nothing extra.
A URL that could not become a photo comes back in photos_rejected[] with a reason —
see the vocabulary below.
Presigned upload, in three steps
Needsmedia:write. You never hold credentials that write into our storage: each form is
signed for one file, one size and one content type, and expires.
A property you push with PUT /v1/properties/{provider_id} is off limits to these three
verbs: 409 conflict, naming the provider_id you pushed it under. The push replaces the
property’s whole photo set on every call and re-scores it from its own list, so a photo
uploaded here would be undone by your next push — and the property taken off sale on the way
past. Change the photo URLs on the push instead. Reading them is still allowed: debugging a
sync is exactly when you need to.
- Declare the files.
POST …/photos/presignwith{ files: [{ content_type, size }] }.sizemust be the real byte length. You get back one form per file, each with aphoto_id, aurl, afieldsobject and anexpires_at. - POST the bytes to that
urlas a multipart form: every entry offieldsfirst, verbatim and in order, then afilepart last. Anything afterfileis ignored. The form pins the destination, the exactContent-Typeand a length range around the size you declared — a file that ships more bytes than it declared is refused there, after you have already sent them. - Poll
GET …/photos. A successful upload is not a published image: each photo ispendinguntil we have processed and reviewed it.
category and no url field on the presign, and there never will be. Position
decides the rest: the first photo on an empty property becomes the cover, everything after
it is a highlight.
Reading, arranging and removing
GET /v1/properties/{property_id}/photos returns every photo in the order guests see it:
url is the published image and is null in every other state — including for a photo our
automated review declined, which reads as rejected rather than ready because no guest
will ever be shown it.
PUT …/photos/order takes the complete set of ready photos, each exactly once, and
index 0 becomes the cover. A partial list is refused rather than half-applied: the
positions of everything you did not name would otherwise have to be invented by us.
DELETE …/photos/{photo_id} removes one and closes the gap, so positions stay dense. If
you delete the cover, position 0 of what remains becomes the cover. It answers 204
whether or not the photo was still there, so a retry is safe.
is_cover is true for at most one photo, and when true it is the one at position 0. It
can be false for every photo: a property whose cover was deleted, or whose cover our review
declined, has no cover until one is chosen again — a reorder is how you name one. It can also
be true on a photo that is still pending, where it names the cover the property is going to
have: url is null, guests see no cover, and the listing is not publishable
(cover_photo in missing) until that photo settles.
What a rejection means
One closed vocabulary covers both routes. Branch on it; do not read the prose.The two ceilings
A property holds at most thirty photos, counting the ones still uploading. And each key may presign 200 photos per UTC day — today’s default rather than contract;quota.remaining_today comes back on every successful presign so you can pace an import
against the live number. See Rate limits — the daily one is
photo_quota_exceeded, which is not the hourly rate_limited and does not clear within the
hour.
At 200 a day, a first catalogue import of a few hundred properties is a job that runs over
several days rather than one afternoon. Plan the import around that rather than discovering
it on day one.
Nothing is reserved on any refusal.
Photos and the validity gate
A property needs a cover photo and a minimum photo count to be publishable, so photos are usually what amissing: ["photos"] or missing: ["photos_processing"] is waiting for.
No photo operation changes whether a property is on sale, in either direction. The
sequence is three steps and the third one is yours:
- upload — presign, PUT the bytes;
- settle — we process and review each photo; it reaches
readyon its own, minutes later, and nothing about your request is still running by then; - list —
PATCH /v1/listings/{listing_id}withstatus: "listed", which runs the validity gate and publishes if it passes.
GET /v1/properties/by-id/{property_id} reports what the property is missing at any time.
Current status
Every operation on this page is live in a sandbox workspace and answers501 against a
production one, where nothing is uploaded, fetched or removed. A 501 means your
integration is correct — see Sandbox.