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.
- Authentication: None
- Idempotency: Not applicable.
- Success response:
object
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.
deals:read— Read deal records (financial fields require deals:financials:read).deals:financials:read— Include deal commission, split, lease-term, and payment financials.deals:write— Create, update, and delete deal records the token's account may edit.tenants:read— Read Commercial Tenant records visible to the token.tenants:write— Create, update, and delete Commercial Tenant records the token may edit.prospects:read— Read prospect records visible to the token.prospects:write— Create, update, and delete prospect records the token may access.clients:read— Read CRM client records visible to the token.clients:write— Create, update, and delete CRM client records the token may access.workspaces:read— Read workspace (leasing brochure) records visible to the token.workspaces:write— Create, update, and delete workspace records the token may edit.brokers:read— Read broker and brokerage records (firm-wide visible).brokers:write— Create, update, and delete broker and brokerage records the token may edit.actions:read— Read universal actions (tasks) visible to the token.actions:write— Create, update, and delete actions the token may edit.campaigns:read— Read marketing campaign templates and read-only send history.campaigns:write— Create, update, and delete campaign templates; never trigger a send.entities:read— Read landlord/entity directory records (firm-wide).entities:write— Create and update entities and manage their aliases.agreements:read— Read agreements visible to the creator or an admin.agreements:write— Create and edit drafts and invoke supported agreement lifecycle transitions.
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):
deal.createddeal.updateddeal.deletedtenant.createdtenant.updatedtenant.deletedprospect.createdprospect.updatedprospect.deletedclient.createdclient.updatedclient.deletedworkspace.createdworkspace.updatedworkspace.deletedaction.createdaction.updatedaction.deletedcampaign.createdcampaign.updatedcampaign.deletedbroker.createdbroker.updatedbroker.deletedbrokerage.createdbrokerage.updatedbrokerage.deletedentity.createdentity.updatedagreement.createdagreement.updatedagreement.deletedagreement.sentagreement.signedagreement.completedagreement.voided
- Entities emit entity.created and entity.updated only; merge/dedupe is in-app and there is no entity.deleted event.
- Deal, tenant, prospect, workspace, and agreement CRUD events may originate from in-app or public API writes.
- Broker, brokerage, action, campaign, entity, and client events currently originate only from public API writes.
- Agreement sent, signed, completed, and voided events originate in the shared state machine and may be triggered in-app or through the public API.
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`
| Field | Type | Required | Notes |
|---|---|---|---|
id | string | Yes | Identifier of the deleted resource. |
`Error`
| Field | Type | Required | Notes |
|---|---|---|---|
error | object | No | — |
`Deal`
| Field | Type | Required | Notes | |
|---|---|---|---|---|
id | string | No | — | |
name | string | No | — | |
transactionType | string | No | — | |
representation | string | null | No | Which side EXR represented on this deal. null means not yet set. Valid values are scoped by transactionType: lease → landlord_rep, tenant_rep, dual_agency; sale → seller_rep, buyer_rep, dual_agency; sublease → sublessor_rep, sublessee_rep, dual_agency; loan_debt → borrower_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 |
assetClass | string | null | No | — |
status | string | No | — | |
property | object | No | Formatted and structured property address. Structured fields are available without the financial-data scope and are null for legacy or incomplete addresses. | |
parties | object | No | — | |
expectedCloseDate | string | null | No | — |
actualCloseDate | string | null | No | — |
ownerUserId | string | No | — | |
createdAt | string | null | No | — |
updatedAt | string | null | No | — |
financials | object | No | Only 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.
| Field | Type | Required | Notes | |
|---|---|---|---|---|
name | string | No | Defaults to "Untitled Deal" when omitted. | |
transactionType | string | Yes | — | |
representation | string | null | No | Which side EXR represented. Must be valid for the chosen transactionType: lease → landlord_rep, tenant_rep, dual_agency; sale → seller_rep, buyer_rep, dual_agency; sublease → sublessor_rep, sublessee_rep, dual_agency; loan_debt → borrower_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 |
assetClass | string | null | No | — |
status | string | No | Defaults to the first pipeline stage when omitted. | |
property | object | Yes | The formatted address is required. Structured fields are optional and nullable; omitted components are stored as null on create. | |
parties | object | No | Free-text counterparty names. | |
expectedCloseDate | string | null | No | YYYY-MM-DD. |
actualCloseDate | string | null | No | YYYY-MM-DD. Required when status is the closed stage. |
financials | DealFinancialsWrite | No | — |
`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.
| Field | Type | Required | Notes | |
|---|---|---|---|---|
name | string | No | — | |
transactionType | string | No | — | |
representation | string | null | No | Which side EXR represented. Must be valid for the deal's transactionType: lease → landlord_rep, tenant_rep, dual_agency; sale → seller_rep, buyer_rep, dual_agency; sublease → sublessor_rep, sublessee_rep, dual_agency; loan_debt → borrower_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 |
assetClass | string | null | No | — |
status | string | No | — | |
property | object | No | When 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. | |
parties | object | No | — | |
expectedCloseDate | string | null | No | YYYY-MM-DD. |
actualCloseDate | string | null | No | YYYY-MM-DD. |
financials | DealFinancialsWrite | No | — | |
splits | object | No | Set 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.
| Field | Type | Required | Notes | |
|---|---|---|---|---|
potentialCommissionCents | string | null | No | Overridden by the computed gross commission when complete leaseTerms are supplied on a lease deal. |
leaseTerms | object | No | Lease 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. | |
commissionSchedule | object | No | Agreement-style commission schedule. Send null to clear. | |
paymentSchedule | object | No | Commission payment milestones. Send null to clear. | |
coBrokerageInvoiceMethod | string | null | No | How 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%).
| Field | Type | Required | Notes |
|---|---|---|---|
escalationBps | integer | No | Increase 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). |
freeRentMonths | number | No | Free 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. |
commissionBps | integer | No | Commission 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.
| Field | Type | Required | Notes | |
|---|---|---|---|---|
sqft | integer | null | No | Rentable square footage (RSF; total; for retail this is ground floor + basement). Used only to derive PPSF for display. |
usableSqft | integer | null | No | Usable square footage (USF) for Office premises. Informational only; it never affects PPSF or commission calculations. |
groundFloorSqft | integer | null | No | Retail-only breakdown of sqft: ground-floor square footage. Display/informational only. |
basementSqft | integer | null | No | Retail-only breakdown of sqft: basement square footage. Display/informational only. |
startingRentAnnualCents | integer | null | No | Year-1 base annual rent, in integer cents. Drives every later year via the per-year escalations. |
referralFeeBps | integer | No | Referral fee taken off the gross commission, in basis points (300 = 3%). Used when referralMode is percent. | |
referralMode | string | No | How the referral fee is entered: percent (use referralFeeBps) or dollar (use referralAmountCents). Defaults to percent. Allowed: percent, dollar | |
referralAmountCents | integer | No | Flat referral amount in integer cents — only used when referralMode is dollar. | |
coBrokerageBps | integer | No | Co-brokerage commission paid to the other broker, in basis points; applied AFTER the referral fee. 0 = no co-broker. | |
coBrokerageMode | string | No | How the co-brokerage commission is entered: percent (use coBrokerageBps) or dollar (use coBrokerageAmountCents). Defaults to percent. Allowed: percent, dollar | |
coBrokerageAmountCents | integer | No | Flat co-brokerage amount in integer cents — only used when coBrokerageMode is dollar. | |
freeRentMode | string | No | How 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 | |
freeRentMonthsTotal | number | No | Total free rent for the whole term, in freeRentUnit units — only used when freeRentMode is amortized. | |
freeRentUnit | string | No | Unit ALL free-rent figures are entered in (years[].freeRentMonths and freeRentMonthsTotal). Weeks convert at 12 months = 52 weeks. Allowed: months, weeks | |
commissionBasis | string | No | What 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 | |
commissionCalc | string | No | How 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 | |
fixedMode | string | No | In 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 | |
fixedMonths | number | No | fixed + months: number of months of the year-1 monthly base rent. | |
fixedAmountCents | integer | No | fixed + amount: the flat commission in integer cents. | |
fixedPercentBps | integer | No | fixed + percent: one flat commission rate in basis points (300 = 3%) applied to EVERY year's rent on the selected basis. | |
billTo | string | No | Billing recipient for the commission invoice (free-form). Display/billing only. | |
billToAddress | string | No | Billing address. Display/billing only. | |
billToEmail | string | No | Billing email. Display/billing only. | |
termMonths | integer | null | No | Total 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. |
leaseStart | string | No | Lease commencement date, ISO YYYY-MM-DD (empty string when not entered). | |
leaseEnd | string | No | Lease expiration date, ISO YYYY-MM-DD (empty string when not entered). | |
years | array | No | One 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 fromYear–toYear inclusive.
| Field | Type | Required | Notes |
|---|---|---|---|
fromYear | integer | No | — |
toYear | integer | No | — |
percent | string | No | Rate 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.
| Field | Type | Required | Notes |
|---|---|---|---|
percent | string | No | Share of the commission as an entered string, e.g. "50" = 50%. |
milestone | string | No | Milestone label, e.g. "Lease Execution". |
expectedDate | string | No | Optional 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`
| Field | Type | Required | Notes | |
|---|---|---|---|---|
id | string | No | — | |
name | string | No | — | |
domain | string | null | No | — |
businessDescription | string | null | No | — |
businessSinceYear | integer | null | No | — |
activelySearching | boolean | No | — | |
createdByUserId | string | No | — | |
relationshipOwnerId | string | null | No | EXR relationship lead-owner. Null means the tenant is firm-wide. |
createdAt | string | null | No | — |
updatedAt | string | null | No | — |
`TenantWrite`
Writable tenant fields. name is required on create. The creator (userId) is set from the token and cannot be supplied.
| Field | Type | Required | Notes | |
|---|---|---|---|---|
name | string | Yes | — | |
domain | string | null | No | — |
businessDescription | string | null | No | — |
businessSinceYear | integer | null | No | — |
activelySearching | boolean | No | — | |
relationshipOwnerId | string | null | No | EXR relationship lead-owner. Null means firm-wide. |
`DeleteResult`
| Field | Type | Required | Notes |
|---|---|---|---|
id | string | No | — |
deleted | boolean | No | — |
`Prospect`
| Field | Type | Required | Notes | |
|---|---|---|---|---|
id | string | No | — | |
propertyAddress | string | null | No | — |
landlordName | string | null | No | — |
status | string | No | — | |
contactMethod | string | null | No | — |
contactEmail | string | null | No | — |
contactNotes | string | null | No | — |
keyQuestions | string | null | No | — |
followUpAction | string | null | No | — |
currentBroker | string | null | No | — |
currentBrokerageId | string | null | No | — |
existingAssetClass | string | null | No | — |
siteClassification | string | null | No | — |
lastContactDate | string | null | No | — |
listingExpirationDate | string | null | No | — |
location | object | No | — | |
featured | boolean | No | — | |
archivedAt | string | null | No | — |
ownerUserId | string | No | — | |
createdByUserId | string | null | No | — |
createdAt | string | null | No | — |
updatedAt | string | null | No | — |
`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.
| Field | Type | Required | Notes | |
|---|---|---|---|---|
propertyAddress | string | Yes | — | |
landlordName | string | null | No | — |
status | string | null | No | — |
contactMethod | string | null | No | — |
contactEmail | string | null | No | — |
contactNotes | string | null | No | — |
keyQuestions | string | null | No | — |
followUpAction | string | null | No | — |
currentBroker | string | null | No | — |
currentBrokerageId | string | null | No | — |
existingAssetClass | string | null | No | — |
siteClassification | string | null | No | — |
lastContactDate | string | null | No | — |
listingExpirationDate | string | null | No | — |
latitude | number | null | No | — |
longitude | number | null | No | — |
`Client`
A CRM client (internally crm_clients). Private to its owner, creator, collaborators, and admins.
| Field | Type | Required | Notes | |
|---|---|---|---|---|
id | string | No | — | |
name | string | No | — | |
clientType | string | No | Allowed: seller, buyer, landlord, tenant | |
entityType | string | No | Allowed: individual, company | |
assetClasses | array | No | — | |
notes | string | null | No | — |
ownerUserId | string | No | — | |
createdByUserId | string | No | — | |
createdAt | string | null | No | — |
updatedAt | string | null | No | — |
`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).
| Field | Type | Required | Notes | |
|---|---|---|---|---|
name | string | Yes | — | |
clientType | string | Yes | Allowed: seller, buyer, landlord, tenant | |
entityType | string | Yes | Allowed: individual, company | |
assetClasses | array | No | — | |
clientOwnerId | string | No | The owning user. Defaults to the token's user when omitted. Must reference an existing user. | |
notes | string | null | No | — |
`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).
| Field | Type | Required | Notes | |
|---|---|---|---|---|
name | string | No | — | |
clientType | string | No | Allowed: seller, buyer, landlord, tenant | |
entityType | string | No | Allowed: individual, company | |
assetClasses | array | No | — | |
clientOwnerId | string | No | Reassign the owning user. Only the current owner or an admin may change this. Must reference an existing user. | |
notes | string | null | No | — |
`Workspace`
A leasing brochure (internally leasing_listings).
| Field | Type | Required | Notes | |
|---|---|---|---|---|
id | string | No | — | |
title | string | No | — | |
propertyName | string | null | No | — |
status | string | No | — | |
assetType | string | No | — | |
address | object | No | — | |
location | object | No | — | |
pricing | object | No | — | |
building | object | No | — | |
shareToken | string | null | No | — |
description | string | null | No | — |
ownerUserId | string | No | — | |
createdAt | string | null | No | — |
updatedAt | string | null | No | — |
`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.
| Field | Type | Required | Notes | |
|---|---|---|---|---|
title | string | Yes | — | |
status | string | No | — | |
propertyName | string | null | No | — |
addressLine1 | string | Yes | — | |
addressLine2 | string | null | No | — |
neighborhood | string | null | No | — |
borough | string | null | No | — |
city | string | null | No | — |
state | string | null | No | — |
zip | string | null | No | — |
assetType | string | No | — | |
pricingDisplayMode | string | No | — | |
askingRentDisplay | string | null | No | — |
askingPpsfCents | integer | null | No | — |
askingRentMonthlyCents | integer | null | No | — |
templateKey | string | null | No | — |
viewerGateEnabled | boolean | No | — | |
activeListingUrl | string | null | No | — |
latitude | string | null | No | — |
longitude | string | null | No | — |
description | string | null | No | — |
matterportUrl | string | null | No | — |
videoUrl | string | null | No | — |
yearBuilt | integer | null | No | — |
numFloors | integer | null | No | — |
buildingSf | integer | null | No | — |
lotSf | integer | null | No | — |
zoning | string | null | No | — |
retailSf | integer | null | No | — |
officeSf | integer | null | No | — |
residentialSf | integer | null | No | — |
`Broker`
A commercial broker (internally brokers).
| Field | Type | Required | Notes | |
|---|---|---|---|---|
id | string | No | — | |
name | string | No | — | |
workEmail | string | null | No | — |
personalEmail | string | null | No | — |
mobilePhone | string | null | No | — |
workPhone | string | null | No | — |
agentUrl | string | null | No | — |
notes | string | null | No | — |
brokerageId | string | null | No | — |
brokerSpecialtyId | string | null | No | — |
linkedImportedBrokerId | string | null | No | Read-only reference to a linked global EXR agent record (enrichment only). Managed by the in-app link flow, never the public API. |
createdByUserId | string | No | — | |
createdAt | string | null | No | — |
updatedAt | string | null | No | — |
`BrokerWrite`
Writable fields for a broker on create (POST). userId and linkedImportedBrokerId are server-managed and ignored (stripped) if sent. name is required.
| Field | Type | Required | Notes | |
|---|---|---|---|---|
name | string | Yes | — | |
workEmail | string | null | No | — |
personalEmail | string | null | No | — |
mobilePhone | string | null | No | — |
workPhone | string | null | No | — |
agentUrl | string | null | No | — |
notes | string | null | No | — |
brokerageId | string | null | No | Must reference an existing brokerage (else 400 invalid_reference). |
brokerSpecialtyId | string | null | No | Must 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.
| Field | Type | Required | Notes | |
|---|---|---|---|---|
name | string | No | — | |
workEmail | string | null | No | — |
personalEmail | string | null | No | — |
mobilePhone | string | null | No | — |
workPhone | string | null | No | — |
agentUrl | string | null | No | — |
notes | string | null | No | — |
brokerageId | string | null | No | Must reference an existing brokerage (else 400 invalid_reference). |
brokerSpecialtyId | string | null | No | Must reference an existing broker specialty (else 400 invalid_reference). |
`Brokerage`
A brokerage firm (internally brokerages).
| Field | Type | Required | Notes | |
|---|---|---|---|---|
id | string | No | — | |
name | string | No | — | |
domain | string | null | No | — |
website | string | null | No | — |
description | string | null | No | — |
linkedImportedBrokerageId | string | null | No | Read-only reference to a linked global EXR firm record (enrichment only). Managed by the in-app link flow, never the public API. |
createdByUserId | string | No | — | |
createdAt | string | null | No | — |
updatedAt | string | null | No | — |
`BrokerageWrite`
Writable fields for a brokerage on create (POST). userId and linkedImportedBrokerageId are server-managed and ignored (stripped) if sent. name is required.
| Field | Type | Required | Notes | |
|---|---|---|---|---|
name | string | Yes | — | |
domain | string | null | No | — |
website | string | null | No | — |
description | string | null | No | — |
`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.
| Field | Type | Required | Notes | |
|---|---|---|---|---|
name | string | No | — | |
domain | string | null | No | — |
website | string | null | No | — |
description | string | null | No | — |
`ActionTarget`
A record this action is linked to. Targets the token cannot access are stripped from the response.
| Field | Type | Required | Notes | |
|---|---|---|---|---|
targetType | string | No | One of prospect, contact, landlord, deal, listing. | |
targetId | string | No | — | |
label | string | null | No | — |
`Action`
| Field | Type | Required | Notes | |
|---|---|---|---|---|
id | string | No | — | |
title | string | No | — | |
actionType | string | No | — | |
status | string | No | — | |
priority | string | No | — | |
visibility | string | No | — | |
plan | string | No | — | |
source | string | No | — | |
description | string | null | No | — |
dueAt | string | null | No | — |
snoozeUntil | string | null | No | — |
createdByUserId | string | No | — | |
assignedToUserId | string | null | No | — |
completedByUserId | string | null | No | — |
completedAt | string | null | No | — |
createdAt | string | null | No | — |
updatedAt | string | null | No | — |
targets | array | No | — | |
questions | array | No | — |
`ActionCreate`
| Field | Type | Required | Notes | |
|---|---|---|---|---|
title | string | Yes | — | |
actionType | string | No | Defaults to call. | |
status | string | No | Defaults to open. | |
priority | string | No | Defaults to normal. | |
visibility | string | No | Defaults to prospect_team. | |
plan | string | No | Defaults to one_time. | |
description | string | null | No | — |
dueAt | string | null | No | Due date/time. Preserved in the firm's local timezone (no UTC drift). |
snoozeUntil | string | null | No | — |
assignedTo | string | null | No | User id to assign to; defaults to the token owner. |
targets | array | Yes | At least one target is required; the token must have access to each. | |
questions | array | No | — |
`ActionUpdate`
All fields optional; only provided fields are changed. Setting status to completed records the completer + completion time.
| Field | Type | Required | Notes | |
|---|---|---|---|---|
title | string | No | — | |
actionType | string | No | — | |
status | string | No | — | |
priority | string | No | — | |
visibility | string | No | — | |
plan | string | No | — | |
description | string | null | No | — |
dueAt | string | null | No | — |
snoozeUntil | string | null | No | — |
assignedTo | string | null | No | — |
targetContactId | string | null | No | Sets/clears the contact target for the action's prospect. |
questions | array | No | — |
`CampaignTemplate`
A reusable marketing campaign email template. Storing one never sends email.
| Field | Type | Required | Notes | |
|---|---|---|---|---|
id | string | No | — | |
name | string | No | — | |
subject | string | No | — | |
body | string | No | TipTap/HTML body, returned verbatim. | |
category | string | null | No | — |
createdByUserId | string | null | No | — |
createdAt | string | null | No | — |
updatedAt | string | null | No | — |
`CampaignTemplateCreate`
| Field | Type | Required | Notes | |
|---|---|---|---|---|
name | string | Yes | — | |
subject | string | Yes | — | |
body | string | Yes | — | |
category | string | null | No | — |
`CampaignTemplateUpdate`
All fields optional; only provided fields are changed.
| Field | Type | Required | Notes | |
|---|---|---|---|---|
name | string | No | — | |
subject | string | No | — | |
body | string | No | — | |
category | string | null | No | — |
`CampaignSendRecipient`
| Field | Type | Required | Notes | |
|---|---|---|---|---|
id | string | No | — | |
email | string | No | — | |
name | string | null | No | — |
sourceType | string | No | — | |
sourceId | string | null | No | — |
status | string | No | — | |
errorReason | string | null | No | — |
sentAt | string | null | No | — |
deliveredAt | string | null | No | — |
firstOpenedAt | string | null | No | — |
lastOpenedAt | string | null | No | — |
openCount | integer | No | — | |
firstClickedAt | string | null | No | — |
lastClickedAt | string | null | No | — |
clickCount | integer | No | — | |
bouncedAt | string | null | No | — |
`CampaignSend`
Read-only record of a marketing campaign blast with engagement rollups.
| Field | Type | Required | Notes | |
|---|---|---|---|---|
id | string | No | — | |
templateId | string | null | No | — |
subject | string | No | — | |
body | string | No | — | |
sentByUserId | string | null | No | — |
recipientCount | integer | No | — | |
status | string | No | — | |
scheduledAt | string | null | No | — |
createdAt | string | null | No | — |
rollup | object | No | Per-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. | |
recipients | array | No | Per-recipient delivery rows; only present on the single-send detail read. |
`EntityAlias`
An alternate name (spelling/variant) on file for an entity.
| Field | Type | Required | Notes | |
|---|---|---|---|---|
id | string | No | — | |
alias | string | No | — | |
source | string | null | No | How the alias was added (e.g. manual). |
verified | boolean | No | — | |
verifiedAt | string | null | No | — |
createdAt | string | null | No | — |
`Entity`
A landlord-directory entity (ownership group, owner, manager, brokerage, person, etc.).
| Field | Type | Required | Notes | |
|---|---|---|---|---|
id | string | No | — | |
canonicalName | string | No | — | |
kind | string | No | — | |
primaryPhone | string | null | No | — |
primaryEmail | string | null | No | — |
website | string | null | No | — |
notes | string | null | No | — |
researchStatus | string | No | — | |
legalName | string | null | No | — |
mailingAddress | string | null | No | — |
territory | string | null | No | — |
preferredAssetClass | string | null | No | — |
researchSource | string | null | No | — |
lastVerifiedAt | string | null | No | — |
tags | array | No | — | |
mergedIntoId | string | null | No | When set, this entity was merged into the referenced survivor and is hidden from the list. Merge/dedupe is in-app only. |
createdByUserId | string | null | No | Who introduced the entity. Null on legacy rows (which anyone may edit). |
createdAt | string | null | No | — |
updatedAt | string | null | No | — |
aliases | array | No | Present only on the single-entity GET (omitted from the list). |
`EntityCreate`
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | Yes | Display name; the canonical name and seed aliases are derived from it. |
kind | string | No | Defaults to unknown when omitted. Allowed: ownership_group, legal_owner, management_company, brokerage, tenant, person, unknown |
primaryPhone | string | No | — |
primaryEmail | string | No | — |
website | string | No | — |
`EntityUpdate`
All fields optional; only provided fields are changed. Send null to clear a nullable field.
| Field | Type | Required | Notes | |
|---|---|---|---|---|
canonicalName | string | No | — | |
kind | string | No | Allowed: ownership_group, legal_owner, management_company, brokerage, tenant, person, unknown | |
primaryPhone | string | null | No | — |
primaryEmail | string | null | No | — |
website | string | null | No | — |
notes | string | null | No | — |
researchStatus | string | No | Allowed: unresearched, needs_review, partially_researched, verified, do_not_contact | |
legalName | string | null | No | — |
mailingAddress | string | null | No | — |
territory | string | null | No | — |
preferredAssetClass | string | null | No | — |
researchSource | string | null | No | — |
tags | array | No | — |
`AgreementRecipient`
A recipient on an agreement. Status-only — never includes sign tokens, signature images, IPs, or user agents.
| Field | Type | Required | Notes | |
|---|---|---|---|---|
id | string | No | — | |
email | string | No | — | |
name | string | null | No | — |
order | integer | No | — | |
role | string | No | — | |
isSigner | boolean | No | Derived server-side from the role registry. | |
status | string | No | — | |
sentAt | string | null | No | — |
viewedAt | string | null | No | — |
signedAt | string | null | No | — |
createdAt | string | null | No | — |
`Agreement`
| Field | Type | Required | Notes | |
|---|---|---|---|---|
id | string | No | — | |
templateId | string | null | No | — |
templateKind | string | No | — | |
templateTitle | string | No | — | |
status | string | No | — | |
signingMode | string | No | — | |
negotiationEnabled | boolean | No | — | |
requiresCounterSign | boolean | No | Whether 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. | |
values | object | No | — | |
hiddenFields | array | No | — | |
fields | array | No | — | |
coverNote | string | null | No | — |
body | string | No | Stored (sanitized) template body with {{tokens}} intact. | |
renderedBody | string | No | Body with values substituted, re-sanitized through the strict policy used by the PDF/signing path. | |
linkedDealId | string | null | No | — |
linkedTenantId | string | null | No | — |
linkedListingId | string | null | No | — |
createdByUserId | string | null | No | — |
sentAt | string | null | No | — |
completedAt | string | null | No | — |
voidedAt | string | null | No | — |
counterSignedAt | string | null | No | Counter-sign status (timestamp) only — never the signature image / IP / UA. |
createdAt | string | null | No | — |
updatedAt | string | null | No | — |
recipients | array | No | — |
`AgreementRecipientInput`
| Field | Type | Required | Notes | |
|---|---|---|---|---|
email | string | Yes | — | |
name | string | null | No | — |
order | integer | No | — | |
role | string | Yes | Recipient role; isSigner is derived from it server-side. |
`AgreementCreate`
| Field | Type | Required | Notes | |
|---|---|---|---|---|
templateId | string | Yes | — | |
linkedDealId | string | null | No | — |
linkedTenantId | string | null | No | — |
linkedListingId | string | null | No | — |
coverNote | string | null | No | — |
signingMode | string | No | Allowed: parallel, sequential | |
values | object | No | Field answers, merged over the template's seeded defaults. | |
hiddenFields | array | No | — | |
recipients | array | No | — |
`AgreementUpdate`
Draft-only. All fields optional; only provided fields change. A provided recipients array fully replaces the list.
| Field | Type | Required | Notes | |
|---|---|---|---|---|
values | object | No | — | |
hiddenFields | array | No | — | |
coverNote | string | null | No | — |
signingMode | string | No | Allowed: parallel, sequential | |
negotiationEnabled | boolean | No | — | |
requiresCounterSign | boolean | No | Whether the agreement should wait for an EXR counter-signature after the final signer signs. | |
linkedDealId | string | null | No | — |
linkedTenantId | string | null | No | — |
linkedListingId | string | null | No | — |
recipients | array | No | — |
`AgreementCounterSign`
The EXR counter-signature applied to a fully-signed agreement.
| Field | Type | Required | Notes |
|---|---|---|---|
typedName | string | Yes | The counter-signer's full typed name. |
signatureDataUrl | string | Yes | A data: URL of the rendered signature image. |
agreed | boolean | Yes | Must be true to consent to counter-signing. Allowed: true |