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 in exact (updatedAt DESC, id DESC) order. The opaque cursor identifies the final tuple on the previous page; the next page begins strictly below that (updatedAt, id) boundary, so equal timestamps continue by descending id without gaps. 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 (updatedAt, id) boundary from a previous response's nextCursor; pass it back unchanged.
- 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. financials.leaseTerms is validated against the lease commission calculator's contract (see DealLeaseTerms): out-of-contract values such as a fractional or inconsistent termMonths, negative or fractional cents, out-of-range basis points, or unknown enum values are rejected with 400 invalid_body naming the key, never coerced or stored.
- 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). Fields and splits commit as one logical change with one immutable deal.updated event durably enqueued in the same transaction before asynchronous delivery. A financials.leaseTerms fragment is merged onto the stored terms and validated against the lease commission calculator's contract (see DealLeaseTerms): every key you send must already be in contract, and when the fragment touches termMonths or years the merged record must keep termMonths consistent with the row count; out-of-contract values are rejected with 400 invalid_body naming the key and the stored record is left untouched.
- 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, triage, 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.
Every action carries a triage state layered over source. System-generated actions (Listings Intelligence research and verify-listing-status actions, listing outreach milestones) start as suggested and only become the broker's own work once added in the app (accepted); a dismissed suggestion is dismissed with status canceled and is never regenerated. Broker-created actions are always accepted. Without a triage filter the list keeps returning every visible action regardless of triage state, so existing integrations see no change; pass triage=accepted to read only committed work.
- 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.
- triage (query; string) — Filter by triage state. accepted returns committed work (broker-created actions and added suggestions), suggested the pending Suggested tray, dismissed suggestions the broker sent away. Omit to return all states.
- 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 |
ownerTeamRole | string | null | No | Role assigned to the deal owner. Required and applicable for sale, lease, and sublease; optional for unsupported transaction types. Allowed: listing_agent, tenant_rep, buyer_rep, tenant_side_agent, buyer_side_agent, 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 | deal_closed is the canonical closed status. Defaults to the first pipeline stage when omitted. Allowed: potential_offer, loi_submitted, loi_negotiations, final_loi, agreement_negotiations, deal_closed, deal_lost | |
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. When a request explicitly supplies both status: "deal_closed" and this field, it cannot be null. A status-only transition may close or reopen a legacy deal whose stored value is null. |
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 | deal_closed is the canonical closed status. Allowed: potential_offer, loi_submitted, loi_negotiations, final_loi, agreement_negotiations, deal_closed, deal_lost | |
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. Validation: every documented key you send must already be in contract (integer cents that are never negative, whole-number or one-decimal basis points inside the stated ranges, the listed enum values, ISO YYYY-MM-DD dates or empty strings, termMonths a whole number of months from 1 to 720, at most 60 years[] rows). Anything the calculator would otherwise have to round, clamp, or fall back on read is rejected with 400 invalid_body and a message naming the exact key (for example financials.leaseTerms.years[1].excludedChargesAnnualCents must be a non-negative integer number of cents); nothing is silently rewritten. Whenever a request touches termMonths or years, the merged record must also satisfy termMonths null or ceil(termMonths / 12) equal to years.length; partial updates that touch neither are exempt, so legacy records stay editable. Unknown keys still pass through unchanged. | |
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 |
referralInvoiceMethod | string | null | No | How the referral fee is invoiced. included_on_exr_invoice keeps the referral visible but does not deduct it from EXR GCI; invoiced_separately deducts it from EXR GCI. null clears the field (normalized automatically when the effective referral is removed). Validation: a positive active-mode referral requires a non-null method whenever the request creates or edits relevant commission fields. Unrelated partial updates on legacy rows are exempt. Allowed: included_on_exr_invoice, invoiced_separately, null |
`DealLeaseTermsYear`
One lease-year row of the commission calculator. Rates are basis points (10000 = 100%, e.g. 300 = 3%); money is integer cents. Each row must be a JSON object; on write, every documented key present must already satisfy the range and precision stated in its description or the request is rejected with 400 invalid_body naming financials.leaseTerms.years[<index>].<key>. Unknown row keys pass through unchanged.
| 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). Must be a whole number of basis points from 0 to 1,000,000. |
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. Must be a number of 0 or more. |
excludedChargesAnnualCents | integer | No | Annual taxes, CAM, BID, or other charges included in this year's stated base rent but excluded from net rent, in integer cents. Defaults to 0; rows saved before the column existed omit it. Entered as an ANNUAL figure: a 12-month row deducts the full amount, while a prorated partial final year (see termMonths) deducts round(amount × months / 12). Net rent is base rent minus free rent minus this deduction (floored at 0), so it lowers any commission charged on net rent; ignored for commission when commissionBasis is base_rent. Must be a non-negative integer number of cents (a JSON number, not a string). |
commissionBps | number | No | Commission rate for this year, in basis points (300 = 3%); may carry one decimal place (312.5 = 3.125%). Applied to net rent by default, or to base rent when commissionBasis is base_rent. Ignored entirely when commissionCalc is fixed. Must be a number of basis points from 0 to 1,000,000 with at most one decimal place. |
`DealLeaseTerms`
The deal's lease commission calculator terms: a per-year rent schedule where each year has its own escalation, free rent, excluded charges, and commission rate. Conventions: all money fields are integer CENTS; all rates are BASIS POINTS (10000 = 100%, e.g. 300 = 3%) and the commission, referral, and co-brokerage rates may carry one decimal place (312.5 = 3.125%); dates are ISO YYYY-MM-DD strings (empty string when not entered). Commission is charged on net rent (base rent minus free rent minus excluded charges, floored at 0) by default, or on base rent when commissionBasis is base_rent. Writes are validated, never normalized: each documented key you send must already meet the range, precision, and type stated in its description (money as JSON numbers of non-negative integer cents, rates inside their basis-point ranges, the listed enum values, whole-month termMonths from 1 to 720 consistent with the row count, at most 60 years[] rows), otherwise the request is rejected with 400 invalid_body naming the key. Reads echo the stored object as written. 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. A whole number of square feet from 0 to 100,000,000, or null; 0 reads back as null. |
usableSqft | integer | null | No | Usable square footage (USF) for Office premises. Informational only; it never affects PPSF or commission calculations. A whole number of square feet from 0 to 100,000,000, or null; 0 reads back as null. |
groundFloorSqft | integer | null | No | Retail-only breakdown of sqft: ground-floor square footage. Display/informational only. A whole number of square feet from 0 to 100,000,000, or null; 0 reads back as null. |
basementSqft | integer | null | No | Retail-only breakdown of sqft: basement square footage. Display/informational only. A whole number of square feet from 0 to 100,000,000, or null; 0 reads back as null. |
startingRentAnnualCents | integer | null | No | Year-1 base annual rent, in integer cents. Drives every later year via the per-year escalations. A non-negative integer number of cents (a JSON number, not a string), or null; 0 reads back as null. |
referralFeeBps | number | No | Referral fee taken off the gross commission, in basis points (300 = 3%); may carry one decimal place (312.5 = 3.125%). Used when referralMode is percent. Must be a number of basis points from 0 to 10000 with at most one decimal place. | |
referralMode | string | No | How the referral fee is entered: percent (use referralFeeBps) or dollar (use referralAmountCents). Defaults to percent. Only the listed values are accepted. Allowed: percent, dollar | |
referralAmountCents | integer | No | Flat referral amount in integer cents — only used when referralMode is dollar. Must be a non-negative integer number of cents (a JSON number, not a string). | |
coBrokerageBps | number | No | Co-brokerage commission paid to the other broker, in basis points (may carry one decimal place, 312.5 = 3.125%); applied AFTER the referral fee. 0 = no co-broker. Must be a number of basis points from 0 to 10000 with at most one decimal place. | |
coBrokerageMode | string | No | How the co-brokerage commission is entered: percent (use coBrokerageBps) or dollar (use coBrokerageAmountCents). Defaults to percent. Only the listed values are accepted. Allowed: percent, dollar | |
coBrokerageAmountCents | integer | No | Flat co-brokerage amount in integer cents — only used when coBrokerageMode is dollar. Must be a non-negative integer number of cents (a JSON number, not a string). | |
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). Only the listed values are accepted. Allowed: by_year, amortized | |
freeRentMonthsTotal | number | No | Total free rent for the whole term, in freeRentUnit units — only used when freeRentMode is amortized. Must be a number from 0 to 600. | |
freeRentUnit | string | No | Unit ALL free-rent figures are entered in (years[].freeRentMonths and freeRentMonthsTotal). Weeks convert at 12 months = 52 weeks. Only the listed values are accepted. Allowed: months, weeks | |
commissionBasis | string | No | What rent figure the commission rate is charged on: net_rent (base rent minus free rent minus years[].excludedChargesAnnualCents, the default) or base_rent (free rent and excluded charges ignored for commission). Used in sliding mode and in fixed mode with fixedMode percent. Only the listed values are accepted. 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). Only the listed values are accepted. 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). Only the listed values are accepted. Allowed: months, amount, percent | |
fixedMonths | number | No | fixed + months: number of months of the year-1 monthly base rent. Must be a number from 0 to 600. | |
fixedAmountCents | integer | No | fixed + amount: the flat commission in integer cents. Must be a non-negative integer number of cents (a JSON number, not a string). | |
fixedPercentBps | number | No | fixed + percent: one flat commission rate in basis points (300 = 3%; may carry one decimal place, 312.5 = 3.125%) applied to EVERY year's rent on the selected basis. Must be a number of basis points from 0 to 1,000,000 with at most one decimal place. | |
commissionPaidBy | string | No | Who pays the commission: landlord, tenant, both, or empty string when not selected (the default). Informational only: in the app it can pre-fill the billTo* fields, but it never changes any commission figure. Only the listed values are accepted (null is not). Allowed: landlord, tenant, both, "" (empty string) | |
billTo | string | No | Billing recipient for the commission invoice (free-form). Display/billing only. A string of at most 2,000 characters. | |
billToAddress | string | No | Billing address. Display/billing only. A string of at most 2,000 characters. | |
billToEmail | string | No | Billing email. Display/billing only. A string of at most 2,000 characters. | |
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. Must be a whole number of months from 1 to 720, or null; on write, whenever a request touches termMonths or years, the merged record must satisfy the row-count rule or the request is rejected with 400 invalid_body naming financials.leaseTerms.termMonths. |
leaseStart | string | No | Lease commencement date, ISO YYYY-MM-DD naming a real calendar day (empty string when not entered). Any other value, including a well-formed but impossible date such as 2026-02-30, is rejected on write. | |
leaseEnd | string | No | Lease expiration date, ISO YYYY-MM-DD naming a real calendar day (empty string when not entered). Any other value, including a well-formed but impossible date such as 2026-02-30, is rejected on write. | |
years | array | No | One row per lease year, in order (index 0 = year 1). Must be an array of at most 60 row objects (60 years = the 720-month termMonths ceiling; the in-app editor has the same limit). Records written before this limit existed may carry more rows and still compute. |
`DealLeaseSchedule`
Read-only, server-computed per-year schedule returned as financials.leaseSchedule on every deal read, write echo, and deal.created / deal.updated webhook payload. It is the exact row-by-row output of the lease commission calculator for the deal's leaseTerms (the same engine that produces grossCommissionCents), so a consumer can display or store the schedule without re-deriving the math: escalation chaining, partial-year proration, free-rent valuation, the excluded-charges deduction, and every rounding point are already applied. Money fields are integer cents rendered as strings; rates are basis points (10000 = 100%); free-rent quantities are numbers. freeRentMode, freeRentUnit, commissionBasis, commissionCalc, and fixedMode are the normalized modes the calculator actually applied, so legacy or invalid leaseTerms values already show their documented fallback here. grossCommissionCents is the schedule's commission total: it equals the sum of rows[].commissionCents (reported as totals.commissionCents) when commissionCalc is sliding or fixedMode is percent. When commissionCalc is fixed with fixedMode months or amount there is no per-year commission: every row's commissionBps and commissionCents are 0, totals.commissionCents is "0", and only grossCommissionCents carries the flat figure. Referral, co-brokerage, and GCI are not part of the schedule; read them from the financials summary. Never write this object back: it is derived from leaseTerms and is not accepted by POST or PATCH. Requires the deals:financials:read scope.
| Field | Type | Required | Notes | |
|---|---|---|---|---|
freeRentMode | string | Yes | Effective free-rent model: by_year (each row's own free rent, priced at that row's monthly rate) or amortized (one whole-term total, valued at the year-1 monthly rate and spread evenly across every row). Allowed: by_year, amortized | |
freeRentUnit | string | Yes | Unit of rows[].freeRentUnits and totals.freeRentUnits. rows[].freeRentMonths and totals.freeRentMonths are always month-equivalents (weeks convert at 12 months = 52 weeks). Allowed: months, weeks | |
commissionBasis | string | Yes | Rent figure each row's commission rate is charged on; rows[].commissionableRentCents is that figure per row (net rent by default, base rent under base_rent). Allowed: net_rent, base_rent | |
commissionCalc | string | Yes | sliding: each row carries its own rate and the row commissions sum to grossCommissionCents. fixed: a single flat commission; see fixedMode for whether the rows still carry a per-year breakdown. Allowed: sliding, fixed | |
fixedMode | string | null | Yes | Set only when commissionCalc is fixed; null for sliding. percent: one flat rate applied to every row, and the row commissions still sum to grossCommissionCents. months or amount: a flat commission with no per-year breakdown, so every row's commissionBps and commissionCents are 0 and grossCommissionCents alone carries the figure. Allowed: months, amount, percent, null |
rows | array | Yes | One DealLeaseScheduleRow per leaseTerms.years[] entry, in order (year 1 first). Empty when the terms have no year rows. | |
totals | DealLeaseScheduleTotals | Yes | Column totals across rows (see DealLeaseScheduleTotals). |
`DealLeaseScheduleRow`
One lease year of DealLeaseSchedule.rows. Every amount is for the months this row covers, so a prorated partial final year carries prorated amounts; the *Annual* names follow the calculator's column names. Money is integer cents as strings; rates are basis points.
| Field | Type | Required | Notes | |
|---|---|---|---|---|
year | integer | Yes | 1-based lease year. | |
monthsInYear | integer | Yes | Months this row covers: 12, or the remaining months of a partial final year when leaseTerms.termMonths is set and valid. | |
baseRentAnnualCents | string | Yes | Base rent charged for this row, in cents: the full-year rate after the escalation chain, prorated as round(rate × monthsInYear / 12) on a partial final year. | |
baseRentMonthlyCents | string | Yes | The full-year rate divided by 12, rounded to the cent. A display rate, so it is not prorated on a partial final year. | |
escalationBps | integer | Yes | Escalation applied to the prior year's full-year rate to reach this row's rate, in basis points (ignored on year 1). | |
freeRentMonths | number | Yes | Free rent the calculator valued for this row, in month-equivalents (capped at monthsInYear). Under amortized free rent this is 0 on every row and the whole-term quantity is reported once in totals.freeRentMonths. | |
freeRentUnits | number | Yes | The same free-rent quantity in freeRentUnit (equal to freeRentMonths when the unit is months). | |
freeRentAmountCents | string | Yes | Value of this row's free rent, in cents. by_year: round(full-year rate / 12 × freeRentMonths). amortized: the whole-term total valued at the year-1 monthly rate, then round(total / number of rows), so every row carries the same slice. | |
excludedChargesAnnualCents | string | Yes | The leaseTerms.years[].excludedChargesAnnualCents input for this row as the calculator read it ("0" when absent), in cents. | |
excludedChargesCents | string | Yes | Excluded charges actually deducted from this row: the annual figure on a 12-month row, or round(annual × monthsInYear / 12) on a partial final year. | |
netRentAnnualCents | string | Yes | baseRentAnnualCents minus freeRentAmountCents minus excludedChargesCents, floored at 0. | |
commissionableRentCents | string | Yes | The rent this row's rate is charged on: netRentAnnualCents under net_rent, baseRentAnnualCents under base_rent. Informational for fixed months / amount commissions, which do not charge per row. | |
basePpsfCents | string | null | Yes | Full-year base rent per rentable square foot, in cents per SF (null when leaseTerms.sqft is not set). A display rate; never prorated and never used for commission. |
commissionBps | number | Yes | Rate applied to commissionableRentCents, in basis points; may carry one decimal (312.5 = 3.125%). Under fixed percent every row shows fixedPercentBps; under fixed months / amount every row is 0. | |
commissionCents | string | Yes | round(commissionableRentCents × commissionBps / 10000), in cents. "0" on every row for fixed months / amount commissions. |
`DealLeaseScheduleTotals`
Column totals of DealLeaseSchedule.rows. Money is integer cents as strings.
| Field | Type | Required | Notes |
|---|---|---|---|
baseRentCents | string | Yes | Sum of rows[].baseRentAnnualCents. |
freeRentCents | string | Yes | Sum of rows[].freeRentAmountCents (the rounded row slices, so under amortized free rent it can differ by a few cents from the one-time whole-term valuation). |
excludedChargesCents | string | Yes | Sum of rows[].excludedChargesCents. |
netRentCents | string | Yes | Sum of rows[].netRentAnnualCents. |
commissionCents | string | Yes | Sum of rows[].commissionCents. Equals financials.grossCommissionCents for sliding and fixed-percent commissions; "0" for fixed months / amount commissions, where grossCommissionCents alone carries the flat figure. |
freeRentMonths | number | Yes | Whole-term free rent in month-equivalents: the sum of rows[].freeRentMonths under by_year, or the entered whole-term quantity under amortized. |
freeRentUnits | number | Yes | The same whole-term free-rent quantity in freeRentUnit. |
`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 | — |
archiveReason | string | null | No | Allowed: no_response, not_a_fit, already_represented, duplicate_or_bad_data, other, null |
archiveNotes | 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. existingAssetClass is a configurable stable string value; unknown or retired values are rejected, while historical stored values remain readable.
| 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 | — |
archivedAt | string | null | No | Archiving an active prospect requires archiveReason. Sending null restores it and clears archive context. |
archiveReason | string | null | No | Allowed: no_response, not_a_fit, already_represented, duplicate_or_bad_data, other, null |
archiveNotes | string | 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 | — | |
triage | string | No | Triage state layered over source. System-generated actions (source listings_intel, listings_intel_verify, listings_intel_milestone) arrive as suggested and become accepted when the broker adds them to their work; dismissed is terminal (status canceled, never regenerated). Broker-created actions are always accepted. Allowed: suggested, accepted, dismissed | |
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 | Field keys whose lines are left out of the document. When omitted, the template's opt-in fields (hiddenByDefault: true on the field definition) start hidden unless values supplies a non-blank value for them; pass an explicit array to take full control. | |
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 |