# API reference (https://docs.propertypixel.app/api) Every REST endpoint, generated from the same operations as the MCP tools. The REST API lives at `https://api.propertypixel.app/v1`. Every endpoint matches an [MCP tool](/docs/mcp-tools): same parameters, same result. ```bash curl https://api.propertypixel.app/v1/account \ -H "Authorization: Bearer $PP_API_KEY" ``` * **Authentication:** a personal API key (`pp_live_…`) or an OAuth access token, sent as a bearer token. See [Authentication](/docs/authentication). * **OpenAPI:** the full OpenAPI 3.1 document is at [`https://api.propertypixel.app/v1/openapi.json`](https://api.propertypixel.app/v1/openapi.json). Import it into Postman, Insomnia or a client generator. * **Spending credits:** spending endpoints return a quote first. Confirm with `quote_id`, or send `auto_confirm: true` with an API key. See [Credits and quotes](/docs/credits-and-quotes). * **Conventions:** errors use RFC 9457 problem details ([Errors](/docs/errors)). Writes accept an `Idempotency-Key`, lists page with `cursor`, and every response carries [rate-limit headers](/docs/rate-limits). ## Endpoints [#endpoints] | Group | What it covers | | ---------------------------------------------------------------- | ------------------------------------------------------------ | | [Account](/docs/api/account/get_account) | The connected workspace, plan and credit balance | | [Projects](/docs/api/projects/list_projects) | Create, list, read and delete listing projects; show results | | [Photos](/docs/api/photos/add_photos) | Add photos by URL, or delete them | | [Uploads](/docs/api/uploads/create_upload) | Signed uploads and upload links | | [Edit options](/docs/api/edit-options/list_edit_options) | Every enhancement type, staging style and video setting | | [Editing](/docs/api/editing/enhance_photos) | Enhance and virtually stage photos | | [Video](/docs/api/video/create_video) | Photo to Video clips and Listing Tours | | [Recommendations](/docs/api/recommendations/get_recommendations) | Free AI suggestions per photo, and applying them | | [Jobs](/docs/api/jobs/get_job_status) | Job status and details, and picking favourite results | | [Downloads](/docs/api/downloads/get_download_link) | Signed download links and zips | | [Quotes](/docs/api/quotes/create_quote) | Price a spending request before running it | # Authentication (https://docs.propertypixel.app/authentication) OAuth for AI assistants and apps, API keys for scripts. Every credential is bound to one workspace. Every request carries a bearer token: ```http Authorization: Bearer ``` The token is either an **API key** or an **OAuth access token**. Both resolve to a **connection**: one user, one workspace, one access level. ## API keys [#api-keys] Use an API key for your own scripts and integrations. * Create keys in [Settings → Developers](https://propertypixel.app/settings/developers). Give each key a name, a workspace and an access level. * Keys look like `pp_live_…`. The full key is shown once; PropertyPixel stores only a hash and the visible prefix. * The list shows when each key was last used. * Keep keys on a server. Never put one in a browser, a mobile app or a public repository. ## OAuth [#oauth] ChatGPT, Claude and other MCP clients use OAuth 2.1 with PKCE. You don't set anything up: the client registers itself, opens the PropertyPixel sign-in page, and you choose a workspace and access level on the consent screen. To build your own app, use the authorization code flow with PKCE (`S256`) against the PropertyPixel authorization server. Its metadata is published at `https://mcp.propertypixel.app/.well-known/oauth-protected-resource`, and dynamic client registration is enabled. Access tokens are short-lived JWTs; use the refresh token to get a new one. ## Workspaces [#workspaces] A connection only ever sees the workspace chosen when it was created: its projects, photos, results and credit balance. To work in another workspace, create another key or connect again and pick it. Your membership is checked on every request. If you leave the workspace, the connection stops working. ## Access levels [#access-levels] | Access | Value | Can do | | ----------------- | ------- | ------------------------------------------------------------------------------------------------------- | | **View only** | `read` | List and read projects, photos, results and jobs. Pick results. Create download links. | | **View and edit** | `write` | Everything above, plus create and delete projects and photos, upload, and run edits that spend credits. | A view-only connection that calls a write operation gets `403` with code `forbidden_scope`. Reviewers in a team workspace can only hold view-only access; if your role is lowered to reviewer, an edit connection becomes view-only. `GET /v1/account` returns `scope.granted` (what the connection was given) and `scope.effective` (what it can do now). ## Revoking [#revoking] Open [Settings → Developers](https://propertypixel.app/settings/developers): * **Connected apps** lists ChatGPT, Claude and other OAuth clients. **Revoke** signs the app out of PropertyPixel. * **API keys** lists your keys. **Revoke** disables the key. Revoking takes effect on the next request, which then gets `401 unauthorized`. # Changelog (https://docs.propertypixel.app/changelog) Changes to the PropertyPixel API and MCP server. ## October 2026: v1 [#october-2026-v1] * The PropertyPixel MCP server at `https://mcp.propertypixel.app`, for ChatGPT, Claude and other MCP clients, with interactive widgets. * The REST API at `https://api.propertypixel.app/v1`, with an [OpenAPI 3.1 document](https://api.propertypixel.app/v1/openapi.json). * OAuth for apps, and personal API keys bound to one workspace with view-only or view-and-edit access. * Projects, photos (URL, ChatGPT files, upload links, signed uploads), enhancement, virtual staging, recommendations, Photo to Video, picks and downloads. * Quote-then-confirm spending, idempotency keys, rate limits, and signed webhooks. # Credits and quotes (https://docs.propertypixel.app/credits-and-quotes) How spending works, what things cost, and how to confirm a spend. Agents and scripts spend the connected workspace's credits, at the same prices as the web app. Nothing is ever spent without a quote that was confirmed, or an explicit `auto_confirm` in a REST call. ## Prices [#prices] | Operation | Cost | | ----------------------- | ----------------------------------------------------- | | `enhance_photos` | 100 credits per photo, all enhancement types combined | | `stage_photos` | 100 credits per variation per photo | | Ultra HD add-on | +100 credits per result | | `create_video` | 400 credits per photo | | `get_recommendations` | Free | | `apply_recommendations` | 100 credits per photo | `list_edit_options` (`GET /v1/edit-options`) returns the current prices as `credit_costs`. Jobs that fail are refunded automatically. ## The quote → confirm flow [#the-quote--confirm-flow] 1. Call a spending operation **without** `quote_id`. You get a **quote**: what will run, the credit cost, your balance before and after, and warnings such as a trial watermark. Nothing is spent. 2. Show the quote to the user. 3. Call the **same operation with the same arguments** plus `quote_id`. The work runs and you get job IDs. A quote: * lasts **10 minutes**, * can be used **once**, * is bound to the connection and to the exact request. Change any argument and you get `409 quote_mismatch`; request a new quote. * with `all_photos: true`, covers the photos in the project when it was quoted. If photos are added or removed before you confirm, or the price goes up, confirming fails with `409 quote_mismatch`. Over MCP, the first call returns the quote as the tool result. ChatGPT shows it as a card with a **Run** button; other clients tell you the cost and wait for your yes. Over REST, the first call returns `409` with code `quote_required` and the quote in the `quote` field. You can also price a request explicitly with `POST /v1/quotes`: ```bash curl -s -X POST "$PP_API/quotes" \ -H "Authorization: Bearer $PP_API_KEY" \ -H "Content-Type: application/json" \ -d '{"operation": "stage_photos", "input": {"photo_ids": ["…"], "design_style": "modern", "variations": 2}}' ``` ## `auto_confirm` (REST only) [#auto_confirm-rest-only] Scripts that already know what they will spend can send `"auto_confirm": true` and skip the quote: ```json { "photo_ids": ["…"], "enhancement_types": ["improve_lighting"], "auto_confirm": true } ``` Use it only in code you control. It works only with a personal API key: app (OAuth) connections and MCP always get a quote. ## Not enough credits [#not-enough-credits] If the balance can't cover a request, the quote says so (`sufficient: false`) and confirming it fails with `402 insufficient_credits`. Add credits or change plan at [propertypixel.app/settings/billing](https://propertypixel.app/settings/billing). Inside ChatGPT, PropertyPixel only tells you to manage your plan at propertypixel.app. ## Trial accounts [#trial-accounts] New accounts start with a free trial of 500 credits. Trial accounts can use every operation, but results carry a PropertyPixel watermark (`watermarked: true` on the quote and on each job), and some enhancement types need a paid plan. `GET /v1/account` shows the plan and whether results are watermarked. # Errors (https://docs.propertypixel.app/errors) Every error code and what to do about it. Errors use [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem details (`application/problem+json`). Branch on `code`, which is stable; `detail` is for people. ```json { "type": "https://docs.propertypixel.app/errors#insufficient_credits", "title": "Insufficient credits", "status": 402, "code": "insufficient_credits", "detail": "Insufficient credits", "credits_required": 400, "credits_available": 120 } ``` | Code | Status | Meaning | | ------------------------- | ------ | ------------------------------------------------------------------------------------------------------------ | | `unauthorized` | 401 | Missing, invalid, expired or revoked credentials. Reconnect the app or use a valid API key. | | `forbidden_scope` | 403 | The connection is view-only, or your workspace role no longer allows changes. | | `forbidden` | 403 | You no longer have access to the connection's workspace. | | `not_found` | 404 | The resource doesn't exist in this workspace, or the endpoint doesn't exist. | | `validation_failed` | 400 | The input is invalid. `errors` lists each problem with its `path`. | | `insufficient_credits` | 402 | Not enough credits. Includes `credits_required` and `credits_available`. | | `quote_required` | 409 | The request spends credits. The response includes a `quote`; resend with `quote_id` after the user confirms. | | `quote_expired` | 409 | The quote is older than 10 minutes or was already used. Request a new one. | | `quote_mismatch` | 409 | The request differs from what was quoted. Request a new quote. | | `idempotency_conflict` | 422 | This `Idempotency-Key` was used with a different request. | | `idempotency_in_progress` | 409 | The original request with this key is still running. Retry shortly. | | `conflict` | 409 | The request conflicts with the current state, for example uploading to a finished upload link. | | `rate_limited` | 429 | Too many requests. Wait `Retry-After` seconds. | | `upload_rejected` | 422 | A photo couldn't be accepted: wrong type, too large, unreachable URL, or the project is full. | | `internal` | 500 | Something went wrong on our side. Retrying with the same `Idempotency-Key` is safe. | Failed jobs are not errors in a response: they appear as jobs with `status: "failed"` and are refunded automatically. # Overview (https://docs.propertypixel.app) Use PropertyPixel from ChatGPT, Claude or your own code. PropertyPixel edits listing photos: lighting, decluttering, blue skies, day to dusk, virtual staging, and short videos made from stills. Everything the web Studio does for photos and video is also available to AI agents and scripts, at the same credit prices. There are two ways in: | Surface | Address | Best for | | ---------- | ---------------------------------- | ---------------------------------------------------------- | | MCP server | `https://mcp.propertypixel.app` | ChatGPT, Claude and other AI assistants. You sign in once. | | REST API | `https://api.propertypixel.app/v1` | Scripts and integrations, with an API key and webhooks. | Both expose the same operations. An MCP tool such as `enhance_photos` is `POST /v1/enhancements` over REST, with the same parameters and the same result. ## How it works [#how-it-works] 1. **Connect.** Sign in through ChatGPT or Claude, or create an API key in [Settings → Developers](https://propertypixel.app/settings/developers). Each connection belongs to one workspace and is either view-only or allowed to make changes. 2. **Add photos.** Create a project for the listing, then add photos from links, from files attached in ChatGPT, from a phone with an upload link, or with a signed upload. 3. **Edit.** Ask for enhancements, staging or a video. You see the price first; nothing is spent until you confirm it. 4. **Collect results.** Jobs usually finish in under a minute (videos take a few minutes). Poll for status, or receive a webhook, then download single files or a zip. ## Start here [#start-here] Use PropertyPixel inside a ChatGPT conversation. Add PropertyPixel to Claude as a custom connector. Enhance a listing from the command line with curl. Every endpoint, generated from the OpenAPI document. ## Things to know [#things-to-know] * **Credits.** Usage spends your workspace's existing credits. 100 credits is one enhanced photo. See [Credits and quotes](/docs/credits-and-quotes). * **Trial accounts** can use everything. Their results carry a watermark, as in the web app. * **Privacy.** Agents only see the workspace you connect them to. You can revoke any connection or key at any time in Settings → Developers. * **For agents.** These docs are available as plain text at [`/llms.txt`](/docs/llms.txt) and [`/llms-full.txt`](/docs/llms-full.txt), and every page has a Markdown version at the same URL plus `.md`. The OpenAPI document is at `https://api.propertypixel.app/v1/openapi.json`. # Jobs and polling (https://docs.propertypixel.app/jobs) How edits run in the background and how to wait for results. Every edit you start (enhance, stage, apply recommendations, video) returns immediately with one **job** per result. Jobs run in the background: | Kind | Typical time | | ------------- | --------------------------- | | `enhancement` | 30–90 seconds | | `staging` | 30–90 seconds per variation | | `video` | a few minutes | ## Statuses [#statuses] `pending` → `processing` → `completed` | `failed` | `cancelled` A failed job is refunded automatically, and its `error` holds a `code` and a `message`. ## Polling [#polling] Call `get_job_status` (REST: `GET /v1/jobs?ids=,`, up to 100 IDs). The response contains: * `jobs`: the current state of each job, with `progress` (0–100) while processing. * `all_done`: `true` once every job has reached a final status. * `poll_after_ms`: how long to wait before polling again (`0` when everything is done). Wait `poll_after_ms` between calls. In ChatGPT and Claude, the PropertyPixel widget polls for you and shows progress live. ```bash curl "https://api.propertypixel.app/v1/jobs?ids=$JOB_ID" \ -H "Authorization: Bearer $PROPERTYPIXEL_API_KEY" ``` ## Results [#results] When a job is `completed`, `result` is `{ url, expires_at }`. The URL is a signed link that stays valid for about an hour. Fetch the job again for a fresh link. `thumbnail` is a smaller preview. Trial accounts receive watermarked results (`watermarked: true`). To download several results as one file, use `get_download_link` (`POST /v1/downloads`) with `job_ids`, or `project_id` plus `picks_only: true`. ## Webhooks instead of polling [#webhooks-instead-of-polling] Scripts can subscribe to `job.completed`, `job.failed` and `video.completed` instead of polling. See [Webhooks](/docs/webhooks). # MCP tools (https://docs.propertypixel.app/mcp-tools) Every tool the PropertyPixel MCP server exposes. Server URL: `https://mcp.propertypixel.app` (Streamable HTTP, OAuth). Each tool maps to one REST endpoint with the same inputs and outputs. See the [API reference](/docs/api). | Tool | What it does | REST | Access | Hints | | ----------------------- | ------------------------------------------------------------------------ | ------------------------------------------- | ------ | ----------- | | `get_account` | Workspace, access level, credit balance and plan. | `GET /account` | read | read-only | | `list_projects` | List projects in the workspace. | `GET /projects` | read | read-only | | `get_project` | One project with its photos and upload-link status. | `GET /projects/{id}` | read | read-only | | `create_project` | Create an empty project for a listing. | `POST /projects` | write | | | `delete_project` | Delete a project with its photos and results. | `DELETE /projects/{id}` | write | destructive | | `add_photos` | Add photos from https URLs or ChatGPT attachments. | `POST /projects/{project_id}/photos` | write | open-world | | `delete_photos` | Delete photos and everything made from them. | `POST /projects/{project_id}/photos/delete` | write | destructive | | `create_upload_link` | A link and QR code to upload photos from a phone. | `POST /projects/{project_id}/upload-links` | write | | | `list_edit_options` | Every enhancement, staging style and video option, with costs. | `GET /edit-options` | read | read-only | | `enhance_photos` | Apply AI edits to photos. **Spends credits.** | `POST /enhancements` | write | | | `stage_photos` | Virtually stage rooms. **Spends credits.** | `POST /stagings` | write | | | `get_recommendations` | Free suggested edits per photo. | `POST /recommendations` | write | | | `apply_recommendations` | Run suggested edits. **Spends credits.** | `POST /recommendations/apply` | write | | | `create_video` | Turn 1–9 photos into a video. **Spends credits.** | `POST /videos` | write | | | `get_job_status` | Progress and results for up to 100 jobs. | `GET /jobs?ids=` | read | read-only | | `show_results` | A project's finished and in-progress results (opens the gallery widget). | `GET /projects/{id}/results` | read | read-only | | `pick_results` | Mark or unmark a result as a pick. | `PATCH /jobs/{id}/pick` | read | | | `get_download_link` | Download links for results, one file or a zip. | `POST /downloads` | read | read-only | | `get_download` | Check a zip being prepared. | `GET /downloads/{id}` | read | read-only | REST-only endpoints: `POST /quotes`, `POST /uploads`, `POST /uploads/{id}/complete` and `GET /jobs/{id}`. ## Spending tools [#spending-tools] Calling a spending tool without `quote_id` returns a quote (`object: "quote_required"`) and spends nothing. The assistant shows you the cost; once you confirm, it calls the tool again with the same arguments plus `quote_id`. In ChatGPT and Claude the confirm card does this for you. See [Credits and quotes](/docs/credits-and-quotes). ## Widgets [#widgets] Tools that show results render the PropertyPixel widget (`ui://propertypixel/app.html`) in clients that support MCP Apps: confirm cards, a live results gallery with before/after comparison, picks and downloads, a video player, photo upload, and an edit picker. Clients without widgets get the same information as text and structured content. # Quickstart: ChatGPT (https://docs.propertypixel.app/quickstart/chatgpt) Edit listing photos inside a ChatGPT conversation. ### Add PropertyPixel [#add-propertypixel] Open ChatGPT, go to **Apps**, search for **PropertyPixel** and select **Connect**. If it isn't listed for your account yet, add it yourself: in **Settings → Apps & Connectors → Advanced settings**, turn on **Developer mode**, then **Create** an app with this URL: ```text https://mcp.propertypixel.app ``` ### Sign in and choose access [#sign-in-and-choose-access] ChatGPT opens the PropertyPixel sign-in page. Sign in, or create an account (new accounts start on a free trial). Then choose: * **Workspace**: your personal workspace or a team workspace you belong to. ChatGPT only sees this one. * **Access**: **View only** lets ChatGPT browse projects and results. **View and edit** also lets it upload photos and start edits. Every edit that costs credits still needs your confirmation. ### Ask for what you need [#ask-for-what-you-need] Attach photos or paste links, and describe the result. For example: * "Create a project for 14 Harbour Street and add these photos." * "Brighten all the interior shots and make the sky blue in the exterior ones." * "Stage the empty living room in a Scandinavian style, two options." * "Make a listing tour video from the five best photos." ChatGPT shows a price card first. Select **Run** to spend the credits, then watch progress and results appear in the conversation. ## Getting photos in [#getting-photos-in] Attaching photos to your message works for most uploads. For photos on your phone, ask ChatGPT for an **upload link**: you get a QR code to scan, and photos you choose on the phone go straight into the project. See [Uploading photos](/docs/uploading-photos). ## Credits [#credits] ChatGPT spends your workspace's credits at the same prices as the web app. If you run out, manage your plan at [propertypixel.app](https://propertypixel.app). PropertyPixel never sells credits inside ChatGPT. ## Disconnecting [#disconnecting] Disconnect from ChatGPT's app settings, or revoke the connection in PropertyPixel under [Settings → Developers](https://propertypixel.app/settings/developers). Revoking takes effect immediately. # Quickstart: Claude (https://docs.propertypixel.app/quickstart/claude) Add PropertyPixel to Claude as a custom connector. PropertyPixel connects to Claude as a custom connector over MCP. ### Add the connector [#add-the-connector] In Claude, open **Settings → Connectors**, select **Add custom connector**, and enter: | Field | Value | | ----- | ------------------------------- | | Name | PropertyPixel | | URL | `https://mcp.propertypixel.app` | Leave the advanced OAuth settings empty. Claude registers itself automatically. On a Team or Enterprise plan, an owner adds the connector once for the organization, and each member then connects their own account. ### Connect your account [#connect-your-account] Select **Connect**. Sign in to PropertyPixel (or create an account), then pick the **workspace** Claude may use and its **access**: **View only**, or **View and edit** to let Claude upload photos and start edits. ### Use it in a chat [#use-it-in-a-chat] Turn on PropertyPixel from the tools menu in a conversation, then ask. For example: * "List my PropertyPixel projects." * "Enhance every photo in the Oak Avenue project with improve lighting and blue skies." * "Which edits does PropertyPixel recommend for these photos?" Before anything that costs credits, Claude shows the quote and asks you to confirm. Claude can't spend credits without a quote you approved. ## Getting photos in [#getting-photos-in] Claude can add photos from links (including public Dropbox and Google Drive share links). For photos on your phone, ask for an upload link and open it on the phone. See [Uploading photos](/docs/uploading-photos). ## Other MCP clients [#other-mcp-clients] Any client that supports remote MCP servers with OAuth (Streamable HTTP) can use the same URL, `https://mcp.propertypixel.app`. Clients without widget support receive the same results as text and structured data. # Quickstart: REST (https://docs.propertypixel.app/quickstart/rest) Enhance a listing from the command line with curl. This walks through one listing: create a project, add photos by URL, enhance them with a quote, wait for the results, and download them. ## 1. Create an API key [#1-create-an-api-key] In PropertyPixel, open [Settings → Developers](https://propertypixel.app/settings/developers) and select **Create key**. Pick the workspace and choose **View and edit** access. Copy the key (it starts with `pp_live_`); it is shown only once. ```bash export PP_API_KEY="pp_live_..." export PP_API="https://api.propertypixel.app/v1" ``` Check that it works: ```bash curl -s "$PP_API/account" -H "Authorization: Bearer $PP_API_KEY" ``` The response shows the workspace, your access level and `credits.available`. ## 2. Create a project [#2-create-a-project] ```bash curl -s -X POST "$PP_API/projects" \ -H "Authorization: Bearer $PP_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "14 Harbour Street", "address": "14 Harbour Street, Sydney"}' ``` Note the `id` in the response: ```bash export PROJECT_ID="..." ``` ## 3. Add photos by URL [#3-add-photos-by-url] ```bash curl -s -X POST "$PP_API/projects/$PROJECT_ID/photos" \ -H "Authorization: Bearer $PP_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"image_urls": [ "https://example.com/photos/living-room.jpg", "https://example.com/photos/front.jpg" ]}' ``` The response lists the new `photos` with their IDs, and a `failed` list for any URL that couldn't be used. To upload files from disk instead, see [Uploading photos](/docs/uploading-photos#signed-upload). ## 4. Get a quote [#4-get-a-quote] Spending operations price the request before running it. Call `POST /enhancements` without a `quote_id`: ```bash curl -s -X POST "$PP_API/enhancements" \ -H "Authorization: Bearer $PP_API_KEY" \ -H "Content-Type: application/json" \ -d '{"project_id": "'"$PROJECT_ID"'", "all_photos": true, "enhancement_types": ["improve_lighting", "blue_skies"]}' ``` Nothing is spent. The response is `409` with code `quote_required`, and a `quote`: ```json { "type": "https://docs.propertypixel.app/errors#quote_required", "title": "Quote required", "status": 409, "code": "quote_required", "detail": "This request spends credits. Confirm the quote by sending quote_id, or send auto_confirm: true.", "quote": { "object": "quote", "id": "5d1c…", "operation": "enhance_photos", "credits": 200, "balance": { "available": 500, "after": 300 }, "sufficient": true, "watermarked": true, "expires_at": "2026-10-04T10:10:00Z" } } ``` ## 5. Confirm and run [#5-confirm-and-run] Send the same body again with the quote's `id`: ```bash curl -s -X POST "$PP_API/enhancements" \ -H "Authorization: Bearer $PP_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"project_id": "'"$PROJECT_ID"'", "all_photos": true, "enhancement_types": ["improve_lighting", "blue_skies"], "quote_id": "5d1c…"}' ``` The response (`201`) is a job batch: one `job` per photo, `credits_spent`, and `poll_after_ms`. Send `"auto_confirm": true` instead of `quote_id` to spend in one call. Only do this in code you control; see [Credits and quotes](/docs/credits-and-quotes). ## 6. Wait for the results [#6-wait-for-the-results] ```bash curl -s "$PP_API/jobs?ids=JOB_ID_1,JOB_ID_2" -H "Authorization: Bearer $PP_API_KEY" ``` Repeat after `poll_after_ms` until `all_done` is `true`. Each completed job has `result.url`, signed for one hour. Or skip polling and subscribe to the `job.completed` [webhook](/docs/webhooks). ## 7. Download [#7-download] One result is a single signed link. Several start a zip: ```bash curl -s -X POST "$PP_API/downloads" \ -H "Authorization: Bearer $PP_API_KEY" \ -H "Content-Type: application/json" \ -d '{"job_ids": ["JOB_ID_1", "JOB_ID_2"]}' ``` If `status` is `processing`, poll `GET /downloads/{id}` until it returns a `url`. ## Next [#next] * [Jobs and polling](/docs/jobs) * [Webhooks](/docs/webhooks) * [API reference](/docs/api) # Rate limits (https://docs.propertypixel.app/rate-limits) Request limits per connection. Limits apply per connection (each API key or connected app): | Bucket | Limit | | ------------------------------------------------- | ----------------------- | | Reads (`GET`, and read-only tools) | 120 requests per minute | | Writes (anything that creates, changes or spends) | 20 requests per minute | Every response includes: * `RateLimit-Limit`: the limit for this bucket * `RateLimit-Remaining`: requests left in the current window * `RateLimit-Reset`: seconds until the window resets When you exceed a limit you get `429 rate_limited` with a `Retry-After` header. Credits cap how much work can actually run, so batch photos into one call (up to 100 photos) rather than sending one request per photo. # Uploading photos (https://docs.propertypixel.app/uploading-photos) Four ways to get listing photos into a project. Every photo belongs to a **project**. Create one first (`create_project`, or `POST /v1/projects`). All routes accept JPEG, PNG, WebP, HEIC and HEIF files up to **25 MB** each, and a project holds up to **100 photos**. HEIC and HEIF are converted to JPEG, and large photos are resized to a 2048px long edge, as in the web app. | Method | Use it when | MCP | REST | | ------------- | -------------------------------------------------- | --- | ---- | | Image URLs | The photos are online (CDN, listing site, Dropbox) | ✓ | ✓ | | ChatGPT files | The user attached photos in ChatGPT | ✓ | | | Upload link | The photos are on someone's phone or computer | ✓ | ✓ | | Signed upload | Your script has the files locally | | ✓ | ## Image URLs [#image-urls] Pass up to 25 public `https` URLs to `add_photos` (`POST /v1/projects/{project_id}/photos`): ```json { "image_urls": ["https://example.com/front.jpg", "https://www.dropbox.com/s/abc/kitchen.jpg?dl=0"] } ``` Public Dropbox and Google Drive share links are converted to direct downloads. URLs must be reachable without signing in; private and internal addresses are refused. The response lists the new `photos` and a `failed` entry, with a reason, for each URL that couldn't be used. ## ChatGPT files [#chatgpt-files] In ChatGPT, photos attached to a message are passed to `add_photos` as `files`, and PropertyPixel downloads them. If ChatGPT can't hand over a file, PropertyPixel shows its upload widget instead. ## Upload links [#upload-links] `create_upload_link` (`POST /v1/projects/{project_id}/upload-links`) returns a private `url` and a QR code (`qr_svg`). Open the link on a phone or computer to add photos without signing in. * A link works for **2 hours** and only for that project. * Photos appear in the project as they upload. * `get_project` shows the latest link's `upload_link.status` (`waiting`, `receiving`, `done` or `expired`) and `photos_received`. * When the person finishes, PropertyPixel sends the `upload.completed` [webhook](/docs/webhooks). Anyone with the link can add photos to that project until it expires, so share it only with the person taking the photos. ## Signed upload [#signed-upload] For files on disk, use the three-step signed upload (REST only). **1. Reserve uploads** with each file's name, type and size: ```bash curl -s -X POST "$PP_API/uploads" \ -H "Authorization: Bearer $PP_API_KEY" \ -H "Content-Type: application/json" \ -d '{"project_id": "'"$PROJECT_ID"'", "files": [{"filename": "kitchen.heic", "content_type": "image/heic", "size": 3481234}]}' ``` The response has an upload `id` and, per file, an `upload_url`, the `method`, the `headers` to send and a `storage_path`. **2. Upload each file's bytes** to its `upload_url` with exactly those headers: ```bash curl -s -X PUT "$UPLOAD_URL" -H "Content-Type: image/heic" --data-binary @kitchen.heic ``` **3. Complete the upload**, listing every file that uploaded: ```bash curl -s -X POST "$PP_API/uploads/$UPLOAD_ID/complete" \ -H "Authorization: Bearer $PP_API_KEY" \ -H "Content-Type: application/json" \ -d '{"project_id": "'"$PROJECT_ID"'", "files": [{"storage_path": "…", "filename": "kitchen.heic", "size": 3481234, "content_type": "image/heic"}]}' ``` PropertyPixel checks each file's size, type and contents, then adds it to the project. Files you leave out are discarded. Rejected files are listed in `failed` with a reason. # Webhooks (https://docs.propertypixel.app/webhooks) Get notified when jobs finish and uploads arrive. Add an endpoint in **Settings → Developers → Webhooks** (workspace owners only). Choose the events you want and copy the signing secret. It's shown once; you can rotate it later. ## Events [#events] | Event | When | | ------------------ | --------------------------------------------------------------- | | `job.completed` | A photo edit (enhancement or staging) finished. | | `job.failed` | A job failed. Its credits were refunded. | | `video.completed` | A Photo to Video or Listing Tour video finished. | | `upload.completed` | Photos arrived through an upload link and the user tapped Done. | ## Payload [#payload] Every delivery is a `POST` with a JSON body: ```json { "type": "job.completed", "timestamp": "2026-10-05T10:15:00.000Z", "data": { "id": "9b1c…", "kind": "enhancement", "status": "completed", "project_id": "2f4e…", "photo_ids": ["a81d…"], "enhancement_types": ["improve_lighting"], "result": { "url": "https://…", "expires_at": "2026-10-06T10:15:00.000Z" } } } ``` `data` for job events follows the [Job](/docs/api) shape, and its result URL lasts 24 hours. Events sent with **Send test event** include `"test": true`. ## Verifying signatures [#verifying-signatures] Deliveries are signed with the [Standard Webhooks](https://www.standardwebhooks.com) scheme, using the `webhook-id`, `webhook-timestamp` and `webhook-signature` headers. Verify against the **raw** request body. ```ts title="Node" import { Webhook } from "standardwebhooks"; const wh = new Webhook(process.env.PROPERTYPIXEL_WEBHOOK_SECRET!); // "whsec_…" export async function POST(request: Request) { const body = await request.text(); const event = wh.verify(body, Object.fromEntries(request.headers)); // throws if invalid // handle event.type … return new Response(null, { status: 204 }); } ``` ```python title="Python" from standardwebhooks import Webhook wh = Webhook(os.environ["PROPERTYPIXEL_WEBHOOK_SECRET"]) event = wh.verify(request.body, dict(request.headers)) # raises if invalid ``` Use `webhook-id` to deduplicate: retries reuse the same ID. ## Retries and disabling [#retries-and-disabling] * Respond with any `2xx` within 10 seconds to acknowledge. * Failed deliveries are retried after 30 seconds, 2 minutes, 10 minutes, 30 minutes, 2 hours, 6 hours and 12 hours (8 attempts over about 21 hours). * After 8 deliveries in a row exhaust their retries, or after 3 days of failures, the endpoint is disabled and the workspace owner is emailed. Re-enable it in Settings once it's fixed. * The delivery log keeps 30 days of attempts, and you can resend any delivery. # Get account (https://docs.propertypixel.app/api/account/get_account) Get the connected PropertyPixel account: the workspace and the user's role in it, whether this connection can make changes, the credit balance (100 credits = one photo enhancement), the plan, and whether results are watermarked (trial). Call this before spending to check the balance, or when the user asks about their credits or plan. Works with view-only access. REST reference. The OpenAPI 3.1 document is at https://api.propertypixel.app/v1/openapi.json. # List projects (https://docs.propertypixel.app/api/projects/list_projects) List the property projects in the connected workspace, newest first by default. Each project groups the photos of one listing. Use `search` to find a project by name or address before asking the user for an ID. Returns project IDs, photo counts, a cover image and a Studio link; page with `next_cursor`. Works with view-only access. REST reference. The OpenAPI 3.1 document is at https://api.propertypixel.app/v1/openapi.json. # Create a project (https://docs.propertypixel.app/api/projects/create_project) Create a new, empty project for one property listing. Name it after the address or listing so the user can find it later. Then add photos with add_photos or create_upload_link. Requires a connection with edit access. REST reference. The OpenAPI 3.1 document is at https://api.propertypixel.app/v1/openapi.json. # Get a project (https://docs.propertypixel.app/api/projects/get_project) Get one project with all its photos (IDs, room types, signed preview URLs) and the status of its most recent upload link (waiting, receiving, done or expired). Use the photo IDs with enhance_photos, stage_photos or create_video. To see finished edits, call show_results instead. Works with view-only access. REST reference. The OpenAPI 3.1 document is at https://api.propertypixel.app/v1/openapi.json. # Delete a project (https://docs.propertypixel.app/api/projects/delete_project) Delete a project with all its photos and edited results. Only do this when the user explicitly asks to delete that project; confirm the project name with them first. It disappears from the Studio immediately. Requires a connection with edit access. REST reference. The OpenAPI 3.1 document is at https://api.propertypixel.app/v1/openapi.json. # Show results (https://docs.propertypixel.app/api/projects/show_results) Show a project's edited results: what is still processing, the newest finished version of each photo and each video, and the versions the user picked. Use this when the user wants to see or review their results. Result URLs are signed for 1 hour. Works with view-only access. REST reference. The OpenAPI 3.1 document is at https://api.propertypixel.app/v1/openapi.json. # Add photos to a project (https://docs.propertypixel.app/api/photos/add_photos) Add photos to a project from public https image URLs (`image_urls`) or from files the user attached in ChatGPT (`files`). Up to 25 photos per call; a project holds at most 100. Each file must be JPEG, PNG, WebP, HEIC or HEIF and at most 25 MB. Dropbox and Google Drive share links work if they are shared publicly. HEIC is converted to JPEG and large photos are downscaled to a 2048px long edge, like the web app. Returns the new photos (use their IDs with enhance_photos, stage_photos or create_video) and a `failed` list with a reason per source; report failures to the user rather than retrying blindly. If the user has photos on their phone or computer and no links, call create_upload_link instead. Requires a connection with edit access. REST reference. The OpenAPI 3.1 document is at https://api.propertypixel.app/v1/openapi.json. # Delete photos (https://docs.propertypixel.app/api/photos/delete_photos) Delete photos from a project, together with every edit, staging and video made from them. Only do this when the user explicitly asks to remove those photos; confirm which ones first. All IDs must belong to the project, otherwise nothing is deleted. Returns the deleted IDs; call get_project to see what remains. Requires a connection with edit access. REST reference. The OpenAPI 3.1 document is at https://api.propertypixel.app/v1/openapi.json. # Start a signed upload (https://docs.propertypixel.app/api/uploads/create_upload) Reserve room in a project for up to 25 photos and get one signed URL per file. PUT each file's raw bytes to its upload_url with exactly the returned headers, then call POST /uploads/{id}/complete with the storage_path, filename, size and content_type of every file that uploaded. Files must be JPEG, PNG, WebP, HEIC or HEIF, at most 25 MB each; a project holds at most 100 photos. The reservation expires at expires_at (about 15 minutes); complete before then. Files are stored exactly as sent, except HEIC/HEIF, which is converted to JPEG on completion. Downscale large images yourself (the web app uses a 2048px long edge). Requires a connection with edit access. REST reference. The OpenAPI 3.1 document is at https://api.propertypixel.app/v1/openapi.json. # Complete a signed upload (https://docs.propertypixel.app/api/uploads/complete_upload) Finish a signed upload: verifies each stored file (size, type and content) and adds it to the project as a photo. List every file that uploaded; files left out are discarded and their reserved room released. HEIC/HEIF files are converted to JPEG. Returns the new photos and a `failed` list with a reason per file. Requires a connection with edit access. REST reference. The OpenAPI 3.1 document is at https://api.propertypixel.app/v1/openapi.json. # Create an upload link (https://docs.propertypixel.app/api/uploads/create_upload_link) Create a private link (with a QR code) the user opens on their phone or computer to add photos to a project, with no login. Use it when the user needs to send photos you can't receive directly, such as photos on their phone. Share the url, or show the QR code, and tell the user the link works for 2 hours. Photos appear in the project as they upload. To follow progress, call get_project and read upload_link.status and upload_link.photos_received until status is done (the user tapped Done), or simply ask the user to tell you when they've finished. Requires a connection with edit access. REST reference. The OpenAPI 3.1 document is at https://api.propertypixel.app/v1/openapi.json. # List edit options (https://docs.propertypixel.app/api/edit-options/list_edit_options) List every edit PropertyPixel can make, with the exact keys to pass to other tools: enhancement types for enhance_photos (combinable, e.g. blue_skies + improve_lighting), design styles and variation limits for stage_photos, room types, Photo to Video settings for create_video (1–9 photos; one makes a clip, several make a Listing Tour), the Ultra HD add-on, and credit prices. Call this when the user asks what you can do, or before choosing keys you are unsure of. Works with view-only access. REST reference. The OpenAPI 3.1 document is at https://api.propertypixel.app/v1/openapi.json. # Enhance photos (https://docs.propertypixel.app/api/editing/enhance_photos) Apply AI edits (lighting, declutter, blue skies, day to dusk, furniture removal, and more) to listing photos. Costs 100 credits per photo, plus 100 per photo with ultra_hd. All enhancement_types are combined into one result per photo. Select photos with photo_ids, or project_id + all_photos: true. Defaults: ultra_hd false; edits start from the original photo unless versions names a prior result. Spends credits in two steps. Call without quote_id first: you get a quote (cost, balance after, warnings) and nothing is spent. Show the cost to the user and wait for a yes, then call again with the same arguments plus quote_id. The quote lasts 10 minutes and only matches identical arguments. Results are asynchronous: this returns job IDs right away (status pending). Poll get_job_status with the job IDs, waiting poll_after_ms between calls, until each job is completed or failed. Failed jobs are refunded automatically. Requires a connection with edit access. Spends credits. Without `quote_id` it returns 409 `quote_required` with a quote and spends nothing. Resend with `quote_id` to run, or send `auto_confirm: true` to skip the quote. REST reference. The OpenAPI 3.1 document is at https://api.propertypixel.app/v1/openapi.json. # Virtually stage photos (https://docs.propertypixel.app/api/editing/stage_photos) Virtually stage rooms: furnish empty rooms or restyle furnished ones in a design_style. Costs 100 credits per variation per photo, plus 100 per variation with ultra_hd. Select photos with photo_ids, or project_id + all_photos: true. Defaults: variations 1, ultra_hd false; staging starts from the original photo unless versions names a prior result (e.g. a decluttered one). Spends credits in two steps. Call without quote_id first: you get a quote (cost, balance after, warnings) and nothing is spent. Show the cost to the user and wait for a yes, then call again with the same arguments plus quote_id. The quote lasts 10 minutes and only matches identical arguments. Results are asynchronous: this returns job IDs right away (status pending). Poll get_job_status with the job IDs, waiting poll_after_ms between calls, until each job is completed or failed. Failed jobs are refunded automatically. Requires a connection with edit access. Spends credits. Without `quote_id` it returns 409 `quote_required` with a quote and spends nothing. Resend with `quote_id` to run, or send `auto_confirm: true` to skip the quote. REST reference. The OpenAPI 3.1 document is at https://api.propertypixel.app/v1/openapi.json. # Create a video (https://docs.propertypixel.app/api/video/create_video) Turn listing photos into a video. One photo makes a Photo to Video clip; 2–9 photos make a Listing Tour, in the order given. Costs 400 credits per photo. Defaults: clip_seconds 3, aspect_ratio "auto"; each photo uses its original unless versions names an enhanced result. Spends credits in two steps. Call without quote_id first: you get a quote (cost, balance after, warnings) and nothing is spent. Show the cost to the user and wait for a yes, then call again with the same arguments plus quote_id. The quote lasts 10 minutes and only matches identical arguments. Results are asynchronous: this returns job IDs right away (status pending). Poll get_job_status with the job IDs, waiting poll_after_ms between calls, until each job is completed or failed. Failed jobs are refunded automatically. Videos take a few minutes. Requires a connection with edit access. Spends credits. Without `quote_id` it returns 409 `quote_required` with a quote and spends nothing. Resend with `quote_id` to run, or send `auto_confirm: true` to skip the quote. REST reference. The OpenAPI 3.1 document is at https://api.propertypixel.app/v1/openapi.json. # Get edit recommendations (https://docs.propertypixel.app/api/recommendations/get_recommendations) Free. Ask PropertyPixel's AI which corrective edits each photo needs (e.g. improve_lighting, clean_and_tidy, straighten_and_reframe). Select photos with photo_ids, or project_id + all_photos: true. Returns the Recommendations that already exist and starts analysis for photos that have none. Analysis is asynchronous: photos in pending_photo_ids are still being analysed, so call again after poll_after_ms to see them. A daily analysis limit applies; photos over it are listed in skipped_photo_ids. To run the suggestions, pass the ready Recommendation IDs to apply_recommendations (paid). Requires a connection with edit access. REST reference. The OpenAPI 3.1 document is at https://api.propertypixel.app/v1/openapi.json. # Apply recommendations (https://docs.propertypixel.app/api/recommendations/apply_recommendations) Run ready Recommendations from get_recommendations as enhancement jobs: one job per photo with all its suggested edits. Costs 100 credits per Recommendation. Only status ready can be applied. Spends credits in two steps. Call without quote_id first: you get a quote and nothing is spent. Show the cost to the user and wait for a yes, then call again with the same arguments plus quote_id (valid 10 minutes). Results are asynchronous: poll get_job_status with the returned job IDs, waiting poll_after_ms between calls. Failed jobs are refunded automatically. Requires a connection with edit access. Spends credits. Without `quote_id` it returns 409 `quote_required` with a quote and spends nothing. Resend with `quote_id` to run, or send `auto_confirm: true` to skip the quote. REST reference. The OpenAPI 3.1 document is at https://api.propertypixel.app/v1/openapi.json. # Check job status (https://docs.propertypixel.app/api/jobs/get_job_status) Check progress of up to 100 jobs returned by enhance_photos, stage_photos, apply_recommendations or create_video. Returns status (pending, processing, completed, failed, cancelled), progress, result URLs (signed for 1 hour) and a poll_after_ms hint. Photo edits usually finish in under a minute and videos in a few minutes; wait poll_after_ms between checks instead of calling in a tight loop. Failed jobs are refunded automatically. Works with view-only access. REST reference. The OpenAPI 3.1 document is at https://api.propertypixel.app/v1/openapi.json. # Get a job (https://docs.propertypixel.app/api/jobs/get_job) Get one job's status, progress and result URL (signed for 1 hour). Works with view-only access. REST reference. The OpenAPI 3.1 document is at https://api.propertypixel.app/v1/openapi.json. # Pick a result (https://docs.propertypixel.app/api/jobs/pick_results) Mark a finished photo or video version as a pick (picked: true) or remove the mark (picked: false). Picks are the versions the user wants to keep; get_download_link with picks_only downloads them together. Picks are shared by the workspace and never change or delete the versions themselves. Only completed enhancement, staging and video jobs can be picked. Works with view-only access. REST reference. The OpenAPI 3.1 document is at https://api.propertypixel.app/v1/openapi.json. # Get a download link (https://docs.propertypixel.app/api/downloads/get_download_link) Get download links for finished results. Pass job_ids (1–100 completed versions from one project), or project_id to download that project's picks (picks_only defaults to true; false downloads every finished version, up to 100). One version returns a signed link immediately. Several photos start a zip: status is processing, so call get_download with the returned id after poll_after_ms. If any videos are included you get one signed link per file in `files` instead of a zip. Links last 1 hour; trial accounts get watermarked files. Works with view-only access. REST reference. The OpenAPI 3.1 document is at https://api.propertypixel.app/v1/openapi.json. # Check a zip download (https://docs.propertypixel.app/api/downloads/get_download) Check a zip started by get_download_link. While status is processing, wait poll_after_ms and call again; when ready, url is the zip (valid for 1 hour). If it failed, call get_download_link again. Works with view-only access. REST reference. The OpenAPI 3.1 document is at https://api.propertypixel.app/v1/openapi.json. # Create a quote (https://docs.propertypixel.app/api/quotes/create_quote) Price a spending request without running it. Pass the returned id as quote_id to the operation within 10 minutes. Requires a connection with edit access. REST reference. The OpenAPI 3.1 document is at https://api.propertypixel.app/v1/openapi.json.