EXR Commercial Developer Documentation

EXR Commercial API

Version: 1.0.0
Base URL: https://cre.exrplatform.com/api/v1
OpenAPI specification: https://cre.exrplatform.com/api/v1/openapi.json

Source precedence

The OpenAPI document is authoritative for operations, parameters, schemas, scopes, and response contracts. This guide explains how to use that contract; do not infer undocumented fields or behavior.

Authentication

Send Authorization: Bearer exr_live_… on every authenticated request. Missing, invalid, or revoked tokens return 401. Documentation is public; API data access still requires a token with the required scope.

Example request

curl -H "Authorization: Bearer exr_live_..." \
  https://cre.exrplatform.com/api/v1/deals?limit=25

Operations

GET `/api/v1/health`

Liveness probe. No authentication required.

GET `/api/v1/deals`

Returns deals visible to the token's owning user, newest first. Cursor-paginated. Requires the deals:read scope. Financial fields are included only when the token also has deals:financials:read.

- Authentication: Bearer; scope deals:read
- Idempotency: Not applicable.
- Parameters:
- limit (query; integer) — Maximum number of deals to return (1–100).
- cursor (query; string) — Opaque pagination cursor from a previous response's nextCursor.
- Success response: object

POST `/api/v1/deals`

Creates a deal owned by the token's user. Requires the deals:write scope. The owner is credited the full 100% commission split; split edits are made via PATCH. Money fields are strings of integer cents. Idempotent via the Idempotency-Key header.

- Authentication: Bearer; scope deals:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- IdempotencyKey
- JSON request: DealCreate
- Success response: object

GET `/api/v1/deals/{id}`

Returns a single deal by id. Responds 404 (not 403) when the deal does not exist OR is not visible to the token, so existence is never disclosed. Requires the deals:read scope.

- Authentication: Bearer; scope deals:read
- Idempotency: Not applicable.
- Parameters:
- id (path; string), required
- Success response: object

PATCH `/api/v1/deals/{id}`

Partially updates a deal. Requires the deals:write scope. Authorizes owner, collaborator, or admin; a missing or not-editable deal responds 404. Only the fields you send are changed. Commission splits are set via the splits block, which must describe exactly the current collaborator set and total 100% (re-validated under a row lock).

- Authentication: Bearer; scope deals:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- id (path; string), required
- IdempotencyKey
- JSON request: DealUpdate
- Success response: object

DELETE `/api/v1/deals/{id}`

Deletes a deal. Requires the deals:write scope. Owner or admin only; a collaborator or non-member responds 404 to conceal existence.

- Authentication: Bearer; scope deals:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- id (path; string), required
- IdempotencyKey
- Success response: object

GET `/api/v1/tenants`

Returns Commercial Tenants visible to the token's owning user, newest first. Cursor-paginated. Requires the tenants:read scope. A tenant with no relationship owner is firm-wide; otherwise visibility is the creator, lead owner, or collaborators (admins see all).

- Authentication: Bearer; scope tenants:read
- Idempotency: Not applicable.
- Parameters:
- limit (query; integer) — Maximum number of tenants to return (1–100).
- cursor (query; string) — Opaque pagination cursor from a previous response's nextCursor.
- Success response: object

POST `/api/v1/tenants`

Creates a Commercial Tenant owned by the token's user (its creator). Requires the tenants:write scope. Emits a tenant.created webhook. Send an Idempotency-Key to make retries safe.

- Authentication: Bearer; scope tenants:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- IdempotencyKey
- JSON request: TenantWrite
- Success response: object

GET `/api/v1/tenants/{id}`

Returns a single tenant by id. Responds 404 (not 403) when the tenant does not exist OR is not visible to the token. Requires the tenants:read scope.

- Authentication: Bearer; scope tenants:read
- Idempotency: Not applicable.
- Parameters:
- id (path; string), required
- Success response: object

PATCH `/api/v1/tenants/{id}`

Partially updates a tenant. Requires the tenants:write scope and edit access (creator, lead owner, or admin; firm-wide tenants are editable by any token holder). Responds 404 (not 403) when not found or not editable. Emits a tenant.updated webhook.

- Authentication: Bearer; scope tenants:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- id (path; string), required
- IdempotencyKey
- JSON request: TenantWrite
- Success response: object

DELETE `/api/v1/tenants/{id}`

Deletes a tenant. Requires the tenants:write scope and delete access (creator, lead owner, or admin — stricter than edit). Responds 404 (not 403) when not found or not deletable. Emits a tenant.deleted webhook.

- Authentication: Bearer; scope tenants:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- id (path; string), required
- IdempotencyKey
- Success response: object

GET `/api/v1/prospects`

Returns prospects visible to the token's owning user, newest first. Cursor-paginated. Requires the prospects:read scope. Visibility is the current owner (ownerUserId) plus any prospecting-team member (admins see all).

- Authentication: Bearer; scope prospects:read
- Idempotency: Not applicable.
- Parameters:
- limit (query; integer) — Maximum number of prospects to return (1–100).
- cursor (query; string) — Opaque pagination cursor from a previous response's nextCursor.
- Success response: object

POST `/api/v1/prospects`

Creates a prospect owned by the token's user. propertyAddress is required. Requires the prospects:write scope. Emits a prospect.created webhook. Send an Idempotency-Key to make retries safe.

- Authentication: Bearer; scope prospects:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- IdempotencyKey
- JSON request: ProspectWrite
- Success response: object

GET `/api/v1/prospects/{id}`

Returns a single prospect by id. Responds 404 (not 403) when it does not exist OR is not visible to the token. Requires the prospects:read scope.

- Authentication: Bearer; scope prospects:read
- Idempotency: Not applicable.
- Parameters:
- id (path; string), required
- Success response: object

PATCH `/api/v1/prospects/{id}`

Partially updates a prospect. Requires the prospects:write scope and access (owner, team member, or admin). Responds 404 (not 403) when not found or not accessible. Emits a prospect.updated webhook.

- Authentication: Bearer; scope prospects:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- id (path; string), required
- IdempotencyKey
- JSON request: ProspectWrite
- Success response: object

DELETE `/api/v1/prospects/{id}`

Hard-deletes a prospect. Requires the prospects:write scope; hard delete is admin-only (non-admins should archive instead). Responds 404 when not accessible, 403 when not an admin. Emits a prospect.deleted webhook.

- Authentication: Bearer; scope prospects:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- id (path; string), required
- IdempotencyKey
- Success response: object

GET `/api/v1/clients`

Returns CRM clients visible to the token's owning user, newest first. Cursor-paginated. Requires the clients:read scope. Clients are private: visibility is the owner (ownerUserId), the creator (createdByUserId), or a collaborator (admins see all).

- Authentication: Bearer; scope clients:read
- Idempotency: Not applicable.
- Parameters:
- limit (query; integer) — Maximum number of clients to return (1–100).
- cursor (query; string) — Opaque pagination cursor from a previous response's nextCursor.
- Success response: object

POST `/api/v1/clients`

Creates a CRM client. name, clientType, and entityType are required. The creator is the token's user. ownerUserId defaults to the creator when omitted; a non-existent owner → 400 invalid_reference. Requires the clients:write scope. Emits a client.created webhook. Send an Idempotency-Key to make retries safe.

- Authentication: Bearer; scope clients:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- IdempotencyKey
- JSON request: ClientWrite
- Success response: object

GET `/api/v1/clients/{id}`

Returns a single CRM client by id. Responds 404 (not 403) when it does not exist OR is not visible to the token. Requires the clients:read scope.

- Authentication: Bearer; scope clients:read
- Idempotency: Not applicable.
- Parameters:
- id (path; string), required
- Success response: object

PATCH `/api/v1/clients/{id}`

Partially updates a CRM client. Requires the clients:write scope and edit rights (owner, creator, or admin). Collaborators are view-only. Responds 404 (not 403) when not found or not editable. Only the current owner or an admin may change ownerUserId (else 403); a non-existent new owner → 400. Emits a client.updated webhook.

- Authentication: Bearer; scope clients:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- id (path; string), required
- IdempotencyKey
- JSON request: ClientUpdate
- Success response: object

DELETE `/api/v1/clients/{id}`

Hard-deletes a CRM client. Requires the clients:write scope and edit rights (owner, creator, or admin). Responds 404 when not found or not editable. Emits a client.deleted webhook.

- Authentication: Bearer; scope clients:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- id (path; string), required
- IdempotencyKey
- Success response: object

GET `/api/v1/workspaces`

Returns workspaces (leasing brochures) visible to the token's owning user, newest first. Cursor-paginated. Requires the workspaces:read scope. Visibility is the creator plus collaborators (admins see all).

- Authentication: Bearer; scope workspaces:read
- Idempotency: Not applicable.
- Parameters:
- limit (query; integer) — Maximum number of workspaces to return (1–100).
- cursor (query; string) — Opaque pagination cursor from a previous response's nextCursor.
- Success response: object

POST `/api/v1/workspaces`

Creates a workspace (leasing brochure) owned by the token's user. title and addressLine1 are required. Requires the workspaces:write scope. Emits a workspace.created webhook. Send an Idempotency-Key to make retries safe.

- Authentication: Bearer; scope workspaces:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- IdempotencyKey
- JSON request: WorkspaceWrite
- Success response: object

GET `/api/v1/workspaces/{id}`

Returns a single workspace (leasing brochure) by id. Responds 404 (not 403) when it does not exist OR is not visible to the token. Requires the workspaces:read scope.

- Authentication: Bearer; scope workspaces:read
- Idempotency: Not applicable.
- Parameters:
- id (path; string), required
- Success response: object

PATCH `/api/v1/workspaces/{id}`

Partially updates a workspace (leasing brochure). Requires the workspaces:write scope and edit access (owner, collaborator, or admin). Responds 404 (not 403) when not found or not accessible. Emits a workspace.updated webhook.

- Authentication: Bearer; scope workspaces:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- id (path; string), required
- IdempotencyKey
- JSON request: WorkspaceWrite
- Success response: object

DELETE `/api/v1/workspaces/{id}`

Deletes a workspace (leasing brochure). Requires the workspaces:write scope; deletion is restricted to the owner or an admin (collaborators may edit but not delete). Responds 404 (not 403) when not found or not accessible. Emits a workspace.deleted webhook.

- Authentication: Bearer; scope workspaces:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- id (path; string), required
- IdempotencyKey
- Success response: object

GET `/api/v1/brokers`

Returns all brokers, newest first. Cursor-paginated. Brokers are firm-wide visible. Requires the brokers:read scope.

- Authentication: Bearer; scope brokers:read
- Idempotency: Not applicable.
- Parameters:
- limit (query; integer) — Maximum number of brokers to return (1–100).
- cursor (query; string) — Opaque pagination cursor from a previous response's nextCursor.
- Success response: object

POST `/api/v1/brokers`

Creates a broker owned by the token's user. Requires the brokers:write scope. brokerageId and brokerSpecialtyId, when sent, must reference existing rows (else 400 invalid_reference).

- Authentication: Bearer; scope brokers:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- IdempotencyKey
- JSON request: BrokerWrite
- Success response: object

GET `/api/v1/brokers/{id}`

Returns a single broker by id. Responds 404 when it does not exist. Requires the brokers:read scope.

- Authentication: Bearer; scope brokers:read
- Idempotency: Not applicable.
- Parameters:
- id (path; string), required
- Success response: object

PATCH `/api/v1/brokers/{id}`

Partially updates a broker. Editable by the creator or an admin; otherwise responds 404. Requires the brokers:write scope.

- Authentication: Bearer; scope brokers:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- id (path; string), required
- IdempotencyKey
- JSON request: BrokerUpdate
- Success response: object

DELETE `/api/v1/brokers/{id}`

Deletes a broker. Editable by the creator or an admin; otherwise responds 404. Requires the brokers:write scope.

- Authentication: Bearer; scope brokers:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- id (path; string), required
- IdempotencyKey

GET `/api/v1/brokerages`

Returns all brokerages (firms), newest first. Cursor-paginated. Brokerages are firm-wide visible. Requires the brokers:read scope.

- Authentication: Bearer; scope brokers:read
- Idempotency: Not applicable.
- Parameters:
- limit (query; integer) — Maximum number of brokerages to return (1–100).
- cursor (query; string) — Opaque pagination cursor from a previous response's nextCursor.
- Success response: object

POST `/api/v1/brokerages`

Creates a brokerage (firm) owned by the token's user. Requires the brokers:write scope.

- Authentication: Bearer; scope brokers:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- IdempotencyKey
- JSON request: BrokerageWrite
- Success response: object

GET `/api/v1/brokerages/{id}`

Returns a single brokerage by id. Responds 404 when it does not exist. Requires the brokers:read scope.

- Authentication: Bearer; scope brokers:read
- Idempotency: Not applicable.
- Parameters:
- id (path; string), required
- Success response: object

PATCH `/api/v1/brokerages/{id}`

Partially updates a brokerage. Editable by the creator or an admin; otherwise responds 404. Requires the brokers:write scope.

- Authentication: Bearer; scope brokers:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- id (path; string), required
- IdempotencyKey
- JSON request: BrokerageUpdate
- Success response: object

DELETE `/api/v1/brokerages/{id}`

Deletes a brokerage. Editable by the creator or an admin; otherwise responds 404. Requires the brokers:write scope.

- Authentication: Bearer; scope brokers:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- id (path; string), required
- IdempotencyKey

GET `/api/v1/actions`

Returns universal actions (tasks) visible to the token's owning user, newest first. Cursor-paginated. Requires the actions:read scope. Visibility follows the in-app rules; linked targets the token cannot access are stripped from each row.

Optional filters compose with pagination and never widen visibility: status, dueBefore/dueAfter (ISO 8601; match actions with a due date in that window), and targetType/targetId (return only actions linked to a matching record). An unknown enum value or malformed date returns 400 invalid_filter.

- Authentication: Bearer; scope actions:read
- Idempotency: Not applicable.
- Parameters:
- limit (query; integer) — Maximum number of actions to return (1–100).
- cursor (query; string)
- status (query; string) — Filter to actions with this status.
- dueBefore (query; string) — Only actions with a due date at or before this instant (ISO 8601). Actions with no due date are excluded.
- dueAfter (query; string) — Only actions with a due date at or after this instant (ISO 8601). Actions with no due date are excluded.
- targetType (query; string) — Only actions linked to a target of this type.
- targetId (query; string) — Only actions linked to the record with this id (combine with targetType to scope to a specific record type).
- Success response: object

POST `/api/v1/actions`

Creates an action with one or more linked targets. Requires the actions:write scope plus access to every target referenced (prospect access / contact-of-prospect / readable deal, listing, or landlord), else 403. Emits action.created.

- Authentication: Bearer; scope actions:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- IdempotencyKey
- JSON request: ActionCreate
- Success response: object

GET `/api/v1/actions/{id}`

Returns a single action by id. Responds 404 (not 403) when it does not exist OR is not visible to the token. Requires the actions:read scope.

- Authentication: Bearer; scope actions:read
- Idempotency: Not applicable.
- Parameters:
- id (path; string), required
- Success response: object

PATCH `/api/v1/actions/{id}`

Updates an existing action. Requires the actions:write scope and the same edit rights as the in-app app (creator, assignee, owner, or admin). 404 when not visible. Emits action.updated.

- Authentication: Bearer; scope actions:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- id (path; string), required
- IdempotencyKey
- JSON request: ActionUpdate
- Success response: object

DELETE `/api/v1/actions/{id}`

Deletes an action. Requires the actions:write scope; only the creator or an admin may delete (else 403). 404 when missing. Emits action.deleted.

- Authentication: Bearer; scope actions:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- id (path; string), required
- IdempotencyKey
- Success response: object

GET `/api/v1/campaigns`

Returns firm-wide marketing campaign email templates, newest first. Cursor-paginated. Requires the campaigns:read scope.

- Authentication: Bearer; scope campaigns:read
- Idempotency: Not applicable.
- Parameters:
- limit (query; integer) — Maximum number of templates to return (1–100).
- cursor (query; string)
- Success response: object

POST `/api/v1/campaigns`

Creates a marketing campaign email template. Requires the campaigns:write scope (NOT admin-gated). Emits campaign.created. This never sends email — it only stores a reusable template.

- Authentication: Bearer; scope campaigns:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- IdempotencyKey
- JSON request: CampaignTemplateCreate
- Success response: object

GET `/api/v1/campaigns/{id}`

Returns a single campaign template by id. Requires the campaigns:read scope. 404 when missing.

- Authentication: Bearer; scope campaigns:read
- Idempotency: Not applicable.
- Parameters:
- id (path; string), required
- Success response: object

PATCH `/api/v1/campaigns/{id}`

Updates a campaign template. Requires the campaigns:write scope. 404 when missing. Emits campaign.updated.

- Authentication: Bearer; scope campaigns:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- id (path; string), required
- IdempotencyKey
- JSON request: CampaignTemplateUpdate
- Success response: object

DELETE `/api/v1/campaigns/{id}`

Deletes a campaign template. Requires the campaigns:write scope. 404 when missing. Emits campaign.deleted.

- Authentication: Bearer; scope campaigns:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- id (path; string), required
- IdempotencyKey
- Success response: object

GET `/api/v1/campaign-sends`

Returns read-only marketing campaign send history (one row per blast) with engagement rollups, newest first. Cursor-paginated. Requires the campaigns:read scope. There is no endpoint to trigger a send.

- Authentication: Bearer; scope campaigns:read
- Idempotency: Not applicable.
- Parameters:
- limit (query; integer) — Maximum number of sends to return (1–100).
- cursor (query; string)
- Success response: object

GET `/api/v1/campaign-sends/{id}`

Returns a single campaign send by id, including per-recipient delivery rows. Requires the campaigns:read scope. 404 when missing.

- Authentication: Bearer; scope campaigns:read
- Idempotency: Not applicable.
- Parameters:
- id (path; string), required
- Success response: object

GET `/api/v1/entities`

Returns landlord-directory entities, newest first. Cursor-paginated. The directory is firm-wide, so any token with entities:read sees every (non-merged) entity. Aliases are omitted from the list — fetch a single entity to get them.

- Authentication: Bearer; scope entities:read
- Idempotency: Not applicable.
- Parameters:
- limit (query; integer) — Maximum number of entities to return (1–100).
- cursor (query; string) — Opaque pagination cursor from a previous response's nextCursor.
- Success response: object

POST `/api/v1/entities`

Creates a new landlord-directory entity from a name (canonical + contact aliases are seeded automatically). Requires the entities:write scope. Emits entity.created.

- Authentication: Bearer; scope entities:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- IdempotencyKey
- JSON request: EntityCreate
- Success response: object

GET `/api/v1/entities/{id}`

Returns a single entity by id, including its aliases. Responds 404 when it does not exist. Requires the entities:read scope.

- Authentication: Bearer; scope entities:read
- Idempotency: Not applicable.
- Parameters:
- id (path; string), required
- Success response: object

PATCH `/api/v1/entities/{id}`

Updates editable fields on an entity. Requires the entities:write scope and edit rights (admin, creator, or no recorded creator) — otherwise 404. Emits entity.updated.

- Authentication: Bearer; scope entities:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- id (path; string), required
- IdempotencyKey
- JSON request: EntityUpdate
- Success response: object

POST `/api/v1/entities/{id}/aliases`

Adds a manual alias (alternate name) to an entity. Requires entities:write and edit rights. Returns the updated entity with its alias list. Emits entity.updated.

- Authentication: Bearer; scope entities:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- id (path; string), required
- IdempotencyKey
- JSON request: object
- Success response: object

POST `/api/v1/entities/{id}/aliases/{aliasId}/verify`

Marks an alias as verified. Requires entities:write and edit rights. Emits entity.updated.

- Authentication: Bearer; scope entities:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- id (path; string), required
- aliasId (path; string), required
- IdempotencyKey
- Success response: object

DELETE `/api/v1/entities/{id}/aliases/{aliasId}`

Removes an alias from an entity. Requires entities:write and edit rights. Emits entity.updated.

- Authentication: Bearer; scope entities:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- id (path; string), required
- aliasId (path; string), required
- IdempotencyKey
- Success response: object

GET `/api/v1/agreements`

Returns agreements created by the token's owning user (admins see all), newest first. Cursor-paginated. Requires the agreements:read scope. Recipients are status-only (no sign tokens, signatures, or IPs).

- Authentication: Bearer; scope agreements:read
- Idempotency: Not applicable.
- Parameters:
- limit (query; integer) — Maximum number of agreements to return (1–100).
- cursor (query; string) — Opaque pagination cursor from a previous response's nextCursor.
- Success response: object

POST `/api/v1/agreements`

Creates a draft agreement from a template. Field values are seeded from the template and merged with any provided values; numeric bounds are enforced. Recipients' isSigner is derived server-side from the role. Requires agreements:write. Emits agreement.created.

- Authentication: Bearer; scope agreements:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- IdempotencyKey
- JSON request: AgreementCreate
- Success response: object

GET `/api/v1/agreements/{id}`

Returns a single agreement by id with its recipients. Responds 404 when it does not exist OR is not visible to the token. Requires the agreements:read scope.

- Authentication: Bearer; scope agreements:read
- Idempotency: Not applicable.
- Parameters:
- id (path; string), required
- Success response: object

PATCH `/api/v1/agreements/{id}`

Updates a draft agreement (values, recipients, links, cover note, signing/negotiation mode). Requires agreements:write and edit rights (admin or creator) — otherwise 404. Returns 409 if the agreement is no longer a draft or is already linked elsewhere. Emits agreement.updated.

- Authentication: Bearer; scope agreements:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- id (path; string), required
- IdempotencyKey
- JSON request: AgreementUpdate
- Success response: object

DELETE `/api/v1/agreements/{id}`

Deletes a draft agreement. Requires agreements:write and edit rights (admin or creator) — otherwise 404. Returns 409 if the agreement is no longer a draft (sent agreements must be voided in-app). Emits agreement.deleted.

- Authentication: Bearer; scope agreements:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- id (path; string), required
- IdempotencyKey
- Success response: object

POST `/api/v1/agreements/{id}/send`

Transitions a draft agreement to sent and emails its recipients, driving the same in-app state machine and side effects. Requires agreements:write and edit rights (admin or creator) — otherwise 404. Returns 409 when the agreement is not a draft, or 400 when it has no signer recipients. Emits agreement.sent. Idempotent via the Idempotency-Key header.

- Authentication: Bearer; scope agreements:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- id (path; string), required
- IdempotencyKey
- Success response: object

POST `/api/v1/agreements/{id}/void`

Voids an agreement (any state except completed), reusing the in-app state machine. Requires agreements:write and edit rights (admin or creator) — otherwise 404. Returns 409 when the agreement is already completed or already voided. Emits agreement.voided. Idempotent via the Idempotency-Key header.

- Authentication: Bearer; scope agreements:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- id (path; string), required
- IdempotencyKey
- Success response: object

POST `/api/v1/agreements/{id}/counter-sign`

Applies the EXR counter-signature to a signed agreement, transitioning it to completed and triggering the fully-executed emails, via the in-app state machine. Counter-signing is admin only — a non-admin token that can see the agreement receives 403 (not 404). Requires agreements:write. Returns 409 when the agreement is not awaiting counter-signature, including when requiresCounterSign is false — such agreements complete automatically on the final signature (agreement.signed then agreement.completed fire back-to-back), so there is nothing to counter-sign. Emits agreement.completed. Idempotent via the Idempotency-Key header.

- Authentication: Bearer; scope agreements:write
- Idempotency: Optional Idempotency-Key supported.
- Parameters:
- id (path; string), required
- IdempotencyKey
- JSON request: AgreementCounterSign
- Success response: object

Scopes

Read and write scopes are independent.

Pagination

List endpoints accept ?limit=<1..100>&cursor=<opaque>. Responses use { data, nextCursor, hasMore }; return nextCursor unchanged as the next cursor until hasMore is false.

Idempotency

Every POST, PATCH, and DELETE accepts an optional Idempotency-Key. Without it, the request has no replay protection. For the same token, method, concrete path, and canonical JSON body, a retry replays the first response, including 4xx or 5xx. Matching concurrent calls do not execute the mutation twice. Retry 409 idempotency_request_in_progress later with the same key; changed reuse returns 409 idempotency_key_conflict. Pending reservations are never automatically reclaimed.

Error envelope

{ "error": { "code", "message", "details?" } }details can hold machine-readable field validation errors.

Webhooks

Supported events (36):

Envelope

{ "id": "…", "type": "deal.created", "createdAt": "…", "apiVersion": "v1", "data": { "deal": { "…": "…" } } }

Delivery is attempted 4 times (immediate → +30 s → +2 min → +10 min), with a 10-second timeout per attempt. Any HTTP 2xx response acknowledges a delivery.

Signature verification

signedPayload = <t>.<rawBody>
v1            = HMAC-SHA256(signing_secret, signedPayload)  // hex
header        = X-EXR-Signature: t=<unix-ts>,v1=<hex>

Reject stale timestamps and compare the signature with a timing-safe comparison. Never put signing secrets in source control or browser code.

Request and response schemas

`WebhookDeletedResource`

FieldTypeRequiredNotes
idstringYesIdentifier of the deleted resource.

`Error`

FieldTypeRequiredNotes
errorobjectNo

`Deal`

FieldTypeRequiredNotes
idstringNo
namestringNo
transactionTypestringNo
representationstringnullNoWhich side EXR represented on this deal. null means not yet set. Valid values are scoped by transactionType: leaselandlord_rep, tenant_rep, dual_agency; saleseller_rep, buyer_rep, dual_agency; subleasesublessor_rep, sublessee_rep, dual_agency; loan_debtborrower_rep, lender_rep; valuation_advisory → any value. dual_agency means EXR represented both sides of the transaction. Consumer mapping — switch on the code (never the display label): landlord_rep → Landlord, tenant_rep → Tenant, seller_rep → Seller, buyer_rep → Buyer, sublessor_rep → Sublessor, sublessee_rep → Sublessee, borrower_rep → Borrower, lender_rep → Lender, dual_agency → Both. Treat unknown values defensively. The counterparty's actual name lives in parties. This field can change after creation; re-apply the mapping on deal.updated webhook events. Allowed: landlord_rep, tenant_rep, seller_rep, buyer_rep, sublessor_rep, sublessee_rep, borrower_rep, lender_rep, dual_agency, null
assetClassstringnullNo
statusstringNo
propertyobjectNoFormatted and structured property address. Structured fields are available without the financial-data scope and are null for legacy or incomplete addresses.
partiesobjectNo
expectedCloseDatestringnullNo
actualCloseDatestringnullNo
ownerUserIdstringNo
createdAtstringnullNo
updatedAtstringnullNo
financialsobjectNoOnly present when the token has the deals:financials:read scope.

`DealCreate`

Body for creating a deal. Mirrors the read shape but omits server-managed fields. Money values are strings of integer cents.

FieldTypeRequiredNotes
namestringNoDefaults to "Untitled Deal" when omitted.
transactionTypestringYes
representationstringnullNoWhich side EXR represented. Must be valid for the chosen transactionType: leaselandlord_rep, tenant_rep, dual_agency; saleseller_rep, buyer_rep, dual_agency; subleasesublessor_rep, sublessee_rep, dual_agency; loan_debtborrower_rep, lender_rep; valuation_advisory → any value. null clears the field. Allowed: landlord_rep, tenant_rep, seller_rep, buyer_rep, sublessor_rep, sublessee_rep, borrower_rep, lender_rep, dual_agency, null
assetClassstringnullNo
statusstringNoDefaults to the first pipeline stage when omitted.
propertyobjectYesThe formatted address is required. Structured fields are optional and nullable; omitted components are stored as null on create.
partiesobjectNoFree-text counterparty names.
expectedCloseDatestringnullNoYYYY-MM-DD.
actualCloseDatestringnullNoYYYY-MM-DD. Required when status is the closed stage.
financialsDealFinancialsWriteNo

`DealUpdate`

Body for a partial deal update. Every field is optional; only the fields you send change. Commission splits are set via the splits block.

FieldTypeRequiredNotes
namestringNo
transactionTypestringNo
representationstringnullNoWhich side EXR represented. Must be valid for the deal's transactionType: leaselandlord_rep, tenant_rep, dual_agency; saleseller_rep, buyer_rep, dual_agency; subleasesublessor_rep, sublessee_rep, dual_agency; loan_debtborrower_rep, lender_rep; valuation_advisory → any value. null clears the field. This field can change after creation; consumers should re-apply their mapping on deal.updated webhook events. Allowed: landlord_rep, tenant_rep, seller_rep, buyer_rep, sublessor_rep, sublessee_rep, borrower_rep, lender_rep, dual_agency, null
assetClassstringnullNo
statusstringNo
propertyobjectNoWhen address changes, omitted structured fields are cleared to null so metadata cannot remain attached to the wrong address. If address is omitted or unchanged, omitted structured fields are preserved.
partiesobjectNo
expectedCloseDatestringnullNoYYYY-MM-DD.
actualCloseDatestringnullNoYYYY-MM-DD.
financialsDealFinancialsWriteNo
splitsobjectNoSet the deal-team commission splits. Must describe EXACTLY the current collaborator set and total 100% (10000 bps); re-validated under a row lock. A 409 means the team changed since you read it.

`DealFinancialsWrite`

Writable financial fields. Money is a string of integer cents.

FieldTypeRequiredNotes
potentialCommissionCentsstringnullNoOverridden by the computed gross commission when complete leaseTerms are supplied on a lease deal.
leaseTermsobjectNoLease commission calculator terms. Send null to clear. On PATCH, a non-null object is shallow-merged with the stored terms, so omitted calculator and legacy keys are preserved; explicit null field values overwrite those fields. The server tolerates partial/unknown-field blobs (missing fields fall back to their documented defaults on read/compute), but the shape below is what a fully-populated calculator stores and what reads echo back.
commissionScheduleobjectNoAgreement-style commission schedule. Send null to clear.
paymentScheduleobjectNoCommission payment milestones. Send null to clear.
coBrokerageInvoiceMethodstringnullNoHow the cooperating broker's share is invoiced. included_in_exr_invoice — EXR includes the co-brokerage amount on its own client invoice and then pays the cooperating broker. invoiced_separately — the cooperating broker invoices the client directly. null clears the field (normalized automatically when the effective co-brokerage is removed). Validation: on create or update, if the effective leaseTerms carries a positive active-mode co-brokerage value (coBrokerageBps > 0 in percent mode, or coBrokerageAmountCents > 0 in dollar mode), a non-null method is required — omitting or sending null returns 400 invalid_body. Unrelated partial updates on legacy rows that carry positive co-brokerage but no classified method are exempt from this check. Sending null explicitly when the request removes or zeroes the co-brokerage is always accepted and normalizes the stored value to null. Allowed: included_in_exr_invoice, invoiced_separately, null

`DealLeaseTermsYear`

One lease-year row of the commission calculator. Rates are basis points (10000 = 100%, e.g. 300 = 3%).

FieldTypeRequiredNotes
escalationBpsintegerNoIncrease over the PRIOR year's base rent, in basis points (300 = 3%). Year 1 is the starting rent itself, so its escalation is ignored (typically 0).
freeRentMonthsnumberNoFree rent granted in this year, in the blob's freeRentUnit (months or weeks; fractional allowed, e.g. 1.5). Only used when freeRentMode is by_year.
commissionBpsintegerNoCommission rate for this year, in basis points (300 = 3%). Applied to net rent by default, or to base rent when commissionBasis is base_rent. Ignored entirely when commissionCalc is fixed.

`DealLeaseTerms`

The deal's lease commission calculator terms — a per-year rent schedule where each year has its own escalation, free rent, and commission rate. Conventions: all money fields are integer CENTS; all rates are BASIS POINTS (10000 = 100%, e.g. 300 = 3%); dates are ISO YYYY-MM-DD strings (empty string when not entered). Commission is charged on net rent (base minus free rent) by default, or on base rent when commissionBasis is base_rent. Reading this field requires the deals:financials:read scope.

FieldTypeRequiredNotes
sqftintegernullNoRentable square footage (RSF; total; for retail this is ground floor + basement). Used only to derive PPSF for display.
usableSqftintegernullNoUsable square footage (USF) for Office premises. Informational only; it never affects PPSF or commission calculations.
groundFloorSqftintegernullNoRetail-only breakdown of sqft: ground-floor square footage. Display/informational only.
basementSqftintegernullNoRetail-only breakdown of sqft: basement square footage. Display/informational only.
startingRentAnnualCentsintegernullNoYear-1 base annual rent, in integer cents. Drives every later year via the per-year escalations.
referralFeeBpsintegerNoReferral fee taken off the gross commission, in basis points (300 = 3%). Used when referralMode is percent.
referralModestringNoHow the referral fee is entered: percent (use referralFeeBps) or dollar (use referralAmountCents). Defaults to percent. Allowed: percent, dollar
referralAmountCentsintegerNoFlat referral amount in integer cents — only used when referralMode is dollar.
coBrokerageBpsintegerNoCo-brokerage commission paid to the other broker, in basis points; applied AFTER the referral fee. 0 = no co-broker.
coBrokerageModestringNoHow the co-brokerage commission is entered: percent (use coBrokerageBps) or dollar (use coBrokerageAmountCents). Defaults to percent. Allowed: percent, dollar
coBrokerageAmountCentsintegerNoFlat co-brokerage amount in integer cents — only used when coBrokerageMode is dollar.
freeRentModestringNoHow free rent applies: by_year (per-year months subtracted from that year's rent) or amortized (one total, valued at the year-1 monthly rate, spread evenly across every year). Allowed: by_year, amortized
freeRentMonthsTotalnumberNoTotal free rent for the whole term, in freeRentUnit units — only used when freeRentMode is amortized.
freeRentUnitstringNoUnit ALL free-rent figures are entered in (years[].freeRentMonths and freeRentMonthsTotal). Weeks convert at 12 months = 52 weeks. Allowed: months, weeks
commissionBasisstringNoWhat rent figure the per-year commission rate is charged on: net_rent (base minus free rent, the default) or base_rent (free rent ignored for commission). Only used in sliding mode. Allowed: net_rent, base_rent
commissionCalcstringNoHow the gross commission is figured: sliding (the per-year rate schedule, the default) or fixed (a single flat commission via the fixed* fields; every year's commissionBps is ignored). Allowed: sliding, fixed
fixedModestringNoIn fixed mode, how the flat commission is entered: months of year-1 monthly base rent (fixedMonths), a flat amount in cents (fixedAmountCents), or one flat percent applied to every year's rent (fixedPercentBps). Allowed: months, amount, percent
fixedMonthsnumberNofixed + months: number of months of the year-1 monthly base rent.
fixedAmountCentsintegerNofixed + amount: the flat commission in integer cents.
fixedPercentBpsintegerNofixed + percent: one flat commission rate in basis points (300 = 3%) applied to EVERY year's rent on the selected basis.
billTostringNoBilling recipient for the commission invoice (free-form). Display/billing only.
billToAddressstringNoBilling address. Display/billing only.
billToEmailstringNoBilling email. Display/billing only.
termMonthsintegernullNoTotal lease term in MONTHS when not a whole number of years (e.g. 63 for "5 years 3 months"). Null = whole-year term defined by years[] alone. When set, ceil(termMonths/12) must equal years.length; the final year row covers only the remaining months and is prorated.
leaseStartstringNoLease commencement date, ISO YYYY-MM-DD (empty string when not entered).
leaseEndstringNoLease expiration date, ISO YYYY-MM-DD (empty string when not entered).
yearsarrayNoOne row per lease year, in order (index 0 = year 1).

`DealCommissionSchedule`

Agreement-style commission schedule. A tagged union on mode: sliding (per-year-bracket rates), fixed (one flat commission), or custom (free-form text). Percent values here are STRINGS as entered (e.g. "5" = 5%) — unlike the lease-terms blob, which uses basis points. Reading requires the deals:financials:read scope.

`DealCommissionSlidingRow`

One sliding-schedule bracket: a commission rate covering lease years fromYeartoYear inclusive.

FieldTypeRequiredNotes
fromYearintegerNo
toYearintegerNo
percentstringNoRate as an entered string, e.g. "5" = 5%.

`DealPaymentSchedule`

When the commission is paid. A tagged union on mode: structured (ordered milestone rows whose percents should sum to 100) or custom (free-form text). Legacy values shaped as {rows:[…]} with no mode are read as structured. Reading requires the deals:financials:read scope.

`DealPaymentScheduleRow`

One payment milestone.

FieldTypeRequiredNotes
percentstringNoShare of the commission as an entered string, e.g. "50" = 50%.
milestonestringNoMilestone label, e.g. "Lease Execution".
expectedDatestringNoOptional ISO YYYY-MM-DD billing helper — when finance expects the milestone. Never persisted on the "Lease Execution" row (it mirrors the deal's actual close date live).

`Tenant`

FieldTypeRequiredNotes
idstringNo
namestringNo
domainstringnullNo
businessDescriptionstringnullNo
businessSinceYearintegernullNo
activelySearchingbooleanNo
createdByUserIdstringNo
relationshipOwnerIdstringnullNoEXR relationship lead-owner. Null means the tenant is firm-wide.
createdAtstringnullNo
updatedAtstringnullNo

`TenantWrite`

Writable tenant fields. name is required on create. The creator (userId) is set from the token and cannot be supplied.

FieldTypeRequiredNotes
namestringYes
domainstringnullNo
businessDescriptionstringnullNo
businessSinceYearintegernullNo
activelySearchingbooleanNo
relationshipOwnerIdstringnullNoEXR relationship lead-owner. Null means firm-wide.

`DeleteResult`

FieldTypeRequiredNotes
idstringNo
deletedbooleanNo

`Prospect`

FieldTypeRequiredNotes
idstringNo
propertyAddressstringnullNo
landlordNamestringnullNo
statusstringNo
contactMethodstringnullNo
contactEmailstringnullNo
contactNotesstringnullNo
keyQuestionsstringnullNo
followUpActionstringnullNo
currentBrokerstringnullNo
currentBrokerageIdstringnullNo
existingAssetClassstringnullNo
siteClassificationstringnullNo
lastContactDatestringnullNo
listingExpirationDatestringnullNo
locationobjectNo
featuredbooleanNo
archivedAtstringnullNo
ownerUserIdstringNo
createdByUserIdstringnullNo
createdAtstringnullNo
updatedAtstringnullNo

`ProspectWrite`

Writable prospect fields. propertyAddress is required on create. The owner (userId) is set from the token; unknown enum values for status/existingAssetClass/siteClassification are silently dropped.

FieldTypeRequiredNotes
propertyAddressstringYes
landlordNamestringnullNo
statusstringnullNo
contactMethodstringnullNo
contactEmailstringnullNo
contactNotesstringnullNo
keyQuestionsstringnullNo
followUpActionstringnullNo
currentBrokerstringnullNo
currentBrokerageIdstringnullNo
existingAssetClassstringnullNo
siteClassificationstringnullNo
lastContactDatestringnullNo
listingExpirationDatestringnullNo
latitudenumbernullNo
longitudenumbernullNo

`Client`

A CRM client (internally crm_clients). Private to its owner, creator, collaborators, and admins.

FieldTypeRequiredNotes
idstringNo
namestringNo
clientTypestringNoAllowed: seller, buyer, landlord, tenant
entityTypestringNoAllowed: individual, company
assetClassesarrayNo
notesstringnullNo
ownerUserIdstringNo
createdByUserIdstringNo
createdAtstringnullNo
updatedAtstringnullNo

`ClientWrite`

Writable fields for a client on create (POST). userId (creator) is server-managed and ignored if sent. name, clientType, and entityType are required. ownerUserId (sent as clientOwnerId) defaults to the creator when omitted and must reference an existing user (else 400 invalid_reference).

FieldTypeRequiredNotes
namestringYes
clientTypestringYesAllowed: seller, buyer, landlord, tenant
entityTypestringYesAllowed: individual, company
assetClassesarrayNo
clientOwnerIdstringNoThe owning user. Defaults to the token's user when omitted. Must reference an existing user.
notesstringnullNo

`ClientUpdate`

Partial update for a client (PATCH). Same writable fields as ClientWrite, but every field is optional — send only the subset you want to change. Only the current owner or an admin may change clientOwnerId (else 403).

FieldTypeRequiredNotes
namestringNo
clientTypestringNoAllowed: seller, buyer, landlord, tenant
entityTypestringNoAllowed: individual, company
assetClassesarrayNo
clientOwnerIdstringNoReassign the owning user. Only the current owner or an admin may change this. Must reference an existing user.
notesstringnullNo

`Workspace`

A leasing brochure (internally leasing_listings).

FieldTypeRequiredNotes
idstringNo
titlestringNo
propertyNamestringnullNo
statusstringNo
assetTypestringNo
addressobjectNo
locationobjectNo
pricingobjectNo
buildingobjectNo
shareTokenstringnullNo
descriptionstringnullNo
ownerUserIdstringNo
createdAtstringnullNo
updatedAtstringnullNo

`WorkspaceWrite`

Writable workspace (leasing brochure) fields. title and addressLine1 are required on create; the owner is set from the token. The address key, share tokens, the linked imported-listing id, and the PLUTO-sync timestamp are server-managed and cannot be set through the API. Monetary fields are integer cents.

FieldTypeRequiredNotes
titlestringYes
statusstringNo
propertyNamestringnullNo
addressLine1stringYes
addressLine2stringnullNo
neighborhoodstringnullNo
boroughstringnullNo
citystringnullNo
statestringnullNo
zipstringnullNo
assetTypestringNo
pricingDisplayModestringNo
askingRentDisplaystringnullNo
askingPpsfCentsintegernullNo
askingRentMonthlyCentsintegernullNo
templateKeystringnullNo
viewerGateEnabledbooleanNo
activeListingUrlstringnullNo
latitudestringnullNo
longitudestringnullNo
descriptionstringnullNo
matterportUrlstringnullNo
videoUrlstringnullNo
yearBuiltintegernullNo
numFloorsintegernullNo
buildingSfintegernullNo
lotSfintegernullNo
zoningstringnullNo
retailSfintegernullNo
officeSfintegernullNo
residentialSfintegernullNo

`Broker`

A commercial broker (internally brokers).

FieldTypeRequiredNotes
idstringNo
namestringNo
workEmailstringnullNo
personalEmailstringnullNo
mobilePhonestringnullNo
workPhonestringnullNo
agentUrlstringnullNo
notesstringnullNo
brokerageIdstringnullNo
brokerSpecialtyIdstringnullNo
linkedImportedBrokerIdstringnullNoRead-only reference to a linked global EXR agent record (enrichment only). Managed by the in-app link flow, never the public API.
createdByUserIdstringNo
createdAtstringnullNo
updatedAtstringnullNo

`BrokerWrite`

Writable fields for a broker on create (POST). userId and linkedImportedBrokerId are server-managed and ignored (stripped) if sent. name is required.

FieldTypeRequiredNotes
namestringYes
workEmailstringnullNo
personalEmailstringnullNo
mobilePhonestringnullNo
workPhonestringnullNo
agentUrlstringnullNo
notesstringnullNo
brokerageIdstringnullNoMust reference an existing brokerage (else 400 invalid_reference).
brokerSpecialtyIdstringnullNoMust reference an existing broker specialty (else 400 invalid_reference).

`BrokerUpdate`

Partial update for a broker (PATCH). Same writable fields as BrokerWrite, but every field is optional — send only the subset you want to change. Server-managed fields are ignored (stripped) if sent.

FieldTypeRequiredNotes
namestringNo
workEmailstringnullNo
personalEmailstringnullNo
mobilePhonestringnullNo
workPhonestringnullNo
agentUrlstringnullNo
notesstringnullNo
brokerageIdstringnullNoMust reference an existing brokerage (else 400 invalid_reference).
brokerSpecialtyIdstringnullNoMust reference an existing broker specialty (else 400 invalid_reference).

`Brokerage`

A brokerage firm (internally brokerages).

FieldTypeRequiredNotes
idstringNo
namestringNo
domainstringnullNo
websitestringnullNo
descriptionstringnullNo
linkedImportedBrokerageIdstringnullNoRead-only reference to a linked global EXR firm record (enrichment only). Managed by the in-app link flow, never the public API.
createdByUserIdstringNo
createdAtstringnullNo
updatedAtstringnullNo

`BrokerageWrite`

Writable fields for a brokerage on create (POST). userId and linkedImportedBrokerageId are server-managed and ignored (stripped) if sent. name is required.

FieldTypeRequiredNotes
namestringYes
domainstringnullNo
websitestringnullNo
descriptionstringnullNo

`BrokerageUpdate`

Partial update for a brokerage (PATCH). Same writable fields as BrokerageWrite, but every field is optional — send only the subset you want to change. Server-managed fields are ignored (stripped) if sent.

FieldTypeRequiredNotes
namestringNo
domainstringnullNo
websitestringnullNo
descriptionstringnullNo

`ActionTarget`

A record this action is linked to. Targets the token cannot access are stripped from the response.

FieldTypeRequiredNotes
targetTypestringNoOne of prospect, contact, landlord, deal, listing.
targetIdstringNo
labelstringnullNo

`Action`

FieldTypeRequiredNotes
idstringNo
titlestringNo
actionTypestringNo
statusstringNo
prioritystringNo
visibilitystringNo
planstringNo
sourcestringNo
descriptionstringnullNo
dueAtstringnullNo
snoozeUntilstringnullNo
createdByUserIdstringNo
assignedToUserIdstringnullNo
completedByUserIdstringnullNo
completedAtstringnullNo
createdAtstringnullNo
updatedAtstringnullNo
targetsarrayNo
questionsarrayNo

`ActionCreate`

FieldTypeRequiredNotes
titlestringYes
actionTypestringNoDefaults to call.
statusstringNoDefaults to open.
prioritystringNoDefaults to normal.
visibilitystringNoDefaults to prospect_team.
planstringNoDefaults to one_time.
descriptionstringnullNo
dueAtstringnullNoDue date/time. Preserved in the firm's local timezone (no UTC drift).
snoozeUntilstringnullNo
assignedTostringnullNoUser id to assign to; defaults to the token owner.
targetsarrayYesAt least one target is required; the token must have access to each.
questionsarrayNo

`ActionUpdate`

All fields optional; only provided fields are changed. Setting status to completed records the completer + completion time.

FieldTypeRequiredNotes
titlestringNo
actionTypestringNo
statusstringNo
prioritystringNo
visibilitystringNo
planstringNo
descriptionstringnullNo
dueAtstringnullNo
snoozeUntilstringnullNo
assignedTostringnullNo
targetContactIdstringnullNoSets/clears the contact target for the action's prospect.
questionsarrayNo

`CampaignTemplate`

A reusable marketing campaign email template. Storing one never sends email.

FieldTypeRequiredNotes
idstringNo
namestringNo
subjectstringNo
bodystringNoTipTap/HTML body, returned verbatim.
categorystringnullNo
createdByUserIdstringnullNo
createdAtstringnullNo
updatedAtstringnullNo

`CampaignTemplateCreate`

FieldTypeRequiredNotes
namestringYes
subjectstringYes
bodystringYes
categorystringnullNo

`CampaignTemplateUpdate`

All fields optional; only provided fields are changed.

FieldTypeRequiredNotes
namestringNo
subjectstringNo
bodystringNo
categorystringnullNo

`CampaignSendRecipient`

FieldTypeRequiredNotes
idstringNo
emailstringNo
namestringnullNo
sourceTypestringNo
sourceIdstringnullNo
statusstringNo
errorReasonstringnullNo
sentAtstringnullNo
deliveredAtstringnullNo
firstOpenedAtstringnullNo
lastOpenedAtstringnullNo
openCountintegerNo
firstClickedAtstringnullNo
lastClickedAtstringnullNo
clickCountintegerNo
bouncedAtstringnullNo

`CampaignSend`

Read-only record of a marketing campaign blast with engagement rollups.

FieldTypeRequiredNotes
idstringNo
templateIdstringnullNo
subjectstringNo
bodystringNo
sentByUserIdstringnullNo
recipientCountintegerNo
statusstringNo
scheduledAtstringnullNo
createdAtstringnullNo
rollupobjectNoPer-send engagement counts, mirroring the in-app campaign detail. delivered/opened/clicked are milestone counts (recipients with a delivered/open/click timestamp); bounced/unsubscribed/spamReport are terminal-status counts; optedOut = unsubscribed + spamReport.
recipientsarrayNoPer-recipient delivery rows; only present on the single-send detail read.

`EntityAlias`

An alternate name (spelling/variant) on file for an entity.

FieldTypeRequiredNotes
idstringNo
aliasstringNo
sourcestringnullNoHow the alias was added (e.g. manual).
verifiedbooleanNo
verifiedAtstringnullNo
createdAtstringnullNo

`Entity`

A landlord-directory entity (ownership group, owner, manager, brokerage, person, etc.).

FieldTypeRequiredNotes
idstringNo
canonicalNamestringNo
kindstringNo
primaryPhonestringnullNo
primaryEmailstringnullNo
websitestringnullNo
notesstringnullNo
researchStatusstringNo
legalNamestringnullNo
mailingAddressstringnullNo
territorystringnullNo
preferredAssetClassstringnullNo
researchSourcestringnullNo
lastVerifiedAtstringnullNo
tagsarrayNo
mergedIntoIdstringnullNoWhen set, this entity was merged into the referenced survivor and is hidden from the list. Merge/dedupe is in-app only.
createdByUserIdstringnullNoWho introduced the entity. Null on legacy rows (which anyone may edit).
createdAtstringnullNo
updatedAtstringnullNo
aliasesarrayNoPresent only on the single-entity GET (omitted from the list).

`EntityCreate`

FieldTypeRequiredNotes
namestringYesDisplay name; the canonical name and seed aliases are derived from it.
kindstringNoDefaults to unknown when omitted. Allowed: ownership_group, legal_owner, management_company, brokerage, tenant, person, unknown
primaryPhonestringNo
primaryEmailstringNo
websitestringNo

`EntityUpdate`

All fields optional; only provided fields are changed. Send null to clear a nullable field.

FieldTypeRequiredNotes
canonicalNamestringNo
kindstringNoAllowed: ownership_group, legal_owner, management_company, brokerage, tenant, person, unknown
primaryPhonestringnullNo
primaryEmailstringnullNo
websitestringnullNo
notesstringnullNo
researchStatusstringNoAllowed: unresearched, needs_review, partially_researched, verified, do_not_contact
legalNamestringnullNo
mailingAddressstringnullNo
territorystringnullNo
preferredAssetClassstringnullNo
researchSourcestringnullNo
tagsarrayNo

`AgreementRecipient`

A recipient on an agreement. Status-only — never includes sign tokens, signature images, IPs, or user agents.

FieldTypeRequiredNotes
idstringNo
emailstringNo
namestringnullNo
orderintegerNo
rolestringNo
isSignerbooleanNoDerived server-side from the role registry.
statusstringNo
sentAtstringnullNo
viewedAtstringnullNo
signedAtstringnullNo
createdAtstringnullNo

`Agreement`

FieldTypeRequiredNotes
idstringNo
templateIdstringnullNo
templateKindstringNo
templateTitlestringNo
statusstringNo
signingModestringNo
negotiationEnabledbooleanNo
requiresCounterSignbooleanNoWhether the agreement waits for a separate EXR counter-signature between signed and completed. When false, the final signer's signature completes the agreement directly (fully-executed emails + agreement.completed fire from the sign path). Even when true, a firm user signing as an explicit signer satisfies the requirement.
valuesobjectNo
hiddenFieldsarrayNo
fieldsarrayNo
coverNotestringnullNo
bodystringNoStored (sanitized) template body with {{tokens}} intact.
renderedBodystringNoBody with values substituted, re-sanitized through the strict policy used by the PDF/signing path.
linkedDealIdstringnullNo
linkedTenantIdstringnullNo
linkedListingIdstringnullNo
createdByUserIdstringnullNo
sentAtstringnullNo
completedAtstringnullNo
voidedAtstringnullNo
counterSignedAtstringnullNoCounter-sign status (timestamp) only — never the signature image / IP / UA.
createdAtstringnullNo
updatedAtstringnullNo
recipientsarrayNo

`AgreementRecipientInput`

FieldTypeRequiredNotes
emailstringYes
namestringnullNo
orderintegerNo
rolestringYesRecipient role; isSigner is derived from it server-side.

`AgreementCreate`

FieldTypeRequiredNotes
templateIdstringYes
linkedDealIdstringnullNo
linkedTenantIdstringnullNo
linkedListingIdstringnullNo
coverNotestringnullNo
signingModestringNoAllowed: parallel, sequential
valuesobjectNoField answers, merged over the template's seeded defaults.
hiddenFieldsarrayNo
recipientsarrayNo

`AgreementUpdate`

Draft-only. All fields optional; only provided fields change. A provided recipients array fully replaces the list.

FieldTypeRequiredNotes
valuesobjectNo
hiddenFieldsarrayNo
coverNotestringnullNo
signingModestringNoAllowed: parallel, sequential
negotiationEnabledbooleanNo
requiresCounterSignbooleanNoWhether the agreement should wait for an EXR counter-signature after the final signer signs.
linkedDealIdstringnullNo
linkedTenantIdstringnullNo
linkedListingIdstringnullNo
recipientsarrayNo

`AgreementCounterSign`

The EXR counter-signature applied to a fully-signed agreement.

FieldTypeRequiredNotes
typedNamestringYesThe counter-signer's full typed name.
signatureDataUrlstringYesA data: URL of the rendered signature image.
agreedbooleanYesMust be true to consent to counter-signing. Allowed: true