RapidEye API

Reference

Roster

Import each customer's complete property list, by push or by feed.

The roster

A roster is the complete list of properties in one workspace. It is the central object in this integration, because it is what makes a recording link safe to issue: a link is only ever issued for a property you have told us about.

Three properties of a roster, all of which follow from it being complete:

  • Omission means deactivation. A property you have sent before and leave out of a later snapshot is marked inactive. Its history, its past inspections and its reports are all kept. Nothing is ever deleted.
  • An applied import activates what it lists as active. Every property an applied import lists as active (the default when status is omitted) is switched on for inspections, and the account with it. One you send as inactive, or leave out, is deactivated, as above. The result is reported as billing_active and roster.billing_active_count.
  • A roster replaces, it does not merge. There is no add-one or remove-one call, deliberately. Every partial-update API grows into a reconciliation problem where the two sides disagree and neither can say which is right.

The roster item

The same shape whether you push it to us or host it on a feed we poll.

JSON
{
  "property_id": "unit_42",
  "name": "Beach House Unit 1",
  "status": "active",
  "address": {
    "line1": "18 Ocean Drive",
    "line2": "Apt 3",
    "city": "Panama City Beach",
    "region": "FL",
    "postal_code": "32413",
    "country_code": "US"
  },
  "building_id": "bldg_7",
  "unit_label": "3B"
}
Field Required Notes
property_id yes Your listing or property id. Opaque to us, matched literally, unique within the workspace, max 200 characters.
name yes Display name. This is what titles reports, so send the name your operator would recognize. Max 200 characters.
status no active or inactive. Defaults to active. Use inactive for a unit that is still yours and is out of service; leave it out of the snapshot entirely only when it has left the roster.
address see below Object with line1, city, region, postal_code, country_code all required, and line2 optional.
address_unavailable_reason see below Free text, max 500 characters. Send this instead of address when you genuinely have no address.
building_id no Groups units in one building. Max 200 characters.
unit_label no The unit within the building. Max 100 characters.

Exactly one of address and address_unavailable_reason. Sending both is rejected, and so is sending neither. Addresses are required because they are what make a report, a cleaner's route and an invoice line legible, and "we will add them later" has not once happened on an integration. The escape hatch is real, though: a property with address_unavailable_reason imports normally, stays in every count, and shows as address_status: "missing" so both sides can see how much of the roster is unaddressed.

Unknown fields are rejected rather than ignored. A typo like unitLabel that we answered 200 to would be an integration that silently never set the field.

POST/v1/roster-imports?workspace_id=...#

Submit one complete snapshot. Answers 202 with a status resource; applying a roster is not work to do inside a request you are holding open.

Idempotency-Key is a required header. An import deactivates every property it omits, so a retry after a timeout is not a repeated read, it is a second complete-snapshot write. Send a new value for every import attempt, and reuse it verbatim only to retry that exact request after a timeout. A run that reached applied, rejected or failed is final: re-sending with its key returns that same run, so any further attempt, including an unchanged roster after a failed run, needs a fresh key.

Field Required Notes
complete yes Must be true. Your assertion that this array is the whole roster.
observed_at yes When your snapshot described your inventory. This is what freshness is measured from, not the time you sent it.
total_count yes Your own count of properties. We check it, which is what catches a truncated upload.
properties yes The array of roster items. Max 10,000 per import by default; ask us if you need more.
allow_empty no Required to be true for a roster with no properties.
acknowledge_inactive_count no Required when an import would deactivate more than half the present properties. Set it to the count we report.

What is checked before the 202, and rejected with a 400 you can act on immediately: the shape of every item, every length bound, the property count against your cap, repeated property_id values, and total_count against the array. A rejection names the offending item in detail.index.

What is decided afterwards, on the run: the two safeguards. An empty roster needs allow_empty, and an import that would deactivate more than half of the present properties needs acknowledge_inactive_count set to the number we compute. Both come back as a rejected run carrying roster_confirmation_required and the count, because they are properties of the difference between your snapshot and the roster we hold, and that comparison is only meaningful under a lock.

Idempotency, precisely:

  • Same key, same roster returns the same run at whatever status it has reached, including a final failed. Retrying after a timeout is free; retrying after a failure needs a new key.
  • Same key, different roster is 409 idempotency_conflict. One key must not stand for two different complete snapshots.
  • Key matching is scoped to the workspace, and the roster comparison is over the normalized snapshot. Re-sending the same roster with your JSON keys in a different order, or your properties in a different order, is the same roster.
Shell
curl -sS -X POST "https://api.rapideyeinspections.com/v1/roster-imports?workspace_id=ws_123" \
  -H "Authorization: Bearer rapideye_pk_REPLACE_ME" \
  -H "Idempotency-Key: roster-2026-09-16T06:00Z" \
  -H "Content-Type: application/json" \
  -d '{
    "complete": true,
    "observed_at": "2026-09-16T06:00:00Z",
    "total_count": 1,
    "properties": [
      { "property_id": "unit_42", "name": "Beach House Unit 1",
        "address": { "line1": "18 Ocean Drive", "city": "Panama City Beach",
                     "region": "FL", "postal_code": "32413", "country_code": "US" } }
    ]
  }'
JSON
{
  "import_id": "b2f4c8e1-9a35-4d77-8e10-2c6a4f9b1d55",
  "workspace_id": "ws_123",
  "status": "queued",
  "source": "push",
  "status_url": "https://api.rapideyeinspections.com/v1/roster-imports/b2f4c8e1-9a35-4d77-8e10-2c6a4f9b1d55?workspace_id=ws_123",
  "observed_at": "2026-09-16T06:00:00Z",
  "submitted_at": "2026-09-16T06:00:04Z",
  "counts": { "submitted": { "total": 1, "active": 1, "inactive": 0, "missing_address": 0 } },
  "base_revision": 3
}

GET/v1/roster-imports/{import_id}?workspace_id=...#

The status, counts and diff of one import. status is one of queued, running, applied, rejected or failed. The snapshot you sent is not echoed back; read GET /v1/properties for what we ended up holding.

A rejected or failed run carries error with a code and a detail. The codes you will see there are roster_empty_not_allowed, roster_confirmation_required, roster_revision_conflict and roster_duplicate_property, plus feed errors on a pull run. Branch on error.code, never on the message.

An import_id from another workspace answers 404 not_found, identical to an id that does not exist.

JSON
{
  "import_id": "b2f4c8e1-9a35-4d77-8e10-2c6a4f9b1d55",
  "workspace_id": "ws_123",
  "status": "applied",
  "source": "push",
  "status_url": "https://api.rapideyeinspections.com/v1/roster-imports/b2f4c8e1-9a35-4d77-8e10-2c6a4f9b1d55?workspace_id=ws_123",
  "observed_at": "2026-09-16T06:00:00Z",
  "submitted_at": "2026-09-16T06:00:04Z",
  "applied_at": "2026-09-16T06:00:09Z",
  "base_revision": 3,
  "result_revision": 4,
  "counts": { "submitted": { "total": 1, "active": 1, "inactive": 0, "missing_address": 0 } },
  "diff": { "created": 0, "updated": 1, "deactivated": 0, "reactivated": 0 }
}

GET/v1/properties?workspace_id=...#

The roster we currently hold. This is the reconciliation endpoint: your inventory is the source of truth and ours is a copy, so the useful question is where the two disagree, and that is only answerable if you can read our copy back.

Parameter Required Notes
workspace_id yes Yours. Scopes the request to one customer.
roster_status no Filter to present, inactive or unconfirmed.
limit no Page size, 1 to 500. Defaults to 100.
cursor no The next_cursor from the previous page, sent back verbatim. Opaque.

property_id here is yours, because this endpoint answers "what is in my roster". Ours is rapideye_property_id, spelled out so neither has to be inferred. next_cursor is omitted on the last page, so while (next_cursor) terminates without an extra call.

roster_status is the roster fact and billing_active is whether the property is switched on for inspections, which each applied import sets. A property that is present and not billing-active is the state property_not_activated reports. It can show for a few seconds just after an import applies, while activation runs; wait and ask again first. If it is still there after that, send the roster again with a new Idempotency-Key (or trigger a sync with POST /v1/roster-syncs if you host a feed).

Pagination is keyset, over your own property_id. An import landing mid-walk does not make rows appear or disappear from your pages.

JSON
{
  "workspace_id": "ws_123",
  "roster_revision": 4,
  "properties": [
    {
      "property_id": "unit_42",
      "rapideye_property_id": "1f0a9c22-6b1e-4a77-9b0f-2d2b0c5f7a10",
      "name": "Beach House Unit 1",
      "roster_status": "present",
      "address_status": "ok",
      "billing_active": true,
      "address": { "line1": "18 Ocean Drive", "city": "Panama City Beach",
                   "region": "FL", "postal_code": "32413", "country_code": "US" },
      "last_seen_at": "2026-09-16T06:00:09Z",
      "last_seen_revision": 4,
      "setup": {
        "checklist": { "items": 12 },
        "matterport": { "state": "imported", "url": "https://my.matterport.com/show/?m=SxQL3iGyoDo" },
        "baseline_photos": { "count": 18, "state": "imported" }
      }
    }
  ],
  "next_cursor": "dW5pdF80Mg=="
}

Hosting a feed instead of pushing

If you would rather we pull, host one HTTPS endpoint that returns the same body as POST /v1/roster-imports, plus an optional next_cursor. We poll it once a day.

Requirement Detail
Body Identical to the push body: complete, observed_at, total_count, properties, and optionally allow_empty and acknowledge_inactive_count on the first page.
Pagination Return next_cursor to continue. We re-request the same URL with ?cursor=<next_cursor>. We never follow a URL you put in the body.
Consistency Every page must carry the same total_count and observed_at, and the properties across all pages must sum to total_count. A repeated cursor fails the run.
Auth Authorization: Bearer <secret>, from a secret we agree out of band.
Redirects Not followed. A 3xx fails the run.
Bounds 200 pages, 20 MiB total, 20 seconds per request.
Empty An intentionally empty roster must be complete: true, total_count: 0, properties: []. Any other empty 200 fails the run and changes nothing, because an empty response is far more often an outage than an emptied roster.

A failed pull never resets freshness and never deactivates anything.

POST/v1/roster-syncs?workspace_id=...#

Poll the feed now instead of waiting for the daily run. 202 with a sync_id, or 202 with status: "in_progress" when a pull is already running. A workspace that pushes its roster answers 409 pull_not_configured.

Freshness

roster.state on the workspace read is unconfirmed, fresh or stale. Stale means observed_at is more than 48 hours old.

It is a warning, not a gate. A stale roster still issues recording links, because a roster we think is old is still the best roster we have and refusing a customer's inspections over a late cron would be the worse failure. We watch it on our side and will talk to you before it ever becomes a block.