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
- 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.
- 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.
- Read before you replace.
PUT /properties/{id}andPUT /listings/{id}replace the whole record. Fields you leave out are reset to empty or zero. AlwaysGETthe record, change only what the landlord asked for, and send the full object back. - 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.”
- Keep the key secret. Never print the full API key back to the landlord or anyone else.
- Upload only photos the landlord took for this handover; never reuse listing photos.
- To correct a landlord’s item, add a corrected one and ask the landlord to delete the original.
- When
readiness.readyis true, tell the landlord to submit in the portal beforesubmit_window.closes_on. - Report every open thread where
awaitingislandlord, withreview_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.
- The landlord signs in to the landlord portal and opens the API keys page (link above this guide).
- 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).
- 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: truewhen 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_renton 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
- Find or create the property.
GET /api/v1/rental/propertiesand look for a match first. If there is none,POST /api/v1/rental/properties. Note theid. - Add photos. Upload files with
POST /properties/{id}/photos, or copy a gallery from another site withPUT /properties/{id}/photos. Up to 10 photos per property. - Check for an ownership proof.
GET /properties/{id}/proofs(needs “Upload documents”). If the list is empty, upload one withPOST /properties/{id}/proofs, or ask the landlord to upload it in the portal. - Create the listing.
POST /api/v1/rental/listingswith the property id and the rental terms. It starts asdraft. - Confirm with the landlord. Show the property, terms, and photo count. Ask them to confirm the ownership declaration.
- Submit.
POST /listings/{id}/submitwith{"declaration_accepted": true}. Status becomespending_review(orlivewith a test key). - Check back later.
GET /listings/{id}showsliveonce approved, with aslug, orrejectedwith areview_reason.
Workflow: change the rent
GET /api/v1/rental/properties/{propertyId}and readcurrent_listing.id.GET /api/v1/rental/listings/{listingId}.- Change
monthly_rentin that object andPUT /api/v1/rental/listings/{listingId}with every listing field. - Tell the landlord that a live listing goes back to review after a rent change.
Workflow: take a listing offline
- Confirm with the landlord. Withdrawal is final: the property cannot get a new listing through the API afterwards.
POST /api/v1/rental/listings/{listingId}/withdraw. Only works when the status islive.
Conventions
- Format. JSON request and response bodies,
snake_casefield names. Photo and proof uploads usemultipart/form-data. - IDs. Opaque strings. Store them; do not build them.
- Money.
monthly_rentis whole Malaysian ringgit (MYR):2800means RM2,800. Not sen. - Deposits.
deposit_monthsis the security deposit in months of rent, decimals allowed (2,1.5).utility_depositis a fixed amount in MYR (300means RM300), shown as-is on the public listing. - Dates. RFC 3339 timestamps, for example
2026-11-01T00:00:00Z. A bare2026-11-01is 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:
- Same
sourceandexternal_refas an existing property: that property is updated with only the fields you sent. Response200. - Same address (building or street, unit number, and postcode) as one existing property: that property is updated and the
source/external_refis linked to it. Response200. - 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/jpegorimage/png. - The photo is added after the existing ones. Response
201with 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
photossets 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
201with{ "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
draftis created. Response201. - The property has a
draft,pending_review, orlivelisting: that listing is updated. Response200. - The property only has finished listings (
withdrawn,rented,rejected,suspended):409 listing_not_reopenable. For arejectedlisting, edit it withPUT /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
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 − 7throughstarts_on + 3Malaysian calendar dates).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.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 havecreated_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 − 7days andstarts_on + 3days (opens_ontocloses_on). Outside this window, report creation returns409 handover_window_not_openor409 handover_window_closed. - Readiness check:
GET /tenancies/{id}/handover-reportincludes areadinessobject indicating whether the draft is ready for submission (ready: true). Problems includeno_areas,area_without_photo,meter_without_photo,photo_not_ready, andoutside_submit_window. Warnings includestaged_photo_unattached. Whenreadyis true, tell the landlord to submit in the portal beforesubmit_window.closes_on. - Threads and awaiting: During tenant review, dispute threads indicate who must respond via
awaiting: "landlord"orawaiting: "tenant". Report every open thread whereawaitingislandlord, withreview_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_readingentry must have at least one attached photo. - Staging vs Attaching: Photos uploaded with
area_idorentry_idare attached immediately. Photos uploaded without a target are staged. Staged photos do not satisfy area or meter requirements until attached withPATCH /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}/mediarequires anIdempotency-Keyheader (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
200with the current resource and the headerIdempotent-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.