RapidEye API

Reference

Webhooks

Signed events on recording, checklist and report completion, with rotation.

What we send back to you

You give us one HTTPS receiver URL and we exchange a shared secret. Three events, in the order they normally arrive, each named for what just finished:

Event Fires when Typical timing
inspection.recording_completed The cleaner finished the walkthrough, with each checklist item's result Seconds after the walkthrough is sealed (usually under a minute after the cleaner taps Done); at most about 2 minutes after the seal while a result is still landing
inspection.completed The report is ready Hours after the walk, once review is done
inspection.failed We could not produce the report Instead of inspection.completed

inspection.recording_completed — the cleaner finished the walkthrough, and here is each checklist item's result. Use it to mark the checklist item complete (the cleaner's part is done) and to drive a "verified by RapidEye" state; the report is not ready yet. Same envelope as inspection.completed below, with recorded_at instead of completed_at, no report_url, video_url or report, and the checklist block (omitted for a home with no checklist). Sent once per walkthrough, and not for a home that is switched off.

It waits for the results, for at most 2 minutes. We hold the event until every checklist item has its final state and its picture, so checklist.status is normally complete. The hold ends 2 minutes after the walkthrough is sealed on our side (the moment the recording is safely saved). An item whose result is still on its way then goes out pending, with checklist.status in_progress: read its final state from GET /v1/inspections?workspace_id=...&task_id=..., or from inspection.completed. In the week before this shipped, about 1 walkthrough in 50 had a result still landing at 2 minutes. A home with no checklist is not held. A walkthrough uploaded as a file in RapidEye, rather than recorded through your link, is not held either: its event goes when the upload finishes, in_progress.

JSON
{
  "version": 1,
  "event_id": "5d0b8f2e-6c1a-4f3e-9a77-1e2d3c4b5a69",
  "event_type": "inspection.recording_completed",
  "workspace_id": "ws_123",
  "task_id": "task_abc",
  "property_id": "unit_42",
  "inspection_id": "3b7f1a90-2c44-4b6d-9e01-7a5c2f8e1d33",
  "recorded_at": "2026-08-01T09:01:02Z",
  "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..." }
    ]
  }
}

Each image_url in the event is signed when the event is queued and lives 15 minutes. A first delivery lands within seconds, so fetch the images then; if yours came on a retry and a link has expired, read GET /v1/inspections?workspace_id=...&task_id=... for fresh ones.

recorded_at is when the walkthrough was sealed, so it sorts before the report's completed_at.

inspection.completed — the inspection is processed and the report is ready. It carries both blocks, report and checklist (omitted for a home with no checklist). The top-level report_url and completed_at predate the report block and stay:

JSON
{
  "version": 1,
  "event_id": "9f1c4d3e-88a0-4a1e-9d2b-5c8e77c1b904",
  "event_type": "inspection.completed",
  "workspace_id": "ws_123",
  "task_id": "task_abc",
  "property_id": "unit_42",
  "inspection_id": "3b7f1a90-2c44-4b6d-9e01-7a5c2f8e1d33",
  "report_url": "https://app.rapideyeinspections.com/reports/3b7f1a90-2c44-4b6d-9e01-7a5c2f8e1d33",
  "video_url": "https://api.rapideyeinspections.com/v1/inspections/3b7f1a90-2c44-4b6d-9e01-7a5c2f8e1d33/video",
  "completed_at": "2026-08-01T17:04:33Z",
  "report": {
    "status": "ready",
    "report_url": "https://app.rapideyeinspections.com/reports/3b7f1a90-2c44-4b6d-9e01-7a5c2f8e1d33"
  },
  "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" }
    ]
  }
}

video_url is the address of GET /v1/inspections/{inspection_id}/video, not a download link. It never expires, which matters because a delivery can be retried hours later. Call it with your partner key and the workspace_id from this event to get a fresh download link.

inspection.failed is the same envelope with no report_url, video_url, report or checklist, plus a "reason". The checklist results are not lost with the report: inspection.recording_completed has already carried them, and GET /v1/inspections keeps carrying the block.

task_id and property_id are echoed back as your ids, so your handler can look up the task without storing any RapidEye identifiers.

Verifying our signature

Every delivery carries:

Text
X-RapidEye-Signature: t=<unix_seconds>,v1=<hex hmac_sha256(secret, "{t}.{raw_body}")>

Recompute over the raw body bytes before any JSON parse. Reject if |now - t| > 300 seconds. Compare in constant time.

There may be more than one v1=. During a signing-secret rotation we sign with the outgoing secret as well as the new one, so a delivery can carry two entries, the current secret first. Accept the delivery if any entry matches. The snippets below do that; verify yours does before we rotate anything, because a verifier that keeps only one entry is the single way an overlap window fails.

Node / TypeScript:

TypeScript
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyRapidEyeSignature(rawBody: Buffer, header: string, secret: string): boolean {
  const pairs = header.split(",").map((kv) => kv.split("=", 2));
  const t = pairs.find(([k]) => k === "t")?.[1] ?? "";
  if (!/^\d+$/.test(t) || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const signed = Buffer.concat([Buffer.from(`${t}.`, "utf8"), rawBody]);
  const expected = Buffer.from(createHmac("sha256", secret).update(signed).digest("hex"), "utf8");
  // EVERY v1, not just the first: a rotation sends two.
  return pairs
    .filter(([k]) => k === "v1")
    .some(([, v]) => {
      const b = Buffer.from(v ?? "", "utf8");
      return expected.length === b.length && timingSafeEqual(expected, b);
    });
}

(In Express, mount express.raw({ type: "application/json" }) on the webhook route so the raw bytes survive; express.json() breaks the HMAC.)

Python:

Python
import hashlib, hmac, time

def verify_rapideye_signature(raw_body: bytes, header: str, secret: str) -> bool:
    pairs = [kv.split("=", 1) for kv in header.split(",") if "=" in kv]
    t = next((v for k, v in pairs if k == "t"), "")
    if not t.isdigit() or abs(time.time() - int(t)) > 300:
        return False
    signed = t.encode() + b"." + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    # EVERY v1, not just the first: a rotation sends two.
    return any(hmac.compare_digest(expected, v) for k, v in pairs if k == "v1")

(In Flask, use request.get_data(), not request.json.)

If you implemented against the earlier snippets, which parsed the header into a dict, upgrade before your first rotation. A dict keeps only the LAST v1=, so it would verify against whichever secret we happen to have put second.

Rotating the webhook signing secret

We mint a new secret and keep the old one valid for an overlap window (7 days by default). For the whole window every delivery carries two v1= entries and either secret verifies, so there is no cutover instant and nothing to coordinate:

  1. We send you the new secret and the date the old one stops working.
  2. You deploy it whenever suits you, any time before that date.
  3. After the date we sign with the new secret only.

Your side needs the any-match verifier above before step 1. Tell us if you need a longer window; tell us immediately if you think a secret has leaked, in which case we cut the window short on purpose.

Rotating your partner key

Same shape, no window to schedule: two of your keys can be active at once.

  1. We issue a second key and send it to you. Both keys now work.
  2. You deploy the new one.
  3. We watch until the old key stops being used, then revoke it.

A revoked key answers 403 key_revoked, never 401 invalid_key, so if anything was still using it the error says so plainly. Ask us to rotate whenever you want one; there is no cost and no downtime.

Delivery semantics

  • At-least-once. Retries with backoff (1m, 5m, 30m, 2h, 12h) until you return 2xx.
  • Duplicates are expected. Dedupe on event_id if you do anything beyond saving report_url.
  • Respond 2xx quickly; do heavy work async. Non-2xx or timeout re-enters the retry schedule.
  • Ordering is not guaranteed. Use completed_at if you need to order events.
  • Ignore event types you do not recognize and answer them 2xx. We add events without a version change; registering a filter that names only the events you handle also works.
  • Nothing for a unit that is switched off. If a unit is switched off in RapidEye while a walkthrough is in flight (your roster dropped it, or your customer turned it off), the walkthrough still finishes but we send no event for it, so no video_url arrives. Its report stays readable at GET /v1/inspections/{inspection_id}/findings, its video at GET /v1/inspections/{inspection_id}/video, and its status at GET /v1/inspections?task_id=....