RapidEye API

Reference

Recording links and inspections

Issue a recording link per task, then read status and findings.

Requesting from us

Call it when a task is created, or whenever you need the link. Idempotent for a task that passes the checks: the same task_id returns the same URL, so it is safe to call on create, on update, and on any render path.

This endpoint does not create properties. The property_id must already be present in the workspace's roster; the import that put it there also activated it, a few seconds after the import applied.

Parameter Required Notes
workspace_id yes Your account or workspace id; identifies the customer
task_id yes Your task id; the idempotency key
property_id yes Your listing or property id, as it appears in the roster
task_name no Free text
scheduled_date no YYYY-MM-DD
return_url no Where to send the cleaner when the walkthrough is saved, for example conduit://rapideye/complete. Must be on your allowlist. See Sending the cleaner back to your app below

Treat recording_url as opaque: store and render it exactly as returned. Save it on your task and render it as a button the cleaner taps.

Note the two property ids. property_id in the response is ours, the uuid inside recording_url. external_property_id echoes yours, so a handler can match the response to the request without inferring which is which.

Sending the cleaner back to your app (return_url)

By default the cleaner's last screen says the recording is saved and that they can close the page. Pass return_url and that screen sends them back to you instead:

The response echoes it as "return_url": "conduit://rapideye/complete".

The allowlist. We only send a cleaner to a URL your integration is set up for, so the recording page can never be used to bounce someone to an arbitrary site. Each entry is a scheme for your app (conduit:// allows any URL under that scheme), or a web origin with an exact host (https://app.example.com; another host, a subdomain or a different port does not match). Email [email protected] to add one. Anything else is 400 invalid_request with detail.parameter set to return_url and detail.reason set to not_allowed (not on your list) or malformed (not an absolute scheme://host/path URL of at most 2,048 characters; it contains spaces, control characters, a backslash or a user@ part; or its host or port is one a browser cannot open, such as a port above 65535, a port with no host, or a host with characters other than letters, digits and . _ ~ -). This check runs with the other parameter checks, before the refusals below. If we cannot read your allowlist at that moment the answer is 503 service_unavailable; retrying is safe.

It is stored per task. A request that carries return_url sets it for that task_id. A request without it leaves the stored value as it is, so a render path that does not know about it never erases it. Send a new value to change it.

When the redirect fires. The cleaner goes through the whole finish flow exactly as they do today. Then, in this order:

  1. Every part of the video has uploaded from the phone.

  2. While we seal the walkthrough on our side, the screen reads "Finishing your upload. Keep this page open." Sealing takes about 30 seconds after the cleaner taps Done (90% within 70 seconds, almost all within 2.5 minutes).

  3. Once it is sealed, the cleaner sees the usual "Recording uploaded" screen for 3 seconds, with a Return to {your name} button where the "you can close this page" line usually is.

  4. The page navigates to your URL with two query parameters added (any query or fragment of your own is kept):

    Text
    conduit://rapideye/complete?inspection_id=6a1d2c80-3f0e-4b3a-9d61-0c8f1b7e2a55&status=submitted
    
    Parameter Value
    inspection_id Our inspection id, the same one the webhooks and GET /v1/inspections carry
    status Always submitted: the walkthrough is safely with us. Results follow on the usual webhooks

We never send the cleaner away before the walkthrough is sealed, because closing the browser while the video is still uploading loses footage. On a poor connection the phone's last request can reach us late or not at all; the screen then keeps waiting, up to the 5-minute limit. If it does not seal within 5 minutes, the cleaner gets today's screen and its "you can close this page" message, with no redirect; the inspection still finishes on our side, and your webhooks arrive as usual. If the upload itself fails, the cleaner sees the same error and retry screens as today, and nothing redirects.

How browsers handle the navigation. A redirect to an app scheme 30 seconds or more after the cleaner's last tap is treated differently by each way of opening the link. The Return to {your name} button is a plain link: a tap on it counts as the cleaner's own action, and that works where the automatic navigation is blocked.

Opened in Automatic redirect What to do
Your own in-app web view (WKWebView, Android WebView) Your app sees it and decides Intercept the navigation (decidePolicyFor on iOS, shouldOverrideUrlLoading on Android), read the two parameters and close the web view. This is the most dependable option
iOS SFSafariViewController Often works. iOS can drop a scripted custom-scheme navigation that comes long after the last tap Handle the URL in your app or scene delegate and dismiss the view controller. The button covers the dropped case
Android Chrome Custom Tabs Usually blocked. Chrome does not launch another app without a recent user action, and may show a "leave Chrome?" message instead Expect the cleaner to tap the button. Handle the URL with an intent filter for your scheme
A full browser (Safari, Chrome) Asks the cleaner, or does nothing The cleaner taps the button, then confirms the browser's "Open in app" prompt

What it refuses, and in what order

The checks run outermost first, so you are always told the largest thing that is wrong. Each one has a different remedy, which is why they are separate codes.

Order Check Refusal Who acts
1 The workspace is provisioned 404 workspace_not_provisioned You: POST /v1/workspaces
2 Your integration is registered and live 403 partner_disabled Us: email support
3 A roster has been confirmed 409 roster_required You: POST /v1/roster-imports
4 The property is in the roster 404 property_not_found You: send a roster containing it
5 The property is still present 409 property_inactive You: include it in your next roster
6 The property is activated 409 property_not_activated You: if an import has only just applied, wait and retry. Otherwise re-send the roster with a new Idempotency-Key, or trigger a sync. Us, if it persists
7 The account is in service 409 workspace_not_enabled You: re-send the roster with a new Idempotency-Key, or trigger a sync. Us, if it persists
8 The task is not bound elsewhere 409 task_property_conflict You: use a new task_id

400 missing_parameter comes before all of them, with detail.parameter naming the parameter, for example "detail": { "parameter": "workspace_id" }. So does a refused return_url (400 invalid_request, see Sending the cleaner back to your app).

A task is bound to one property for its whole life. The recording, the report and your webhook all resolve through that binding, so re-pointing a task_id at a different property would attach a walkthrough to the wrong unit.

Shell
curl -sS -G https://api.rapideyeinspections.com/v1/recording-link \
  -H "Authorization: Bearer rapideye_pk_REPLACE_ME" \
  --data-urlencode "workspace_id=ws_123" \
  --data-urlencode "task_id=task_abc" \
  --data-urlencode "property_id=unit_42"
JSON
{
  "recording_url": "https://app.rapideyeinspections.com/record/...",
  "task_id": "task_abc",
  "property_id": "1f0a9c22-6b1e-4a77-9b0f-2d2b0c5f7a10",
  "external_property_id": "unit_42",
  "roster_revision": 4
}
Shell
curl -sS -G https://api.rapideyeinspections.com/v1/recording-link \
  -H "Authorization: Bearer rapideye_pk_REPLACE_ME" \
  --data-urlencode "workspace_id=ws_123" \
  --data-urlencode "task_id=task_abc" \
  --data-urlencode "property_id=unit_42" \
  --data-urlencode "return_url=conduit://rapideye/complete"

GET/v1/inspections?workspace_id=...&task_id=...#

Status poll; a fallback for the webhook and a way to backfill after downtime. The webhook is the primary signal. Poll no more than once per minute per task.

An inspection has two parts that finish at different times, and the response describes each as a block with its own status:

  • checklist: the per-item results. Final minutes after the walkthrough. See Per-item checklist results below.
  • report: the findings report. Ready hours later.

status is the inspection's overall lifecycle: pending, recording, processing, completed or failed. completed means the report is ready.

While the report is being produced:

Once it is ready:

media_expires_in comes with the checklist block: the lifetime of its image_url links, as on the findings response. Every read signs the links afresh, so this is where you get a new link for a picture whose 15 minutes have run out.

The report block:

Field Present Values
status always processing or ready
stage while processing analyzing (our models are working through the walkthrough) or reviewing (the findings are with our reviewers)
report_url when ready The report link, the same URL as everywhere else

report appears on processing and completed. It is omitted on pending and recording, where no report is under way yet, and on failed, where none is coming. checklist appears as soon as the walkthrough has any results, on any status, and is omitted for a home with no checklist.

inspection_id appears as soon as the cleaner opens the recording link. report_url and completed_at at the top level appear only on completed. They predate the report block and stay, so an integration built on them keeps working; new code should read report. Unknown task_id returns 404 task_not_found. Treat an unrecognized status, block status or stage as in-flight so we can add states without breaking you.

inspection_id is ours, and this is how you get it if you missed the webhook. It is what the findings endpoint below takes.

JSON
{
  "status": "processing",
  "inspection_id": "3b7f1a90-2c44-4b6d-9e01-7a5c2f8e1d33",
  "report": { "status": "processing", "stage": "reviewing" },
  "media_expires_in": 900,
  "checklist": {
    "status": "complete",
    "counts": { "verified": 9, "missed": 2, "explained": 1, "not_checked": 0, "pending": 0 },
    "items": [
      { "id": "c1f0a2e4-...", "name": "Bedrooms: beds made, linens fresh", "state": "verified",
        "timestamp": 84.2, "image_url": "https://...signed..." }
    ]
  }
}
JSON
{
  "status": "completed",
  "inspection_id": "3b7f1a90-2c44-4b6d-9e01-7a5c2f8e1d33",
  "report_url": "https://app.rapideyeinspections.com/reports/3b7f1a90-2c44-4b6d-9e01-7a5c2f8e1d33",
  "completed_at": "2026-08-01T17:04:33Z",
  "report": {
    "status": "ready",
    "report_url": "https://app.rapideyeinspections.com/reports/3b7f1a90-2c44-4b6d-9e01-7a5c2f8e1d33"
  },
  "media_expires_in": 900,
  "checklist": {
    "status": "complete",
    "counts": { "verified": 9, "missed": 2, "explained": 1, "not_checked": 0, "pending": 0 },
    "items": [
      { "id": "c1f0a2e4-...", "name": "Bedrooms: beds made, linens fresh", "state": "verified",
        "timestamp": 84.2, "image_url": "https://...signed..." }
    ]
  }
}

GET/v1/inspections/{inspection_id}/findings?workspace_id=...#

The findings behind a completed inspection, if you want the detail rather than the report link. Call it when inspection.completed arrives; the webhook carries both ids you need.

Parameter Required Notes
inspection_id yes Path segment. Ours, from the webhook or from GET /v1/inspections.
workspace_id yes Query. Yours. Scopes the request to one of your customers.
Field Type Always present Notes
id string (uuid) yes Ours, and stable. Use it to deduplicate if you call twice.
description string yes One sentence, written for a work order.
room string yes The area of the property, e.g. Kitchen, Bedroom 2. Route the follow-up task on this.
severity string yes Exactly one of MINOR, MODERATE, MAJOR.
category string yes Exactly one of damage, cleaning, lost_item.
evidence object yes { "before": [...], "after": [...] }. Both keys always present; either may be [].
video_timestamp_seconds number no Where in the walkthrough it was seen. Present on video-sourced findings only.

severity and category are closed sets: we will not send a value outside the three listed, so it is safe to branch on them exhaustively.

evidence has exactly two keys, before and after, each an array of media references. after is the state we found, what the finding is about. before is the same area on the previous visit, which is what makes a new problem legible as new; it is empty on a property's first inspection and anywhere we had no baseline, which is about a quarter of findings.

evidence is the one place we send an empty value rather than omitting the key, and it is deliberate. Elsewhere an optional field is omitted entirely when it does not apply, never null or "", because absence is the natural way to say "not applicable" about a single value, and testing for the key is the natural way to read it. For a list the useful guarantee is the other one: that iterating is always safe. So evidence, evidence.before and evidence.after are always there, and you can write for image in finding.evidence.before without a guard on any finding we will ever send you. Both lists are empty on a finding whose images we could not produce.

Every other optional field follows the omit rule, same as the error envelope. Test for the key.

There is no pagination. One call returns every finding for the inspection in one response, capped at 5000 findings (no real inspection approaches it). The order is stable between calls but carries no meaning; sort by severity or room if you need one.

workspace_id is a boundary, not a hint. Your key spans every workspace we have provisioned for you, so this parameter is what scopes a request to one of your customers. An inspection_id belonging to a different workspace answers 404 not_found, identical to an id that does not exist.

Media URLs are short-lived and are minted per request. A media reference is { "url": "..." }, a signed URL good for media_expires_in seconds from the moment we answered, and no longer. Fetch the bytes when the webhook arrives; do not store the URL. A stored URL is dead long before the task it is attached to is, and re-reading this endpoint is the supported way to get fresh ones. There is no permanent image URL to ask us for; the images live in a private bucket, which is the point. It is an image: fetch it with a plain GET and no auth header, and read the format from the response's Content-Type rather than the URL. Do not assume a particular file extension.

Only released reports. Until human review is finished the endpoint answers 409 report_not_ready, which is a "not yet", not a failure. The inspection is fine and inspection.completed is coming. We never expose findings that have not been through review, so a 409 is also the correct answer for an inspection whose analysis is still running. An unknown or out-of-workspace inspection_id is 404 not_found.

Shell
curl -sS -G "https://api.rapideyeinspections.com/v1/inspections/3b7f1a90-2c44-4b6d-9e01-7a5c2f8e1d33/findings" \
  -H "Authorization: Bearer rapideye_pk_REPLACE_ME" \
  --data-urlencode "workspace_id=ws_123"
JSON
{
  "inspection_id": "3b7f1a90-2c44-4b6d-9e01-7a5c2f8e1d33",
  "report_url": "https://app.rapideyeinspections.com/reports/3b7f1a90-2c44-4b6d-9e01-7a5c2f8e1d33",
  "media_expires_in": 900,
  "checklist": {
    "status": "complete",
    "counts": { "verified": 1, "missed": 0, "explained": 0, "not_checked": 0, "pending": 0 },
    "items": [
      { "id": "c1f0a2e4-...", "name": "Bedrooms: beds made, linens fresh", "state": "verified" }
    ]
  },
  "findings": [
    {
      "id": "8c2d1f60-4a11-4de2-9b73-0e5a6c1d2f88",
      "description": "Burn mark on the arm of the sofa, roughly 3cm across.",
      "room": "Living Room",
      "severity": "MAJOR",
      "category": "damage",
      "video_timestamp_seconds": 128.4,
      "evidence": {
        "before": [ { "url": "https://...signed..." } ],
        "after": [ { "url": "https://...signed..." } ]
      }
    }
  ]
}

GET/v1/inspections/{inspection_id}/video?workspace_id=...#

A download link for the inspection's walkthrough video, so you can keep your own copy. It is the same video the report plays, one per inspection. Items a cleaner goes back to record from the checklist are already inside it.

Parameter Required Notes
inspection_id yes Path segment. Ours, from the webhook or from GET /v1/inspections.
workspace_id yes Query. Yours. Scopes the request to one of your customers, exactly as on the findings endpoint.
Field Type Always present Notes
inspection_id string (uuid) yes Echoes the path.
download_url string yes A signed link to the video file. Fetch it with a plain GET and no auth header.
expires_at string yes When download_url stops working, ISO 8601 UTC.
media_expires_in number yes The same lifetime in seconds from our answer, as on the findings response.
content_type string no The file's media type. Almost always video/mp4; a few early recordings are video/webm or video/quicktime.
duration_seconds number no Length of the video. Omitted when we did not measure it.
size_bytes number no Size of the file. Omitted when we did not record it.
recorded_at string no When the walkthrough was recorded.

Each call returns a fresh link good for media_expires_in seconds, the same 15 minutes as finding images. Start the download within that window. Do not store download_url. Store the file, or store this endpoint's URL together with the workspace_id, which never expire: the inspection.completed webhook carries the URL as video_url next to workspace_id, and calling it again is the supported way to get a new link. Read the format from content_type or the download's Content-Type header rather than from the URL.

The video is available before the report. The report waits for review; the video does not, because review never changes it. It is ready by the time inspection.recording_completed arrives, so you can fetch it from that event onward.

Answer Meaning What to do
200 The video is ready. Download it from download_url.
409 video_not_ready Not recorded yet, still being processed, or being recorded again. Wait for inspection.recording_completed or inspection.completed, then call again.
404 video_not_available A photo inspection, or a recording that could not be processed. Stop polling. There is no video for this inspection.
404 not_found No such inspection in that workspace, including one that belongs to another workspace. Check the ids from the webhook.
503 service_unavailable We could not create a link just now. Retry; nothing is wrong with your request.
Shell
curl -sS -G "https://api.rapideyeinspections.com/v1/inspections/3b7f1a90-2c44-4b6d-9e01-7a5c2f8e1d33/video" \
  -H "Authorization: Bearer rapideye_pk_REPLACE_ME" \
  --data-urlencode "workspace_id=ws_123"
JSON
{
  "inspection_id": "3b7f1a90-2c44-4b6d-9e01-7a5c2f8e1d33",
  "download_url": "https://...signed...",
  "expires_at": "2026-10-05T17:19:33Z",
  "media_expires_in": 900,
  "content_type": "video/mp4",
  "duration_seconds": 313,
  "size_bytes": 341087869,
  "recorded_at": "2026-10-05T17:01:12+00:00"
}

Per-item checklist results

When a home has a checklist, we report what the walkthrough showed for each item, not just an outcome for the whole clean. The results do not wait for the report: each verdict lands about 20 seconds after the cleaner checks the item off, almost all of them while the walk is still going, so the block is available long before review finishes.

JSON
{
  "checklist": {
    "status": "complete",
    "counts": { "verified": 9, "missed": 2, "explained": 1, "not_checked": 0, "pending": 0 },
    "items": [
      {
        "id": "c1f0a2e4-...",
        "name": "Bedrooms: beds made, linens fresh",
        "state": "verified",
        "timestamp": 84.2,
        "image_url": "https://...signed..."
      },
      {
        "id": "9b73d5a1-...",
        "name": "Hot tub: cover on and latched",
        "state": "missed",
        "timestamp": 131.7,
        "image_url": "https://...signed..."
      },
      {
        "id": "4d20bb77-...",
        "name": "Patio: furniture straightened",
        "state": "explained",
        "reason": "Patio furniture is stacked in the shed for the winter",
        "timestamp": 201.0
      }
    ]
  }
}

The same block, with the same shape, rides on every surface that describes an inspection:

Where What it holds
the inspection.recording_completed webhook every item final, complete; or, if a result is still landing 2 minutes after the walkthrough is sealed, the results as they stand then, in_progress
the inspection.completed webhook the results at the time the report was released
GET /v1/inspections the results now, on every status
GET /v1/inspections/{inspection_id}/findings the results now

status is in_progress until the walkthrough's recording has finished processing and every item has its final state, then complete. There is no timeout: complete means every item is final. An item whose footage never reached us (the phone lost it, or it was checked off after recording stopped) is closed as not_checked within about 15 minutes of the walk ending, so a walkthrough does not stay in_progress waiting for it.

Four final states, exactly the chips the RapidEye report shows, plus pending while the walk is still being checked:

State Means Picture
verified The walkthrough showed the item done. Almost always (see below)
missed The walkthrough did not confirm the item: it showed it not done, never showed it clearly, or the cleaner finished without re-filming it ("Finish anyway"). Almost always (see below); a "Finish anyway" item shows the picture of the check it skipped, when that check had one
explained The cleaner explained why the camera missed an item they did. Carries reason, their words. When the walkthrough had one for that moment
not_checked We could not check the item: our check failed, or the footage of that moment never reached us. Not a result about the cleaner's work; do not count it as missed. Only when our check got as far as the footage of that moment
pending The verdict is still on its way. Only seen while status is in_progress. Never

pending is not a result. Show it as "checking" and read GET /v1/inspections?workspace_id=...&task_id=... again, once a minute is enough, until status is complete; inspection.completed carries the final results too. A pending item becomes one of the other four.

A complete result can still change, in two rare ways, and the API shows the new state from then on while a webhook already delivered is not re-sent:

  • the cleaner explains an item after the walk (a missed becoming explained);
  • an item that was not_checked because our model was over capacity is re-run once the model answers again, for up to 24 hours. While it re-runs the item reads pending and status reads in_progress; then it settles on its result and status returns to complete.

If you store results, read the block again when inspection.completed arrives: it carries the latest.

counts names all five states every time, with zeros, so a badge never has to test for a key.

A picture of each step. A verified, missed, explained or not_checked item carries:

Field Type Notes
image_url string A still from the walkthrough at the moment the item was checked off: the same picture the RapidEye report shows beside it. A signed link, good for 15 minutes from our answer (media_expires_in), exactly like a finding image. Present whenever the picture exists (see the table above).
timestamp number Where in the walkthrough video (GET /v1/inspections/{inspection_id}/video) the item was checked off, in seconds from the start: the same clock as a finding's video_timestamp_seconds. A number of seconds, never a date-time. Seek there to see the moment in motion. Present whenever the cleaner's tap was recorded, with or without a picture.
reason string On explained only: what the cleaner wrote.

Use these instead of asking the cleaner for a photo per step. Fetch the image when you receive it and store the bytes, not the URL: the link dies after 15 minutes, and GET /v1/inspections?workspace_id=...&task_id=... signs fresh ones on every read. Every read signs a new link for the same picture, so key your copy by the item's id and state (a new state means a new result, and so possibly a new picture), not by the URL. Fetch it with a plain GET and no auth header, and read the format from the response's Content-Type.

pending items carry none of these. inspection.recording_completed waits until every item we judged from the footage has its picture, and until every stored picture can be linked, within the same 2 minutes as the results; in the rare case one is not ready by then (the video did not survive processing, or the stored picture is gone), the event goes without it. Nothing is made for a not_checked item that has no picture, so it never holds the event, and a few items on walkthroughs recorded before 2026-10-08 may also lack one.

id is the checklist item's id from the checklist endpoints, so results join straight back to the list you sent. It is omitted on the occasional item we could not tie back to a current list entry. items is in the order the cleaner worked through them, and holds the items the cleaner checked off.

The whole checklist key is omitted when the home has no checklist, rather than sent as an empty block with zero counts: those are different claims, and only one of them is true.

Linking back to the full report

Findings over the API are the machine-readable half. The report is the page a person opens, and every response that can carry a link to it does.

report_url is the same URL in every place, derived from the inspection, so it is stable and safe to store alongside your own task:

Where When it appears
the inspection.completed webhook always, on the event that says the report exists, at the top level and in report
GET /v1/inspections?task_id=... in report once its status is ready, and at the top level on "status": "completed"
GET /v1/inspections/{inspection_id}/findings always, since that endpoint already refuses anything earlier

If you have only your own task id and want the link, that is the middle row and it is one call:

HTTP
GET /v1/inspections?workspace_id=ws_7Qv3&task_id=clean-88231

Before the report exists, report carries "status": "processing" and no report_url, because there is nothing to link to yet. Test for the key.

The report page requires a RapidEye login. It is a customer-facing page inside the account, so whoever opens it needs a seat in that workspace. An unauthenticated click lands on our login page and is forwarded to the report after signing in, so the link is safe to put in front of someone who has an account and has not signed in yet. It is not a public link, and it is not something to paste into a channel that reaches people outside the workspace. POST /v1/workspace-claims is how you get your customer their seat.