## Provisioning workspaces

A workspace is one of your customers. Each one maps to an account on our side
where that customer's properties, inspections and reports live, and a recording
link cannot be issued for a workspace that does not exist yet.

You provision them yourself; we are not in the loop.

### `POST /v1/workspaces`

Call it once, when you sign a customer. Idempotent create-or-return keyed on your
`workspace_id`: the first call creates the account and answers `201`, every later
call answers `200` with the **same** `company_id`, and concurrent calls for one
workspace all resolve to one account. There is no update or delete; a workspace
is never re-pointed by an API call.

| Field | Required | Notes |
|---|---|---|
| `workspace_id` | yes | Your account or workspace id. Opaque to us: surrounding whitespace is trimmed, and what remains is stored and matched literally, never split, case-folded or normalized. Max 200 characters. |
| `name` | yes | The customer's display name, as it should read on their reports. Max 200 characters. |

```bash
curl -sS -X POST https://api.rapideyeinspections.com/v1/workspaces \
  -H "Authorization: Bearer rapideye_pk_REPLACE_ME" \
  -H "Content-Type: application/json" \
  -d '{"workspace_id": "ws_123", "name": "Sunset Property Management"}'
```

```json
{
  "workspace_id": "ws_123",
  "name": "Sunset Property Management",
  "company_id": "7c3f1a90-2c44-4b6d-9e01-7a5c2f8e1d33",
  "status": "provisioned",
  "provisioned_at": "2026-08-13T14:02:11Z"
}
```

`company_id` is **ours**, the account your workspace resolves to. Store it if it
helps your support team; nothing else in this API requires you to send it back,
and it is never accepted in a request body.

Notes worth knowing before you build against it:

- **The partner is taken from your API key.** Sending `partner`, `partner_id` or
  `company_id` in the body is rejected with `400 invalid_request` rather than
  ignored, so a wrong assumption fails loudly on your first call instead of
  quietly on your customer's.
- **Workspace ids are scoped to you.** Another partner using the same string is a
  different workspace and a different account, and neither of you can see the
  other's.
- **A repeat call does not rename.** The first call names the account; later calls
  return the name we currently hold. If a customer renames, tell us. A rename can
  cross into reports and invoices, so it is not a thing a retry should do silently.
- **Provisioning is rate-limited** (creation only; repeat calls for an existing
  workspace are not). A `429` here means back off and retry, or email support if you
  are onboarding a batch.

### `GET /v1/workspaces/{workspace_id}`

Everything you need to know about one workspace: whether it is provisioned, how
current its roster is, whether we are polling a feed, whether inspections are
switched on, and whether an owner has claimed it. This is the endpoint your
onboarding flow polls.

```bash
curl -sS https://api.rapideyeinspections.com/v1/workspaces/ws_123 \
  -H "Authorization: Bearer rapideye_pk_REPLACE_ME"
```

```json
{
  "workspace_id": "ws_123",
  "name": "Sunset Property Management",
  "company_id": "7c3f1a90-2c44-4b6d-9e01-7a5c2f8e1d33",
  "status": "provisioned",
  "provisioned_at": "2026-08-13T14:02:11Z",
  "roster": {
    "revision": 4,
    "state": "fresh",
    "roster_confirmed_at": "2026-09-16T06:12:00Z",
    "observed_at": "2026-09-16T06:00:00Z",
    "last_source": "push",
    "present_count": 128,
    "inactive_count": 3,
    "unconfirmed_count": 0,
    "missing_address_count": 2,
    "billing_active_count": 121
  },
  "sync": { "mode": "push", "health": "not_configured" },
  "service": { "state": "enabled", "recording_enabled": true },
  "claim": { "state": "claimed" }
}
```

Four blocks, because there are four different questions and a different party
acts on each one.

| Block | Field | Meaning |
|---|---|---|
| `roster` | `revision` | Increments on every applied import. Also returned by `GET /v1/recording-link`. |
| `roster` | `state` | `unconfirmed` before your first import, then `fresh`, then `stale` once `observed_at` is more than 48 hours old. Warn only; a stale roster never blocks a recording. |
| `roster` | `observed_at` | The `observed_at` you sent on the latest applied import, which is when your snapshot described your inventory, not when we applied it. |
| `roster` | `last_source` | `push` or `pull`. |
| `roster` | `present_count` | Properties in the latest snapshot. |
| `roster` | `inactive_count` | Properties we have seen before that the latest snapshot left out. |
| `roster` | `unconfirmed_count` | Properties we hold that no validated roster has confirmed. Non-zero only during onboarding; it should reach zero after your first import. |
| `roster` | `missing_address_count` | Present properties that carry `address_unavailable_reason` instead of an address. |
| `roster` | `billing_active_count` | Properties switched on for inspections. Set by each applied import. |
| `sync` | `mode` | `push` if you send rosters, `pull` if we poll a feed you host. |
| `sync` | `health` | `not_configured`, `never_run`, `ok` or `failing`. |
| `service` | `state` | `pending_setup` until the first roster import applies, then `enabled` a few seconds later. |
| `service` | `recording_enabled` | Whether `GET /v1/recording-link` will issue links at all. |
| `claim` | `state` | `unclaimed`, `pending` (an invite is outstanding) or `claimed`. |

`404 workspace_not_provisioned` otherwise. The id is matched literally and has to
survive as one path segment, so percent-encode anything reserved (`/`, `?`, `#`).
A workspace belonging to another partner returns the same `404` as one that does
not exist anywhere.

`POST /v1/workspaces` returns the first five fields only. At the instant a
workspace is provisioned every count is zero and every state is its initial one,
so there is nothing for the four blocks to say.

### We do not match your workspace to an existing RapidEye account

A new `workspace_id` **always** gets a new, empty account, even when the property
manager behind it is already a RapidEye customer under their own contract. We do
not match on company name, on email domain, or on anything else.

This is a decision, not a gap. A duplicate account is something we merge in a few
minutes. A wrong match silently routes one operator's inspections, reports and
recording links into another operator's account, and every API call keeps
answering `200` while it happens. If you know a customer of yours is already with
us, tell us and we will link the two by hand.

### What a freshly provisioned account can do

Nothing yet, on purpose. Nobody can sign in against it, it has no email domains
that would let anyone attach to it, no notification destinations, no properties
and no active properties. It is a routing destination. Properties arrive when you
import a roster, and the account switches on for inspections when that import
applies; people arrive through a workspace claim.
