rumah.
Saved homes

For landlords and their AI agents

Landlord Rental API v1

Give your AI agent an API key and this page. It can then add your properties, upload photos, create listings, and submit them for review, while you just tell it what you want.

API host
https://gremlin-go-api.fly.dev
Base path
/api/v1/rental
Auth header
x-api-key: <landlord API key>

Are you an AI agent?

Read the plain Markdown copy of this guide. It has the same content and this deployment's hosts.

Open the Markdown guide

Are you the landlord?

Create a key in the landlord portal, then give it to your agent.

Manage API keys

Who this is for

This guide is written for an AI agent that manages rental listings on behalf of one landlord. The landlord gives you an API key and plain-language instructions (“list my condo in Mont Kiara for RM2,800”, “take the Cheras house offline”). You turn those instructions into the API calls below.

You act for exactly one landlord: the owner of the API key. You can only see and change that landlord’s properties and listings.

Rules for agents

  1. Get explicit confirmation before you submit or withdraw. Submitting a listing makes a legal ownership declaration in the landlord’s name. Withdrawing takes a live listing offline for good. Ask the landlord first, every time.
  2. Never invent facts. Address, unit number, rent, deposits, and photos must come from the landlord or from a source the landlord pointed you to. If a required value is missing, ask.
  3. Read before you replace. PUT /properties/{id} and PUT /listings/{id} replace the whole record. Fields you leave out are reset to empty or zero. Always GET the record, change only what the landlord asked for, and send the full object back.
  4. Report the outcome in plain words. After each change, tell the landlord what changed and the listing status, for example “Submitted. Status is now pending review.”
  5. Keep the key secret. Never print the full API key back to the landlord or anyone else.
  6. Upload only photos the landlord took for this handover; never reuse listing photos.
  7. To correct a landlord’s item, add a corrected one and ask the landlord to delete the original.
  8. When readiness.ready is true, tell the landlord to submit in the portal before submit_window.closes_on.
  9. Report every open thread where awaiting is landlord, with review_due_on.

Quick start

All paths below are relative to the API host shown above this guide. In the examples, $RENTAL_API is that host and $RENTAL_API_KEY is the landlord’s key.

export RENTAL_API="https://api.example.com"   # the API host shown above
export RENTAL_API_KEY="grm_live_..."          # the landlord's key

curl -s "$RENTAL_API/api/v1/rental/properties" \
  -H "x-api-key: $RENTAL_API_KEY"

A 200 with {"items": [...], "pagination": {...}} means the key works.

Getting an API key

The landlord creates the key, not you.

  1. The landlord signs in to the landlord portal and opens the API keys page (link above this guide).
  2. They choose a mode, permissions, an expiry (30 days to 1 year, 90 days recommended), and optionally an hourly request allowance (1 to 10,000, default 1,000).
  3. The full key is shown once. The landlord copies it and gives it to you.

Key modes:

Prefix Mode Behaviour
grm_live_ Live Real data. Submitted listings go to an admin for review before they go public.
grm_test_ Test Sandbox. Records are marked is_test: true and are invisible to live keys (and vice versa). A submitted test listing goes straight to live with no review and never appears on the public site.

Use a test key to rehearse a workflow. Test and live records cannot see each other: a live key gets 404 for a test record.

Permissions

The landlord picks permission bundles when creating the key. Each endpoint needs one scope.

Bundle in the portal Scopes granted What it unlocks
Manage my listings properties:read, properties:write, listings:read, listings:write Properties, listings, submit, withdraw, reading photos
Upload photos media:read, media:write Uploading and syncing photos
Upload documents proofs:read, proofs:write Uploading and listing ownership proofs
Money, agreements, tenants handover:read, handover:write Move-in handover reports, areas, entries, and photos (no contacts yet)

“Manage my listings” and “Upload photos” are ticked by default. “Upload documents” is opt-in: the landlord ticks it when creating the key if they want you to upload ownership proofs. Without it, the landlord uploads the proof themselves in the landlord portal (on the property), and you submit the listing afterwards.

A call without the needed scope returns 403 insufficient_permission. Tell the landlord which bundle to add; you cannot change a key’s permissions.

Core concepts

  • Property: the physical unit (address, type, rooms, size, amenities, photos, ownership proofs). One landlord can have many.
  • Listing: the rental offer for one property (rent, deposits, tenancy length, description, house rules) and its review status. A property has at most one active listing at a time.
  • Ownership proof: a document (PDF, JPEG, or PNG) that shows the landlord owns the property, such as a quit rent or assessment bill. At least one is required before a listing can be submitted.
  • Declaration: the landlord’s statement that they own the property and the listing is accurate. Sent as declaration_accepted: true when submitting. Only send it after the landlord has confirmed.

Listing status

draft ──submit──▶ pending_review ──admin approves──▶ live ──withdraw──▶ withdrawn
                        │                              │
                        └──admin rejects──▶ rejected   └──rent change──▶ pending_review
                                              │
                                              └──edit, then submit──▶ pending_review
Status Meaning What you can do
draft Created, not yet submitted. Edit, upload photos, submit.
pending_review Waiting for an admin. Edit. Wait.
live Public on the marketplace. Edit, withdraw.
rejected Admin rejected it. The reason is in review_reason on GET /listings/{id}. Fix the issue with PUT /listings/{id} or PUT /properties/{id}, then submit the same listing again.
suspended Held by an admin. Nothing that needs a new review. Tell the landlord to contact support.
withdrawn Taken offline by the landlord. Final. Nothing.
rented Let to a tenant. Final. Nothing.

Approving, rejecting, suspending, and marking as rented happen outside this API.

Changes that send a live listing back to pending_review:

  • changing monthly_rent on the listing,
  • changing the property’s address_line,
  • uploading a new ownership proof.

Other listing edits (deposits, tenancy length, dates, description, house_rules) keep the listing live.

Workflow: list a new property

  1. Find or create the property. GET /api/v1/rental/properties and look for a match first. If there is none, POST /api/v1/rental/properties. Note the id.
  2. Add photos. Upload files with POST /properties/{id}/photos, or copy a gallery from another site with PUT /properties/{id}/photos. Up to 10 photos per property.
  3. Check for an ownership proof. GET /properties/{id}/proofs (needs “Upload documents”). If the list is empty, upload one with POST /properties/{id}/proofs, or ask the landlord to upload it in the portal.
  4. Create the listing. POST /api/v1/rental/listings with the property id and the rental terms. It starts as draft.
  5. Confirm with the landlord. Show the property, terms, and photo count. Ask them to confirm the ownership declaration.
  6. Submit. POST /listings/{id}/submit with {"declaration_accepted": true}. Status becomes pending_review (or live with a test key).
  7. Check back later. GET /listings/{id} shows live once approved, with a slug, or rejected with a review_reason.

Workflow: change the rent

  1. GET /api/v1/rental/properties/{propertyId} and read current_listing.id.
  2. GET /api/v1/rental/listings/{listingId}.
  3. Change monthly_rent in that object and PUT /api/v1/rental/listings/{listingId} with every listing field.
  4. Tell the landlord that a live listing goes back to review after a rent change.

Workflow: take a listing offline

  1. Confirm with the landlord. Withdrawal is final: the property cannot get a new listing through the API afterwards.
  2. POST /api/v1/rental/listings/{listingId}/withdraw. Only works when the status is live.

Conventions

  • Format. JSON request and response bodies, snake_case field names. Photo and proof uploads use multipart/form-data.
  • IDs. Opaque strings. Store them; do not build them.
  • Money. monthly_rent is whole Malaysian ringgit (MYR): 2800 means RM2,800. Not sen.
  • Deposits. deposit_months is the security deposit in months of rent, decimals allowed (2, 1.5). utility_deposit is a fixed amount in MYR (300 means RM300), shown as-is on the public listing.
  • Dates. RFC 3339 timestamps, for example 2026-11-01T00:00:00Z. A bare 2026-11-01 is rejected.
  • Not yours = not found. A property or listing that belongs to someone else returns 404, the same as one that does not exist.

Properties

GET /api/v1/rental/properties

Lists the landlord’s properties. Scope properties:read.

Query Default Notes
page 1 1-based.
limit 20 1 to 100. Values outside that range fall back to 20.

Response 200:

{
  "items": [
    {
      "id": "8f1c…",
      "property_type": "condo",
      "building_name": "Arcoris",
      "address_line": "10 Jalan Kiara",
      "unit_number": "A-12-3",
      "area": "Mont Kiara",
      "city": "Kuala Lumpur",
      "state": "Kuala Lumpur",
      "postcode": "50480",
      "bedrooms": 2,
      "bathrooms": 2,
      "furnishing": "fully_furnished",
      "has_parking": true,
      "size_sqft": 950,
      "amenities": ["air_conditioning", "pool", "gym"],
      "is_test": false,
      "origin_channel": "api",
      "external_refs": [],
      "current_listing": { "id": "c41d…", "status": "live" },
      "created_at": "2026-09-30T08:00:00Z",
      "updated_at": "2026-09-30T08:00:00Z"
    }
  ],
  "pagination": { "page": 1, "limit": 20, "totalRecords": 1, "totalPages": 1, "hasNext": false, "hasPrev": false, "mode": "offset" }
}

current_listing is null when the property has no listing.

POST /api/v1/rental/properties

Creates a property, or updates the matching one if it already exists. Scope properties:write. Supports Idempotency-Key.

Request body:

Field Type Required Rules
property_type string yes condo, landed, or room.
address_line string yes Street address.
area string yes Neighbourhood, e.g. Mont Kiara.
city string yes
state string yes Malaysian state or federal territory, e.g. Selangor, Kuala Lumpur, Penang.
postcode string yes 5 digits.
unit_number string for condo and room Needed to tell units in the same building apart.
building_name string no Condo or apartment name.
bedrooms integer no
bathrooms integer no
furnishing string no Use fully_furnished, partially_furnished, or unfurnished.
has_parking boolean no
size_sqft integer no 0 (unknown) or 50 to 100,000.
amenities string[] no Only values from the amenity list below.
source string with external_ref Where the property came from, e.g. propertyguru. Lowercase letters, digits, _, -; up to 32 characters.
external_ref string with source The property’s id on that source.

Amenities: air_conditioning, water_heater, water_tank, washing_machine, refrigerator, kitchen, wifi, parking, ev_charging, security, gated_guarded, pool, gym, lift, balcony.

How a match is found:

  1. Same source and external_ref as an existing property: that property is updated with only the fields you sent. Response 200.
  2. Same address (building or street, unit number, and postcode) as one existing property: that property is updated and the source/external_ref is linked to it. Response 200.
  3. No match: a new property is created. Response 201.

So POST is safe to repeat. Sending source and external_ref makes repeat imports from another site land on the same property.

Example:

curl -s -X POST "$RENTAL_API/api/v1/rental/properties" \
  -H "x-api-key: $RENTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6c1b3f0e-1a2b-4c5d-8e9f-001122334455" \
  -d '{
    "property_type": "condo",
    "building_name": "Arcoris",
    "address_line": "10 Jalan Kiara",
    "unit_number": "A-12-3",
    "area": "Mont Kiara",
    "city": "Kuala Lumpur",
    "state": "Kuala Lumpur",
    "postcode": "50480",
    "bedrooms": 2,
    "bathrooms": 2,
    "furnishing": "fully_furnished",
    "has_parking": true,
    "size_sqft": 950,
    "amenities": ["air_conditioning", "pool", "gym"]
  }'

Conflicts specific to this endpoint:

Code Meaning What to do
unit_number_required The landlord already has a property in this building and you sent no unit_number. fields lists the existing property ids. Ask for the unit number, or use one of the listed properties if it is the same unit.
ambiguous_match The address matches more than one existing property. fields lists their ids. Ask the landlord which one is meant, then use PUT /properties/{id}.
duplicate_address Another landlord already registered this address. Stop and tell the landlord; do not retry with a changed address.
duplicate_external_ref This source and external_ref are already linked to another property. Use that property instead.

GET /api/v1/rental/properties/{id}

Returns one property, with current_listing ({id, status} or null). Scope properties:read.

PUT /api/v1/rental/properties/{id}

Replaces the property. Scope properties:write. Same body as POST; only property_type and address_line are checked as required, but every field you omit is cleared. Send the full object you got from GET, with your changes applied.

Changing address_line sends a live listing back to review.

Photos

Each property holds up to 10 photos. Photos are shown in position order.

GET /api/v1/rental/properties/{id}/photos

Scope properties:read. Response 200:

{ "photos": [ { "id": "…", "position": 0, "url": "https://…", "source": null, "created_at": "…" } ] }

POST /api/v1/rental/properties/{id}/photos

Uploads one photo. Scope media:write. Supports Idempotency-Key.

  • Multipart form field photo.
  • JPEG or PNG, up to 5 MB. The part’s content type must be image/jpeg or image/png.
  • The photo is added after the existing ones. Response 201 with the photo.
curl -s -X POST "$RENTAL_API/api/v1/rental/properties/$PROPERTY_ID/photos" \
  -H "x-api-key: $RENTAL_API_KEY" \
  -F "photo=@living-room.jpg;type=image/jpeg"

PUT /api/v1/rental/properties/{id}/photos

Copies a gallery from public image URLs, for example from the landlord’s listing on another site. Scope media:write.

{
  "source": "propertyguru",
  "photos": [
    { "url": "https://cdn.example.com/1.jpg" },
    { "url": "https://cdn.example.com/2.jpg" }
  ]
}
  • The list is the complete set for that source. Photos from that source that are not in the list are removed. Photos uploaded directly or from other sources are never touched. An empty list removes all photos from that source.
  • The order of photos sets the display order.
  • URLs must be https, publicly reachable, at most 2,048 characters, JPEG or PNG, up to 5 MB each. No duplicate URLs.
  • One bad URL does not fail the request. Check results:
{
  "photos": [ … ],
  "results": [
    { "url": "https://cdn.example.com/1.jpg", "status": "created", "photo_id": "…" },
    { "url": "https://cdn.example.com/2.jpg", "status": "fetch_timeout", "photo_id": null }
  ]
}
Status Meaning
created New photo stored.
unchanged Already stored, same image.
replaced The image at that URL changed and was replaced.
duplicate_image The same image is already in the gallery from another upload.
url_not_https The URL is not https.
url_blocked The URL points to a private or blocked address.
too_many_redirects More than 3 redirects.
fetch_timeout The image took too long to download.
fetch_failed The download failed.
unsupported_media_type Not a JPEG or PNG.
too_large Over 5 MB.

Repeating the same PUT is safe and retries failed URLs.

Ownership proofs

GET /api/v1/rental/properties/{id}/proofs

Scope proofs:read. Returns a JSON array, newest first, with metadata only:

[ { "id": "…", "created_at": "2026-09-30T08:00:00Z" } ]

An empty array means the listing cannot be submitted yet.

POST /api/v1/rental/properties/{id}/proofs

Uploads one ownership proof. Scope proofs:write. Supports Idempotency-Key.

  • Multipart form field proof.
  • PDF, JPEG, or PNG, up to 5 MB. Up to 10 proofs per property.
  • Response 201 with { "id", "created_at" }.
  • Uploading a proof sends a live listing back to review.
curl -s -X POST "$RENTAL_API/api/v1/rental/properties/$PROPERTY_ID/proofs" \
  -H "x-api-key: $RENTAL_API_KEY" \
  -F "proof=@assessment-bill.pdf;type=application/pdf"

Listings

POST /api/v1/rental/listings

Creates the property’s listing as draft, or updates its active listing. Scope listings:write. Supports Idempotency-Key.

Field Type Required Rules
property_id string yes
monthly_rent integer yes Whole MYR, greater than 0.
deposit_months number yes Security deposit in months of rent, 0 or more. Typical: 2.
utility_deposit number yes Utility deposit in MYR, 0 to 999. Typical: 300.
tenancy_months integer yes Tenancy length in months, 0 or more. Typical: 12.
available_from string no RFC 3339 timestamp.
description string no What a renter sees. Write it from the landlord’s facts.
house_rules string no E.g. no smoking, no pets.

What happens:

  • The property has never had a listing: a draft is created. Response 201.
  • The property has a draft, pending_review, or live listing: that listing is updated. Response 200.
  • The property only has finished listings (withdrawn, rented, rejected, suspended): 409 listing_not_reopenable. For a rejected listing, edit it with PUT /listings/{id} and submit it again instead.
curl -s -X POST "$RENTAL_API/api/v1/rental/listings" \
  -H "x-api-key: $RENTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "property_id": "'"$PROPERTY_ID"'",
    "monthly_rent": 2800,
    "deposit_months": 2,
    "utility_deposit": 300,
    "tenancy_months": 12,
    "available_from": "2026-11-01T00:00:00Z",
    "description": "Fully furnished 2-bedroom unit with a city view, 5 minutes walk to Publika.",
    "house_rules": "No smoking. No pets."
  }'

Response:

{
  "id": "c41d…",
  "property_id": "8f1c…",
  "monthly_rent": 2800,
  "deposit_months": 2,
  "utility_deposit": 300,
  "tenancy_months": 12,
  "available_from": "2026-11-01T00:00:00Z",
  "description": "Fully furnished 2-bedroom unit with a city view, 5 minutes walk to Publika.",
  "house_rules": "No smoking. No pets.",
  "status": "draft",
  "origin_channel": "api",
  "created_at": "2026-10-01T08:00:00Z",
  "updated_at": "2026-10-01T08:00:00Z"
}

GET /api/v1/rental/listings/{id}

Scope listings:read. Returns the listing plus review fields: review_reason (why it was rejected), reviewed_at, published_at, and slug (set once live).

PUT /api/v1/rental/listings/{id}

Replaces the listing terms. Scope listings:write. Same fields as POST without property_id. Every field you omit is reset (available_from to empty, description to empty, numbers to 0). Send the full object.

A rent change on a live listing sends it back to pending_review. A listing that is suspended refuses changes that need review (409 listing_status_conflict).

POST /api/v1/rental/listings/{id}/submit

Sends a draft or rejected listing for review. Scope listings:write.

{ "declaration_accepted": true }

Only send true after the landlord has confirmed that they own the property and the listing is accurate.

Result Meaning
200, status pending_review Submitted. An admin will review it.
200, status live Test key: published to the sandbox right away.
400 declaration_not_accepted declaration_accepted was missing or false.
400 no_ownership_proof Upload an ownership proof first.
409 duplicate_under_review Another verified owner has a matching property. The listing is held for an admin. Tell the landlord.
409 listing_status_conflict The listing is not draft or rejected.

POST /api/v1/rental/listings/{id}/withdraw

Takes a live listing offline. Final. Scope listings:write. No body. Returns 409 listing_status_conflict if the listing is not live.

Move-in handover

A move-in Handover Report records the condition, inventory, access devices, and meter readings of a property at the start of a tenancy. It provides an objective baseline against which move-out condition and deposit returns are evaluated.

Lifecycle

draft ──landlord submits in portal──▶ tenant_review ──tenant accepts or 7 days elapse──▶ finalized
  1. draft: The agent and landlord author areas, entries, and attach photo evidence. The agent can create, update, and delete items they created (created_via: "api"). The report can only be created during the submit window (starts_on − 7 through starts_on + 3 Malaysian calendar dates).
  2. tenant_review: The landlord submits the Baseline in the portal. The Baseline content is frozen. The tenant has a 7-day review window (review_due_on) to review the Baseline and open dispute threads on areas or entries. The landlord replies to threads in the portal.
  3. finalized: All open threads are resolved and the tenant accepts the report, or the 7-day window closes with no tenant response. The report is permanent.

Rules and Roles

  • Agent drafts; Landlord submits: The API key allows agents to build and edit the draft. Only the landlord can submit the Baseline in the portal. The agent never submits, signs, or approves.
  • Own-items rule: Agents can only update (PATCH) or delete (DELETE) areas, entries, and photos that have created_via = 'api'. Items created by the landlord in the portal cannot be modified or removed by the agent (409 handover_item_not_editable). To correct a landlord item, add a corrected item and ask the landlord to remove the original in the portal.
  • Submit window: Reports can only be created between starts_on − 7 days and starts_on + 3 days (opens_on to closes_on). Outside this window, report creation returns 409 handover_window_not_open or 409 handover_window_closed.
  • Readiness check: GET /tenancies/{id}/handover-report includes a readiness object indicating whether the draft is ready for submission (ready: true). Problems include no_areas, area_without_photo, meter_without_photo, photo_not_ready, and outside_submit_window. Warnings include staged_photo_unattached. When ready is true, tell the landlord to submit in the portal before submit_window.closes_on.
  • Threads and awaiting: During tenant review, dispute threads indicate who must respond via awaiting: "landlord" or awaiting: "tenant". Report every open thread where awaiting is landlord, with review_due_on.

Entry types and fields

Each entry belongs to an area and has an entry_type:

entry_type Description Specific Fields Rules
condition Room element condition (walls, flooring, doors) condition_note (string, optional) Does not require photo
inventory Furnishings and appliances (sofa, fridge, bed) quantity (integer, optional) Does not require photo
access_device Keys, access cards, remotes quantity (integer, optional) Does not require photo
meter_reading Utility meter readings (electricity, water) meter_value (decimal string), meter_unit (kwh or m3) Requires at least one attached photo. meter_value must be a decimal string with up to 3 decimal places (e.g. "10234.500").

Photo evidence, staging, and budgets

  • Evidence requirement: Every area must have at least one directly attached photo. Every meter_reading entry must have at least one attached photo.
  • Staging vs Attaching: Photos uploaded with area_id or entry_id are attached immediately. Photos uploaded without a target are staged. Staged photos do not satisfy area or meter requirements until attached with PATCH /handover-media/{id}. Staged photos with uploading or failed status do not block submission.
  • Photo budgets: Up to 10 photos per area, up to 100 photos per baseline report. Exceeding budgets returns 409 handover_image_limit_reached.
  • Idempotency: POST /handover-reports/{id}/media requires an Idempotency-Key header (UUID).

GET /api/v1/rental/tenancies/{id}/handover-report

Scope handover:read. Returns the tenancy’s move-in handover report including status, submit window, readiness evaluation, areas, entries, media metadata, and dispute threads. Returns 404 handover_report_not_found if no report exists or the tenancy is not visible.

POST /api/v1/rental/tenancies/{id}/handover-report

Scope handover:write. Creates the move-in handover report draft for a visible tenancy, or returns 200 with the existing report. Must be called within the submit window (starts_on - 7 through starts_on + 3 Malaysian calendar dates). Returns 409 handover_window_not_open before the window or 409 handover_window_closed after the window.

GET /api/v1/rental/handover-reports/{id}

Scope handover:read. Returns the move-in handover report by ID. Returns 404 handover_report_not_found if not found or not visible.

POST /api/v1/rental/handover-reports/{id}/areas

Scope handover:write. Adds an area to a draft handover report. Supports Idempotency-Key. Items created by the agent are marked created_via: "api".

Field Type Required Rules
name string yes Name of the area (e.g. “Living Room”, “Kitchen”). Max 100 characters.
sort_order integer no Display order among areas (default: appended to end).

Returns 409 handover_item_not_editable if the report is not draft.

PATCH /api/v1/rental/handover-areas/{id}

Scope handover:write. Updates an area’s name or sort_order. Only allowed on areas created by the agent (created_via = 'api') and while the report is draft. Returns 409 handover_item_not_editable for landlord-created areas or submitted reports.

DELETE /api/v1/rental/handover-areas/{id}

Scope handover:write. Deletes an area and all its entries. Only allowed on areas created by the agent while the report is draft. Returns 204. Returns 409 handover_item_not_editable for landlord-created areas or submitted reports.

POST /api/v1/rental/handover-areas/{id}/entries

Scope handover:write. Adds an entry to an area in a draft report. Supports Idempotency-Key. Items created by the agent are marked created_via: "api".

Field Type Required Rules
entry_type string yes One of condition, inventory, meter_reading, access_device.
label string yes Entry label (e.g. “Air Conditioner”, “Electric Meter”).
sort_order integer no Display order within the area.
quantity integer no Used for inventory and access_device. Minimum 1.
condition_note string no Used for condition. Description of current state.
meter_value string yes for meter Decimal string with up to 3 decimal places (e.g. "10423.500").
meter_unit string yes for meter kwh or m3.

Returns 409 handover_item_not_editable if the report is not draft.

PATCH /api/v1/rental/handover-entries/{id}

Scope handover:write. Updates an entry’s fields. Only allowed on entries created by the agent (created_via = 'api') and while the report is draft. Returns 409 handover_item_not_editable on landlord items or submitted reports.

DELETE /api/v1/rental/handover-entries/{id}

Scope handover:write. Deletes an entry. Only allowed on entries created by the agent while the report is draft. Returns 204. Returns 409 handover_item_not_editable on landlord items or submitted reports.

POST /api/v1/rental/handover-reports/{id}/media

Scope handover:write. Uploads a photo to the draft handover report using multipart form data. Idempotency-Key header is required.

Part Type Required Description
file binary yes JPEG or PNG file, up to 5MB. HEIC is not supported.
area_id string no Directly attaches the photo to this area.
entry_id string no Directly attaches the photo to this entry.

If neither area_id nor entry_id is provided, the photo is uploaded as staged. Only allowed while the report is draft. Returns 409 handover_image_limit_reached if area (10) or report (100) budget is reached.

PATCH /api/v1/rental/handover-media/{id}

Scope handover:write. Attaches a staged photo to an area or entry. Send {"area_id": "..."} or {"entry_id": "..."}. Only allowed on photos created by the agent while the report is draft. Returns 409 handover_item_not_editable otherwise.

DELETE /api/v1/rental/handover-media/{id}

Scope handover:write. Deletes a photo from the draft. Only allowed on photos created by the agent while the report is draft. Returns 204. Returns 409 handover_item_not_editable otherwise.

GET /api/v1/rental/handover-media/{id}/content

Scope handover:read. Streams photo content: original image bytes by default, or thumbnail with ?variant=thumbnail. Available for all photos in a visible report, including tenant dispute photos. Invalid variant returns 400 validation_failed with fields: ["variant"].

Errors

Every error has the same shape:

{
  "error": {
    "code": "validation_failed",
    "message": "Validation failed",
    "fields": ["property_type"]
  },
  "request_id": "req-12345"
}

Act on code, not on message. fields appears for validation_failed (the fields to fix), ambiguous_match, and unit_number_required (candidate property ids). Quote request_id when the landlord contacts support.

Status Code What to do
400 validation_failed Fix the fields in fields and retry.
400 declaration_not_accepted Ask the landlord to confirm, then send declaration_accepted: true.
400 no_ownership_proof Upload a proof, or ask the landlord to.
401 invalid_api_key The key is missing or wrong. Ask the landlord for the key.
401 key_expired Ask the landlord to create a new key.
401 key_disabled The key was revoked. Ask the landlord for a new key.
403 insufficient_permission The key lacks the bundle for this endpoint. Tell the landlord which bundle to add.
403 insufficient_role The account is not a landlord account.
403 account_unavailable The account is suspended. The landlord must contact support.
404 property_not_found, listing_not_found Wrong id, wrong key mode (test vs live), or not this landlord’s. Re-list properties to find the right id.
404 handover_report_not_found Handover report does not exist or tenancy is not visible.
404 handover_item_not_found Handover area, entry, or photo does not exist or is not visible.
404 route_not_found Check the path.
405 method_not_allowed Check the HTTP method.
409 listing_status_conflict The listing’s status does not allow this. GET it and re-plan.
409 listing_not_reopenable The property cannot get a new listing through the API.
409 idempotency_conflict You reused an Idempotency-Key with a different body. Use a new key.
409 duplicate_under_review Held for admin review. Tell the landlord.
409 ambiguous_match, unit_number_required, duplicate_address, duplicate_external_ref See POST /properties above.
409 handover_item_not_editable Handover area, entry, or photo cannot be edited or deleted (created via portal or report submitted/finalized).
409 handover_window_not_open Move-in handover draft cannot be created before starts_on - 7 days.
409 handover_window_closed Move-in handover draft cannot be created after starts_on + 3 days.
409 handover_image_limit_reached Area (10 images) or report baseline (100 images) photo budget reached.
410 upload_expired Photo upload session has expired. Upload a new photo.
429 rate_limited Wait for Retry-After seconds.
500 internal_error Retry once later. If it repeats, report the request_id.
503 auth_unavailable, media_unavailable, rate_limit_unavailable Temporary. Retry after a short wait.

Rate limits

Each key has an hourly allowance, 1,000 requests by default. The counter resets at the top of every UTC hour. Every request with a valid key counts, including ones refused for validation or permissions.

Header Meaning
X-RateLimit-Limit The hourly allowance.
X-RateLimit-Remaining Requests left this hour.
X-RateLimit-Reset Unix time (seconds) of the next reset.
Retry-After On 429 only: seconds to wait.

Retries and Idempotency-Key

POST /properties, POST /properties/{id}/photos, POST /properties/{id}/proofs, and POST /listings accept an optional Idempotency-Key header. Generate one random value (a UUID) per intended action and reuse it only when retrying that same action, for example after a timeout.

  • Same key, same body: you get 200 with the current resource and the header Idempotent-Replay: true. Nothing is created twice.
  • Same key, different body: 409 idempotency_conflict.

The PUT endpoints are already safe to repeat. Do not blindly retry submit or withdraw: GET the listing first and check its status.