# GET Account Source: https://docs.prophia.com/api-reference/v2/accounts/get-account /api-reference/openapi.json get /rest/v2/accounts/{accountId} Returns a single account by id. To retrieve a list of accounts your api key has access to, use the [list accounts endpoint](/api-reference/v2/accounts/list-account) api. **Required capabilities:** `READ:ACCOUNT` # LIST Account Source: https://docs.prophia.com/api-reference/v2/accounts/list-account /api-reference/openapi.json get /rest/v2/accounts Returns a paged list of the accounts your api key has access to. In practice API keys are granted access to a single account per api key, so the list will contain just this single account. **Required capabilities:** `READ:ACCOUNT` ## Filters Filters combine with AND: a result must match every filter you supply. | Filter | Description | | - | - | | `accountName` | Case-insensitive contains match on the account name. | | `accountStatus` | Filter by account status. | | `apiName` | Case-insensitive contains match on the account's API name. | | `accountManager` | Case-insensitive contains match on the account manager's name. | # LIST Leases Recently Updated Source: https://docs.prophia.com/api-reference/v2/accounts/list-leases-recently-updated /api-reference/openapi.json get /rest/v2/accounts/{accountId}/recent_changed_leases Returns a paged list of leases whose documents have had data updated between `start_date` and `end_date` (both inclusive), limited to buildings your api key is permitted to access. This is the change-detection primitive for sync jobs: poll it and re-fetch only the leases it names instead of re-crawling the account. **Required capabilities:** `READ:LEASE` # Beta endpoints Source: https://docs.prophia.com/api-reference/v2/beta Endpoints for Labs features that are still changing > [!WARNING] > > A beta endpoint's path, parameters, and response may change as its feature develops, and the endpoint may be removed. The endpoints in this section belong to **Labs** features, which Prophia is still building with customers. They use the same base URL, [authentication](/developers/authentication), [includes](/api-reference/v2/concepts/includes), [pagination](/api-reference/v2/concepts/pagination-and-filtering), and [errors](/api-reference/v2/concepts/errors-and-permissions) as every other endpoint. When a feature graduates from Labs, its endpoints move from this section into **Endpoints**. # CREATE Building Space Source: https://docs.prophia.com/api-reference/v2/building-spaces/create-building-space /api-reference/openapi.json post /rest/v2/accounts/{accountId}/buildings/{buildingId}/spaces Creates a Building Space representing a unit, space, floor, room, or area in the Building. **Required capabilities:** `WRITE:BUILDING_SPACE` # Delete a building space Source: https://docs.prophia.com/api-reference/v2/building-spaces/delete-a-building-space /api-reference/openapi.json delete /rest/v2/accounts/{accountId}/buildings/{buildingId}/spaces/{spaceId} Permanently deletes a space — this is a hard delete, not an archival; to hide a space while keeping its history, PATCH `archived` to true instead. Returns 200 with an empty body. Requires the REST write module on the account; the caller must be a Prophia team member or an edit user on the account. **Required capabilities:** `WRITE:BUILDING_SPACE` # GET Building Space Source: https://docs.prophia.com/api-reference/v2/building-spaces/get-building-space /api-reference/openapi.json get /rest/v2/accounts/{accountId}/buildings/{buildingId}/spaces/{spaceId} Returns a single space by id. **Required capabilities:** `READ:BUILDING_SPACE` # LIST Building Space Source: https://docs.prophia.com/api-reference/v2/building-spaces/list-building-space /api-reference/openapi.json get /rest/v2/accounts/{accountId}/buildings/{buildingId}/spaces Returns a paged list of the Building's spaces, archived and non-archived alike — filter with `archived=false` for current units. **Required capabilities:** `READ:BUILDING_SPACE` ## Filters Filters combine with AND: a result must match every filter you supply. | Filter | Description | | - | - | | `type` | Exact match on the space type; an unrecognized value returns no results. | | `floor` | Exact match on the floor designation. | | `suiteNumber` | Exact match on the suite name. | | `rentable` | Filters to spaces with this rentable flag. | | `occupied` | Filters to spaces with this occupied override value. | | `archived` | Filters to archived (true) or non-archived (false) spaces; omit for both. | # UPDATE Building Space Source: https://docs.prophia.com/api-reference/v2/building-spaces/update-building-space /api-reference/openapi.json patch /rest/v2/accounts/{accountId}/buildings/{buildingId}/spaces/{spaceId} Updates a Building Space's fields. Omitted fields are left unchanged. **Required capabilities:** `WRITE:BUILDING_SPACE` # CREATE Building Source: https://docs.prophia.com/api-reference/v2/buildings/create-building /api-reference/openapi.json post /rest/v2/accounts/{accountId}/buildings Creates a Building under a Property in the Account. A Building is required in order to create a Lease. **Required capabilities:** `WRITE:BUILDING` # GET Building Source: https://docs.prophia.com/api-reference/v2/buildings/get-building /api-reference/openapi.json get /rest/v2/accounts/{accountId}/buildings/{buildingId} Returns a single building by id. **Required capabilities:** `READ:BUILDING` # LIST Building Source: https://docs.prophia.com/api-reference/v2/buildings/list-building /api-reference/openapi.json get /rest/v2/accounts/{accountId}/buildings Returns a paged list of the Account's Buildings **Required capabilities:** `READ:BUILDING` ## Filters Filters combine with AND: a result must match every filter you supply. | Filter | Description | Requires | | - | - | - | | `name` | Case-insensitive contains match on the building name. | | | `city` | Case-insensitive exact match on the address city. | | | `state` | Case-insensitive exact match on the address state. | | | `activeStatus` | Filter by the building's active status. | | | `customId` | Exact match on the building's custom id. | | | `buildingType` | Filter by building type. | | | `implementationManager` | Case-insensitive contains match on the implementation manager's name. | | | `customFields` | Custom Field filters, one entry per repeated parameter. | `READ:CUSTOM_FIELD` | # LIST Lease Source: https://docs.prophia.com/api-reference/v2/buildings/list-lease /api-reference/openapi.json get /rest/v2/accounts/{accountId}/buildings/{buildingId}/leases Returns a paged list of the Building's Leases. **Required capabilities:** `READ:LEASE` ## Filters Filters combine with AND: a result must match every filter you supply. | Filter | Description | | - | - | | `status` | Exact match on the lease status; an unrecognized value returns no results. | | `tenantName` | Case-insensitive contains match against both the tenant DBA and the tenant legal name. | | `expirationBefore` | Leases expiring on or before this calendar date (yyyy-MM-dd); inclusive. | | `expirationAfter` | Leases expiring on or after this calendar date (yyyy-MM-dd); inclusive. | | `leaseType` | Filter by lease type. | | `suiteNumber` | Case-insensitive contains match on the lease's suite names. | | `commenceBefore` | Leases commencing on or before this calendar date (yyyy-MM-dd); inclusive. | | `commenceAfter` | Leases commencing on or after this calendar date (yyyy-MM-dd); inclusive. | | `squareFootageMin` | Minimum square footage; inclusive. | | `squareFootageMax` | Maximum square footage; inclusive. | | `customId` | Exact match on the lease's custom id. | # UPDATE Building Source: https://docs.prophia.com/api-reference/v2/buildings/update-building /api-reference/openapi.json patch /rest/v2/accounts/{accountId}/buildings/{buildingId} Updates a Building's fields. Omitted fields are left unchanged. **Required capabilities:** `WRITE:BUILDING` # Data model Source: https://docs.prophia.com/api-reference/v2/concepts/data-model How accounts, properties, buildings, spaces, leases, documents, and tags fit together Everything in the Prophia API is scoped to an **account**, and underneath the account run two parallel spines: * the **physical spine** — Property → Building → BuildingSpace — describes real estate you own or manage * the **contractual spine** — Lease → LeaseDoc → DocTag — describes the agreements occupying it The two spines meet at the **building**: every lease belongs to a building (and through it, a property). ```mermaid theme={null} flowchart TB Account["Account
id · accountName"] Property["Property
id · propertyName"] Building["Building
id · buildingName
onboardingStatus · activeStatus"] Lease["Lease
id · dba · leaseStatus
suiteNumber (list of spaces)"] LeaseDoc["LeaseDoc
id · filename · documentStatus"] DocTag["DocTag
id · tagName · category
applicableSpaces"] BuildingSpace["BuildingSpace
id · suiteNumber · floor · archived"] Sublease["Sublease
a Lease with a parent"] Account -->|"has many"| Property Property -->|"has many"| Building Building -->|"houses"| Lease Lease -->|"documented by"| LeaseDoc LeaseDoc -->|"tagged with"| DocTag Building -.->|"is divided into"| BuildingSpace Lease -.->|"parent of"| Sublease BuildingSpace -. "suiteNumber is matched against
DocTag.applicableSpaces
by name, not by id" .-> DocTag ``` The solid path down the middle is the id-linked spine: every resource carries the ids of its ancestors (`accountId`, `propertyId`, `buildingId`, `leaseId`, `leaseDocId`), so you can walk it in either direction. The dashed links are the two side relationships — spaces hanging off the building, and subleases hanging off a lease — plus the one association that is a *name match* rather than a foreign key. ## The entities | Entity | What it is | Where to get it | | - | - | - | | **Account** | Your tenancy in Prophia — the scope for every other resource and every API key | [`GET /rest/v2/accounts`](/api-reference/v2/accounts/list-account) | | **Property** | A named holding within an account; groups one or more buildings | [`GET .../properties`](/api-reference/v2/properties/list-property) | | **Building** | A physical structure with an address; leases and spaces attach here | [`GET .../buildings`](/api-reference/v2/buildings/list-building) | | **BuildingSpace** | A suite, floor, or other discrete unit inside a building | [`GET .../buildings/{buildingId}/spaces`](/api-reference/v2/building-spaces/list-building-space) | | **Lease** | An agreement between a landlord and a tenant for space in a building; subleases are full leases with a `parent` | [`GET .../leases/{leaseId}`](/api-reference/v2/leases/get-lease) | | **LeaseDoc** | A document — original lease, amendment, exhibit, rent roll — attached to a lease. Documents can also be filed at the **property** or **building** level rather than against a lease; those are reached through [`include=documents`](/api-reference/v2/concepts/includes) on a property or building | [`GET .../leases/{leaseId}/docs`](/api-reference/v2/leases/list-document) | | **DocTag** | One extracted lease term — a value cited to a location in a document | [`GET .../leases/{leaseId}/tags`](/api-reference/v2/leases/list-tag) | Most resources also embed a `url` that deep-links into the Prophia web app. ## The soft link: tags to spaces > [!WARNING] > > A tag's `applicableSpaces[].name` refers to a `BuildingSpace` **by suite name, not by id**. There is no foreign key: match `applicableSpaces[].name` against `BuildingSpace.suiteNumber` **within the same `buildingId`**. Renaming a suite silently breaks the association, and the sentinel names `"Entire building"` and `"All spaces"` never correspond to a real space record. This is the one relationship in the diagram that is a name match rather than an id reference — the dashed line on the right. See [Spaces, applicable spaces, and encumbrances](/api-reference/v2/concepts/spaces-and-encumbrances) for the matching rules and a worked recipe. ## Absent means "not requested" One convention is worth stating once, because it applies to every response: **the API omits `null` fields entirely.** A field populated by an [include](/api-reference/v2/concepts/includes) you didn't request is simply missing from the JSON. Branch on *presence*, not on emptiness — a missing `tags` key means "not requested", never "no tags". An empty array means you asked and there were none. Everything else — which fields exist, what type each one is, what its values mean — is documented per field on the endpoint pages, and those are authoritative. # Documents Source: https://docs.prophia.com/api-reference/v2/concepts/documents Uploading documents, the documentStatus lifecycle, categories, and change detection A **document** (`LeaseDoc`) is a file attached to a lease — the original lease agreement, an amendment, a work letter, an exhibit. Documents are the raw material for everything Prophia extracts: [tags](/api-reference/v2/concepts/tags-and-values) come from documents, and a lease's data is only as complete as its documents are. A filename cannot be duplicated within a single lease. Documents can also be filed against a **property** or a **building** rather than a lease — rent rolls, stacking plans — and those are reached with [`include=documents`](/api-reference/v2/concepts/includes) on the property or building. ## `documentStatus`: the curation lifecycle `documentStatus` reports how far a document has moved through Prophia's pipeline and review. It is not a simple binary, and the last step is a human one. A document normally travels: ``` processing → incomplete → [account-specific review states] → complete ``` | Value | What it means | | - | - | | `processing` | The document is in Prophia's AI ingestion pipeline. The pipeline's internal states — text extraction, indexing, prediction — are deliberately collapsed to this one value. Pages and tags aren't final yet. | | `incomplete` | The AI pipeline has finished. The document has been read and machine-tagged, and it is now **ready for human review**. This is where every document lands when it leaves the pipeline. Data is readable but not signed off. | | *account-specific values* | Review states configured on your account by Prophia's operations team, used to track where a document sits in the curation workflow. An account with none configured goes straight from `incomplete` to `complete`. | | `complete` | The document's data has been **fully curated** — Prophia's team has reviewed and finished it. This is the only value that means the extracted data is final. | > [!NOTE] > > That order is the normal path, not an enforced state machine. A reviewer can move a document to any state at any point, so don't build a client that assumes it will observe each step, or that a document only ever moves forward. > > The one guarantee to lean on: **`documentStatus === "complete"` is the whole test for curated.** Account-specific status names are validated against the built-in set when they're created, so a custom state can never be named `complete` or `incomplete` — which means any value you don't recognize is a review state that hasn't reached the end yet. (If an account-specific status is later deleted, documents sitting on it fall back to `incomplete`.) `documentStatus` is typed as a plain string rather than an enum for exactly this reason — the account-specific values are open-ended. `processing` is also the only value that means "not readable yet"; everything after it is readable, and the question is just how much of it has been reviewed. If you are mirroring Prophia data, `complete` is the line to gate on. A lease rolls this up into [`docCompletionStatus`](/api-reference/v2/concepts/leases#three-status-fields-three-questions), which applies exactly that test: `complete` only when *every* document on the lease is `complete`, and `incomplete` when any one of them sits on anything else — a built-in state or an account-specific one. A building rolls the leases up in turn — see [Understanding onboarding](/api-reference/v2/concepts/onboarding). ## Uploading a document **1. Upload** [`POST /rest/v2/accounts/{accountId}/docs/upload`](/api-reference/v2/documents/create-document-via-upload) — a multipart form with `file` and `leaseId` required, plus optional `filename`, `title`, and `docCategory`. Or [`POST .../docs`](/api-reference/v2/documents/create-document-via-url) to have Prophia fetch it from a presigned https URL instead. **2. Poll** Re-fetch the lease's documents with [`GET .../leases/{leaseId}/docs`](/api-reference/v2/leases/list-document) on a modest interval — pipeline processing takes minutes, so start around 30–60 seconds — until the new document's `documentStatus` is no longer `processing`. **3. Read** Once it clears, page geometry ([`GET .../docs/{leaseDocId}/pages`](/api-reference/v2/documents/list-page)) and extracted tags ([`GET .../docs/{leaseDocId}/tags`](/api-reference/v2/documents/list-tag)) are available. Remember they are machine-extracted until the document reaches `complete`. Both create endpoints require [write access](/developers/write-access). > [!WARNING] > > Only PDFs are processed, and the test is the **filename**, not the file's contents. Both create endpoints accept any file type up to 200 MB, but a document whose name doesn't end in `.pdf` is stored and attached as-is: it settles at `documentStatus: "complete"` without a processing pass, and never has pages or tags. Name a PDF something else and it won't be read; name a non-PDF `.pdf` and ingestion will try, then fail. ## `docCategory` `docCategory` classifies the document — `Main Lease Agreement`, `Extension - Amendment`, `Rent Roll`, `Work Letter`, and so on. It is filterable on the document list endpoints and it is not cosmetic: some categories determine which tags and in-app features a document participates in. Leave it blank on create and Prophia's AI will inspect the document and set it, with human review to follow. The AI will **not** overwrite a category you set yourself, so supply one when you already know it. ## Citations A tag's provenance lives on the document. Request [`include=tagSelections,pages`](/api-reference/v2/concepts/includes) on any tag endpoint and you get the rectangles where each value was captured, plus the geometry of the pages they sit on. See [Tags and tag values](/api-reference/v2/concepts/tags-and-values#citations-tagselections-and-pages) for the coordinate system. ## Change detection for sync jobs If you mirror Prophia data into a warehouse, don't re-crawl the whole tree. [`GET /rest/v2/accounts/{accountId}/recent_changed_leases`](/api-reference/v2/accounts/list-leases-recently-updated) returns leases whose **documents** changed inside a `start_date`–`end_date` window (inclusive, `yyyy-MM-dd`) — poll it and re-fetch only what it names. It accepts the same [`include`](/api-reference/v2/concepts/includes) keys as other lease reads (`tags`, `documents`, `subleases`). Because the window is keyed on document change, this catches the events that matter most for a mirror: a new amendment landing, and a document moving to `complete` as curation finishes. # Errors and permissions Source: https://docs.prophia.com/api-reference/v2/concepts/errors-and-permissions Status codes, the error body, the capabilities model, and why 404 sometimes means 403 For how to obtain and send an API key, see [Authentication](/developers/authentication). This page covers what happens once your key is on the request. ## Status codes | Status | Meaning | | - | - | | `400` | Malformed request — an unknown `include` key, an invalid parameter value, or a body that failed validation or could not be parsed. On `POST /docs` (create via URL) it also covers a document URL that could not be fetched, including one whose content is over 200 MB. The body's `message` says what was wrong (unknown-include errors list every supported key; validation errors name each field). | | `401` | A JWT was sent where an API key was expected. This response carries only `error`. | | `403` | The key was refused: it is missing, unknown or revoked, is scoped to a different account than the path, or is valid but lacks a required capability or module or isn't allowed to see the requested building. When the block is a missing capability the message names which; other denials return a generic message rather than disclosing the reason. | | `404` | The resource isn't visible in the account named in the path — which may mean it doesn't exist, or that it exists and you can't see it (see below). Building-scoped paths (`/buildings/{buildingId}/…`) answer `403` instead when the building isn't one your key can see. | | `409` | The write conflicts with something already there. On both document-create endpoints, a non-deleted document with that filename already exists on the lease — nothing is stored. | | `413` | The file sent to `POST /docs/upload` (create via multipart upload) exceeds 200 MB. Only the multipart endpoint uses this code; the URL-based create reports the same limit as a `400`. | ## The error body Errors raised by an endpoint share one JSON body: ```json theme={null} { "error": "Entity Not Found", "errorCode": "EntityNotFound", "message": "This entity is not found in the system.", "status": "404 NOT_FOUND", "timestamp": "2026-09-02T20:41:03.118Z", "localizedMessage": "This entity is not found in the system." } ``` | Field | Use it for | | - | - | | `errorCode` | Programmatic handling. Stable identifiers such as `InvalidInput`, `AccessDenied`, `EntityNotFound`, `NonUniqueEntry`, `UploadTooLarge`. | | `error` | The same category as a readable label. | | `message` | Human-readable detail — what was wrong with the request, or which capabilities are missing. Do not match on it. | | `status` | The HTTP status as code and reason phrase, matching the response's own status line. | | `timestamp` | When the error was produced, as an ISO-8601 UTC instant. Quote it when contacting support. | | `localizedMessage` | The same text as `message`. Prefer `message`. | The body may also include `stackTrace` and `suppressed` as empty arrays; ignore them. Refusals decided before the request reaches an endpoint — a missing, unknown or revoked key, or a key scoped to a different account — return the platform's standard `403` error body instead, and the JWT `401` carries only `error`. ## The capabilities model Every operation requires a **capability** — a read or write grant on a resource type: account, property, building, building space, lease, document, tag, or custom tag. Which capabilities your key carries depends on the key: * **Account keys** carry the REST API and MCP permissions chosen when the key was created. On accounts without granular permissions, they carry the account's full grant set. * **User keys** carry their user's permissions, or the subset chosen when the key was created — two keys on the same account can see different data. Capability checks apply to the **primary resource and to each `include` key**. `GET /leases/{leaseId}` needs lease read; adding `?include=tags` additionally needs tag read. A request missing any required capability is rejected with `403` — embedded collections are never silently dropped. Each endpoint page in this reference states the capabilities that operation requires. ## Writes need the REST write module `POST`, `PATCH`, and `DELETE` require the **REST API write module** to be enabled on the account, on top of the relevant write capability. If writes fail with `403` while reads work, the module is the first thing to check. See [Write access](/developers/write-access) for how it is granted. ## When 404 means "no access" > [!NOTE] > > For resource ids inside an account your key can see, the API deliberately does not distinguish "doesn't exist" from "exists but you can't see it" — so callers can't probe for the existence of data they aren't entitled to. Practically: a `404` during integration debugging is often a **permissions problem**, not a wrong id. Check, in order: 1. Is the `{accountId}` in the path the account the resource actually belongs to? A key scoped to a different account is refused with `403` before the lookup, so a `404` means the account matched but the resource did not. 2. Does your key have the read capability for this resource type? 3. Is it a user key whose user lacks access to this building? Building-level permissions filter what you can see, and building-scoped paths report that as `403`. 4. Only then: is the id actually wrong? # Includes Source: https://docs.prophia.com/api-reference/v2/concepts/includes Embed related resources in one request with the include query parameter Most read endpoints accept an `include=` query parameter that embeds related collections in the response, saving you a round trip per relationship. Keys are comma-separated: ```bash theme={null} curl "https://api.prophia.com/rest/v2/accounts/{accountId}/leases/{leaseId}?include=tags,subleases" \ -H "X-API-Key: " ``` ## Before and after Without `include`, a property is just its own fields: ```json theme={null} { "id": "prop_123", "propertyName": "Waterfront Plaza", "customId": "WFP-01" } ``` With `?include=buildings`, the related collection is embedded: ```json theme={null} { "id": "prop_123", "propertyName": "Waterfront Plaza", "customId": "WFP-01", "buildings": [ { "id": "bld_456", "buildingName": "Waterfront Plaza — North Tower" }, { "id": "bld_789", "buildingName": "Waterfront Plaza — South Tower" } ] } ``` ## The rules * **One level deep, strictly.** There is no `include=buildings.leases` — includes never chain. Walk the tree with successive requests instead: list buildings, then request each building with `include=leases`. * **Unknown keys are rejected with `400`.** Misspell a key and the error message lists every key the endpoint supports. This is deliberately stricter than `sortBy`, which [silently falls back](/api-reference/v2/concepts/pagination-and-filtering) — don't assume the two behave alike. * **Each key can require an extra capability.** Your key needs the read capability for the *embedded* resource as well as the primary one — for example, `GET /leases/{leaseId}?include=tags` requires tag read on top of lease read. A request missing a capability is **rejected with `403`**, not silently trimmed. See [Errors and permissions](/api-reference/v2/concepts/errors-and-permissions). * **Absent ≠ empty.** Responses omit fields that are `null`, so an include you didn't request is missing from the JSON entirely. An empty array means you asked and there were none — [branch on presence](/api-reference/v2/concepts/data-model#absent-means-not-requested). ## Supported keys by endpoint | Endpoint | Keys | Extra capability per key | | - | - | - | | `GET /accounts`, `GET /accounts/{accountId}` | `properties` | property read | | `GET .../recent_changed_leases` | `tags`, `documents`, `subleases` | tag read / document read / — | | `GET .../properties`, `GET .../properties/{propertyId}` | `buildings`, `documents` | building read / document read | | `GET .../properties/{propertyId}/buildings` | `leases`, `documents`, `spaces` | lease read / document read / building-space read | | `GET .../properties/{propertyId}/leases` | `tags`, `documents`, `subleases` | tag read / document read / — | | `GET .../buildings`, `GET .../buildings/{buildingId}` | `leases`, `documents`, `spaces` | lease read / document read / building-space read | | `GET .../buildings/{buildingId}/leases` | `tags`, `documents`, `subleases` | tag read / document read / — | | `GET .../leases/{leaseId}` | `tags`, `documents`, `subleases` | tag read / document read / — | | `GET .../leases/{leaseId}/tags` | `tagSelections`, `pages`, `applicableSpaces` | — (covered by tag read) | | `GET .../docs/{leaseDocId}/tags` | `tagSelections`, `pages`, `applicableSpaces` | — (covered by tag read) | | `GET .../tags/{tagId}` | `tagSelections`, `pages`, `applicableSpaces` | — (covered by tag read) | Endpoints not in this table don't take `include`: the document list (`GET .../leases/{leaseId}/docs`), document pages, building spaces, and custom terms. ## What the keys mean * `properties` / `buildings` / `leases` — the flat child list under the resource, limited to what your key is permitted to see. * `spaces` — the building's spaces, **archived and non-archived alike**. The dedicated [spaces endpoint](/api-reference/v2/concepts/spaces-and-encumbrances#building-spaces) takes an `archived` filter; this key does not. * `documents` — on properties and buildings: documents filed at that scope (rent rolls, stacking plans). On leases: per-document page geometry keyed by `leaseDocId`, for resolving a tag's `tagSelections[].docPageId` references. * `tags` — the consolidated [DocTag list](/api-reference/v2/concepts/tags-and-values) assembled from the lease's documents, filtered by the lease's current `leaseType`. Each tag carries only the spaces it applies to; see [space-dependent and space-independent tags](/api-reference/v2/concepts/tags-and-values#space-dependent-and-space-independent-tags). * `subleases` — lightweight refs (`id`, `url`, current `leaseStatus`/`leaseSubStatus`) to the lease's subleases; an empty list means the lease has none. * `tagSelections` — the tag's citations: bounding boxes on document pages. * `pages` — per-page geometry for the cited pages. * `applicableSpaces` — the suite/floor list the tag applies to. See [Spaces and encumbrances](/api-reference/v2/concepts/spaces-and-encumbrances). > [!WARNING] > > Combining `tags` and `documents` on a heavily-tagged lease can produce a very large response. Request one at a time when you can. # Leases Source: https://docs.prophia.com/api-reference/v2/concepts/leases Lease status fields, lease type, subleases, and the spaces a lease occupies A **lease** is an agreement between a landlord and a tenant for space in a building. It is the anchor for everything Prophia extracts: its [documents](/api-reference/v2/concepts/documents) are what get processed, and its [tags](/api-reference/v2/concepts/tags-and-values) are what comes out. ## Three status fields, three questions A lease carries three status-like fields. They answer different questions — read the one that matches yours: | Field | Question it answers | Values | | - | - | - | | `leaseStatus` | Is this lease active? | One of `Active`, `Expired`, or `Not Started` — note the spacing and casing. Typed as a free-form string, so don't switch on it exhaustively. | | `leaseSubStatus` | *How* is it currently running? | `{status, startDate}` where status is `MONTH_TO_MONTH`, `YEAR_TO_YEAR`, `HOLDOVER`, `VACATED`, or `EXTENDED`. **Omitted entirely when none applies** — most leases in their original term have no sub-status. | | `docCompletionStatus` | Has Prophia finished curating this lease's data? | `complete` when every document on the lease has `documentStatus: complete` exactly, otherwise `incomplete`. [Account-specific review states](/api-reference/v2/concepts/documents#documentstatus-the-curation-lifecycle) count as not complete. | ### `leaseStatus` is derived, and the sub-status can override the dates `leaseStatus` is computed per request, not stored. Prophia resolves it in this order: 1. **Is the lease holding over?** If its sub-status is `HOLDOVER`, `MONTH_TO_MONTH`, or `YEAR_TO_YEAR`, it is **`Active`** — full stop. This check runs before any date comparison. 2. **Otherwise, is it expired?** `Expired` when `expirationDate` is in the past, or when the sub-status is `VACATED` — which forces `Expired` even on a future expiration date. 3. **Otherwise, has it commenced?** `Not Started` when `commenceDate` is absent or still in the future. 4. **Otherwise** — `Active`. `EXTENDED` is the one sub-status that doesn't steer the outcome; it annotates a lease whose status still follows its dates. > [!WARNING] > > **An `expirationDate` in the past does not mean `leaseStatus` is `Expired`.** A tenant still in place month-to-month past the end of the original term reads `leaseStatus: "Active"` with `leaseSubStatus: {status: "MONTH_TO_MONTH"}`. Read `leaseStatus`; don't recompute expiry from `expirationDate` yourself, or you will report occupied space as vacant. > [!WARNING] > > `docCompletionStatus` is about **data curation, not availability**. A lease's tags are readable long before curation finishes, but until it reads `complete` Prophia's team has not signed off on them and values can still change. Treat `incomplete` leases as provisional. See [Understanding onboarding](/api-reference/v2/concepts/onboarding) for the whole arc. ## Lease type shapes what you see `leaseType` is more than a label — it filters which extracted terms surface on the lease. Prophia keeps type-specific term sets, and a term belonging to one type only appears on leases of that type. `RETAIL` and `GSA_OFFICE` each carry tags no other type does; see the [tag reference](/api-reference/v2/concepts/tag-reference#lease-type-and-country-scoping) for which ones. > [!WARNING] > > Changing a lease's `leaseType` changes its tag list. Switch a `GSA_OFFICE` lease to `OFFICE` and its GSA-only terms stop appearing in the consolidated tags — the tags still exist on the documents, but they no longer match the lease's type. Treat a `leaseType` change as a change to the lease's data shape, not just its classification. Types: `OFFICE` (the default when omitted on create), `GSA_OFFICE`, `RETAIL`, `INDUSTRIAL`, `STORAGE`, `LICENSE_AGREEMENT`, `TELECOM_ANTENNA`, `PARKING_AGREEMENT`, `ROOFTOP_SOLAR`. ## Where a lease sits: `suiteNumber` `Lease.suiteNumber` is a **list** of space objects `{name, buildingId, scope}` — every space the lease occupies: ```json theme={null} "suiteNumber": [ { "name": "Suite 300", "buildingId": "bld_456", "scope": "SUITE" }, { "name": "Suite 400", "buildingId": "bld_456", "scope": "SUITE" } ] ``` An entry's `buildingId` can differ from the lease's own `buildingId` when the lease spans multiple buildings or has relocated. The lease's `buildingId` records where the lease was filed, not necessarily every building it now occupies. `leasedFloors` is derived from this list rather than captured: each entry is matched to a building space and contributes that space's `floor`. It inherits the same [name-matching caveats](/api-reference/v2/concepts/spaces-and-encumbrances#the-soft-link-to-buildingspace). ## Subleases A sublease is a **full lease object**, not a separate type: * `parent` is present **if and only if** the lease is a sublease — a lightweight ref (`id`, `url`, and the parent's current `leaseStatus`/`leaseSubStatus` when available). * `subleases` lists the lease's subleases as the same lightweight refs, and is only populated when you request [`include=subleases`](/api-reference/v2/concepts/includes). An empty list means the lease has none. Both are refs rather than full objects, so fetch a sublease by id to read its own terms. ## Rent classification `rentType` is the headline classification for the lease — `Gross`, `Modified Gross`, `Single Net`, `NN`, `NNN`, `Net`, `Net of E`, or `Unknown`. `opexTaxTypes` refines it per occupied space, falling back to `rentType` where no per-space mapping exists. A lease whose spaces are all on the same terms will simply repeat the headline value. ## Reading a lease's terms The lease's extracted terms arrive through [`include=tags`](/api-reference/v2/concepts/includes) or the dedicated [`GET .../leases/{leaseId}/tags`](/api-reference/v2/leases/list-tag) endpoint. Beyond the `leaseType` filtering above, one thing to know before you parse them: the list is a **consolidated** view assembled across the lease's whole document history, flattened from a per-space map. The same `tagName` can appear more than once — once per space — with different values. See [Tags and tag values](/api-reference/v2/concepts/tags-and-values#space-dependent-and-space-independent-tags). # Understanding onboarding Source: https://docs.prophia.com/api-reference/v2/concepts/onboarding How a building goes from an address to fully curated data — and how to tell where it is Prophia data is not just uploaded, it is **curated**. Between a document landing in the system and its terms being trustworthy sit an AI extraction pass and a human review by Prophia's team. Onboarding is that arc, and several status fields report where a given asset sits on it. If you are mirroring Prophia into a warehouse, this is the page that tells you what to gate on. ## The arc ```mermaid theme={null} flowchart TB subgraph you["You — in the app, or over the API"] direction TB S1["1 · Create the Property, Building, and its spaces"] S2["2 · Add a Lease per tenant"] S3["3 · Upload the documents — leases, amendments, exhibits"] S1 --> S2 --> S3 end subgraph prophia["Prophia"] direction TB S4["4 · AI extraction reads every PDF and machine-tags it"] S5["5 · The implementation team reviews and curates"] S6["6 · The building is signed off and handed over"] S4 --> S5 --> S6 end S3 --> S4 ``` Steps 1–3 are yours: they map onto the onboarding wizard in the app, and onto `POST` calls in this API if you have [write access](/developers/write-access). Steps 4–6 are Prophia's, and none of the status fields they drive are writable over REST. Onboarding is not one-and-done. Every new document added to a live building runs through the same extraction-and-curation loop, so `documentStatus` matters long after a building is handed off. ## Document status: the atom Everything else rolls up from here. A document normally goes `processing` → `incomplete` → any account-specific review states configured on your account → `complete`. Only `complete` means the data has been curated and signed off — every other value, built-in or account-specific, means it hasn't. See [Documents](/api-reference/v2/concepts/documents#documentstatus-the-curation-lifecycle) for the full table. ## Lease rollup: `docCompletionStatus` A lease's [`docCompletionStatus`](/api-reference/v2/concepts/leases#three-status-fields-three-questions) is `complete` when **every** document on it is `complete`, and `incomplete` otherwise. One un-curated amendment holds the whole lease at `incomplete` — which is the point: a lease whose latest amendment hasn't been read yet is not a lease you want to report on. ## Building rollup: `onboardingStatus` `onboardingStatus` tracks how far the building has moved through the process. It is read-only over this API. | Value | Meaning | | - | - | | `NOT_STARTED` | The building record exists. Nothing else has been done — this is the default on create, including via `POST .../buildings`. | | `INITIAL_SETUP` | Spaces and tenants have been entered. The building is set up but its documents haven't been supplied. | | `ONBOARDING_IN_PROGRESS` | Setup is finished and handed to Prophia. Documents are uploaded and moving through extraction and curation. | | `ONBOARDING_COMPLETE` | Every lease in the building has all of its documents `complete`. | > [!WARNING] > > **The middle two values are driven by the app, not the API.** `INITIAL_SETUP` and `ONBOARDING_IN_PROGRESS` advance when someone works through the onboarding wizard in the UI and marks those steps done. Nothing in the REST API advances them, so a building created and populated purely over the API will likely sit at `NOT_STARTED` until it flips straight to `ONBOARDING_COMPLETE` — and even that path isn't guaranteed. Don't read `NOT_STARTED` on an API-driven building as "nothing here yet." > [!NOTE] > > `ONBOARDING_COMPLETE` is set **automatically**, and it happens **once in a building's lifetime**. The system flips it when the building is not `INACTIVE` *and* every lease in it has finished document curation; the moment it flips, `onboardingCompleteDate` is stamped. It does not revert when a new document is later uploaded — a live building taking on a new amendment stays `ONBOARDING_COMPLETE` while that amendment works through the pipeline. That last point matters: **`onboardingStatus` answers "did this building ever finish its initial onboarding?", not "is this building's data current right now."** For the current question, roll up `docCompletionStatus` across the building's leases yourself. The progression is also one-way in practice — a building at `ONBOARDING_IN_PROGRESS` or `ONBOARDING_COMPLETE` cannot be pushed back to `INITIAL_SETUP` or `NOT_STARTED`. ## Building state: `activeStatus` `activeStatus` is a different axis: it tracks the building record's operational state and the internal review checkpoints, and it is also read-only over this API. | Value | Shown in-app as | Meaning | | - | - | - | | `WORK_IN_PROGRESS` | In Progress | The default on create. Data is being assembled. | | `AI_PROCESSED` | AI Processed | Set automatically once every document *currently* in the building has cleared the AI pipeline. Machine-tagged, awaiting human review. | | `QA_PENDING` | QA Pending | Prophia's internal quality-assurance checkpoint. | | `ACTIVE` | Active | **Prophia has fully curated the building's data and handed it off.** This is the signal that the asset is ready for you to rely on. | | `INACTIVE` | Inactive | The building will receive no further updates — a purely historical record. | > [!WARNING] > > `AI_PROCESSED` means "everything uploaded so far has been machine-tagged", not "everything has been uploaded." Documents are frequently supplied across several sessions, so a building can reach `AI_PROCESSED` while more of its documents are still to come. Treat it as a checkpoint on the building's current contents rather than a statement that the building is fully loaded. > [!NOTE] > > `activeStatus: ACTIVE` is the handoff signal, and it is the one worth building on. `onboardingStatus: ONBOARDING_COMPLETE` says the documents finished processing; `ACTIVE` says Prophia's team is satisfied with the result and has released the asset. `implementationManager` on the building names the Prophia person responsible for that onboarding and review. It is not the account manager. ## What lags during onboarding Some building fields are caches rebuilt from the stacking plan rather than computed per request, so they can be stale while leases are still in onboarding: `totalRentableSquareFootage`, `squareFootageLeased`, `squareFootageOccupied`, `squareFootageVacant` Each carries this caveat on the [building endpoint](/api-reference/v2/buildings/get-building). Don't use them for point-in-time occupancy on a building that isn't `ACTIVE` yet. ## Putting it together for a sync job **1. Decide your bar** For reporting you can stand behind, filter buildings on `activeStatus=ACTIVE` and leases on `docCompletionStatus: "complete"`. For a fuller mirror, take everything and carry the status fields through so downstream consumers can decide. **2. Poll for change, don't re-crawl** [`GET .../recent_changed_leases`](/api-reference/v2/accounts/list-leases-recently-updated) names the leases whose documents changed in a window. That covers both new documents arriving and existing ones reaching `complete`. **3. Re-check status on every pass** A lease that was `incomplete` last week may be `complete` now, with different tag values. Curation corrects machine extraction — treat pre-`complete` values as provisional and expect them to change. # Pagination, sorting, and filtering Source: https://docs.prophia.com/api-reference/v2/concepts/pagination-and-filtering The list envelope, paging rules, sort fields, and per-resource filters Every list endpoint shares one response envelope and one set of paging parameters. ## The envelope ```json theme={null} { "totalCount": 137, "page": 0, "limit": 50, "hasMore": true, "items": [ { "...": "..." } ] } ``` ## Paging * `page` is **0-based**. Negative values are clamped to `0`. * `limit` defaults to `50` and is **clamped to 1–200** — out-of-range values are adjusted, not rejected. If you ask for `limit=500` you get 200 items back; check `limit` in the response for the value that was actually applied. * Iterate on `hasMore` rather than computing page counts from `totalCount` — data can change between calls. ```bash theme={null} page=0 while : ; do resp=$(curl -s "https://api.prophia.com/rest/v2/accounts/{accountId}/properties?page=$page&limit=100" \ -H "X-API-Key: ") echo "$resp" | jq -r '.items[].propertyName' [ "$(echo "$resp" | jq '.hasMore')" = "true" ] || break page=$((page + 1)) done ``` ## Sorting Pass `sortBy` and `sortDir` (`asc` — the default — or `desc`; anything else is treated as `asc`). > [!WARNING] > > An unrecognized `sortBy` value **silently falls back to the endpoint's default sort** — it does not return an error. If your sort seems ignored, check the field name against the table below. (Contrast with [`include`](/api-reference/v2/concepts/includes), where unknown keys are rejected with `400`.) | Resource | Sort fields | Default | | - | - | - | | Accounts | `accountName`, `apiName`, `accountStatus`, `accountManager`, `createdAt` | `accountName` | | Properties | `propertyName`, `customId`, `createdAt` | `propertyName` | | Buildings | `buildingName`, `address.city`, `address.state`, `activeStatus`, `customId`, `buildingType`, `implementationManager`, `createdAt` | `buildingName` | | Building spaces | `floor`, `suiteNumber`, `type`, `squareFootage`, `createdAt`, `updatedAt` | `suiteNumber` | | Leases | `dba`, `tenantLegalName`, `expirationDate`, `commenceDate`, `leaseStatus`, `leaseType`, `suiteNumber`, `squareFootage`, `customId` | `dba` | | Recently changed leases | `dba` — the only accepted value | `dba` | | Lease documents | `filename`, `createdAt`, `numberOfPages`, `docCategory` | `filename` | | Document pages | `pageSequence` | `pageSequence` | | Tags | `tagName`, `createdAt`, `updatedAt` | `tagName` | | Custom terms | `tagName`, `definitionType`, `createdAt`, `updatedAt` | `tagName` | | Custom fields | `fieldName`, `fieldType`, `active`, `createdAt`, `updatedAt` | `fieldName` | ## Filtering List endpoints take resource-specific filter parameters as plain query params, combinable with paging and sorting. Multiple filters combine with AND. Filters that take a fixed set of values list the accepted values on each endpoint's page. | Resource | Filter | Matching | | - | - | - | | **Accounts** | `accountName`, `apiName`, `accountManager` | Case-insensitive *contains* | | | `accountStatus` | Case-insensitive exact; an unrecognized value returns no results | | **Properties** | `propertyName` | Case-insensitive *contains* | | | `customId` | Exact | | **Buildings** | `name`, `implementationManager` | Case-insensitive *contains* | | | `city`, `state` | Case-insensitive exact | | | `activeStatus` | Case-insensitive exact; an unrecognized value returns no results | | | `buildingType` | Case-insensitive exact; an unrecognized value returns `400` | | | `customId` | Exact | | **Building spaces** | `type`, `floor`, `suiteNumber` | Exact | | | `rentable`, `occupied`, `archived` | Boolean; omit the param for both | | **Leases** | `tenantName` | Case-insensitive *contains*, against both `dba` and `tenantLegalName` | | | `suiteNumber` | Case-insensitive *contains*, against the lease's suite names | | | `status`, `customId` | Exact | | | `leaseType` | Case-insensitive; an unrecognized value returns no results | | | `expirationBefore`, `expirationAfter`, `commenceBefore`, `commenceAfter` | Calendar date `yyyy-MM-dd`, inclusive | | | `squareFootageMin`, `squareFootageMax` | Inclusive bounds | | **Recently changed leases** | `start_date`, `end_date` | **Required.** Calendar date `yyyy-MM-dd`, inclusive; the window in which the leases' documents changed | | **Lease documents** | `filename` | Case-insensitive *contains* | | | `docCategory` | Case-insensitive exact | | | `numberOfPagesMin`, `numberOfPagesMax` | Inclusive bounds | | **Tags** | `tagName` | Case-insensitive *contains* | | | `lifecycleStatus` | Exact — `ACTIVE`, `VOID`, `STRICKEN`, `EXERCISED` | | | `isCustomTag` | Boolean; omit the param for both | | | `createdAtAfter`, `createdAtBefore` | Timestamp, inclusive | | **Custom terms** | `tagName` | Case-insensitive *contains* | | | `definitionType` | Case-insensitive exact — `CURRENCY`, `TEXT`, `MULTI`, `DATE`; an unrecognized value returns no results | | | `aiEnabled` | Boolean; omit the param for both | | **Custom fields** | `fieldName` | Case-insensitive *contains* | | | `fieldType` | Case-insensitive exact — `SINGLE`, `MULTI`; an unrecognized value returns no results | | | `active` | Boolean; omit the param for both | Two date conventions are in play: lease filters take **calendar dates** (`yyyy-MM-dd`), while the tag `createdAt` filters take **timestamps**. Each endpoint page states the exact type per parameter. # Spaces, applicable spaces, and encumbrances Source: https://docs.prophia.com/api-reference/v2/concepts/spaces-and-encumbrances Building spaces, how tags reference space by name, and which spaces are burdened by options ## Building spaces A **BuildingSpace** is a discrete unit inside a building — a suite, a floor, a storage unit, a lobby — with `suiteNumber` (its canonical name), `floor`, `squareFootage`, `type`, `rentable`, `occupied`, and `archived`. List them with [`GET /rest/v2/accounts/{accountId}/buildings/{buildingId}/spaces`](/api-reference/v2/building-spaces/list-building-space). Four fields deserve care: * **`suiteNumber` is the join key.** It is what tag `applicableSpaces` entries match against, by string. Renaming a space breaks those associations. * **`floor` is not just a label.** It groups spaces in the stacking plan *and* determines which spaces pick up floor-level encumbrances: a right whose space entry names `Floor 1` reaches every space with that floor. * **`rentable` marks leasable inventory.** Non-rentable spaces — lobbies, elevators, shared areas — drop out of reports like the rent roll, along with any leases still associated with them. * **`occupied` is an override, not a derived truth.** It marks a space as occupied even without an active lease attached. `occupied: false` does *not* mean the space is vacant — only that the override isn't set. Derive real occupancy from leases. ### Archived spaces Archiving is for a space that historical leases still reference but that no longer represents a discrete unit in the building. Archived spaces are **not** removed from results by default — how you exclude them depends on how you're reading: | Reading via | Archived spaces | How to exclude them | | - | - | - | | [`GET .../buildings/{buildingId}/spaces`](/api-reference/v2/building-spaces/list-building-space) | Returned alongside current ones | Pass `archived=false`. Pass `archived=true` for only archived spaces; omit the parameter for both | | [`include=spaces`](/api-reference/v2/concepts/includes) on a building | Returned alongside current ones | No filter — the key takes no parameters. Filter client-side on the `archived` field, or use the spaces endpoint instead | `DELETE` on a building space is a hard delete, not an archive. ## `applicableSpaces`: where a tag applies Request a tag with `include=applicableSpaces` and you get the list of spaces the term applies to, as space objects: ```json theme={null} { "name": "Suite 300", "buildingId": "bld_456", "scope": "SUITE" } ``` `scope` sets how to read `name`: | `scope` | `name` | Meaning | | - | - | - | | `SUITE` | The suite name, e.g. `"Suite 300"` | One specific suite | | `FLOOR` | A floor designation, e.g. `"Floor 4"` | An entire floor | | `BUILDING` | Always the sentinel `"Entire building"` | The whole building | | `ALL` | Always the sentinel `"All spaces"` | Everything the lease covers | The same space-object shape appears on `Lease.suiteNumber` (the spaces a lease occupies) and inside complex-tag components (`expansionSpace`, `refusalSpace`, `offeredSpace`, `contractionSpace`, `encumberedSpaces`). ## The soft link to BuildingSpace > [!WARNING] > > `applicableSpaces[].name` connects to a `BuildingSpace` **by name, not by id**. There is no foreign key and no referential integrity: renaming a suite in either place silently breaks the association. To join tags to your space inventory: 1. Restrict candidates to the entry's `buildingId` — suite names are only meaningful within a building, and an entry's building can differ from the lease's primary building. 2. Handle scopes first: `BUILDING` and `ALL` sentinels never match a real space record — expand them to the building's (or lease's) space list yourself. `FLOOR` entries name a floor, not a suite — match against `BuildingSpace.floor`. 3. For `SUITE` entries, match `name` against `BuildingSpace.suiteNumber` exactly; fall back to a whitespace/case-insensitive comparison for resilience, and treat non-matches as data to reconcile rather than discard. ## Encumbrances: rights that burden space An **encumbrance** is an option-type term that gives someone a claim on space — the space isn't just leased, it's *promised*. In the API these are `COMPLEX` tags whose components are the option kinds: `RENEWAL`, `AUTO_RENEWAL`, `EARLY_TERMINATION`, `CONTRACTION`, `EXPANSION`, `RIGHT_OF_FIRST_REFUSAL`, and `RIGHT_OF_FIRST_OFFER`. Two different questions arise, and they are answered by different fields. ### Which of the lease's own spaces does the right cover? Read the tag's **`applicableSpaces`**. Most option terms are [space-dependent](/api-reference/v2/concepts/tags-and-values#space-dependent-and-space-independent-tags), which means they are captured and consolidated per space — a right does **not** automatically extend to everything the lease occupies. > [!WARNING] > > A renewal option on a lease covering Suites 100, 200, and 300 with `applicableSpaces: [{ "name": "Suite 100" }]` encumbers **Suite 100 only**. Suites 200 and 300 carry no renewal. Do not fall back to `Lease.suiteNumber` for a space-dependent right — that would over-report the encumbrance across the whole premises. `renewal`, `autoRenewal`, `earlyTermination`, `contractionRight`, `fee`, and `tenantImprovement` are all space-dependent, so the same lease can carry several instances of one of them — one per space, with different terms. A handful of option terms are space-independent — `expansion`, `rightOfFirstOffer`, `rightOfFirstRefusal`, `purchaseOffer`, and the various clause-style rights (`sublet`, `permittedUse`, `signage`…). These are consolidated once for the lease rather than per space. The [tag reference](/api-reference/v2/concepts/tag-reference) marks which is which. ### Which space does the right reach for? Some rights point at space the tenant does not currently occupy. That space is named on the component, not on `applicableSpaces`: | Component kind | Field | Names | | - | - | - | | `EXPANSION` | `expansionSpace` | Space the tenant may take | | `CONTRACTION` | `contractionSpace` | Space the tenant may give back | | `RIGHT_OF_FIRST_OFFER` | `offeredSpace` | Space that must be offered first | | `RIGHT_OF_FIRST_REFUSAL` | `refusalSpace` | Space the tenant may match an offer on | | `FEE`, `OTHER` | `encumberedSpaces` | Space the provision burdens | This is why `expansion` and the two first-rights are space-independent as *terms*: what matters isn't which of the tenant's suites the clause sits under, it's the target space named in the component. That target can be a suite in a different lease's premises, or space that is currently vacant. `RENEWAL`, `AUTO_RENEWAL`, `EARLY_TERMINATION`, and `PURCHASE` components carry no space field at all — they act on the space the tag already applies to. ### Is the right still live? Whether a right still burdens space depends on its [`rightStatus`](/api-reference/v2/concepts/tags-and-values#two-status-enums-that-are-not-the-same-thing): `AVAILABLE` encumbers; `EXERCISED`, `DECLINED`, `EXPIRED`, and `STRICKEN` generally do not (an exercised expansion has become plain occupancy). `UNKNOWN` means no status could be resolved — decide how you want to treat it, and be consistent. ## Recipe: which of my spaces are encumbered? For a building: 1. `GET .../buildings/{buildingId}/spaces?archived=false` — your current space inventory. 2. `GET .../buildings/{buildingId}/leases` — the leases in play. 3. For each lease, `GET .../leases/{leaseId}/tags?include=applicableSpaces` and keep `category: COMPLEX` tags whose component `rightStatus` is `AVAILABLE`. 4. Join each kept tag to the inventory: * If the component names a target space (`expansionSpace`, `offeredSpace`, `refusalSpace`, `contractionSpace`, `encumberedSpaces`), that is the encumbered space. * Otherwise, use the tag's own `applicableSpaces` — and only those entries. Don't widen to the lease's full `suiteNumber` list. 5. Resolve each entry against the inventory using the scope and name-matching rules above. The result is a map of space → outstanding rights: the API-side equivalent of an encumbrance overlay on a stacking plan. # Tag reference Source: https://docs.prophia.com/api-reference/v2/concepts/tag-reference Every term Prophia captures: tag names, the units they carry, space-dependence, and customValue meanings This page lists all **218 tag definitions** Prophia can capture, grouped by subject. Use it to find the `tagName` behind a term you're looking for, and to know what shape its value will arrive in before you parse it. For how tags are structured in a response — categories, typed values, schedules, options — read [Tags and tag values](/api-reference/v2/concepts/tags-and-values) first. ## Reading the tables Every column below is something you can read off a tag in a response — there are no internal registry values here. | Column | The response field it describes | | - | - | | **`tagName`** | The tag's `tagName`. A dotted name (`renewal.rentalRate`) belongs to the option named before the dot. | | **`displayName`** | The tag's `displayName` — its label in the Prophia app. | | **`category`** | The tag's `category`: `STANDARD`, `COMPLEX`, or `EXHIBIT`. | | **Where the data lands** | Which field on the response actually carries the substance — see below. | | **`value.unit` can be** | The values you can expect in `value.unit`, which is also what decides `value.kind`. *(fixed)* means the term only ever carries that one unit; a dash means no unit applies. | | **Per-space** | `Yes` when the same `tagName` can appear more than once in a lease's tag list, once per space. See [space-dependent and space-independent tags](/api-reference/v2/concepts/tags-and-values#space-dependent-and-space-independent-tags). | | **Scope** | Restrictions on which leases or documents the term can appear on at all. | ### Where the data lands Reading `value` is right for most terms, but not all — for an option it is nearly empty, and for a schedule-bearing term it's a summary. This column tells you where to actually look: | Entry | What to read | | - | - | | `value` | The tag's own `value`. Branch on `value.kind`. | | `tagComponents` · `RENEWAL` | A `COMPLEX` tag: the substance is in `tagComponents`, and its `kind` is the one named. | | `schedule` · `RENT` | The term carries a series in `schedule`, of the `kind` named. The schedule is authoritative; `value` is a summary. | | `customValue`s | Part of the term lives in the positional `customValue1`–`customValue6` fields. | | → `renewal` component | A component tag, normally **folded into its parent option** — you read it as a field on that option's `tagComponents` rather than as a tag of its own. | > [!NOTE] > > Component tags are listed here because you can still meet them as standalone tags: fetch a document's tags with [`GET .../docs/{leaseDocId}/tags`](/api-reference/v2/documents/list-tag) and they come back individually, each with its own `category: STANDARD` and its own `value` resolved from the unit it was captured with. It is the consolidated lease view that folds them away. ## Units are the signal, not a separate data type Internally a tag carries both a data type and a unit, but **the data type is not exposed anywhere in this API** — and it isn't a stable property of the term either. The two are set together per tag instance: picking a unit also sets the data type that goes with it, so a term whose definition defaults to text can be stored as a number the moment someone gives it a numeric unit. There is no useful "this term is always a number" fact to publish, and this reference doesn't try to. What you observe instead is [`value.kind`](/api-reference/v2/concepts/tags-and-values#typed-values-branch-on-kind) and `value.unit`. The unit is what decides the kind, by this rule, in order: | If the value was captured… | `value.kind` | | - | - | | as free text | `text` | | with unit `range of dates` | `dateRange` | | with any other `range of …` unit | `numberRange` | | as a date | `date` | | with a unit starting with `$` | `money` | | with a unit starting with `%` | `percent` | | as any other number | `number` | | otherwise | `text` | Three consequences worth designing around: * **A `$` unit is the whole test for money, and a `%` unit the whole test for percent.** Both are prefix tests over the unit string, so `$psf (annual)` is money and `% of FMV` is percent without any per-tag special-casing. Branch on `kind`, never on the tag name. * **A percentage arrives as the percentage itself**, not a fraction: `3.5` with unit `%` means three and a half percent. Terms that accept both percent and dollar units — `rentBump`, `opexCap` — return `percent` or `money` according to what was captured, so the same `tagName` can hand you either. * **Free text wins over the unit.** A term stored as text returns `kind: text` even when it carries a percent or dollar unit. Rental rates are the common case: a rate the lease words rather than states — most often fair market value — comes back as `text` with `unit: "FMV"` rather than as a money value with nothing in it, and a rate the lease leaves to the landlord comes back as `text` with `unit: "landlordDefined"`. The general parsing rule sits underneath all of this: the typed field is populated by parsing `capturedData`, so it is omitted whenever the captured text couldn't be parsed into that kind — a range that wouldn't split leaves `start` and `end` unset, a relative date leaves `value` unset. `capturedData` always holds the raw text and is the fallback. ## Lease type and country scoping By default a term is available on any lease. Some are restricted, and a restricted term simply never appears on a lease that doesn't qualify: | Scope | Terms | | - | - | | `RETAIL` leases only | Cotenancy (opening and operating), sales statements and kickouts, tenant radius restriction, no-build area, landlord parking space restrictions | | `GSA_OFFICE` leases only | `tiRent`, `bsacRent`, their interest rates, `fixedParking`, `cpiIncreaseAmount`, `cpiIncreaseDate` | | Buildings in `IE` / `UK` only | `rentReviewDate`, `typeOfRevisedRent` | | Rent roll documents | The `rentRollTenant` family | The tables below flag each of these in the **Scope** column. This is the mechanism behind the warning on [lease type](/api-reference/v2/concepts/leases#lease-type-shapes-what-you-see): change a lease's type and its type-scoped terms stop surfacing. ## `customValue` meanings Sixteen terms carry data in `customValue1`–`customValue6`. The positional table lives on [Tags and tag values](/api-reference/v2/concepts/tags-and-values#customvalue1-customvalue6-positional-and-tag-dependent); the tables below flag them in the **Where the data lands** column. The recurring pattern is a **`:-:`-delimited parallel array** — several `customValue` fields each holding one token per period, read together by index. The abatement terms are the clearest case: ```json theme={null} { "tagName": "rentAbatement", "capturedData": "3", "customValue1": "months:-:months", "customValue2": "$:-:%", "customValue3": "1-3:-:6", "customValue4": "10000:-:50", "customValue5": "0:-:0" } ``` Two periods: months 1–3 abated at \$10,000/month, and month 6 abated at 50%. Split each field on `:-:` and zip by index. ## Terms that break the pattern A few terms don't behave the way the tables would lead you to expect: * **`autoRenewal`** groups its component tags by **space** rather than by the `referenceId` every other option chain uses. Its components also remain visible as space-keyed tags in their own right instead of being folded away entirely. If you are walking option chains by `referenceId`, auto-renewal won't be found that way — read the `AUTO_RENEWAL` component off the tag and its per-space effects from `Lease.autoRenewalExpirationDates`. * **`rentSchedule`, `percentageRent`, and the abatement terms** carry their real payload in a [`schedule`](/api-reference/v2/concepts/tags-and-values#schedules) or in `customValue` arrays, not in `value`. The tag's own `value` is close to meaningless for them. * **`suiteNumber`** is a term as well as a field name. As a tag its `capturedData` is a comma-separated list of the spaces named in the document — distinct from `Lease.suiteNumber`, which is the resolved list of space objects the lease occupies. * **`opexCap`** and `percentageRent` both use `customValue1` as a mode switch that changes how the remaining `customValue` fields should be read. Check it first. * **`securityDeposit` and `letterOfCreditAmount`** hold the *original* amount in `value` and the amount after the refund schedule in `customValue1`. Whichever you report, be consistent. ## All tag definitions ### Basic Lease Information | `tagName` | `displayName` | `category` | Where the data lands | `value.unit` can be | Per‑space | Scope | | - | - | - | - | - | - | - | | `squareFootage` | Square Footage | STANDARD | `value` | `SF` *(fixed)* | Yes | | | `suiteNumber` | Suite Number | STANDARD | `value` + `customValue`s | — | Yes | | | `tenantLegalName` | Tenant Legal Name | STANDARD | `value` | — | No | | ### Lease Term | `tagName` | `displayName` | `category` | Where the data lands | `value.unit` can be | Per‑space | Scope | | - | - | - | - | - | - | - | | `autoRenewal` | Auto Renewal | COMPLEX | `tagComponents` · `AUTO_RENEWAL` | — | Yes | | | `autoRenewal.advancedNoticePeriod` | Auto Renewal: Advanced Notice Period | STANDARD | → `autoRenewal` component | `days`, `business days`, `weeks`, `months`, `years`, `date`, `range of days`, `range of business days`, `range of weeks`, `range of months`, `range of dates` | Yes | | | `autoRenewal.autoRenewalPeriods` | Auto Renewal: Number of Additional Periods | STANDARD | → `autoRenewal` component | `period` *(fixed)* | Yes | | | `autoRenewal.autoRenewalRate` | Auto Renewal: Renewal Rate | STANDARD | → `autoRenewal` component | `$ per month`, `$ per year` | Yes | | | `autoRenewal.autoRenewalRateBump` | Auto Renewal: Renewal Rate Bump | STANDARD | → `autoRenewal` component | `% per year`, `% per month`, `$ per month`, `$ per year` | Yes | | | `autoRenewal.autoRenewalTerm` | Auto Renewal: Renewal Term | STANDARD | → `autoRenewal` component | `years`, `months` | Yes | | | `autoRenewal.rightIsStricken` | Right is Stricken | STANDARD | → `autoRenewal` component | — | Yes | | | `commenceDate` | Commencement Date | STANDARD | `value` | `date` *(fixed)* | Yes | | | `effectiveDate` | Effective Date | STANDARD | `value` | `date` *(fixed)* | No | | | `expectedCommenceDate` | Expected Commencement Date | STANDARD | `value` | `date` *(fixed)* | Yes | | | `expirationDate` | Expiration Date | STANDARD | `value` | `date` *(fixed)* | Yes | | | `floatingCommenceDate` | Floating Commencement Date | STANDARD | `value` | — | Yes | | | `holdover` | Holdover | STANDARD | `value` | — | No | | | `mtm` | Month-To-Month | STANDARD | `value` | — | Yes | | | `rentCommenceDate` | Rent Commencement Date | STANDARD | `value` | `date` *(fixed)* | Yes | | | `supportPartialDays` | Support partial days in month | STANDARD | `value` | — | Yes | | | `term` | Term | STANDARD | `value` | `months`, `years` | Yes | | | `yty` | Year-To-Year | STANDARD | `value` | — | Yes | | ### Rent | `tagName` | `displayName` | `category` | Where the data lands | `value.unit` can be | Per‑space | Scope | | - | - | - | - | - | - | - | | `annualRent` | Annual Rent | STANDARD | `value` | `$`, `$psf` | Yes | | | `deferredRent` | Deferred Rent | STANDARD | `value` | `$`, `$ per month` | Yes | | | `deferredRentPeriod` | Deferred Rent Period | STANDARD | `value` | `range of dates`, `range of months` | Yes | | | `grossUp` | Gross Up | STANDARD | `value` | `%` *(fixed)* | No | | | `monthlyRent` | Monthly Rent | STANDARD | `value` | `$`, `$psf` | Yes | | | `opexPrs` | Opex Pro-Rata Share | STANDARD | `value` | `%` *(fixed)* | Yes | | | `percentageRent` | Percentage Rent | STANDARD | `value` + `schedule` · `PERCENT_RENT` + `customValue`s | — | Yes | | | `prepaidRent` | Prepaid Rent | STANDARD | `value` + `customValue`s | `$` *(fixed)* | Yes | | | `prepaidRentPeriod` | Prepaid Rent Period | STANDARD | `value` | `range of dates`, `range of months` | Yes | | | `prs` | Pro-Rata Share | STANDARD | `value` | `%` *(fixed)* | Yes | | | `rentBump` | Rent Bump | STANDARD | `value` | `% increase (annual)`, `$ increase (annual)`, `% increase $psf (annual)`, `$psf increase (annual)` | Yes | | | `rentBumpIndex` | Rent Bump : Index | STANDARD | `value` | — | Yes | | | `rentReviewDate` | Rent Review | STANDARD | `value` | `date` *(fixed)* | Yes | `IE` / `UK` only | | `rentSchedule` | Rent Schedule | STANDARD | `value` + `schedule` · `RENT` + `customValue`s | — | Yes | | | `taxPrs` | Tax Pro-Rata Share | STANDARD | `value` | `%` *(fixed)* | Yes | | | `typeOfRevisedRent` | Type of Revised Rent | STANDARD | `value` | — | Yes | `IE` / `UK` only | ### Abatements & Concessions | `tagName` | `displayName` | `category` | Where the data lands | `value.unit` can be | Per‑space | Scope | | - | - | - | - | - | - | - | | `concessionOther` | Other Concession | STANDARD | `value` | `$`, `$psf` | Yes | | | `opexAbatement` | Opex Abatement | STANDARD | `value` + `schedule` · `ABATEMENT` + `customValue`s | — | Yes | | | `rentAbatement` | Rent Abatement | STANDARD | `value` + `schedule` · `ABATEMENT` + `customValue`s | — | Yes | | | `taxAbatement` | Tax Abatement | STANDARD | `value` + `schedule` · `ABATEMENT` + `customValue`s | — | Yes | | | `tenantImprovement` | Tenant Improvement | COMPLEX | `tagComponents` · `TENANT_IMPROVEMENT` | — | Yes | | | `tenantImprovement.rightIsStricken` | Right is Stricken | STANDARD | → `tenantImprovement` component | — | Yes | | | `tenantImprovement.tiAmount` | TI Amount | STANDARD | → `tenantImprovement` component | `$`, `$psf`, `$ per year` | Yes | | | `tenantImprovement.tiRemaining` | Tenant Improvement Remaining | STANDARD | → `tenantImprovement` component | — | Yes | | | `tenantImprovement.tiSunset` | TI Sunset | STANDARD | → `tenantImprovement` component | `months`, `years`, `days`, `date` | Yes | | ### Operating Expenses & Taxes | `tagName` | `displayName` | `category` | Where the data lands | `value.unit` can be | Per‑space | Scope | | - | - | - | - | - | - | - | | `baseOperatingYear` | Base Operating Year | STANDARD | `value` | `year`, `range of dates` | Yes | | | `baseOpexYearStop` | Base Opex Year Stop | STANDARD | `value` | `$`, `$psf` | Yes | | | `baseTaxYear` | Base Tax Year | STANDARD | `value` | `year`, `range of dates` | Yes | | | `baseTaxYearStop` | Base Tax Year Stop | STANDARD | `value` | `$`, `$psf` | Yes | | | `baseYear` | Base Year | STANDARD | `value` | `year`, `range of dates` | Yes | | | `baseYearStop` | Base Year Stop | STANDARD | `value` | `$`, `$psf` | Yes | | | `expenseAccounts` | Expanse Accounts | STANDARD | `value` | — | No | | | `opexCap` | Opex Cap | STANDARD | `value` + `customValue`s | `$ per year`, `% per year`, `$psf (annual)` | Yes | | | `taxAccounts` | Tax Accounts | STANDARD | `value` | — | No | | ### Security & Financial | `tagName` | `displayName` | `category` | Where the data lands | `value.unit` can be | Per‑space | Scope | | - | - | - | - | - | - | - | | `guarantor` | Guarantor | STANDARD | `value` | — | No | | | `lateFee` | Late Fee | STANDARD | `value` | — | No | | | `letterOfCreditAmount` | Letter of Credit: Amount | STANDARD | `value` + `customValue`s | `$` *(fixed)* | Yes | | | `letterOfCreditInstitution` | Letter of Credit: Institution | STANDARD | `value` | — | No | | | `letterOfCreditRefundSchedule` | Letter of Credit Refund Schedule | STANDARD | `value` + `schedule` · `REFUND` + `customValue`s | — | Yes | | | `purchaseOffer` | Purchase Offer | COMPLEX | `tagComponents` · `PURCHASE` | — | No | | | `purchaseOffer.rightIsDeclined` | Right is Declined | STANDARD | → `purchaseOffer` component | — | No | | | `purchaseOffer.rightIsExercised` | Right is Exercised | STANDARD | → `purchaseOffer` component | — | No | | | `purchaseOffer.rightIsStricken` | Right is Stricken | STANDARD | → `purchaseOffer` component | — | No | | | `securityDeposit` | Security Deposit | STANDARD | `value` + `customValue`s | `$` *(fixed)* | Yes | | | `securityDepositRefundSchedule` | Security Deposit Refund Schedule | STANDARD | `value` + `schedule` · `REFUND` + `customValue`s | — | Yes | | ### Fees | `tagName` | `displayName` | `category` | Where the data lands | `value.unit` can be | Per‑space | Scope | | - | - | - | - | - | - | - | | `fee` | Fee | COMPLEX | `tagComponents` · `FEE` + `schedule` · `FEE` + `customValue`s | — | Yes | | | `fee.feeAmount` | Fee Amount | STANDARD | → `fee` component | `$ per month`, `$ per year`, `$`, `$psf`, `$psf (annual)`, `$psf (monthly)` | Yes | | | `fee.feeIncrease` | Fee Increase | STANDARD | → `fee` component | `$`, `%` | Yes | | | `fee.feeTerm` | Fee Term | STANDARD | → `fee` component | `range of months`, `range of dates` | Yes | | | `fee.rightIsStricken` | Right is Stricken | STANDARD | → `fee` component | — | Yes | | ### Rights & Options | `tagName` | `displayName` | `category` | Where the data lands | `value.unit` can be | Per‑space | Scope | | - | - | - | - | - | - | - | | `contractionRight` | Contraction Right | COMPLEX | `tagComponents` · `CONTRACTION` | — | Yes | | | `contractionRight.advancedNoticePeriod` | Contraction Right | STANDARD | → `contractionRight` component | `days`, `business days`, `weeks`, `months`, `years`, `date`, `range of days`, `range of business days`, `range of weeks`, `range of months`, `range of dates` | Yes | | | `contractionRight.contractionFee` | Contraction Fee | STANDARD | → `contractionRight` component | `$` *(fixed)* | Yes | | | `contractionRight.contractionSpace` | Contraction Space | STANDARD | → `contractionRight` component | — | Yes | | | `contractionRight.earlyMoveOutDate` | Early Move Out Date | STANDARD | → `contractionRight` component | `date`, `months`, `days`, `business days`, `weeks`, `years` | Yes | | | `contractionRight.effectiveDateOfRight` | Effective Date of Right | STANDARD | → `contractionRight` component | `date`, `range of dates`, `months`, `days`, `business days`, `weeks`, `years` | Yes | | | `contractionRight.optionEndDate` | Move Out Date | STANDARD | → `contractionRight` component | `date`, `months`, `days`, `business days`, `weeks`, `years`, `range of dates` | Yes | | | `contractionRight.rightIsDeclined` | Right is Declined | STANDARD | → `contractionRight` component | — | Yes | | | `contractionRight.rightIsExercised` | Right is Exercised | STANDARD | → `contractionRight` component | — | Yes | | | `contractionRight.rightIsStricken` | Right is Stricken | STANDARD | → `contractionRight` component | — | Yes | | | `earlyTermination` | Early Termination | COMPLEX | `tagComponents` · `EARLY_TERMINATION` | — | Yes | | | `earlyTermination.advancedNoticePeriod` | Advanced Notice Periods | STANDARD | → `earlyTermination` component | `days`, `business days`, `weeks`, `months`, `years`, `date`, `range of days`, `range of business days`, `range of weeks`, `range of months`, `range of dates` | Yes | | | `earlyTermination.earlyMoveOutDate` | Early Move Out Date | STANDARD | → `earlyTermination` component | `date`, `months`, `days`, `business days`, `weeks`, `years` | Yes | | | `earlyTermination.effectiveDateOfRight` | Effective Date of Right | STANDARD | → `earlyTermination` component | `date`, `range of dates`, `months`, `days`, `business days`, `weeks`, `years` | Yes | | | `earlyTermination.optionEndDate` | Move Out Date | STANDARD | → `earlyTermination` component | `date`, `months`, `days`, `business days`, `weeks`, `years`, `range of dates` | Yes | | | `earlyTermination.rightIsDeclined` | Right is Declined | STANDARD | → `earlyTermination` component | — | Yes | | | `earlyTermination.rightIsExercised` | Right is Exercised | STANDARD | → `earlyTermination` component | — | Yes | | | `earlyTermination.rightIsStricken` | Right is Stricken | STANDARD | → `earlyTermination` component | — | Yes | | | `earlyTermination.terminationFee` | Termination Fee | STANDARD | → `earlyTermination` component | `$` *(fixed)* | Yes | | | `exclusiveUse` | Exclusive Use | COMPLEX | `tagComponents` · `OTHER` | — | No | | | `exclusiveUse.clause` | Clause | STANDARD | → `exclusiveUse` component | — | No | | | `exclusiveUse.rightIsStricken` | Right is Stricken | STANDARD | → `exclusiveUse` component | — | No | | | `expansion` | Expansion | COMPLEX | `tagComponents` · `EXPANSION` | — | No | | | `expansion.advancedNoticePeriod` | Advanced Notice Period | STANDARD | → `expansion` component | `days`, `business days`, `weeks`, `months`, `years`, `date`, `range of days`, `range of business days`, `range of weeks`, `range of months`, `range of dates` | No | | | `expansion.expansionSpace` | Expansion Space | STANDARD | → `expansion` component | — | No | | | `expansion.expirationDate` | Expansion Expiration Date | STANDARD | → `expansion` component | `date`, `weeks`, `months`, `years` | No | | | `expansion.rentalRate` | Rental Rate | STANDARD | → `expansion` component | `$`, `$ per month`, `$ per year`, `$psf (annual)`, `$psf`, `% of FMV`, `FMV`, `landlordDefined` | No | | | `expansion.rightIsDeclined` | Right is Declined | STANDARD | → `expansion` component | — | No | | | `expansion.rightIsExercised` | Right is Exercised | STANDARD | → `expansion` component | — | No | | | `expansion.rightIsStricken` | Right is Stricken | STANDARD | → `expansion` component | — | No | | | `goDark` | Go Dark | STANDARD | `value` | — | No | | | `landlordParkingSpaceRestrictions` | Landlord Parking Space Restrictions | COMPLEX | `tagComponents` · `OTHER` | — | No | `RETAIL` only | | `landlordParkingSpaceRestrictions.clause` | Clause | STANDARD | → `landlordParkingSpaceRestrictions` component | — | No | `RETAIL` only | | `landlordParkingSpaceRestrictions.rightIsDeclined` | Right is Declined | STANDARD | → `landlordParkingSpaceRestrictions` component | — | No | `RETAIL` only | | `landlordParkingSpaceRestrictions.rightIsExercised` | Right is Exercised | STANDARD | → `landlordParkingSpaceRestrictions` component | — | No | `RETAIL` only | | `landlordParkingSpaceRestrictions.rightIsStricken` | Right is Stricken | STANDARD | → `landlordParkingSpaceRestrictions` component | — | No | `RETAIL` only | | `llKickout` | LL - Kickout | COMPLEX | `tagComponents` · `OTHER` | — | Yes | | | `llKickout.rightIsExercised` | Right is Exercised | STANDARD | → `llKickout` component | — | Yes | | | `llKickout.rightIsStricken` | Right is Stricken | STANDARD | → `llKickout` component | — | Yes | | | `llRelocation` | LL - Relocation | COMPLEX | `tagComponents` · `OTHER` | — | Yes | | | `llRelocation.rightIsExercised` | Right is Exercised | STANDARD | → `llRelocation` component | — | Yes | | | `llRelocation.rightIsStricken` | Right is Stricken | STANDARD | → `llRelocation` component | — | Yes | | | `naming` | Naming | COMPLEX | `tagComponents` · `OTHER` | — | No | | | `naming.rightIsStricken` | Right is Stricken | STANDARD | → `naming` component | — | No | | | `parking` | Parking | COMPLEX | `tagComponents` · `OTHER` | — | No | | | `parking.clause` | Clause | STANDARD | → `parking` component | — | No | | | `parking.rightIsStricken` | Right is Stricken | STANDARD | → `parking` component | — | No | | | `renewal` | Renewal | COMPLEX | `tagComponents` · `RENEWAL` | — | Yes | | | `renewal.advancedNoticePeriod` | Advanced Notice Period | STANDARD | → `renewal` component | `days`, `business days`, `weeks`, `months`, `years`, `date`, `range of days`, `range of business days`, `range of weeks`, `range of months`, `range of dates` | Yes | | | `renewal.numberOfAdditionalPeriods` | # of Additional Periods | STANDARD | → `renewal` component | `period` *(fixed)* | Yes | | | `renewal.renewalPeriodTerm` | Renewal Period Term | STANDARD | → `renewal` component | `years`, `months`, `weeks` | Yes | | | `renewal.rentalRate` | Rental Rate | STANDARD | → `renewal` component | `$`, `$ per month`, `$ per year`, `$psf (annual)`, `$psf`, `% of FMV`, `FMV`, `landlordDefined` | Yes | | | `renewal.rightIsDeclined` | Right is Declined | STANDARD | → `renewal` component | — | Yes | | | `renewal.rightIsExercised` | Right is Exercised | STANDARD | → `renewal` component | — | Yes | | | `renewal.rightIsStricken` | Right is Stricken | STANDARD | → `renewal` component | — | Yes | | | `rightOfFirstOffer` | Right of First Offer | COMPLEX | `tagComponents` · `RIGHT_OF_FIRST_OFFER` | — | No | | | `rightOfFirstOffer.acceptancePeriod` | Acceptance Period | STANDARD | → `rightOfFirstOffer` component | `days`, `business days`, `weeks`, `months` | No | | | `rightOfFirstOffer.expirationDateOfRight` | Expiration Date of Right | STANDARD | → `rightOfFirstOffer` component | `date`, `years`, `months` | No | | | `rightOfFirstOffer.offeredSpace` | Offered Space | STANDARD | → `rightOfFirstOffer` component | — | No | | | `rightOfFirstOffer.oneTimeOrOngoing` | One time or Ongoing | STANDARD | → `rightOfFirstOffer` component | — | No | | | `rightOfFirstOffer.rentalRate` | Rental Rate | STANDARD | → `rightOfFirstOffer` component | `$`, `$ per month`, `$ per year`, `$psf (annual)`, `$psf`, `% of FMV`, `FMV`, `landlordDefined` | No | | | `rightOfFirstOffer.rightIsDeclined` | Right is Declined | STANDARD | → `rightOfFirstOffer` component | — | No | | | `rightOfFirstOffer.rightIsExercised` | Right is Exercised | STANDARD | → `rightOfFirstOffer` component | — | No | | | `rightOfFirstOffer.rightIsStricken` | Right is Stricken | STANDARD | → `rightOfFirstOffer` component | — | No | | | `rightOfFirstRefusal` | Right of First Refusal | COMPLEX | `tagComponents` · `RIGHT_OF_FIRST_REFUSAL` | — | No | | | `rightOfFirstRefusal.acceptancePeriod` | Acceptance Period | STANDARD | → `rightOfFirstRefusal` component | `days`, `business days`, `weeks`, `months` | No | | | `rightOfFirstRefusal.expirationDateOfRight` | Expiration Date of Right | STANDARD | → `rightOfFirstRefusal` component | `date`, `years`, `months` | No | | | `rightOfFirstRefusal.oneTimeOrOngoing` | One time or Ongoing | STANDARD | → `rightOfFirstRefusal` component | — | No | | | `rightOfFirstRefusal.refusalSpace` | Refusal Space | STANDARD | → `rightOfFirstRefusal` component | — | No | | | `rightOfFirstRefusal.rentalRate` | Rental Rate | STANDARD | → `rightOfFirstRefusal` component | `$`, `$ per month`, `$ per year`, `$psf (annual)`, `$psf`, `% of FMV`, `FMV`, `landlordDefined` | No | | | `rightOfFirstRefusal.rightIsDeclined` | Right is Declined | STANDARD | → `rightOfFirstRefusal` component | — | No | | | `rightOfFirstRefusal.rightIsExercised` | Right is Exercised | STANDARD | → `rightOfFirstRefusal` component | — | No | | | `rightOfFirstRefusal.rightIsStricken` | Right is Stricken | STANDARD | → `rightOfFirstRefusal` component | — | No | | | `salesKickout` | Sales Kickout (Landlord Option) | COMPLEX | `tagComponents` · `OTHER` | — | Yes | `RETAIL` only | | `salesKickout.clause` | Clause | STANDARD | → `salesKickout` component | — | Yes | `RETAIL` only | | `salesKickout.rightIsExercised` | Right is Exercised | STANDARD | → `salesKickout` component | — | Yes | `RETAIL` only | | `salesKickout.rightIsStricken` | Right is Stricken | STANDARD | → `salesKickout` component | — | Yes | `RETAIL` only | | `salesKickoutTenant` | Sales Kickout (Tenant Option) | COMPLEX | `tagComponents` · `OTHER` | — | Yes | `RETAIL` only | | `salesKickoutTenant.clause` | Clause | STANDARD | → `salesKickoutTenant` component | — | Yes | `RETAIL` only | | `salesKickoutTenant.rightIsExercised` | Right is Exercised | STANDARD | → `salesKickoutTenant` component | — | Yes | `RETAIL` only | | `salesKickoutTenant.rightIsStricken` | Right is Stricken | STANDARD | → `salesKickoutTenant` component | — | Yes | `RETAIL` only | | `signage` | Signage | COMPLEX | `tagComponents` · `OTHER` | — | No | | | `signage.clause` | Clause | STANDARD | → `signage` component | — | No | | | `signage.rightIsStricken` | Right is Stricken | STANDARD | → `signage` component | — | No | | | `sublet` | Right to Sublet | COMPLEX | `tagComponents` · `OTHER` | — | No | | | `sublet.clause` | Clause | STANDARD | → `sublet` component | — | No | | | `sublet.rightIsDeclined` | Right Is Declined | STANDARD | → `sublet` component | — | No | | | `sublet.rightIsStricken` | Right Is Stricken | STANDARD | → `sublet` component | — | No | | | `telecomAntenna` | Telecom/Antenna | COMPLEX | `tagComponents` · `OTHER` | — | No | | | `telecomAntenna.clause` | Clause | STANDARD | → `telecomAntenna` component | — | No | | | `telecomAntenna.rightIsDeclined` | Right is Declined | STANDARD | → `telecomAntenna` component | — | No | | | `telecomAntenna.rightIsStricken` | Right is Stricken | STANDARD | → `telecomAntenna` component | — | No | | ### Amenity Usage | `tagName` | `displayName` | `category` | Where the data lands | `value.unit` can be | Per‑space | Scope | | - | - | - | - | - | - | - | | `conferenceRoomUsage` | Conference Room Usage | COMPLEX | `tagComponents` · `OTHER` | — | No | | | `conferenceRoomUsage.rightIsStricken` | Right is Stricken | STANDARD | → `conferenceRoomUsage` component | — | No | | | `fitnessCenterUsage` | Fitnesss Center Usage | COMPLEX | `tagComponents` · `OTHER` | — | No | | | `fitnessCenterUsage.rightIsStricken` | Right is Stricken | STANDARD | → `fitnessCenterUsage` component | — | No | | | `loungeUsage` | Lounge Usage | COMPLEX | `tagComponents` · `OTHER` | — | No | | | `loungeUsage.rightIsStricken` | Right is Stricken | STANDARD | → `loungeUsage` component | — | No | | | `petAllowance` | Pet Allowance | COMPLEX | `tagComponents` · `OTHER` | — | No | | | `petAllowance.rightIsStricken` | Right is Stricken | STANDARD | → `petAllowance` component | — | No | | | `storageUsage` | Storage Usage | COMPLEX | `tagComponents` · `OTHER` | — | No | | | `storageUsage.rightIsStricken` | Right is Stricken | STANDARD | → `storageUsage` component | — | No | | ### Sales Reporting | `tagName` | `displayName` | `category` | Where the data lands | `value.unit` can be | Per‑space | Scope | | - | - | - | - | - | - | - | | `annualSalesStatement` | Annual Sales Statement | STANDARD | `value` | `days` *(fixed)* | No | `RETAIL` only | | `monthlySalesStatement` | Monthly Sales Statement | STANDARD | `value` | `days` *(fixed)* | No | `RETAIL` only | | `openingCotenancy` | opening Co-tenancy | COMPLEX | `tagComponents` · `OTHER` | — | No | `RETAIL` only | | `openingCotenancy.clause` | Clause | STANDARD | → `openingCotenancy` component | — | No | `RETAIL` only | | `openingCotenancy.rightIsDeclined` | Right is Declined | STANDARD | → `openingCotenancy` component | — | No | `RETAIL` only | | `openingCotenancy.rightIsExercised` | Right is Exercised | STANDARD | → `openingCotenancy` component | — | No | `RETAIL` only | | `openingCotenancy.rightIsStricken` | Right is Stricken | STANDARD | → `openingCotenancy` component | — | No | `RETAIL` only | | `operatingCotenancy` | Operating Co-tenancy | COMPLEX | `tagComponents` · `OTHER` | — | No | `RETAIL` only | | `operatingCotenancy.clause` | Clause | STANDARD | → `operatingCotenancy` component | — | No | `RETAIL` only | | `operatingCotenancy.rightIsDeclined` | Right is Declined | STANDARD | → `operatingCotenancy` component | — | No | `RETAIL` only | | `operatingCotenancy.rightIsExercised` | Right is Exercised | STANDARD | → `operatingCotenancy` component | — | No | `RETAIL` only | | `operatingCotenancy.rightIsStricken` | Right is Stricken | STANDARD | → `operatingCotenancy` component | — | No | `RETAIL` only | | `quarterlySalesStatement` | Quarterly Sales Statement | STANDARD | `value` | `days` *(fixed)* | No | `RETAIL` only | ### GSA-Specific Tags | `tagName` | `displayName` | `category` | Where the data lands | `value.unit` can be | Per‑space | Scope | | - | - | - | - | - | - | - | | `bsacRent` | BSAC Rent | STANDARD | `value` + `customValue`s | `$ per year`, `$psf (annual)` | Yes | `GSA_OFFICE` only | | `bsacRentInterestRate` | BSAC Rent Interest Rate | STANDARD | `value` | — | Yes | `GSA_OFFICE` only | | `cpiIncreaseAmount` | CPI Increase Amount | STANDARD | `value` | `$` *(fixed)* | Yes | `GSA_OFFICE` only | | `cpiIncreaseDate` | CPI Increase Date | STANDARD | `value` | `date` *(fixed)* | Yes | `GSA_OFFICE` only | | `fixedOpex` | Fixed Opex | STANDARD | `value` | `$ per year`, `$ per month`, `$psf (annual)` | Yes | | | `fixedParking` | Fixed Parking | STANDARD | `value` | `$ per year`, `$ per month`, `$psf (annual)` | Yes | `GSA_OFFICE` only | | `fixedRETaxes` | Fixed Real Estate Taxes | STANDARD | `value` | `$ per year`, `$ per month`, `$psf (annual)` | Yes | | | `tiRent` | TI Rent | STANDARD | `value` + `customValue`s | `$ per year`, `$psf (annual)` | Yes | `GSA_OFFICE` only | | `tiRentInterestRate` | TI Rent Interest Rate | STANDARD | `value` | — | Yes | `GSA_OFFICE` only | ### Other | `tagName` | `displayName` | `category` | Where the data lands | `value.unit` can be | Per‑space | Scope | | - | - | - | - | - | - | - | | `estoppel` | Estoppel | STANDARD | `value` | — | No | | | `exhibit` | Exhibit | EXHIBIT | `value` | — | No | | | `noBuildArea` | No Build Area | COMPLEX | `tagComponents` · `OTHER` | — | No | `RETAIL` only | | `noBuildArea.clause` | Clause | STANDARD | → `noBuildArea` component | — | No | `RETAIL` only | | `noBuildArea.rightIsDeclined` | Right is Declined | STANDARD | → `noBuildArea` component | — | No | `RETAIL` only | | `noBuildArea.rightIsExercised` | Right is Exercised | STANDARD | → `noBuildArea` component | — | No | `RETAIL` only | | `noBuildArea.rightIsStricken` | Right is Stricken | STANDARD | → `noBuildArea` component | — | No | `RETAIL` only | | `permittedUse` | Permitted Use | COMPLEX | `tagComponents` · `OTHER` | — | No | | | `permittedUse.clause` | Clause | STANDARD | → `permittedUse` component | — | No | | | `permittedUse.rightIsStricken` | Right is Stricken | STANDARD | → `permittedUse` component | — | No | | | `policyExpirationDate` | Policy Expiration Date | STANDARD | `value` | `date` *(fixed)* | No | | | `policyStartDate` | Policy Start Date | STANDARD | `value` | `date` *(fixed)* | No | | | `prohibitedUse` | Prohibited Use | COMPLEX | `tagComponents` · `OTHER` | — | No | | | `prohibitedUse.clause` | Clause | STANDARD | → `prohibitedUse` component | — | No | | | `prohibitedUse.rightIsStricken` | Right is Stricken | STANDARD | → `prohibitedUse` component | — | No | | | `rentRollTenant` | Tenant | COMPLEX | `tagComponents` · `OTHER` | — | No | rent-roll documents | | `rentRollTenant.commenceDate` | Commencement Date | STANDARD | → `rentRollTenant` component | `date` *(fixed)* | No | rent-roll documents | | `rentRollTenant.expirationDate` | Expiration Date | STANDARD | → `rentRollTenant` component | `date` *(fixed)* | No | rent-roll documents | | `rentRollTenant.monthlyRent` | Monthly Rent | STANDARD | → `rentRollTenant` component | `$` *(fixed)* | No | rent-roll documents | | `rentRollTenant.squareFootage` | Square Footage | STANDARD | → `rentRollTenant` component | `SF` *(fixed)* | No | rent-roll documents | | `rentRollTenant.suiteNumber` | Suite Number | STANDARD | → `rentRollTenant` component | — | No | rent-roll documents | | `rentRollTenant.tenantLegalName` | Tenant Legal Name | STANDARD | → `rentRollTenant` component | — | No | rent-roll documents | | `tenantRadiusRestriction` | Tenant Radius Restriction | STANDARD | `value` | — | No | `RETAIL` only | # Tags and tag values Source: https://docs.prophia.com/api-reference/v2/concepts/tags-and-values How extracted lease terms are structured: categories, typed values, schedules, options, and amendment chains A **tag** (`DocTag`) is one extracted lease term — a rent amount, a renewal option, a commencement date — anchored to the exact place in a document it came from. Tags are the payoff of Prophia's document processing: ask a lease for [its tags](/api-reference/v2/leases/list-tag) and you get the abstracted deal terms with citations. ```bash theme={null} curl "https://api.prophia.com/rest/v2/accounts/{accountId}/leases/{leaseId}/tags?include=applicableSpaces" \ -H "X-API-Key: " ``` For the complete list of terms Prophia captures — every `tagName`, the units it can carry, and what its `customValue` fields hold — see the [tag reference](/api-reference/v2/concepts/tag-reference). ## Three unrelated "custom" things Three features share the word *custom*. They have nothing to do with each other: | Term | What it is | Where | | - | - | - | | **Custom terms** | Account-level *definitions* of extra term types your team tracks beyond Prophia's standard set | `GET /rest/v2/accounts/{accountId}/custom-terms` | | **Custom tags** | Tag *instances* of those definitions — ordinary tags with `isCustomTag: true` | Any tag endpoint; filter with `isCustomTag` | | **`customValue1`–`customValue6`** | Positional extra fields that appear on *any* tag, custom or not | Fields on the tag object (see below) | ## Categories `category` tells you how much structure to expect: | Category | Meaning | | - | - | | `STANDARD` | A single captured value — read `value`, and `schedule` when the term is a series. | | `COMPLEX` | An option or right (renewal, termination, expansion…) — the substance lives in `tagComponents`. | | `EXHIBIT` | A reference to exhibit material in the document. | ## Typed values: branch on `kind` `value` is polymorphic. Every shape carries a `kind` discriminator plus the common fields `unit`, `capturedData` (the value as captured, as text) and `ocrText` (the raw source text). Empty fields are omitted. | `kind` | Payload | Produced when the value was captured… | | - | - | - | | `money` | `value` (decimal **string**) + `currency` | as a number with a unit starting with `$` | | `percent` | `value` (JSON number) | as a number with a unit starting with `%` | | `number` | `value` (JSON number) | as any other number — read `unit` for what it counts | | `date` | `value` (`yyyy-MM-dd`) | as a date | | `dateRange` | `start`, `end` (`yyyy-MM-dd`) | with unit `range of dates` | | `numberRange` | `start`, `end` (integers) | with any other `range of …` unit | | `text` | `value` | as free text, or anything else — the fallback kind | > [!WARNING] > > **Money values are decimal strings, not JSON numbers** — `"value": "120000.00"` — to preserve cent precision. Parse them as decimals, never as floats. > [!NOTE] > > **`kind` alone is not enough — always read `unit` too.** A single `number` covers durations, areas and plain counts, and only `unit` separates them. And because the kind follows the captured unit rather than the term, the same `tagName` can hand you `money` on one lease and `percent` or `text` on another: `rentBump` and `opexCap` accept both dollar and percent units, and a rental rate captured as `FMV` or `landlordDefined` comes back as `text`. > > A `percent` value carries the percentage itself, so `3.5` means three and a half percent. **Typed fields are best-effort.** They are populated by parsing `capturedData`, so a typed field is *omitted* when the captured text couldn't be parsed into that kind — common for dates a lease states in relative terms ("ninety days after substantial completion"). `capturedData` always holds the raw text, and it is the fallback whenever a typed field is missing. > [accordion] Example: money > > ```json theme={null} > { "kind": "money", "value": "8250.00", "currency": "USD", "capturedData": "$8,250.00" } > ``` > [accordion] Example: percent > > ```json theme={null} > { "kind": "percent", "value": 6, "unit": "%", "capturedData": "6", "ocrText": "six percent (6%)" } > ``` > > `value` is the percentage itself — six percent, not `0.06`. > [accordion] Example: a rental rate captured as FMV > > ```json theme={null} > { "kind": "text", "value": "FMV", "unit": "FMV", "ocrText": "Fair Market Rental Rate" } > ``` > > Stored as free text, so it arrives as `text` despite sitting on a field you might expect to be money. > [accordion] Example: date that could not be parsed > > ```json theme={null} > { "kind": "date", "capturedData": "90 days after substantial completion" } > ``` > > No `value` — only `capturedData`. ## Space-dependent and space-independent tags Every term is defined as either **space-dependent** or **space-independent**, and this decides how the lease's tag list is assembled. * **Space-dependent** terms are consolidated *per space*. Square footage, rent schedules, renewals, early terminations — each space the lease occupies can carry its own value. In the flattened list you get back, the same `tagName` can therefore appear **more than once**, once per space, with different values and different `applicableSpaces`. * **Space-independent** terms are consolidated *per term name*, one value for the whole lease. Tenant legal name, effective date, exclusive use, permitted use. In a lease's consolidated `tags` (`include=tags` on a lease), each tag appears once, with the spaces it wins in the consolidation: * A space-independent tag has no `applicableSpaces`. It applies to the lease, not to a particular suite. * A space-dependent tag lists the spaces it applies to. A term captured without any spaces applies to every space, so it lists all of them. * Rent schedules, and the monthly and annual rents derived from them, can be consolidated differently per space. When that happens the same tag `id` appears once per space group, each copy with its own values and `applicableSpaces`. Lease-level values that summarize every space, such as the lease's commencement and expiration dates and its total square footage, are on the lease's top-level fields (`commenceDate`, `expirationDate`, `squareFootage`, `suiteNumber`), not in `tags`. > [!NOTE] > > Keying a client-side map on `tagName` alone will silently drop values for a multi-space lease. Key on `tagName` + `applicableSpaces` for space-dependent terms. The [tag reference](/api-reference/v2/concepts/tag-reference) marks which terms are which. Request [`include=applicableSpaces`](/api-reference/v2/concepts/includes) to see which spaces a tag applies to. See [Spaces and encumbrances](/api-reference/v2/concepts/spaces-and-encumbrances) for how those entries match your space inventory. ## `customValue1`–`customValue6`: positional and tag-dependent These six string fields mean **different things on different tags** — the position, not the name, carries the meaning. Some encode a whole multi-period schedule as `:-:`-delimited parallel arrays. | Tag | Field | Holds | | - | - | - | | `rentAbatement`, `opexAbatement`, `taxAbatement` | `customValue1`–`4` | Parallel `:-:`-delimited arrays, one token per abatement period: period unit, discount unit, duration, amount | | `rentAbatement` | `customValue5` | `:-:`-delimited `1`/`0` per period — whether opex and tax are included in the abated amount | | `opexCap` | `customValue1`–`3` | `"varies"` / `"does not vary"`, then `:-:`-delimited years and amounts when it varies | | `percentageRent` | `customValue1` | `"true"` when the rate applies against minimum rent (no breakpoints), `"false"` when breakpoints are given | | `securityDeposit`, `letterOfCreditAmount` | `customValue1` | The reduced amount after the refund schedule is applied | | `securityDepositRefundSchedule`, `letterOfCreditRefundSchedule` | `customValue1` | `"appliesToBaseRent"` when the reduction can be credited toward base rent | | `prepaidRent` | `customValue1`, `customValue5` | Whether opex is included; the interval when structured as recurring rather than a lump sum | | `tiRent`, `bsacRent` (GSA) | `customValue1`–`6` | How the rent period is expressed, its duration, and any delay | | `rentSchedule`, `fee` | `customValue1` | Your MRI income category for the charge | | `suiteNumber` | `customValue1` | The primary suite when several are selected | > [!WARNING] > > Never interpret a `customValue` without knowing which `tagName` it belongs to. Treat undocumented combinations as opaque — they are not guaranteed to mean anything. ## Schedules Tags whose value is a series carry a `schedule` of `{kind, entries}`. Branch on `kind`, which determines the entry shape: `RENT`, `PERCENT_RENT`, `ABATEMENT`, `REFUND`, or `FEE`. Every entry has a 0-indexed `sequence`, and every entry except `REFUND` is bounded **either** by calendar dates (`startDate`/`endDate`) **or** by 1-indexed months from lease commencement (`startMonth`/`endMonth`, where month 1 is the commencement month) — whichever way the lease states it. Handle both. * **`RENT`** — `baseRent` plus any fixed taxes, opex, and parking for the step, and `squareFootage` when the lease states rent per step against a changing premises size. The GSA-only fields (`fixedParking`, `annualTiRent`, `annualBsacRent`, `cpiIncreaseAmount`) are null on every other lease type. * **`PERCENT_RENT`** — `percent` plus the sales `breakpoint` above which it applies, and `breakpointTerm` for breakpoints the lease states in prose. * **`ABATEMENT`** — the `amount` abated per period, and `includesTaxAndOpex`. * **`FEE`** — a recurring charge's `amount` per step. * **`REFUND`** — a deposit or letter-of-credit burn-down. Dated by `effectiveDate`, derived from `unitType`/`unitValue` (months or years from commencement, or a date). > [!WARNING] > > On a `REFUND` entry, **read `refundType` before `amount`** — it changes what the number means: > > | `refundType` | `amount` is | > | - | - | > | `TO` | The balance that **remains** after this step | > | `BY` | The **size of the reduction** applied at this step | > > `reducedAmount` saves you the arithmetic: it is the deposit as it stands at that step, with every reduction up to and including it applied. ## Complex tags: components and rights For `COMPLEX` tags, the substance is in `tagComponents` — polymorphic on its own `kind`: `RENEWAL`, `EARLY_TERMINATION`, `EXPANSION`, `CONTRACTION`, `RIGHT_OF_FIRST_REFUSAL`, `RIGHT_OF_FIRST_OFFER`, `PURCHASE`, `AUTO_RENEWAL`, `TENANT_IMPROVEMENT`, `FEE`, or `OTHER`. All components share `rightStatus`, `effectiveDateOfRight`, `clause`, `customLabel`, an optional `amendingTag`, and `componentTags` (refs to the constituent tags the component was assembled from, keyed by component field). Each kind then adds its own: | Kind | Adds | | - | - | | `RENEWAL` | `renewalPeriodTerm`, `numberOfAdditionalPeriods`, `advancedNoticePeriod`, `acceptancePeriod`, `rentalRate` | | `AUTO_RENEWAL` | `autoRenewalTerm`, `autoRenewalPeriods`, `autoRenewalRate`, `autoRenewalRateBump`, and an `advancedNoticePeriod` that is the notice to **opt out**, not to exercise | | `EARLY_TERMINATION` | `terminationFee`, `optionEndDate`, `earlyMoveOutDate`, `advancedNoticePeriod` | | `CONTRACTION` | `contractionFee`, `contractionSpace`, `optionEndDate`, `earlyMoveOutDate` | | `EXPANSION` | `expansionSpace`, `rentalRate`, `expirationDate` | | `RIGHT_OF_FIRST_OFFER` | `offeredSpace`, `rentalRate`, `acceptancePeriod`, `expirationDateOfRight`, `oneTimeOrOngoing` | | `RIGHT_OF_FIRST_REFUSAL` | `refusalSpace`, `rentalRate`, `acceptancePeriod`, `expirationDateOfRight`, `oneTimeOrOngoing` | | `TENANT_IMPROVEMENT` | `tiAmount`, `tiRemaining`, `tiSunset` | | `FEE` | `feeAmount`, `feeTerm`, `feeIncrease`, `encumberedSpaces` | | `PURCHASE` | `advancedNoticePeriod` | | `OTHER` | `encumberedSpaces` — everything else lives in `clause` and `componentTags` | Where a deadline is stated in relative terms rather than as a parseable date, the date field is null and a paired `…Text` field carries the source wording (`expirationDateText`, `expirationDateOfRightText`). The space-naming fields — `expansionSpace`, `contractionSpace`, `offeredSpace`, `refusalSpace`, `encumberedSpaces` — use the same shape and the same suite-name matching as a tag's `applicableSpaces`. See [Spaces and encumbrances](/api-reference/v2/concepts/spaces-and-encumbrances). ## Two status enums that are not the same thing | | `lifecycleStatus` (on the tag) | `rightStatus` (on a component) | | - | - | - | | Tracks | The **tag record** across the lease's document history | The **option/right** the component describes | | Values | `ACTIVE` (in force), `VOID` (superseded/withdrawn), `STRICKEN` (removed, commonly via amendment), `EXERCISED` (option taken) | `AVAILABLE` (still exercisable), `EXERCISED`, `STRICKEN`, `DECLINED` (tenant declined), `EXPIRED` (window passed unused), `UNKNOWN` (no status could be resolved) | The two are **orthogonal, independently-resolved signals** — they can disagree, and neither overrides the other. A tag can be `lifecycleStatus: ACTIVE` while its component's right is `EXPIRED`. ## Amendment chains Amendment chains track an **option**, not a lease. Every option carries a `referenceId`, and options that belong to the same chain share it. That shared key is what `amendingTag` exposes. `amendingTag` resolves to different things depending on what carries it: | On a… | `amendedTagId` points at | | - | - | | Option tag | The **earlier iteration of the same option chain** — the version in a prior document that this one amends | | Component tag | The **parent option container** the component belongs to | ```mermaid theme={null} flowchart TB subgraph chainA["Chain A — the 5-year renewal"] direction TB A1["renewal · original lease
referenceId = A
self-referencing — no amendingTag"] A2["renewal · 1st amendment
amendingTag.amendedTagId → A1"] A3["renewal · 3rd amendment
amendingTag.amendedTagId → A2"] A1 --> A2 --> A3 end subgraph chainB["Chain B — a new expansion right"] direction TB B1["expansion · 2nd amendment
referenceId = B
starts its own chain"] end ``` Three consequences worth designing around: 1. **The pointer runs backwards.** `amendingTag.amendedTagId` is the id of the *earlier* tag. To find the current state of an option, start from the tag no other tag points at. 2. **`amendingTag.effectiveDate` is the *referenced* tag's effective date**, not this tag's. For when the current version takes effect, read the component's own `effectiveDateOfRight`. 3. **`amendingTag` is omitted on the first option in a chain**, which is stored as self-referencing. Its absence means "chain start", not "no chain". A single amendment can start a new chain, amend an existing one, or do both — and a lease commonly runs several chains in parallel. `customLabel` is the human-readable name of the family ("Option 1 — 5 Year Renewal"); options sharing a `referenceId` should share a `customLabel`, so it is a useful grouping key when presenting chains to a user. Superseded tags typically carry `lifecycleStatus: VOID` or `STRICKEN`, but treat that as corroboration rather than the primary signal — walk the chain. ## Citations: `tagSelections` and `pages` With [`include=tagSelections,pages`](/api-reference/v2/concepts/includes), a tag brings its provenance: `tagSelections` are rectangles on document pages, and `pages` the geometry of those pages. Bounding boxes are expressed in **basis points of page dimensions** — each axis runs 0–10,000 where 10,000 is 100% of the page's width or height, origin top-left. To place a box on a rendered page image: ``` pixel_x = box.x * page.width / 10000 ``` and likewise for `y`, `width`, and `height`. # CREATE Custom Field Source: https://docs.prophia.com/api-reference/v2/custom-fields/create-custom-field /api-reference/openapi.json post /rest/v2/accounts/{accountId}/custom-fields Creates a Building Custom Field on the Account. A `MULTI` field is created with its options. **Required capabilities:** `WRITE:CUSTOM_FIELD` # CREATE Custom Field Assignment Source: https://docs.prophia.com/api-reference/v2/custom-fields/create-custom-field-assignment /api-reference/openapi.json post /rest/v2/accounts/{accountId}/custom-fields/{fieldId}/assignments Sets the field's value on each of the given Buildings, up to 200 per request. **Required capabilities:** `WRITE:CUSTOM_FIELD` # CREATE Custom Field Option Source: https://docs.prophia.com/api-reference/v2/custom-fields/create-custom-field-option /api-reference/openapi.json post /rest/v2/accounts/{accountId}/custom-fields/{fieldId}/options Adds an option to a `MULTI` Building Custom Field. **Required capabilities:** `WRITE:CUSTOM_FIELD` # DELETE Custom Field Source: https://docs.prophia.com/api-reference/v2/custom-fields/delete-custom-field /api-reference/openapi.json delete /rest/v2/accounts/{accountId}/custom-fields/{fieldId} Deletes a Building Custom Field along with its options and every Building's value for it. **Required capabilities:** `WRITE:CUSTOM_FIELD` # DELETE Custom Field Assignment Source: https://docs.prophia.com/api-reference/v2/custom-fields/delete-custom-field-assignment /api-reference/openapi.json delete /rest/v2/accounts/{accountId}/custom-fields/{fieldId}/assignments/{buildingId} Clears the field's value from a Building. **Required capabilities:** `WRITE:CUSTOM_FIELD` # DELETE Custom Field Option Source: https://docs.prophia.com/api-reference/v2/custom-fields/delete-custom-field-option /api-reference/openapi.json delete /rest/v2/accounts/{accountId}/custom-fields/{fieldId}/options/{optionId} Deletes an option and clears it from every Building assigned it. A `MULTI` field's last remaining option cannot be deleted. **Required capabilities:** `WRITE:CUSTOM_FIELD` # GET Custom Field Source: https://docs.prophia.com/api-reference/v2/custom-fields/get-custom-field /api-reference/openapi.json get /rest/v2/accounts/{accountId}/custom-fields/{fieldId} Returns a single Building Custom Field by id, with its options. **Required capabilities:** `READ:CUSTOM_FIELD` # GET Custom Field Option Source: https://docs.prophia.com/api-reference/v2/custom-fields/get-custom-field-option /api-reference/openapi.json get /rest/v2/accounts/{accountId}/custom-fields/{fieldId}/options/{optionId} Returns a single option of a Building Custom Field. **Required capabilities:** `READ:CUSTOM_FIELD` # LIST Custom Field Source: https://docs.prophia.com/api-reference/v2/custom-fields/list-custom-field /api-reference/openapi.json get /rest/v2/accounts/{accountId}/custom-fields Returns a paged list of the Account's Building Custom Fields, each with its options. **Required capabilities:** `READ:CUSTOM_FIELD` ## Filters Filters combine with AND: a result must match every filter you supply. | Filter | Description | | - | - | | `fieldName` | Case-insensitive contains match on the field name. | | `fieldType` | Filter by field type. | | `active` | Filters to active (true) or inactive (false) fields; omit for both. | # LIST Custom Field Option Source: https://docs.prophia.com/api-reference/v2/custom-fields/list-custom-field-option /api-reference/openapi.json get /rest/v2/accounts/{accountId}/custom-fields/{fieldId}/options Returns the options of a Building Custom Field. **Required capabilities:** `READ:CUSTOM_FIELD` # UPDATE Custom Field Source: https://docs.prophia.com/api-reference/v2/custom-fields/update-custom-field /api-reference/openapi.json patch /rest/v2/accounts/{accountId}/custom-fields/{fieldId} Renames, activates, or deactivates a Building Custom Field. Omitted fields are left unchanged. **Required capabilities:** `WRITE:CUSTOM_FIELD` # UPDATE Custom Field Option Source: https://docs.prophia.com/api-reference/v2/custom-fields/update-custom-field-option /api-reference/openapi.json patch /rest/v2/accounts/{accountId}/custom-fields/{fieldId}/options/{optionId} Changes an option's value. Buildings assigned the option show the new value. **Required capabilities:** `WRITE:CUSTOM_FIELD` # CREATE Custom Term Definition Source: https://docs.prophia.com/api-reference/v2/custom-terms/create-custom-term-definition /api-reference/openapi.json post /rest/v2/accounts/{accountId}/custom-terms Creates a Custom Term Definition on the Account. **Required capabilities:** `WRITE:CUSTOM_TERM` # DELETE Custom Term Definition Source: https://docs.prophia.com/api-reference/v2/custom-terms/delete-custom-term-definition /api-reference/openapi.json delete /rest/v2/accounts/{accountId}/custom-terms/{customTermId} Permanently deletes a Custom Term Definition AND every Custom Tag instance created from it across the Account's Leases — this cannot be undone. **Required capabilities:** `WRITE:CUSTOM_TERM` # GET Custom Term Definition Source: https://docs.prophia.com/api-reference/v2/custom-terms/get-custom-term-definition /api-reference/openapi.json get /rest/v2/accounts/{accountId}/custom-terms/{customTermId} Returns a single Custom Term Definition by id, with usage counts populated. **Required capabilities:** `READ:CUSTOM_TERM` # LIST Custom Term Definition Source: https://docs.prophia.com/api-reference/v2/custom-terms/list-custom-term-definition /api-reference/openapi.json get /rest/v2/accounts/{accountId}/custom-terms Returns a paged list of the Account's Custom Term Definitions with usage counts populated. **Required capabilities:** `READ:CUSTOM_TERM` ## Filters Filters combine with AND: a result must match every filter you supply. | Filter | Description | | - | - | | `tagName` | Case-insensitive contains match on the custom term's tag name. | | `definitionType` | Filter by definition type. | | `aiEnabled` | Filters to definitions with AI extraction enabled (true) or disabled (false); omit for both. | # UPDATE Custom Term Definition Source: https://docs.prophia.com/api-reference/v2/custom-terms/update-custom-term-definition /api-reference/openapi.json patch /rest/v2/accounts/{accountId}/custom-terms/{customTermId} Updates a Custom Term Definition's fields. Omitted fields are left unchanged. **Required capabilities:** `WRITE:CUSTOM_TERM` # CREATE Document via Upload Source: https://docs.prophia.com/api-reference/v2/documents/create-document-via-upload /api-reference/openapi.json post /rest/v2/accounts/{accountId}/docs/upload Uploads a Document as multipart form data and attaches it to a Lease. Any file type is accepted, up to 200MB. Only documents whose filename ends in `.pdf` are processed into Pages and Tags. To upload a document to a building or property level folder, simply use the buildingId or propertyId as the leaseId in the request. **Required capabilities:** `WRITE:DOCUMENT` # CREATE Document via URL Source: https://docs.prophia.com/api-reference/v2/documents/create-document-via-url /api-reference/openapi.json post /rest/v2/accounts/{accountId}/docs Attaches a Document to a Lease by fetching it from a presigned URL you provide. The URL must use https and its response must carry a `Content-Length`. Any file type is accepted, up to 200MB. Only documents whose `filename` ends in `.pdf` are processed into Pages and Tags; anything else is stored and attached as-is. **Required capabilities:** `WRITE:DOCUMENT` # LIST Page Source: https://docs.prophia.com/api-reference/v2/documents/list-page /api-reference/openapi.json get /rest/v2/accounts/{accountId}/docs/{leaseDocId}/pages Returns a paged list of the Document's Pages including its geometry. **Required capabilities:** `READ:DOCUMENT` # LIST Raw Tag Source: https://docs.prophia.com/api-reference/v2/documents/list-raw-tag /api-reference/openapi.json get /rest/v2/accounts/{accountId}/docs/{leaseDocId}/tags/raw Returns a paged list of this Document's Tags, each built from its stored row, in the same shape as LIST Tag. Skips the lease's consolidated view, so a tag change shows up immediately; nothing consolidation derives across documents is applied (amended values, consolidated rent schedules), and an option's child terms are listed as separate Tags rather than nested under it. Rent periods stored by lease term keep `startMonth`/`endMonth` with no `startDate`/`endDate`; only consolidation resolves them to dates from the commencement date. **Required capabilities:** `READ:TAG` ## Filters Filters combine with AND: a result must match every filter you supply. | Filter | Description | | - | - | | `tagName` | Case-insensitive contains match on the tag name. | | `isCustomTag` | Filters to custom tags (true) or standard-definition tags (false); omit for both. | | `lifecycleStatus` | Exact match on the tag's lifecycle status. | | `createdAtAfter` | Tags created at or after this timestamp; inclusive. | | `createdAtBefore` | Tags created at or before this timestamp; inclusive. | # LIST Tag Source: https://docs.prophia.com/api-reference/v2/documents/list-tag /api-reference/openapi.json get /rest/v2/accounts/{accountId}/docs/{leaseDocId}/tags Returns a paged list of the Tags extracted from this Document, resolved against the lease's consolidated tag view. That view is rebuilt in the background about 10 minutes after the most recent tag change on the lease and returns the previous values until then; use LIST Raw Tag to see tags as they were just created or edited. **Required capabilities:** `READ:TAG` ## Filters Filters combine with AND: a result must match every filter you supply. | Filter | Description | | - | - | | `tagName` | Case-insensitive contains match on the tag name. | | `isCustomTag` | Filters to custom tags (true) or standard-definition tags (false); omit for both. | | `lifecycleStatus` | Exact match on the tag's lifecycle status. | | `createdAtAfter` | Tags created at or after this timestamp; inclusive. | | `createdAtBefore` | Tags created at or before this timestamp; inclusive. | # UPDATE Document Source: https://docs.prophia.com/api-reference/v2/documents/update-document /api-reference/openapi.json patch /rest/v2/accounts/{accountId}/docs/{leaseDocId} Updates a Document's fields. Omitted fields are left unchanged, and a present-but-blank `title` resets it to the original filename. `documentStatus` must be one of the account's configured statuses, matched case-sensitively, and cannot be set while the Document is still being processed. **Required capabilities:** `WRITE:DOCUMENT` # API Introduction Source: https://docs.prophia.com/api-reference/v2/introduction Base URL, authentication, and where to start with the Prophia REST API The Prophia REST API is a JSON API over your portfolio data — the same accounts, properties, buildings, leases, documents, and tags you see in the app. ## Base URL ``` https://api.prophia.com/rest/v2 ``` Every resource is scoped to an account: ``` GET /rest/v2/accounts/{accountId}/properties GET /rest/v2/accounts/{accountId}/leases/{leaseId} ``` ## Your first call **1. Get an API key** See [Authentication](/developers/authentication) for key types and how to obtain one. If you plan to write as well as read, [write access](/developers/write-access) is a separate entitlement. **2. Find your account** ```bash theme={null} curl https://api.prophia.com/rest/v2/accounts \ -H "X-API-Key: " ``` The response lists the accounts your key can access — grab the `id`. **3. List your properties** ```bash theme={null} curl "https://api.prophia.com/rest/v2/accounts/{accountId}/properties" \ -H "X-API-Key: " ``` Every endpoint page in this reference has a **Try it** playground — paste your key into the auth field and call the live API from your browser. ## Learn the concepts The endpoint pages document each operation; these pages explain the ideas that span them. **Data concepts** — what the objects mean > [card] [Data model](/api-reference/v2/concepts/data-model) > > How accounts, properties, buildings, spaces, leases, documents, and tags relate — with the diagram. > [card] [Leases](/api-reference/v2/concepts/leases) > > Status fields, lease type, subleases, and the spaces a lease occupies. > [card] [Documents](/api-reference/v2/concepts/documents) > > Upload, the `documentStatus` lifecycle, categories, and change detection. > [card] [Tags](/api-reference/v2/concepts/tags-and-values) > > Extracted lease terms — how they're structured, plus a reference to every term Prophia captures. > [card] [Spaces and encumbrances](/api-reference/v2/concepts/spaces-and-encumbrances) > > Building spaces, applicable spaces, and which spaces are burdened by options. > [card] [Understanding onboarding](/api-reference/v2/concepts/onboarding) > > Why a building's data is incomplete until Prophia finishes curating it — and how to tell. **API mechanics** — how requests behave > [card] [Includes](/api-reference/v2/concepts/includes) > > Embed related resources in one request — and the rules that make it predictable. > [card] [Pagination, sorting, and filtering](/api-reference/v2/concepts/pagination-and-filtering) > > The list envelope, paging rules, and per-resource sort and filter fields. > [card] [Errors and permissions](/api-reference/v2/concepts/errors-and-permissions) > > Status codes, the capabilities model, and why 404 sometimes means 403. ## Errors Requests fail with conventional status codes — `400` for malformed requests, `401` for key problems, `403`/`404` for permission and visibility issues. See [Errors and permissions](/api-reference/v2/concepts/errors-and-permissions) for the full semantics, including why a 404 can mean "no access". # CREATE Lease Source: https://docs.prophia.com/api-reference/v2/leases/create-lease /api-reference/openapi.json post /rest/v2/accounts/{accountId}/leases Creates a Lease in the given Building. A Lease is required in order to create a Document **Required capabilities:** `WRITE:LEASE` # GET Lease Source: https://docs.prophia.com/api-reference/v2/leases/get-lease /api-reference/openapi.json get /rest/v2/accounts/{accountId}/leases/{leaseId} Returns a single Lease by id. **Required capabilities:** `READ:LEASE` # LIST Document Source: https://docs.prophia.com/api-reference/v2/leases/list-document /api-reference/openapi.json get /rest/v2/accounts/{accountId}/leases/{leaseId}/docs Returns a paged list of the Lease's Documents. **Required capabilities:** `READ:DOCUMENT` ## Filters Filters combine with AND: a result must match every filter you supply. | Filter | Description | | - | - | | `filename` | Case-insensitive contains match on the document filename. | | `docCategory` | Case-insensitive exact match on the document category label; an unrecognized value returns no results. | | `documentStatus` | Case-sensitive exact match on the review status. | | `numberOfPagesMin` | Minimum page count; inclusive. | | `numberOfPagesMax` | Maximum page count; inclusive. | # LIST Raw Tag Source: https://docs.prophia.com/api-reference/v2/leases/list-raw-tag /api-reference/openapi.json get /rest/v2/accounts/{accountId}/leases/{leaseId}/tags/raw Returns a paged list of the Lease's Tags across all of its Documents, each built from its stored row, in the same shape as LIST Tag. Skips the lease's consolidated view, so a tag change shows up immediately; nothing consolidation derives across documents is applied (amended values, consolidated rent schedules), and an option's child terms are listed as separate Tags rather than nested under it. Rent periods stored by lease term keep `startMonth`/`endMonth` with no `startDate`/`endDate`; only consolidation resolves them to dates from the commencement date. **Required capabilities:** `READ:TAG` ## Filters Filters combine with AND: a result must match every filter you supply. | Filter | Description | | - | - | | `tagName` | Case-insensitive contains match on the tag name. | | `isCustomTag` | Filters to custom tags (true) or standard-definition tags (false); omit for both. | | `lifecycleStatus` | Exact match on the tag's lifecycle status. | | `createdAtAfter` | Tags created at or after this timestamp; inclusive. | | `createdAtBefore` | Tags created at or before this timestamp; inclusive. | # LIST Tag Source: https://docs.prophia.com/api-reference/v2/leases/list-tag /api-reference/openapi.json get /rest/v2/accounts/{accountId}/leases/{leaseId}/tags Returns a paged list of the Tags (structured lease terms) extracted from the Lease's Documents, resolved against the lease's consolidated tag view. That view is rebuilt in the background about 10 minutes after the most recent tag change on the lease and returns the previous values until then; use LIST Raw Tag to see tags as they were just created or edited. **Required capabilities:** `READ:TAG` ## Filters Filters combine with AND: a result must match every filter you supply. | Filter | Description | | - | - | | `tagName` | Case-insensitive contains match on the tag name. | | `isCustomTag` | Filters to custom tags (true) or standard-definition tags (false); omit for both. | | `lifecycleStatus` | Exact match on the tag's lifecycle status. | | `createdAtAfter` | Tags created at or after this timestamp; inclusive. | | `createdAtBefore` | Tags created at or before this timestamp; inclusive. | # UPDATE Lease Source: https://docs.prophia.com/api-reference/v2/leases/update-lease /api-reference/openapi.json patch /rest/v2/accounts/{accountId}/leases/{leaseId} Updates a Lease's fields. Omitted fields are left unchanged; `dba`, when present, cannot be blank. **Required capabilities:** `WRITE:LEASE` # GET Loan Source: https://docs.prophia.com/api-reference/v2/loans/get-loan /api-reference/openapi.json get /rest/v2/accounts/{accountId}/loans/{loanId} Returns a single Loan by id, with its collateral buildings. `status` is the processing status of the loan's documents, not whether the loan is active, matured or paid off. **Required capabilities:** `READ:LOAN` # LIST Loan Source: https://docs.prophia.com/api-reference/v2/loans/list-loan /api-reference/openapi.json get /rest/v2/accounts/{accountId}/loans Returns a paged list of the Loans in the account that the caller can see, each with its collateral buildings. Narrow to the loans secured by a property's buildings or by one building. **Required capabilities:** `READ:LOAN` ## Filters Filters combine with AND: a result must match every filter you supply. | Filter | Description | | - | - | | `propertyId` | Keep only loans with at least one collateral building in this property. | | `buildingId` | Keep only loans with this building among their collateral. | | `name` | Case-insensitive contains match on the loan name. | | `loanType` | Case-insensitive exact match on the loan type, such as 'Construction Loan'; an unrecognized value returns no results. | | `borrower` | Case-insensitive contains match on the borrower. | | `lender` | Case-insensitive contains match on the lender's legal name. | | `closingAfter` | Loans closing on or after this calendar date (yyyy-MM-dd); inclusive. | | `closingBefore` | Loans closing on or before this calendar date (yyyy-MM-dd); inclusive. | | `status` | Exact match on the loan's processing status, ignoring case: whether Prophia has read its documents, not whether the loan is active, matured or paid off. | # CREATE Property Source: https://docs.prophia.com/api-reference/v2/properties/create-property /api-reference/openapi.json post /rest/v2/accounts/{accountId}/properties Creates a Property in the Account. A Property is required in order to create a Building. **Required capabilities:** `WRITE:PROPERTY` # GET Property Source: https://docs.prophia.com/api-reference/v2/properties/get-property /api-reference/openapi.json get /rest/v2/accounts/{accountId}/properties/{propertyId} Returns a single property by id. **Required capabilities:** `READ:PROPERTY` # LIST Building Source: https://docs.prophia.com/api-reference/v2/properties/list-building /api-reference/openapi.json get /rest/v2/accounts/{accountId}/properties/{propertyId}/buildings Returns a paged list of the Property's Buildings. For a list of all Buildings in the Account try this [list buildings endpoint](/api-reference/v2/buildings/list-building). **Required capabilities:** `READ:BUILDING` ## Filters Filters combine with AND: a result must match every filter you supply. | Filter | Description | Requires | | - | - | - | | `name` | Case-insensitive contains match on the building name. | | | `city` | Case-insensitive exact match on the address city. | | | `state` | Case-insensitive exact match on the address state. | | | `activeStatus` | Filter by the building's active status. | | | `customId` | Exact match on the building's custom id. | | | `buildingType` | Filter by building type. | | | `implementationManager` | Case-insensitive contains match on the implementation manager's name. | | | `customFields` | Custom Field filters, one entry per repeated parameter. | `READ:CUSTOM_FIELD` | # LIST Lease Source: https://docs.prophia.com/api-reference/v2/properties/list-lease /api-reference/openapi.json get /rest/v2/accounts/{accountId}/properties/{propertyId}/leases Returns a paged list of the Leases across the Buildings in the Property. **Required capabilities:** `READ:LEASE` ## Filters Filters combine with AND: a result must match every filter you supply. | Filter | Description | | - | - | | `status` | Exact match on the lease status; an unrecognized value returns no results. | | `tenantName` | Case-insensitive contains match against both the tenant DBA and the tenant legal name. | | `expirationBefore` | Leases expiring on or before this calendar date (yyyy-MM-dd); inclusive. | | `expirationAfter` | Leases expiring on or after this calendar date (yyyy-MM-dd); inclusive. | | `leaseType` | Filter by lease type. | | `suiteNumber` | Case-insensitive contains match on the lease's suite names. | | `commenceBefore` | Leases commencing on or before this calendar date (yyyy-MM-dd); inclusive. | | `commenceAfter` | Leases commencing on or after this calendar date (yyyy-MM-dd); inclusive. | | `squareFootageMin` | Minimum square footage; inclusive. | | `squareFootageMax` | Maximum square footage; inclusive. | | `customId` | Exact match on the lease's custom id. | # LIST Property Source: https://docs.prophia.com/api-reference/v2/properties/list-property /api-reference/openapi.json get /rest/v2/accounts/{accountId}/properties Returns a paged list of the properties in the account. **Required capabilities:** `READ:PROPERTY` ## Filters Filters combine with AND: a result must match every filter you supply. | Filter | Description | | - | - | | `propertyName` | Case-insensitive contains match on the property name. | | `customId` | Exact match on the property's custom id. | # UPDATE Property Source: https://docs.prophia.com/api-reference/v2/properties/update-property /api-reference/openapi.json patch /rest/v2/accounts/{accountId}/properties/{propertyId} Updates a single Property by id. Omitted fields are left unchanged. **Required capabilities:** `WRITE:PROPERTY` # GET Raw Tag Source: https://docs.prophia.com/api-reference/v2/tags/get-raw-tag /api-reference/openapi.json get /rest/v2/accounts/{accountId}/tags/{tagId}/raw Returns a single Tag by id built from its stored row, in the same shape as GET Tag. Skips the lease's consolidated view, so a tag change shows up immediately; nothing consolidation derives across documents is applied (amended values, consolidated rent schedules), and an option's child terms are not nested under it. Rent periods stored by lease term keep `startMonth`/`endMonth` with no `startDate`/`endDate`; only consolidation resolves them to dates from the commencement date. The tag's lease must belong to the account in the path and be within your key's building permissions. When this tag amends an earlier one, `amendingTag.amendedTagId` points backwards at the superseded tag and `amendingTag.effectiveDate` is the PRIOR tag's date. **Required capabilities:** `READ:TAG` # GET Tag Source: https://docs.prophia.com/api-reference/v2/tags/get-tag /api-reference/openapi.json get /rest/v2/accounts/{accountId}/tags/{tagId} Returns a single Tag by id, resolved against the lease's consolidated tag view when available. That view is rebuilt in the background about 10 minutes after the most recent tag change on the lease and returns the previous value until then; use GET Raw Tag to see a tag as it was just created or edited. The tag's lease must belong to the account in the path and be within your key's building permissions. When this tag amends an earlier one, `amendingTag.amendedTagId` points backwards at the superseded tag and `amendingTag.effectiveDate` is the PRIOR tag's date. **Required capabilities:** `READ:TAG` # Authentication Source: https://docs.prophia.com/developers/authentication API keys for the REST API and MCP server The Prophia REST API authenticates with **API keys**. The MCP server accepts the same keys, or — recommended for MCP — [OAuth through your organization's identity provider](/developers/mcp/oauth-setup). ## Key types | | Account API key | User API key | | - | - | - | | Scope | One account, not tied to any user — the REST API and MCP permissions chosen when it was created | One user in one account — that user's permissions, or fewer | | Best for | Server-to-server integrations | Per-person tooling, least-privilege access | | Issued via | **Self-serve** — an admin creates it in the web app | **Self-serve** — an admin creates it for any user in the web app | Both are created on the **API/MCP Config** tab in **Account Settings**, under **Authorization Options**. The tab appears for account admins on accounts with the REST API module enabled. On accounts with granular permissions, it also appears for users with REST API or MCP access, who can manage their own user keys there. > [!NOTE] > > On accounts without granular permissions, an account key has full read and write access to the account's data, and a user key has exactly its user's access. The web app doesn't show permission choices on those accounts. ### Creating an account key **1. Select Account Tokens** On the **Authorization Options** card, choose the **Account Tokens** segment, then **Create New Account Token**. **2. Choose its permissions** Every REST API and MCP permission starts selected. Untick anything the key shouldn't be able to do. An account key isn't tied to a user, so these permissions are the whole of what it can do. To check them later, click the key in the list or choose **View Grants** from its menu. **3. Create the key** Give it a description that says where it will be used, then copy the key — it is shown once. ### Creating a user key **1. Select User tokens** On the **Authorization Options** card, choose the **User tokens** segment, then **Create New User Token**. **2. Pick the user** Admins choose which user the key belongs to. Anyone else creating a key creates it for themselves. **3. Choose its permissions** Every REST API and MCP permission the user holds starts selected. Untick anything the key shouldn't be able to do. Permissions the user doesn't hold can't be selected. **4. Create the key** Give it a description, then copy the key — it is shown once. Prefer user keys over account keys where you can: a user key never sees more than that person sees. ## What a key can reach The two key types are gated differently, which matters when a key authenticates fine but a whole surface is closed to it: * **User keys carry scopes.** A key needs the `REST` scope to work against the REST API and the `MCP` scope to work against the MCP server. A key without the right scope is rejected as unauthenticated, even though the key itself is valid. A key created in the web app gets the scope for each surface you selected permissions on, or both scopes on accounts without granular permissions. * **Account keys carry no scopes.** They work on both surfaces, but only while the account has the **REST API module** enabled — the same module gates account-key access to REST *and* to MCP. Underneath either, per-resource permissions still apply. See [Errors and permissions](/api-reference/v2/concepts/errors-and-permissions) for the capabilities model. > [!WARNING] > > Treat API keys like passwords: use one key per application, and revoke keys you no longer use. A key is shown once at creation — store it in your secrets manager. ## Sending the key The REST API accepts the key three ways — the header forms are preferred: ```bash theme={null} curl https://api.prophia.com/rest/v2/accounts/{accountId}/properties \ -H "X-API-Key: " ``` ```bash theme={null} curl https://api.prophia.com/rest/v2/accounts/{accountId}/properties \ -H "Authorization: Bearer " ``` A `?api_key=` query parameter is also accepted, but avoid it — URLs end up in logs. > [!NOTE] > > JWTs from the Prophia web session are **not** accepted on `/rest` endpoints. If you send one you'll get `401 — JWT authentication is not supported. Use an API key.` ## Errors you'll see | Status | Meaning | | - | - | | `401` | A JWT where a key was expected; the body carries only `error` | | `403` | No key, an unknown or revoked key, a user key without the scope for the surface you're calling — or a valid key that lacks a capability (e.g. reading documents) or access to that account | | `404` | Resource isn't in the account your key can see | For the full status table, the error body, and the capabilities model behind `403` — what each key can read and write, and how `include=` affects the check — see [Errors and permissions](/api-reference/v2/concepts/errors-and-permissions). ## Writing as well as reading A key that reads your portfolio cannot write to it by default. `POST`, `PATCH`, and `DELETE` need the account's REST API write module enabled on top of the key's own write capability — see [Write access](/developers/write-access). # Connect AI to These Docs Source: https://docs.prophia.com/developers/docs-mcp Add the Prophia documentation MCP server to Claude, Cursor, or VS Code This documentation site is also an MCP server. Point your AI assistant at it and the assistant can search and read these pages on demand — so answers about the REST API and the MCP tools come from the current documentation rather than from whatever the model happens to remember. ``` https://docs.prophia.com/mcp ``` No account, no key, no configuration. The documentation is public and so is this endpoint. ## Two MCP servers, two jobs Prophia runs two MCP servers and they are easy to mix up: | | Docs MCP server | Prophia MCP server | | - | - | - | | **Exposes** | This documentation | Your portfolio data | | **URL** | `https://docs.prophia.com/mcp` | `https://api.prophia.com/mcp/v1/{your-api-name}` | | **Auth** | None — public | [OAuth](/developers/mcp/oauth-setup) or an [API key](/developers/mcp/api-key-access) | | **Ask it** | *"How do I filter leases by expiration date?"* | *"When does the Initech lease expire?"* | They complement each other. Connect both and an assistant can look up how an endpoint works and then call it against your real portfolio. This page covers the docs server — the [MCP server overview](/developers/mcp/overview) covers the one that reaches your data. ## Connect your client ### Claude Desktop and Claude on the web **1. Open connector settings** Open **Settings → Connectors** and choose **Add custom connector**. **2. Add the server** Name it `Prophia Docs` and enter `https://docs.prophia.com/mcp`. **3. Ask something** Try: *"Using the Prophia docs, what does `include=tags` return?"* There is no sign-in step — unlike the [Prophia MCP server](/developers/mcp/connect-claude), this one serves public content and never prompts for authorization. ### Claude Code ```bash theme={null} claude mcp add --transport http prophia-docs https://docs.prophia.com/mcp ``` ### Cursor Open the command palette (**⌘⇧P** / **Ctrl+Shift+P**), run **Open MCP settings**, and add: ```json theme={null} { "mcpServers": { "prophia-docs": { "url": "https://docs.prophia.com/mcp" } } } ``` ### VS Code Create `.vscode/mcp.json` in your workspace: ```json theme={null} { "servers": { "prophia-docs": { "type": "http", "url": "https://docs.prophia.com/mcp" } } } ``` ### Any other client The server speaks the MCP **Streamable HTTP** transport. Give your client the URL `https://docs.prophia.com/mcp` and no auth headers. > [!TIP] > > The AI menu at the top of every page on this site has one-click shortcuts — **Copy MCP Server**, **Connect to Cursor**, and **Connect to VS Code** — so you can add the server without retyping anything. ## What a connected assistant can do The server gives your assistant three abilities: * **Search** the whole documentation site and get back matching passages with their titles and links * **Browse** the site's page tree — listing and reading pages in bulk rather than guessing at URLs * **Report a problem** with a page as incorrect, outdated, confusing, or incomplete. That feedback lands in our documentation analytics, so telling your assistant *"this page is wrong about pagination"* actually reaches us. Assistants generally reach for these on their own once the server is connected. If yours doesn't, naming the source usually settles it: *"Check the Prophia docs first."* ## Worth asking * *"Using the Prophia docs, how do I page through more than 100 leases?"* * *"What does `include=tags` return, and what's the catch with per-space values?"* * *"Write a Python function that fetches every lease in a building from the Prophia REST API, handling pagination."* * *"Which Prophia MCP tools can write data, and what entitlement do they need?"* * *"What's the difference between an account API key and a user API key?"* ## Without an MCP client If your tool doesn't speak MCP, the same content is available as plain text: | | | | - | - | | [`/llms.txt`](https://docs.prophia.com/llms.txt) | An index of every page, with descriptions — a map for an AI tool to work from | | [`/llms-full.txt`](https://docs.prophia.com/llms-full.txt) | The entire site as one Markdown file, for pasting or fetching wholesale | The per-page AI menu also offers **Copy page as Markdown** and one-click handoffs to Claude and ChatGPT with the current page as context. ## Automatic discovery Clients that discover MCP servers on their own will find this one at: ``` https://docs.prophia.com/.well-known/mcp ``` You don't need to visit it — it exists so that agents pointed at `docs.prophia.com` can locate the server without being handed the URL. # API Key Access Source: https://docs.prophia.com/developers/mcp/api-key-access Connect to the MCP server headlessly with an API key If your MCP client can't drive a browser OAuth login — CI jobs, server-side agents, or clients without OAuth support — connect with an **API key** instead. For most organizations [OAuth](/developers/mcp/oauth-setup) is the better default: users sign in with credentials they already have, each person's own Prophia permissions apply, and there is no long-lived secret to distribute or rotate. ## Step 1 — Get a key | Key type | Access level | How to get one | | - | - | - | | **Account API key** | The REST API and MCP permissions chosen when it was created, whoever is using it | Self-serve. An admin creates it in **Account Settings → API/MCP Config → Authorization Options → Account Tokens**. Keep at least one MCP permission selected. | | **User API key** | The access that user has in Prophia, or less | Self-serve. Create it in **Account Settings → API/MCP Config → Authorization Options → User tokens**, and keep at least one MCP permission selected. | See [Authentication](/developers/authentication) for key handling guidance — the same keys and precautions apply across the REST API and MCP. > [!NOTE] > > Claude **Desktop** does not support API-key headers for this type of MCP server — use [OAuth](/developers/mcp/connect-claude) there. Claude **Code** and most configurable clients support headers. ## Step 2 — Configure the client ### Claude Code ```bash theme={null} claude mcp add prophia \ --transport http \ --header "Authorization: Bearer {your-api-key}" \ https://api.prophia.com/mcp/v1/{your-api-name} ``` ### Cursor / other clients Set these in your client's MCP configuration: * **URL**: `https://api.prophia.com/mcp/v1/{your-api-name}` * **Transport**: Streamable HTTP * **Header**: `Authorization: Bearer {your-api-key}` (an `X-API-Key: {your-api-key}` header also works) ## Verify from the command line A quick smoke test that your key and API Name are right — list the server's tools: ```bash theme={null} curl -s https://api.prophia.com/mcp/v1/{your-api-name} \ -H "Authorization: Bearer {your-api-key}" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` A successful response returns a JSON list of tools. A `401` means the key is wrong, revoked, or — for a user key — missing the `MCP` scope. *Not found* usually means the API Name doesn't match. ## Security tips * API keys grant access to your Prophia data — treat them like passwords * Use a unique key per application, and revoke keys you no longer use * Prefer user keys (least privilege) over account keys where possible — an account key isn't limited by any user's permissions, whoever is holding it * Give each key only the permissions its client needs # Connect Claude to Prophia Source: https://docs.prophia.com/developers/mcp/connect-claude Add the Prophia MCP server to Claude Desktop or Claude Code This page covers Claude Desktop and Claude Code. [Other clients have their own page](/developers/mcp/connect-other-clients), and the [MCP overview](/developers/mcp/overview) explains what a connected assistant can and can't do. ## What you need * A Prophia user account * Your organization's **API Name** — the short identifier for your Prophia account that goes in the server URL * OAuth set up once for your organization — see [OAuth setup](/developers/mcp/oauth-setup). Already done? Continue below. Your server URL is: ``` https://api.prophia.com/mcp/v1/{your-api-name} ``` > [!NOTE] > > An admin on your Prophia account can copy both values from **Account Settings → API/MCP Config**: the **MCP** card shows **API Name** and **Connection URL**, each with a **Copy** button. That's the surest way to get it right. > [!WARNING] > > The API Name must match exactly — it's a lowercase, hyphenated identifier (e.g. `acme-real-estate`) that may differ from your account's display name, and it is case-sensitive. A wrong value will fail to connect. ## Claude Desktop **1. Open connector settings** In Claude Desktop, open **Settings → Connectors** and choose **Add custom connector**. **2. Paste the server URL** Enter `https://api.prophia.com/mcp/v1/{your-api-name}` and save. **3. Sign in** The first time Claude uses a Prophia tool, a browser window opens and redirects to **your organization's own sign-in** (Okta, Azure AD / Entra ID, Auth0, …). Sign in with your corporate credentials. **4. Choose what the connection can access** If your Prophia account uses role-based permissions, Prophia then shows the consent screen that every OAuth client goes through. Uncheck anything you don't want Claude to use and click **Allow Access**. See [Choosing what a connection can access](/developers/mcp/overview#choosing-what-a-connection-can-access). **5. Done** Claude now has access to your Prophia data. Try: *"List the buildings in my portfolio."* > [!NOTE] > > Claude Desktop requires the OAuth connection — it does not support API-key headers for this type of MCP server. ## Claude Code (CLI) ```bash theme={null} claude mcp add prophia --transport http https://api.prophia.com/mcp/v1/{your-api-name} ``` On first use, a browser window opens for the same OAuth sign-in and, if your account uses role-based permissions, the same [consent screen](/developers/mcp/overview#choosing-what-a-connection-can-access). Claude Code can alternatively connect headlessly with an API key — see [API key access](/developers/mcp/api-key-access). ## How the sign-in works There is no client ID or secret to configure. The OAuth flow is fully discovery-driven: your MCP client discovers Prophia's authorization endpoints, registers itself automatically, and completes an authorization-code + PKCE flow that hands login off to **your company's identity provider**. Your Prophia permissions apply to every tool call — you can only see the data your Prophia user can see, and on accounts that use role-based permissions, only what you allowed on the consent screen. ## Example prompts * *"List all my Prophia accounts"* * *"What leases are in 100 Bayside Drive?"* * *"Find all rent escalation clauses in the Initech lease documents"* * *"Search for 'force majeure' across all documents at Bayside Corporate Center"* * *"Which leases had document changes in the last 30 days?"* ## Troubleshooting > [accordion] Authentication required > > Disconnect and reconnect the Prophia connector to re-authenticate. > [accordion] Access denied after login > > The email returned by your identity provider must exactly match the email on your Prophia user account. If they differ, ask an admin at your organization — or Prophia Support — to reconcile them. > [accordion] Server not responding / connection fails > > Check the URL and API Name (case-sensitive, exact match). If your organization hasn't completed [OAuth setup](/developers/mcp/oauth-setup), the connection cannot start — the server returns *not found* until an identity provider is configured for the account. Setting OAuth up and it isn't working? The [OAuth setup page](/developers/mcp/oauth-setup#troubleshooting) has the admin-side checklist, in the order worth checking. # Connect Other MCP Clients Source: https://docs.prophia.com/developers/mcp/connect-other-clients Cursor and any Streamable-HTTP MCP client The Prophia MCP server works with any client that supports the MCP **Streamable HTTP** transport. The configuration is always the same three values: * **Server URL**: `https://api.prophia.com/mcp/v1/{your-api-name}` — an admin on your Prophia account can copy this from **Account Settings → API/MCP Config** * **Transport**: Streamable HTTP * **Auth**: OAuth (discovered automatically) or an API key header For OAuth connections, your organization needs the one-time [OAuth setup](/developers/mcp/oauth-setup) first, and your corporate email must match your Prophia user email exactly. An [API key](/developers/mcp/api-key-access) needs neither. ## Cursor 1. Open Cursor settings and navigate to the **MCP** section. 2. Add a new server: * **Name**: Prophia * **Type**: Streamable HTTP * **URL**: `https://api.prophia.com/mcp/v1/{your-api-name}` 3. On first use you'll be prompted to authenticate in your browser via your organization's sign-in. If your account uses role-based permissions, you then choose what Cursor can access on the [consent screen](/developers/mcp/overview#choosing-what-a-connection-can-access), the same one every OAuth client gets. ## Generic client configuration For clients configured with a JSON file: ```json theme={null} { "mcpServers": { "prophia": { "type": "streamable-http", "url": "https://api.prophia.com/mcp/v1/{your-api-name}" } } } ``` With OAuth, the client discovers Prophia's authorization endpoints automatically and walks you through login, then through the [consent screen](/developers/mcp/overview#choosing-what-a-connection-can-access) if your account uses role-based permissions — there is no client ID or secret to paste. If your client can't drive a browser login, use an [API key](/developers/mcp/api-key-access) instead. ## Staying connected How long you stay signed in depends on how your client registers with Prophia, which it does on its own the first time you connect: * **A client that registers with a client secret** (`token_endpoint_auth_method` of `client_secret_basic` or `client_secret_post`) gets refresh tokens and stays connected without signing in again, up to your organization's maximum connection lifetime. * **A client that registers without one** (`none`) asks you to sign in again whenever its 12-hour access token expires. See [How long a connection lasts](/developers/mcp/oauth-setup#how-long-a-connection-lasts) for the details. # OAuth Setup for Your Organization Source: https://docs.prophia.com/developers/mcp/oauth-setup One-time identity-provider configuration so your users can connect via SSO This is a **one-time setup** performed once for your organization. Once complete, everyone at your company can connect their MCP clients through your corporate sign-in — no API keys to manage, and nothing for individual users to configure beyond a URL. Prophia supports any standards-compliant OIDC identity provider: Okta, Azure AD / Entra ID, Auth0, Google Workspace, Keycloak, and others. ## Who does what The setup spans two admin consoles, and in most organizations two different people. Expect to pass values back and forth between them. | Role | Does | | - | - | | **An admin user for your account** | Copies the redirect URL out of Prophia (Step 1), then registers the finished application back in Prophia (Step 3) | | **Your identity-provider admin** | Creates the OIDC application and assigns users to it (Step 2) | | **Each user** | Pastes a URL into their MCP client and signs in — nothing else | > [!NOTE] > > "Admin" here means an administrator **at your organization**, not a Prophia employee. Prophia Support can complete the registration for you if you'd rather not do it yourself — see [If you get stuck](#if-you-get-stuck). ## Before you start * **Admin access on your Prophia account.** The setup lives on the **API/MCP Config** tab in Account Settings, which appears only for account admins on accounts with API access enabled. If you don't see the tab, contact Prophia Support to confirm API access is enabled. * **Admin access to your identity provider**, so you can create an OIDC application. > [!NOTE] > > **Setup is per Prophia account.** If your company has more than one Prophia account, repeat these steps for each — every account has its own API Name and its own redirect URL. ## How the connection flow works Once setup is complete, this is what happens when a user connects an MCP client — no client IDs or secrets ever reach the user: ```mermaid theme={null} sequenceDiagram autonumber participant Client as MCP client (Claude, Cursor, …) participant Prophia as Prophia MCP server participant IdP as Your identity provider Client->>Prophia: Connect to /mcp/v1/{api-name} Prophia-->>Client: 401 + OAuth discovery metadata Client->>Prophia: Register client automatically (RFC 7591) Prophia-->>Client: client_id, plus a client secret if the client asked for one Client->>Prophia: Authorization request (browser opens) Prophia->>IdP: Redirect to your corporate sign-in IdP-->>Prophia: Signed-in user (email claim) Prophia->>Prophia: Consent screen: the user chooses what the client can access Prophia-->>Client: Access token, plus a refresh token if the client has a secret Client->>Prophia: Tool calls with the user's Prophia permissions ``` The user experiences steps 1–9 as a single browser prompt: they sign in with their work account and, on accounts that use role-based permissions, go through the consent screen (step 8). Every OAuth client gets the consent screen, whichever one the user connects. There the user chooses which Prophia capabilities the client gets, and every tool call on that connection is limited to them. A client with a refresh token keeps that choice when it refreshes. A client without one repeats steps 5–9 when its access token expires after 12 hours, consent screen included, and so does any client once the connection reaches its maximum lifetime. See [Choosing what a connection can access](/developers/mcp/overview#choosing-what-a-connection-can-access) and [How long a connection lasts](#how-long-a-connection-lasts). ### How long a connection lasts * **Access tokens last 12 hours.** A client that registered without a client secret signs the user in again when its access token expires. * **A client that registered with a client secret also gets a refresh token**, which it uses to get new access tokens without the user signing in again. Each refresh token lasts 30 days and is replaced every time it's used, so a connection left unused for 30 days ends. * **Every connection ends at your account's maximum connection lifetime**, counted from the user's sign-in, however actively it's used. The client can't refresh past that point, but the access token it already has keeps working until it expires, so a connection can last up to 12 hours longer. After it ends, the user signs in again through your IdP. The lifetime defaults to 30 days. To change it, enter any number of days from 1 to 180 in the **Maximum connection lifetime (days)** field on the **OAuth client for MCP** card (Account Settings → **API/MCP Config** → **Authorization Options** → **MCP OAuth**). ## Step 1 — Copy your redirect URL from Prophia Start in Prophia, not in your IdP — you need the redirect URL before you can create the OIDC application. **1. Open the API/MCP Config tab** In Prophia, go to **Account Settings** and open the **API/MCP Config** tab. **2. Select MCP OAuth** Scroll to the **Authorization Options** card and select the **MCP OAuth** segment. **3. Copy the redirect URL** In the **OAuth client for MCP** card, find **Redirect URL** and click **Copy**. The value is specific to your account and looks like this: ``` https://yosemite-api.prophia.com/mcp/auth/login/oauth2/code/{your-api-name} ``` > [!WARNING] > > **Copy it from the app rather than typing it.** Note the host: the customer-facing MCP URL uses `api.prophia.com`, but the redirect URL uses `yosemite-api.prophia.com`. Registering the wrong host is the most common *configuration* mistake, and it fails silently until a user tries to log in. ## Step 2 — Create an OIDC application in your IdP In your identity provider's admin console, create a new OIDC / OAuth2 application for Prophia: | Setting | Value | | - | - | | **Application type** | Web application, confidential or public. See [Client secret or PKCE?](#client-secret-or-pkce) below before you choose. | | **Redirect URI** | The redirect URL you copied in Step 1. Okta calls this field *Sign-in redirect URIs*. | | **Grant type** | Authorization Code | | **Scopes** | `openid`, `profile`, `email` | | **ID token claims** | Must include the user's `email` | The application you create represents **Prophia** — not Claude, not Cursor. You create one no matter how many users or clients you have. > [!NOTE] > > On some identity providers — Okta custom authorization servers in particular — the `email` claim has to be added to the ID token explicitly rather than relying on the `email` scope. ### Client secret or PKCE? Prophia supports both. Your IdP registration must match the **Public client (PKCE) — no client secret** checkbox you set in Prophia in Step 3; if the two disagree, login fails. **A confidential client with a client secret is the recommended setup**, and the more widely supported of the two. Prophia's server is what talks to your IdP, so it can hold a secret safely. Register the app as a confidential / web application, copy the client secret your IdP generates, and leave the PKCE box unchecked in Prophia. **Use a public client with PKCE** if your IdP won't issue a secret for the app type you need, or your security policy prohibits sharing a client secret with a third party. PKCE (Proof Key for Code Exchange) replaces the stored secret with a one-time challenge generated per login, so there is no shared credential to distribute or rotate. Register the app as a public client with PKCE required, and check the PKCE box in Prophia. If your IdP showed you a client secret to copy, you have a confidential client — public clients don't get one. You can switch later by changing the registration in your IdP and updating the matching setting in Prophia. ### Assign your users to the application Most identity providers won't let anyone sign in to an application they haven't been assigned to. In Okta this is the **Assignments** tab; in Entra ID it's **Users and groups** under **Enterprise applications**, and if the app's **Assignment required?** setting is Yes, unassigned users are refused at the Microsoft sign-in screen. Assign every user or group who should be able to reach Prophia from an AI assistant. > [!WARNING] > > Skipping this produces a login that fails immediately after the IdP screen appears, and it is the most common cause of a setup that looks correct but doesn't work. ### Email matching is required The `email` claim returned by your IdP must exactly match the email on an **existing** Prophia user account. Prophia does not create users on the fly — a user who doesn't already exist in Prophia is rejected with *access denied*, however successfully they signed in. ### Microsoft Entra ID (Azure AD) Here are some typical pitfalls and suggested settings when creating an Entra ID app registration for connecting to Prophia's MCP server. **1. Register the app on the Web platform** In **App registrations → your app → Authentication**, add the Prophia redirect URL under the **Web** platform. > [!WARNING] > > **Do not use "Single-page application."** Entra treats a redirect URI on that platform as a public client and won't let Prophia's server complete the sign-in, with or without a client secret. * Leave **Allow public client flows** set to **Disabled**. * Leave the implicit-grant checkboxes (Access tokens, ID tokens) unchecked. **2. Create a client secret and copy its Value** In **Certificates & secrets → New client secret**, copy the **Value** column, not **Secret ID**. * Entra shows the value only once, right after you create it. If you didn't copy it then, create a new secret. * A secret value is about 40 characters and contains `Q~` near the start, for example `abc8Q~…`. If what you have is a GUID, it's the Secret ID. If it has no `~`, part of it was lost when copying. * Note the expiry date. When the secret expires, logins stop working until you create a new one and save it in Prophia. **3. Find your issuer URL and client ID** * **Issuer URL:** `https://login.microsoftonline.com/{Directory (tenant) ID}/v2.0`, using your tenant ID from the app's Overview page. Don't use `common` or `organizations`. * **Client ID:** the **Application (client) ID** on the app's Overview page. This is a confidential client, so leave the PKCE box unchecked when you enter these values in Step 3. **4. Make sure each user's primary email matches Prophia** Entra fills the `email` claim from the user's **primary email address** (the `mail` attribute), which is not always the sign-in name (UPN). * The primary email must exactly match the email on the user's Prophia account. * **Aliases don't count.** Secondary addresses and proxy addresses aren't sent in the token, so an alias that matches Prophia won't help. * If a user signs in successfully at Microsoft but then gets *access denied*, this mismatch is almost always the reason. Change the user's primary email in Entra or the email in Prophia so they match. ### Collect these values When you're done, take these from your IdP to Step 3: | Field | What it is | Example | | - | - | - | | **Issuer URL** | Base URL of your IdP's OIDC discovery document, *without* `/.well-known/openid-configuration` | `https://acme.okta.com/oauth2/default` | | **Client ID** | The OAuth2 client ID your IdP issued | `0oa1b2c3d4e5f6g7h8i9` | | **Client Secret** | The secret your IdP issued. Skip this only if you registered a public client using PKCE. | — | > [!WARNING] > > **Confirm the issuer URL before you move on.** Many IdPs expose more than one. Okta uses `https://your-org.okta.com` for the org authorization server and `https://your-org.okta.com/oauth2/default` for the default custom one, and only one is right for your application. Open `{issuer-url}/.well-known/openid-configuration` in a browser and check that the `issuer` value in the response matches exactly what you're about to paste into Prophia. ## Step 3 — Register the application with Prophia Return to the **OAuth client for MCP** card — Account Settings → **API/MCP Config** → **Authorization Options** → **MCP OAuth** — and fill in the form: | Field | What to enter | | - | - | | **Issuer URL** | The issuer URL from Step 2. Must start with `http://` or `https://`, or Prophia rejects it. | | **Client ID** | The client ID from Step 2. | | **Public client (PKCE) — no client secret** | Check this **only** if you registered a public client using PKCE. Checking it hides the client secret field. | | **Client secret** | The client secret from Step 2. Required unless you checked the PKCE box. Stored securely and never shown again — on return visits you'll see a masked hint. | | **Maximum connection lifetime (days)** | How many days, from 1 to 180, a user's connection lasts after they sign in before they have to sign in again. Defaults to 30. See [How long a connection lasts](#how-long-a-connection-lasts). | | **Enabled** | Leave checked to turn the OAuth connection on. Uncheck to save the configuration without activating it. | Click **Create**. On success you'll see a confirmation that the OAuth client configuration was saved, and the **Remove** button appears alongside **Save changes**. > [!WARNING] > > **The PKCE checkbox must match how you registered the application in your IdP.** A confidential client with the box checked, or a public client with it unchecked, fails at login. If your IdP showed you a client secret to copy, you have a confidential client — leave the box unchecked. The **Create** button stays greyed out until the issuer URL and client ID are both filled in, the issuer URL starts with `http://` or `https://`, and either a client secret is entered or the PKCE box is checked. When you return later to edit an existing configuration, the secret already stored also satisfies that last condition, so **Save changes** stays available without re-typing it. Prefer not to do this yourself? Send the three values from Step 2 to your Prophia contact or Prophia Support and they'll complete the registration for you. ## Step 4 — Verify There is no test button, and a successful save means the details were *stored*, not that they're *correct*. Verify end to end. Ask a Prophia user at your organization — yourself, if you have an account — to connect an MCP client to `https://api.prophia.com/mcp/v1/{your-api-name}` following [Connect Claude](/developers/mcp/connect-claude) or [Connect other clients](/developers/mcp/connect-other-clients). They should be redirected to your IdP's login page. After signing in, and choosing what the client can access if your account uses role-based permissions, they should be able to use Prophia tools from their assistant. If login fails, work through [Troubleshooting](#troubleshooting) in order. ## Changing or removing the configuration later Return to the same card: * **Update a value** — edit the field and click **Save changes**. * **Rotate the client secret** — create the new secret in your IdP first, paste it in, and click **Save changes**. Leaving the field blank keeps the secret currently stored, so you can change other fields without re-entering it. Remove the old secret from your IdP once you've verified the new one works. * **Turn the connection off** — uncheck **Enabled** and click **Save changes**. Your details are kept, so you can switch it back on later. * **Delete the configuration** — click **Remove** and confirm. This deletes the stored OIDC details entirely; you'd need to re-enter them to restore OAuth access. API key connections are unaffected. > [!NOTE] > > **Turning the connection off, removing it, or changing the issuer URL or client ID ends existing connections.** Users who are already connected keep working until their current access token expires, at most 12 hours, and then have to sign in again. A new maximum connection lifetime also applies to existing connections, so lowering it ends any connection already older than the new limit in the same way. Rotating the client secret or editing other settings doesn't affect them. ## Troubleshooting ### OAuth login opens but fails Check these in order — the first two account for most failures: **1. Are your users assigned to the OIDC application in your IdP?** An unassigned user fails right after the IdP login screen. This is the most common cause. **2. Does the redirect URI in your IdP exactly match the Redirect URL on the card?** Including the `yosemite-api.prophia.com` host. Copy it again from Prophia and compare character for character. **3. Is the issuer URL right?** Fetch `{issuer-url}/.well-known/openid-configuration` and confirm the `issuer` in the response matches what you entered. **4. Does your IdP registration match the PKCE checkbox?** A confidential client with the box checked, or a public client with it unchecked, will fail. **5. Is Enabled checked and saved?** On the **OAuth client for MCP** card. **6. Does the ID token include an email claim that matches a Prophia user?** Your IdP's system log will usually show the claims it issued. ### If you use Microsoft Entra ID Also check these, in order: **1. Is the redirect URI on the Web platform?** Not **Single-page application**. Entra won't let Prophia's server complete the sign-in from a redirect URI registered there. **2. Is Allow public client flows disabled?** In **App registrations → your app → Authentication**. **3. Is the client secret in Prophia the secret's Value, and is it still valid?** Not the **Secret ID**. Check the expiry date in **Certificates & secrets**. **4. Does the user's primary email in Entra exactly match their Prophia email?** Entra sends the primary email (the `mail` attribute), not the sign-in name or any alias. ### Other problems > [accordion] I don't see the API/MCP Config tab > > The tab appears only if you're an **admin** on the Prophia account you're currently viewing, and that account has API access enabled. If you have access to more than one account, check you're focused on the right one. If you're an admin and still don't see it, contact Prophia Support to confirm API access is enabled. > [accordion] The MCP card shows an API Name of "Unknown" > > Your account doesn't have one configured yet, and the connection URL shown alongside it will not work. Contact Prophia Support to have one set before continuing. > [accordion] Prophia rejects the issuer URL when I save > > It must start with `http://` or `https://`. A bare hostname like `acme.okta.com` is rejected. > [accordion] Login succeeds but the user gets "access denied" > > The email your IdP returns doesn't match that user's Prophia email, or they don't have a Prophia user account at all. Prophia won't create one for them. ### If you get stuck If you can't reach the **API/MCP Config** tab, or setup fails and none of the above helps, contact your Prophia contact or Prophia Support and we'll configure it for you. # MCP Server Overview Source: https://docs.prophia.com/developers/mcp/overview What the Prophia MCP server is, what a connected assistant can do, and what it can't Prophia operates a **hosted, remote MCP server** that exposes your portfolio — accounts, properties, buildings, spaces, leases, documents, and the terms extracted from them — as tools an AI assistant can call. There is nothing to install or run on your side. **You bring your own AI.** Prophia provides the data connection, not the model or the chat interface. Any client that speaks the MCP **Streamable HTTP** transport works. > [!NOTE] > > This is the server that reaches **your portfolio data**. The documentation site runs a separate, public MCP server that lets an assistant search **these pages** — see [Connect AI to these docs](/developers/docs-mcp). > [card] [Set up OAuth](/developers/mcp/oauth-setup) > > One-time, org-wide. An admin connects your identity provider so everyone signs in with corporate credentials. > [card] [Connect a client](/developers/mcp/connect-claude) > > Claude, Cursor, or anything else that speaks Streamable HTTP. ## Two ways to connect | | OAuth | API key | | - | - | - | | **Users sign in with** | Your corporate SSO | A shared secret string | | **Users choose what the client can access** | Yes, on accounts that use [role-based permissions](#choosing-what-a-connection-can-access), whichever client connects | No — the key's own access applies | | **One-time admin setup** | Yes — an admin at your organization [registers your identity provider](/developers/mcp/oauth-setup) in Prophia | Yes — an admin [generates the key](/developers/mcp/api-key-access) | | **Works with Claude Desktop** | **Yes** | **No** | | **Works with Microsoft Copilot Studio** | **No** | **Yes** | | **Works with Claude Code, Cursor, other clients** | Yes | Yes | | **Best for** | Most organizations, especially with multiple users | Single-user or scripted setups where the client can't do OAuth | With OAuth, users sign in with credentials they already have, and there is no long-lived secret to distribute or rotate. If your goal is to connect Claude Desktop, you must use OAuth; if it is Copilot Studio, you must use an API key. ## What your assistant can do Ask questions in natural language and the assistant picks the tools. In practice that covers four things: | | For example | | - | - | | **Navigate the portfolio** | "Which buildings are in Bayside Corporate Center, and what's the stacking plan?" | | **Read lease terms** | "When does the Initech lease expire, and what are the renewal options?" | | **Search document text** | "Find the assignment clause in the Initech lease and quote it, with the page it's on." | | **Query analytics** | "What's my occupied square footage by property?" | Every answer traces back to a source document — tools return the page and the position on it, so the assistant can cite rather than paraphrase. ## What it cannot do The MCP server is **read-only, with two exceptions**: your assistant can manage [custom terms](/api-reference/v2/concepts/tags-and-values) and save portfolio reports. Everything else is read-only. If you need to write portfolio data programmatically, that's the [REST API](/api-reference/v2/introduction), which has its own [write access](/developers/write-access) entitlement. ## Permissions Your Prophia permissions apply to every tool call. The same [capabilities model](/api-reference/v2/concepts/errors-and-permissions#the-capabilities-model) that governs the REST API governs MCP: * **Connecting over OAuth** gives the assistant at most what *you* can see. Building-level permissions filter the results, so two people at the same company can ask the same question and get different answers. On accounts that use role-based permissions, every OAuth connection is further limited to what you [allow when you connect](#choosing-what-a-connection-can-access), whichever client you use. * **Connecting with an account API key** gives the assistant admin-level access to the whole account, whoever is typing. * Tools appear in the list whether or not you can use them. A tool you lack the capability for fails at call time with a message naming what's missing — it isn't hidden from the list. ### Choosing what a connection can access On accounts that use role-based permissions, every OAuth connection goes through a consent screen after you sign in. The client makes no difference: Claude Desktop, Claude Code, Cursor and every other MCP client get the same screen. It names the client and the Prophia account, and lists what the connection may read and, for custom terms and portfolio reports, change. * **Everything you can access starts selected.** Uncheck anything you don't want the client to use. A capability you don't hold is greyed out: a connection can't do more than you can. * **Allow Access** requires at least one capability. Every tool call on the connection is then limited to what you allowed, on top of your own permissions. Account admins are limited the same way. * **Cancel** stops the connection, and your client reports that access was denied. Each connection gets its own choice, and Prophia doesn't remember it for the next one. A client that registered with a client secret refreshes its access token without signing in again and keeps the choice until the connection reaches your account's [maximum connection lifetime](/developers/mcp/oauth-setup#how-long-a-connection-lasts). A client that registered without one signs in again when its 12-hour access token expires, and the consent screen appears again. To change what a client can access before then, disconnect and reconnect it. On accounts that don't use role-based permissions there is no consent screen, and the connection gets everything you can see. [API key](/developers/mcp/api-key-access) connections never show it. ## Built-in guides The server publishes its own documentation as MCP **resources** — `guide://` documents your assistant reads on demand to use the tools well. Assistants discover them with `list_guides` and fetch them with `read_guides`, usually without being asked. | Guide | Covers | | - | - | | `guide://navigation/conventions` | Id formats, naming, and how the hierarchy fits together | | `guide://navigation/patterns` | Getting from a tenant name to the right lease efficiently | | `guide://navigation/tools` | Which navigation tool to reach for | | `guide://smart-search/strategies` | Phrasing document searches that actually hit | | `guide://stacking-plan/reading` | Interpreting stacking plan output | | `guide://portfolio-analytics/querying` | Composing analytics queries without inflating aggregates | | `guide://save-portfolio-report/filters` | Filter syntax for saved reports | | `guide://schema/index` | Result schema for each tool | This is why the server tends to work well without prompt engineering on your side: the instructions travel with the tools. ## Next steps > [card] [OAuth setup](/developers/mcp/oauth-setup) > > For the admin doing the one-time identity-provider configuration. > [card] [Connect Claude](/developers/mcp/connect-claude) > > Claude Desktop and Claude Code. > [card] [Other clients](/developers/mcp/connect-other-clients) > > Cursor and any Streamable-HTTP client. > [card] [Tools reference](/developers/mcp/tools-reference) > > Every tool the server exposes. # Tools Reference Source: https://docs.prophia.com/developers/mcp/tools-reference Every tool the Prophia MCP server exposes, by category Once connected, your assistant can call these tools. Your Prophia permissions apply to every call — a tool appears in the list even if a given user lacks the capability to use it, in which case the call is denied with a message naming what's missing. The **Requires** column names the [capability](/api-reference/v2/concepts/errors-and-permissions#the-capabilities-model) each tool checks, the same model the REST API uses. Building-level permissions apply underneath, filtering which rows come back. ## Portfolio navigation | Tool | What it does | Requires | | - | - | - | | `list_accounts` | List the Prophia accounts you can access | Account read | | `get_account` | Details of one account | Account read | | `list_properties_for_account` | Properties in an account | Property read | | `get_property` | Details of one property | Property read | | `list_buildings_for_account` | Buildings across the account | Building read | | `list_buildings_for_property` | Buildings in one property | Building read | | `get_building` | Details of one building | Building read | | `get_stacking_plan_for_property` | Floor-by-floor stacking plan data | Property, building space, and lease read | | `recent_changed_leases` | Leases whose documents changed in a date range | Lease read | ## Leases | Tool | What it does | Requires | | - | - | - | | `get_lease` | Details of one lease | Lease read — plus document read with `include=documents`, tag read with `include=tags` | | `list_leases_for_property` | Leases across a property | Lease read | | `list_leases_for_building` | Leases in a building | Lease read | | `get_lease_doc_list` | Documents attached to a lease | Document read | | `get_tags_by_lease` | Extracted terms (tags) for a lease | Lease and tag read | | `get_raw_tags_by_lease` | A lease's tags built from their stored rows, without consolidation — see [consolidated vs. raw](#consolidated-vs-raw-tag-reads) | Lease and tag read | ## Documents & search | Tool | What it does | Requires | | - | - | - | | `get_doc_pages` | Page inventory of a document | Document read | | `get_doc_status_list` | The document statuses an account uses (`incomplete`, `complete`, and any custom statuses), for filtering `get_lease_doc_list` | Document read | | `get_doc_page_text` | OCR text of a page | Document read | | `get_doc_page_image` | Page image | Document read | | `smart_search` | Full-text search scoped to account, property, lease, or document | Document read | | `search_text_bounding_boxes` | Locate exact text positions on a page | Document read | | `get_tags_by_lease_doc` | Tags extracted from one document | Lease and tag read | | `get_raw_tags_by_lease_doc` | One document's tags built from their stored rows, without consolidation | Lease and tag read | ## Tags & custom terms | Tool | What it does | Requires | | - | - | - | | `get_doc_tag` | One tag with its citation | Tag read | | `get_raw_doc_tag` | One tag built from its stored row, without consolidation | Tag read | | `list_tag_definitions` | The term library for the account | Tag read | | `list_custom_terms` | List the account's custom terms | Custom term read | | `get_custom_term` | Details of one custom term | Custom term read | | `create_custom_term` | Define a new custom term | Custom term **write** | | `update_custom_term` | Change a custom term | Custom term **write** | | `delete_custom_term` | Remove a custom term | Custom term **write** | ### Consolidated vs. raw tag reads `get_doc_tag`, `get_tags_by_lease`, and `get_tags_by_lease_doc` return each tag as part of the lease's **consolidated** view, the same one the Prophia app shows. Consolidation combines terms across a lease's documents: amendments override the values they amend, rent schedules are merged, and an option's child terms are nested under it. Prophia rebuilds this view in the background about 10 minutes after the most recent tag change on a lease, so for that window these tools can still return a tag's previous value. The `get_raw_*` tools take the same arguments and return the same shape, but build each tag from its stored row alone. A tag change shows up in them immediately. They apply nothing that consolidation derives across documents, and an option's child terms come back as separate tags rather than nested under it. Rent periods stored by lease term ("months 1–12") keep `startMonth` and `endMonth` with no `startDate` or `endDate`; only consolidation resolves them to dates from the commencement date. Use the consolidated tools for a lease's effective terms. Use the raw tools when you need a tag exactly as it was just created or edited. ## Building spaces | Tool | What it does | Requires | | - | - | - | | `list_building_spaces` | Spaces (suites/floors) in a building | Building space read | | `get_building_space` | Details of one space | Building space read | ## Portfolio analytics & reporting | Tool | What it does | Requires | | - | - | - | | `portfolio_analytics_get_workspace_info` | The account's analytics workspace | Portfolio report read | | `portfolio_analytics_list_workspace_metrics` | Available metrics | Portfolio report read | | `portfolio_analytics_list_workspace_attributes` | Available attributes (dimensions) | Portfolio report read | | `portfolio_analytics_compute_label_elements` | Values of an attribute | Portfolio report read | | `portfolio_analytics_execute_query` | Run an analytics query | Portfolio report read | | `portfolio_analytics_fetch_execution_data` | Fetch query results | Portfolio report read | | `portfolio_analytics_ai_search` | Natural-language analytics search | Portfolio report read | | `save_portfolio_report` | Save a filtered portfolio report | Portfolio report **write** | ## Built-in guides The server publishes its own documentation as MCP **resources** — `guide://` documents covering navigation patterns, search strategies, analytics querying, and per-tool result schemas. Assistants consult them automatically, and they need no capability. | Tool | What it does | Requires | | - | - | - | | `list_guides` | List the guides available to the caller | — | | `read_guides` | Read one or more guides by uri | — | The [MCP overview](/developers/mcp/overview#built-in-guides) lists what each guide covers. ## The only writes Everything above is read-only except the four tools marked **write**: the three `*_custom_term` tools and `save_portfolio_report`. Tag, lease, document, building space, and note writes are not exposed to customer connections at all — see [what your assistant can't do](/developers/mcp/overview#what-it-cannot-do). ## Example prompts * *"Show me the stacking plan for Bayside Corporate Center"* * *"Which leases expire in the next 18 months, by building?"* * *"Search all Initech documents for the assignment clause and quote it"* * *"What's my occupied square footage by property?"* # Integration Overview Source: https://docs.prophia.com/developers/overview Two ways to work with Prophia data: the MCP server and the REST API Prophia exposes your portfolio data through two integration surfaces. Both are hosted by Prophia, both are read-optimized views of the same underlying data, and both scope everything to an **account**. ## Choose your surface > [card] [MCP Server](/developers/mcp/overview) > > Connect AI assistants (Claude, Cursor, any MCP client) to your Prophia data. Best when a person is asking questions in natural language. > [card] [REST API](/api-reference/v2/introduction) > > Versioned JSON API for programmatic integration — sync leases into your data warehouse, feed your BI stack, automate reporting. | | MCP Server | REST API | | - | - | - | | Base URL | `https://api.prophia.com/mcp/v1/{api-name}` | `https://api.prophia.com/rest/v2` | | Consumer | AI assistants / MCP clients | Your code | | Auth | OAuth via your IdP (recommended) or API key | API key | | Shape | Tools the model calls (`get_lease`, `smart_search`, …) | Resources (`/accounts/{accountId}/leases`, …) | | Writes | Custom terms and saved reports only | Full read/write, with the [write module](/developers/write-access) enabled | ## The account scope Every request — MCP or REST — operates inside one account: * REST paths embed the account id: `/rest/v2/accounts/{accountId}/properties` * The MCP URL embeds your account's **API Name**: `/mcp/v1/acme-real-estate` Your credentials determine which accounts you can reach, and per-resource permissions (property, lease, document, tag) apply underneath — two users connected to the same account can see different data. Both surfaces expose the same hierarchy you see in the web application, so if you know where something lives in Prophia you know where to find it here. The [data model](/api-reference/v2/concepts/data-model) page has the entity diagram, field conventions, and how tags reference spaces by suite name. ## Getting started > [card] [Authentication](/developers/authentication) > > API keys, what each type can reach, and how to send them. > [card] [MCP server](/developers/mcp/overview) > > What a connected assistant can do, and how to connect one. > [!NOTE] > > There is a third, separate MCP server that serves **this documentation** rather than your data — public, no credentials, and useful alongside either surface above. See [Connect AI to these docs](/developers/docs-mcp). # Write access Source: https://docs.prophia.com/developers/write-access The module and capability a REST key needs before POST, PATCH, and DELETE will work Read access and write access to the **REST API** are separate entitlements. An API key that reads your whole portfolio cannot create a property until write access is turned on for the account. > [!NOTE] > > This page is about the REST API. The [MCP server](/developers/mcp/overview) is read-only apart from custom terms and saved portfolio reports, and those two do **not** require the write module — they need only the matching write capability. ## Two gates, both of which must pass Every `POST`, `PATCH`, and `DELETE` in the [API reference](/api-reference/v2/introduction) is checked twice: **1. The account must have the REST API write module enabled** A per-account entitlement. It is checked before anything else, and it is not something you can grant yourself. **2. The key must carry the write capability for that resource** The ordinary [capabilities model](/api-reference/v2/concepts/errors-and-permissions#the-capabilities-model) — write on property, building, building space, lease, document, tag, or custom tag, depending on the endpoint. Each endpoint page states which it needs. Both failures return `403`. If reads work and writes return `403` across the board — every resource, every endpoint — the module is the thing to check first. If only *some* writes fail, it's the capability, and the message names which one is missing. ## Getting the module turned on The REST API write module is **not self-service**. A Prophia employee has to enable it on your account, and because it is a paid add-on rather than a default, that starts as a commercial conversation. > [!NOTE] > > To enable write access, talk to your Prophia account manager or the sales team. Support cannot flip it on request — it is negotiated as part of your agreement, then enabled by Prophia. Two details worth knowing when you ask: * **Write depends on read.** The write module only takes effect on an account that already has the REST API module enabled. Both are needed. * **It is explicitly opt-in.** Unlike most modules, write access is never granted implicitly by a broader "all access" entitlement on the account. It is always a deliberate, separate switch. # Welcome to Prophia Source: https://docs.prophia.com/index Documentation for the Prophia platform, API, and MCP server Prophia turns commercial real estate leases into structured, searchable portfolio data. Upload your lease documents and Prophia extracts the terms that matter — rent schedules, options, key dates, clauses — and keeps them at your fingertips across your whole portfolio. ## Find what you need > [card] [Developers](/developers/overview) > > Connect AI assistants through the Prophia MCP server, or integrate directly with the REST API. > [card] [API Reference](/api-reference/v2/introduction) > > Every endpoint of the Prophia REST API, with an interactive playground to try calls live. > [!TIP] > > Prefer to ask rather than browse? These docs are an MCP server — point Claude, Cursor, or VS Code at `https://docs.prophia.com/mcp` and your assistant can search them for you. [Connect AI to these docs](/developers/docs-mcp). ## How Prophia is organized Everything in Prophia hangs off a simple hierarchy: ``` Account → Properties → Buildings → Leases → Documents → Tags ``` An **account** is your organization's portfolio. It contains **properties** (assets), each with one or more **buildings**. Every tenant's **lease** lives in a building and carries its source **documents** — and Prophia extracts **tags** (structured lease terms) from those documents. This hierarchy is the same one the [REST API](/api-reference/v2/introduction) and [MCP server](/developers/mcp/connect-claude) expose, so knowing where something lives in the web application tells you where to find it in either integration. The [data model](/api-reference/v2/concepts/data-model) page has the full diagram and field-level detail.