Reference
Recording links and inspections
Issue a recording link per task, then read status and findings.
Requesting from us
GET/v1/recording-link#
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:
-
Every part of the video has uploaded from the phone.
-
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).
-
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.
-
The page navigates to your URL with two query parameters added (any query or fragment of your own is kept):
Textconduit://rapideye/complete?inspection_id=6a1d2c80-3f0e-4b3a-9d61-0c8f1b7e2a55&status=submittedParameter Value inspection_idOur inspection id, the same one the webhooks and GET /v1/inspectionscarrystatusAlways 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.
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"
{
"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
}
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.
{
"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..." }
]
}
}
{
"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.
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"
{
"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. |
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"
{
"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.
{
"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
missedbecomingexplained); - an item that was
not_checkedbecause our model was over capacity is re-run once the model answers again, for up to 24 hours. While it re-runs the item readspendingandstatusreadsin_progress; then it settles on its result andstatusreturns tocomplete.
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:
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.