openapi: 3.0.3 info: title: PropServ External API version: 1.2.0 description: | The PropServ External API gives estate agent companies programmatic access to their PropServ data: work requests (submissions), contractor responses, tenant reports, tenants, users, properties and contractor categories. Companies with write access can also create work requests directly from their own systems. ## Access API access is included with the **Ultimate** plan and is only available to estate agent companies. An agency **admin** creates company API tokens in the web app under **Settings → Integrations & API → API**, or over HTTP with the token management endpoints below (which use the admin's own user token, not a company token). Creating a token requires the Ultimate plan; tokens that already exist keep working after a downgrade. An agency can hold up to 10 active tokens. Tokens can expire after 30, 90, 180 or 365 days, or never. An expired or revoked token is refused with a `401`. ## Permissions Each token carries one or both of these abilities: | Ability | Needed for | |---|---| | `read:submission-responses` | Every `GET` endpoint | | `write:submissions` | `POST /external/submissions` | A token without the read ability gets a `403` on every read endpoint. Creating work requests also needs write access enabled for your company by PropServ — it is not switched on automatically. Contact your PropServ account manager or info@propserv.co to request it. In practice an integration that creates work requests needs both abilities, because the reference data it depends on (`/external/users`, `/external/properties`, `/external/categories`) is read with the read ability. ## Requests and responses Send the token as a Bearer token in the `Authorization` header together with `Accept: application/json`. Every response under `/external` is JSON, even when the `Accept` header is missing. List endpoints are paginated with `page` and `per_page` (default 50, maximum 100) and return a `pagination` object alongside `data`. Timestamps are ISO 8601 with a UTC offset. ## Rate limits | Scope | Limit | |---|---| | Read endpoints | 240 requests per minute, per API token | | `POST /external/submissions` | 30 requests per minute, per API token, counted separately from reads | | Failed authentication | 60 failed attempts per minute from one IP address | After 60 failed authentications in a minute from one IP address, requests from that IP that present no token or an unknown token are refused for the rest of the minute. Requests with a valid token are unaffected. A throttled request gets a `429` with a `Retry-After` header (seconds). The read and create limits send `X-RateLimit-Limit` and `X-RateLimit-Remaining` on every response, and `X-RateLimit-Reset` only on a `429`. ## Errors Errors return a JSON body with a `message`. Validation errors (`422`) add an `errors` object keyed by field. Other fields may appear in error bodies; rely on `message`. contact: name: PropServ Support email: info@propserv.co servers: - url: https://api.propfees.co/api description: Production - url: https://dev.api.propfees.co/api description: Development (contact PropServ for development credentials) security: - CompanyBearerAuth: [] tags: - name: Submissions description: Work requests. Reading needs the read ability; creating needs the write ability and write access enabled for your company by PropServ. - name: Submission Responses description: Contractor responses (quotes, deposits, callouts and declines) to your work requests. - name: Tenant Reports description: Issues reported by tenants under your agency. - name: Tenants description: Tenants linked to your agency. - name: Users description: Users in your company. - name: Reference Data description: Categories and properties, used to build a work request create payload. - name: Token Management description: Create, list and revoke company API tokens. Authenticated with an agency admin's own user token, not a company token. - name: Documentation description: Machine-readable documentation. paths: /external/submissions: post: operationId: createSubmission summary: Create work request description: | Creates a work request (submission) and releases it to the contractor market. **Access must be requested.** On top of a valid company token: 1. The token must carry the `write:submissions` ability. 2. Write access must be enabled for your company by PropServ. Contact your account manager or info@propserv.co to request it. A token without the ability, or a company without write access, gets a `403` naming the failed check. Resolve `agent_uuid` with `GET /external/users`, `property_uuid` with `GET /external/properties` and category IDs with `GET /external/categories` — those need the read ability. **Idempotency.** Pass your own `external_reference`. If your company already has a work request with that reference, it is returned with `200` instead of `201`: nothing is created, no notifications are sent, and the new payload is neither compared with nor applied to the existing work request. The request is still validated first, so a replay with an invalid body gets a `422`. Without an `external_reference`, two identical calls create two work requests. **What gets created.** The work request starts at status `requested` with `handled_by_agent: true` (contractors contact the assigned agent) and `pay_via_works_order` following your company's works order setting. It is never urgent or a new build. The property's address is copied onto it and geocoded if needed. Ultimate contractors can see it immediately and all contractors after two hours. Creation also sends contractor notifications, starts any auto follow-ups you have configured, syncs an Alto work order for Alto-linked properties and sends status updates for SME Professional-linked properties. Outbound webhooks, if you have configured them, are delivered asynchronously and may arrive before or after this response. Creation does some work synchronously, so allow a generous client timeout and retry with the same `external_reference` if a call times out. **Rate limit:** 30 requests per minute per API token, counted separately from reads. tags: - Submissions requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/WorkRequestCreate' example: agent_uuid: 4c1fa938-9abe-4eb9-85ba-3b93594332ae property_uuid: 563bc697-3fe9-45c3-821c-09c4e6fd1779 external_reference: CRM-88231 external_details_url: https://crm.example.com/jobs/88231 categories: - id: 13 description: Boiler not firing, no hot water. Tenant on site weekday mornings. max_quotes: 3 date_required: '2026-10-04' responses: '201': description: Work request created. headers: X-RateLimit-Limit: $ref: '#/components/headers/X-RateLimit-Limit' X-RateLimit-Remaining: $ref: '#/components/headers/X-RateLimit-Remaining' content: application/json: schema: $ref: '#/components/schemas/SubmissionEnvelope' '200': description: Your company already has a work request with this `external_reference`. It is returned as it is; nothing was created or changed. content: application/json: schema: $ref: '#/components/schemas/SubmissionEnvelope' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/WriteForbidden' '422': $ref: '#/components/responses/SubmissionValidationError' '429': $ref: '#/components/responses/RateLimited' get: operationId: listSubmissions summary: List work requests description: Paginated list of work requests raised by your company's users, newest first. tags: - Submissions parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' - name: status in: query description: Filter by work request status. required: false schema: $ref: '#/components/schemas/SubmissionStatus' - name: external_reference in: query description: Find a work request by the reference your own system supplied when creating it through the API. Only matches work requests your company created through the API. required: false schema: type: string maxLength: 255 - name: created_by_uuid in: query description: Filter by the UUID of the user who created the work request. Matches user creators only, so work requests created through the API (created by your company) never match. required: false schema: type: string format: uuid - name: urgent in: query description: Filter by the urgent flag. required: false schema: $ref: '#/components/schemas/BooleanFilter' - name: new_build in: query description: Filter by the new build flag. required: false schema: $ref: '#/components/schemas/BooleanFilter' - name: handled_by_agent in: query description: Filter by whether contractors contact the agent rather than the client. required: false schema: $ref: '#/components/schemas/BooleanFilter' - name: pay_via_works_order in: query description: Filter by whether the work is paid via works order. required: false schema: $ref: '#/components/schemas/BooleanFilter' responses: '200': description: A page of work requests. headers: X-RateLimit-Limit: $ref: '#/components/headers/X-RateLimit-Limit' X-RateLimit-Remaining: $ref: '#/components/headers/X-RateLimit-Remaining' content: application/json: schema: $ref: '#/components/schemas/SubmissionList' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/ReadForbidden' '422': $ref: '#/components/responses/ValidationError' '429': $ref: '#/components/responses/RateLimited' /external/submissions/{uuid}: get: operationId: getSubmission summary: Get work request description: A single work request raised by one of your company's users. tags: - Submissions parameters: - name: uuid in: path required: true description: Work request UUID. schema: type: string format: uuid responses: '200': description: The work request. content: application/json: schema: $ref: '#/components/schemas/SubmissionEnvelope' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/ReadForbidden' '404': description: No work request with this UUID belongs to your company. content: application/json: schema: $ref: '#/components/schemas/Error' example: message: Submission not found. '429': $ref: '#/components/responses/RateLimited' /external/submissions/{uuid}/responses: get: operationId: listResponsesForSubmission summary: List responses for a work request description: Paginated contractor responses for one work request, newest first. Poll this after creating a work request to collect quotes. tags: - Submissions parameters: - name: uuid in: path required: true description: Work request UUID. schema: type: string format: uuid - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' - name: status in: query description: Filter by response status. required: false schema: $ref: '#/components/schemas/SubmissionResponseStatus' - name: type in: query description: Filter by response type. required: false schema: $ref: '#/components/schemas/SubmissionResponseType' responses: '200': description: A page of contractor responses. content: application/json: schema: $ref: '#/components/schemas/SubmissionResponseList' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/ReadForbidden' '404': description: No work request with this UUID belongs to your company. content: application/json: schema: $ref: '#/components/schemas/Error' example: message: Submission not found. '422': $ref: '#/components/responses/ValidationError' '429': $ref: '#/components/responses/RateLimited' /external/submission-responses: get: operationId: listSubmissionResponses summary: List contractor responses description: Paginated contractor responses across all of your company's work requests, newest first. tags: - Submission Responses parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' - name: status in: query description: Filter by response status. required: false schema: $ref: '#/components/schemas/SubmissionResponseStatus' - name: type in: query description: Filter by response type. required: false schema: $ref: '#/components/schemas/SubmissionResponseType' - name: submission_uuid in: query description: Only responses to this work request. required: false schema: type: string format: uuid responses: '200': description: A page of contractor responses. content: application/json: schema: $ref: '#/components/schemas/SubmissionResponseList' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/ReadForbidden' '422': $ref: '#/components/responses/ValidationError' '429': $ref: '#/components/responses/RateLimited' /external/submission-responses/{uuid}: get: operationId: getSubmissionResponse summary: Get contractor response description: A single contractor response to one of your company's work requests. tags: - Submission Responses parameters: - name: uuid in: path required: true description: Contractor response UUID. schema: type: string format: uuid responses: '200': description: The contractor response. content: application/json: schema: type: object properties: data: $ref: '#/components/schemas/SubmissionResponse' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/ReadForbidden' '404': description: No contractor response with this UUID belongs to your company's work requests. content: application/json: schema: $ref: '#/components/schemas/Error' example: message: Submission response not found. '429': $ref: '#/components/responses/RateLimited' /external/tenant-reports: get: operationId: listTenantReports summary: List tenant reports description: Paginated tenant reports raised under your agency, newest first. tags: - Tenant Reports parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' - name: status in: query description: Filter by tenant report status. required: false schema: $ref: '#/components/schemas/TenantReportStatus' responses: '200': description: A page of tenant reports. content: application/json: schema: $ref: '#/components/schemas/TenantReportList' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/ReadForbidden' '422': $ref: '#/components/responses/ValidationError' '429': $ref: '#/components/responses/RateLimited' /external/tenant-reports/{uuid}: get: operationId: getTenantReport summary: Get tenant report description: A single tenant report raised under your agency. tags: - Tenant Reports parameters: - name: uuid in: path required: true description: Tenant report UUID. schema: type: string format: uuid responses: '200': description: The tenant report. content: application/json: schema: type: object properties: data: $ref: '#/components/schemas/TenantReport' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/ReadForbidden' '404': description: No tenant report with this UUID belongs to your agency. content: application/json: schema: $ref: '#/components/schemas/Error' example: message: Tenant report not found. '429': $ref: '#/components/responses/RateLimited' /external/tenants: get: operationId: listTenants summary: List tenants description: Paginated tenants whose managing agent is one of your company's users, newest first. tags: - Tenants parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' - name: search in: query description: Matches name or email, containing the term. `%` and `_` are matched literally, not as wildcards. required: false schema: type: string maxLength: 255 - name: active in: query description: Filter by whether the tenant's account is active. required: false schema: $ref: '#/components/schemas/BooleanFilter' responses: '200': description: A page of tenants. content: application/json: schema: $ref: '#/components/schemas/TenantList' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/ReadForbidden' '422': $ref: '#/components/responses/ValidationError' '429': $ref: '#/components/responses/RateLimited' /external/users: get: operationId: listUsers summary: List users description: | Paginated users belonging to your company, newest first: your staff (`estate_agent`) and tenants recorded against your agency (`client`). Use this to resolve an `agent_uuid` for work request creation. The create endpoint only accepts an estate agent who is active, not revoked and has finished setting up their account (set a password or signs in with SSO). tags: - Users parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' - name: search in: query description: Matches name, email or phone number, containing the term. `%` and `_` are matched literally, not as wildcards. required: false schema: type: string maxLength: 255 - name: type in: query description: Filter by user type. required: false schema: type: string enum: [estate_agent, client] - name: active in: query description: Filter by the active flag. required: false schema: $ref: '#/components/schemas/BooleanFilter' - name: revoked in: query description: Filter by whether the user's access has been revoked. required: false schema: $ref: '#/components/schemas/BooleanFilter' responses: '200': description: A page of users. content: application/json: schema: $ref: '#/components/schemas/UserList' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/ReadForbidden' '422': $ref: '#/components/responses/ValidationError' '429': $ref: '#/components/responses/RateLimited' /external/users/{uuid}: get: operationId: getUser summary: Get user description: A single user belonging to your company. tags: - Users parameters: - name: uuid in: path required: true description: User UUID. schema: type: string format: uuid responses: '200': description: The user. content: application/json: schema: type: object properties: data: $ref: '#/components/schemas/User' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/ReadForbidden' '404': description: No user with this UUID belongs to your company. content: application/json: schema: $ref: '#/components/schemas/Error' example: message: User not found. '429': $ref: '#/components/responses/RateLimited' /external/categories: get: operationId: listCategories summary: List contractor categories description: | The contractor categories your company can raise work in, sorted by name. Categories your agency has excluded in its own settings are hidden here and rejected by the create endpoint, so this list is authoritative when building a `POST /external/submissions` payload. Not paginated. tags: - Reference Data responses: '200': description: All available contractor categories. content: application/json: schema: type: object properties: data: type: array items: $ref: '#/components/schemas/ContractorCategory' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/ReadForbidden' '429': $ref: '#/components/responses/RateLimited' /external/properties: get: operationId: listProperties summary: List properties description: | Paginated properties belonging to your company, newest first, so you can map your own address records onto a PropServ `property_uuid`. Deleted properties are not listed. A property must have an address on file to be used in `POST /external/submissions`; one without an address is listed with every `address` field null. tags: - Reference Data parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' - name: search in: query description: Matches address line 1, line 2, city or postcode, containing the term. `%` and `_` are matched literally, not as wildcards. required: false schema: type: string maxLength: 255 - name: active in: query description: Filter by the active flag. required: false schema: $ref: '#/components/schemas/BooleanFilter' responses: '200': description: A page of properties. content: application/json: schema: $ref: '#/components/schemas/PropertyList' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/ReadForbidden' '422': $ref: '#/components/responses/ValidationError' '429': $ref: '#/components/responses/RateLimited' /external/docs: get: operationId: getDocs summary: Machine-readable documentation description: A JSON summary of this API. No authentication required. Rate limited to 240 requests per minute per IP address. tags: - Documentation security: [] responses: '200': description: API summary. content: application/json: schema: type: object additionalProperties: true /external-api/tokens: get: operationId: listApiTokens summary: List API tokens description: | Lists your company's API tokens, including expired ones. The plain-text token is never returned here. `meta.write_enabled` says whether PropServ has enabled work request creation for your company, and `meta.max_tokens` is the cap on active tokens. Agency admins only. tags: - Token Management security: - UserBearerAuth: [] responses: '200': description: Your company's API tokens. content: application/json: schema: $ref: '#/components/schemas/ApiTokenList' '401': $ref: '#/components/responses/UserUnauthorized' '403': $ref: '#/components/responses/NotAnAdmin' post: operationId: createApiToken summary: Create API token description: | Creates a company API token. The plain-text token is in `data.token` and is shown only once. Omitting `abilities` gives a read-only token; `write:submissions` is never granted implicitly. Agency admins only. Creating a token requires the Ultimate plan, and an agency may hold at most 10 active (unexpired) tokens. tags: - Token Management security: - UserBearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ApiTokenCreate' example: name: CRM integration abilities: [read:submission-responses, write:submissions] expires_in_days: 90 responses: '201': description: Token created. content: application/json: schema: $ref: '#/components/schemas/ApiTokenCreated' '401': $ref: '#/components/responses/UserUnauthorized' '403': description: The user is not an agency admin, or the company is not on the Ultimate plan. content: application/json: schema: $ref: '#/components/schemas/Error' examples: notUltimate: summary: Not on the Ultimate plan value: message: API access is included with the Ultimate plan. notAdmin: summary: Not an agency admin value: message: User does not have the right roles. '422': description: Validation error, or the agency already holds the maximum number of active tokens. content: application/json: schema: $ref: '#/components/schemas/ValidationErrorBody' examples: tokenCap: summary: Token cap reached value: message: Your agency already has 10 active API tokens. Revoke one before creating another. errors: tokens: [Your agency already has 10 active API tokens. Revoke one before creating another.] badExpiry: summary: Unsupported lifetime value: message: Tokens can expire after 30, 90, 180, 365 days, or never. errors: expires_in_days: [Tokens can expire after 30, 90, 180, 365 days, or never.] delete: operationId: revokeAllApiTokens summary: Revoke all API tokens description: Revokes every API token your company holds. Any live integration stops working immediately. Agency admins only. tags: - Token Management security: - UserBearerAuth: [] responses: '200': description: All tokens revoked. content: application/json: schema: $ref: '#/components/schemas/Message' example: message: All API tokens revoked successfully. '401': $ref: '#/components/responses/UserUnauthorized' '403': $ref: '#/components/responses/NotAnAdmin' /external-api/tokens/{tokenId}: delete: operationId: revokeApiToken summary: Revoke API token description: Revokes one API token by its numeric ID. Agency admins only. tags: - Token Management security: - UserBearerAuth: [] parameters: - name: tokenId in: path required: true description: Token ID, from the token list. schema: type: integer responses: '200': description: Token revoked. content: application/json: schema: $ref: '#/components/schemas/Message' example: message: API token revoked successfully. '401': $ref: '#/components/responses/UserUnauthorized' '403': $ref: '#/components/responses/NotAnAdmin' '404': description: No token with this ID belongs to your company. content: application/json: schema: $ref: '#/components/schemas/Error' example: message: Token not found. components: securitySchemes: CompanyBearerAuth: type: http scheme: bearer description: Company API token, created by an agency admin under Settings → Integrations & API → API. UserBearerAuth: type: http scheme: bearer description: An agency admin's own PropServ user token. Only accepted by the token management endpoints; company API tokens are refused there, and user tokens are refused on /external. parameters: PageParam: name: page in: query description: Page number. required: false schema: type: integer minimum: 1 default: 1 PerPageParam: name: per_page in: query description: Items per page. required: false schema: type: integer minimum: 1 maximum: 100 default: 50 headers: Retry-After: description: Seconds to wait before retrying. schema: type: integer X-RateLimit-Limit: description: Requests allowed per minute for this limit. schema: type: integer X-RateLimit-Remaining: description: Requests left in the current window. schema: type: integer X-RateLimit-Reset: description: Unix timestamp at which the window resets. Sent only on a 429 from the read or create limit. schema: type: integer responses: Unauthorized: description: The token is missing, unknown or revoked, expired, not a company token, or belongs to an inactive company. content: application/json: schema: $ref: '#/components/schemas/Error' examples: missing: summary: No token sent value: message: No authentication token provided. invalid: summary: Unknown or revoked token value: message: Invalid authentication token. notCompanyToken: summary: A user token was sent value: message: Invalid token type. Company token required. expired: summary: Token has expired value: message: This API token has expired. inactiveCompany: summary: Company inactive value: message: Company not found or inactive. ReadForbidden: description: The token lacks the `read:submission-responses` ability, or the company is not an estate agent. content: application/json: schema: $ref: '#/components/schemas/Error' examples: missingAbility: summary: Token lacks the read ability value: message: This API token does not have the 'read:submission-responses' ability. notEstateAgent: summary: Not an estate agent company value: message: External API access is only available for estate agent companies. WriteForbidden: description: The token lacks the `write:submissions` ability, write access has not been enabled for your company, or the company is not an estate agent. Contact your PropServ account manager or info@propserv.co to request write access. content: application/json: schema: $ref: '#/components/schemas/Error' examples: missingAbility: summary: Token lacks the write ability value: message: This API token does not have the 'write:submissions' ability. writeNotEnabled: summary: Write access not enabled value: message: Write access is not enabled for your company. notEstateAgent: summary: Not an estate agent company value: message: External API access is only available for estate agent companies. ValidationError: description: A query parameter is invalid. content: application/json: schema: $ref: '#/components/schemas/ValidationErrorBody' example: message: The selected active is invalid. errors: active: [The selected active is invalid.] SubmissionValidationError: description: | The body is invalid. Another company's `agent_uuid` or `property_uuid` reads as a validation error, never a `403`, so the API does not confirm whether another company's record exists. content: application/json: schema: $ref: '#/components/schemas/ValidationErrorBody' examples: agent: summary: Agent not usable value: message: The agent_uuid must belong to an active estate agent in your company who has finished setting up their account. errors: agent_uuid: [The agent_uuid must belong to an active estate agent in your company who has finished setting up their account.] noAddress: summary: Property has no address value: message: This property has no address on file. Add an address to it before creating a work request. errors: property_uuid: [This property has no address on file. Add an address to it before creating a work request.] excludedCategory: summary: Category excluded by your agency value: message: One or more selected contractor categories are unavailable to your company. errors: categories.0.id: [One or more selected contractor categories are unavailable to your company.] RateLimited: description: Too many requests. Wait for `Retry-After` seconds. After 60 failed authentications in a minute from one IP address, requests from it that present no token or an unknown token are refused with this status and the message "Too many failed authentication attempts. Try again later.", also with a `Retry-After` header. Requests with a valid token are unaffected. headers: Retry-After: $ref: '#/components/headers/Retry-After' X-RateLimit-Limit: $ref: '#/components/headers/X-RateLimit-Limit' X-RateLimit-Remaining: $ref: '#/components/headers/X-RateLimit-Remaining' X-RateLimit-Reset: $ref: '#/components/headers/X-RateLimit-Reset' content: application/json: schema: $ref: '#/components/schemas/Error' examples: throttled: summary: Rate limit exceeded value: message: Too Many Attempts. failedAuth: summary: Too many failed authentications from one IP value: message: Too many failed authentication attempts. Try again later. UserUnauthorized: description: The user token is missing or invalid. content: application/json: schema: $ref: '#/components/schemas/Error' example: message: Unauthenticated. NotAnAdmin: description: The user is not an admin of an estate agent company. content: application/json: schema: $ref: '#/components/schemas/Error' example: message: User does not have the right roles. schemas: Error: type: object required: [message] properties: message: type: string additionalProperties: true Message: type: object properties: message: type: string ValidationErrorBody: type: object required: [message, errors] properties: message: type: string description: The first error, with a count of any others. errors: type: object description: Error messages keyed by field name. additionalProperties: type: array items: type: string BooleanFilter: type: string enum: ['1', '0', 'true', 'false'] Pagination: type: object properties: current_page: type: integer last_page: type: integer per_page: type: integer total: type: integer from: type: integer nullable: true description: Position of the first item on this page; null when the page is empty. to: type: integer nullable: true description: Position of the last item on this page; null when the page is empty. has_more_pages: type: boolean Document: type: object properties: name: type: string nullable: true url: type: string format: uri Contact: type: object description: A person or organisation. Every field is null when there is none. properties: uuid: type: string format: uuid nullable: true name: type: string nullable: true email: type: string nullable: true phone_number: type: string nullable: true AgentSummary: type: object properties: uuid: type: string format: uuid nullable: true name: type: string nullable: true email: type: string nullable: true IdName: type: object properties: id: type: integer nullable: true name: type: string nullable: true SubmissionStatus: type: string enum: [requested, in_progress, closed] SubmissionAgentStatus: type: string description: | The agent-facing stage of the work request. - `requested` — waiting for contractor responses - `offers_received` — at least one contractor has responded - `instructed` — a contractor has been instructed - `in_progress` — work is under way - `pending_approval` — the contractor has submitted finished work for approval - `complete` — the work is complete - `closed` — the work request has been closed enum: [requested, offers_received, instructed, in_progress, pending_approval, complete, closed] SubmissionResponseStatus: type: string description: | - `offered` — the contractor has quoted (offer or deposit response) - `callout_requested` — the contractor has asked to attend a callout visit - `callout_complete` — the callout visit has happened - `declined` — the contractor declined the job, or the quote was declined - `no_longer_required` — the work request was closed or went to another contractor - `accepted` — the quote was accepted - `ntu` — Not Taken Up - `paid` — paid in full - `paid_by_estate_agent` — paid in full by the agency - `deposit_paid` — deposit paid - `deposit_paid_by_estate_agent` — deposit paid by the agency - `works_order_submitted` — instructed by works order - `awaiting_landlord_approval` — waiting for the landlord to approve the quote - `pending_approval` — the contractor has submitted the finished work for approval - `further_works_required` — further work was requested before approval - `complete` — work complete enum: - offered - callout_requested - callout_complete - declined - no_longer_required - accepted - ntu - paid - paid_by_estate_agent - deposit_paid - deposit_paid_by_estate_agent - works_order_submitted - awaiting_landlord_approval - pending_approval - further_works_required - complete SubmissionResponseType: type: string description: | - `offer` — a quote - `deposit` — a quote that asks for a deposit - `decline` — the contractor declined the job - `callout` — a request to attend a callout visit enum: [offer, deposit, decline, callout] TenantReportStatus: type: string description: | - `requested` — reported by the tenant, not yet accepted - `accepted` — accepted by the agency - `awaiting_completion` — work is under way - `complete` — resolved - `no_longer_required` — no longer needed - `off_platform` — being handled outside PropServ enum: [requested, accepted, awaiting_completion, complete, no_longer_required, off_platform] ContractorCategory: type: object properties: id: type: integer example: 13 name: type: string example: Plumbing key: type: string example: plumbing PropertyAddress: type: object description: Every field is null when the property has no address on file. properties: line_1: type: string nullable: true line_2: type: string nullable: true city: type: string nullable: true county: type: string nullable: true postcode: type: string nullable: true country: type: string nullable: true Property: type: object properties: uuid: type: string format: uuid property_type: type: string nullable: true example: house active: type: boolean address: $ref: '#/components/schemas/PropertyAddress' PropertyList: type: object properties: data: type: array items: $ref: '#/components/schemas/Property' pagination: $ref: '#/components/schemas/Pagination' WorkRequestCreate: type: object required: - agent_uuid - property_uuid - categories properties: agent_uuid: type: string format: uuid description: An estate agent in your company who is active, not revoked and has finished setting up their account. Contractors contact this agent about the work. Resolve via GET /external/users. property_uuid: type: string format: uuid description: A property in your company that has not been deleted and has an address on file. Resolve via GET /external/properties. external_reference: type: string maxLength: 255 nullable: true description: Your own reference, unique per company. Makes the call idempotent — repeating it returns the original work request with 200 instead of creating another. The repeated payload is not compared with or applied to the original. external_details_url: type: string format: uri maxLength: 255 nullable: true description: An http or https link back into your own system, shown against the work request. categories: type: array minItems: 1 maxItems: 10 description: One to ten contractor categories, each included at most once. items: type: object required: - id - description properties: id: type: integer description: A category ID from GET /external/categories. description: type: string maxLength: 5000 description: What needs doing in this category. max_quotes: type: integer minimum: 1 maximum: 5 nullable: true description: Caps how many contractor quotes this category accepts. Omit for no cap. date_required: type: string format: date nullable: true description: YYYY-MM-DD, today or later. SubmissionCategory: type: object properties: id: type: integer description: The contractor category ID, as listed by GET /external/categories. name: type: string date_required: type: string format: date nullable: true example: '2026-10-04' max_quotes: type: integer nullable: true description: Most quotes accepted in this category. Only offer and deposit responses count towards it. Null means no cap. description: type: string nullable: true questions_answers: type: string nullable: true description: The client's answers to this category's questions, as a JSON-encoded string. Decode it before use. Null when there are none. additional_information: type: array description: Pricing details captured for this category. Empty when there are none. items: type: object properties: name: type: string key: type: string label: type: string nullable: true value: type: string nullable: true SubmissionCreator: type: object description: What created the work request. Every field except `type` is null when the creator has no such detail. properties: uuid: type: string format: uuid nullable: true name: type: string nullable: true email: type: string nullable: true phone_number: type: string nullable: true type: type: string nullable: true description: The kind of creator, such as `user`, `company` (work requests created through this API), `planned_maintenance`, `property_compliance_requirement`, `tenant_report` or `sales_agent`. example: company SubmissionAddress: type: object properties: line_1: type: string nullable: true line_2: type: string nullable: true city: type: string nullable: true postcode: type: string nullable: true county: type: string nullable: true country: type: string nullable: true Submission: type: object properties: uuid: type: string format: uuid job_number: type: integer example: 23138844 status: $ref: '#/components/schemas/SubmissionStatus' agent_status: $ref: '#/components/schemas/SubmissionAgentStatus' external_reference: type: string nullable: true description: Your reference, for work requests created through the API with one. external_details_url: type: string nullable: true property_uuid: type: string format: uuid nullable: true agent: type: object nullable: true description: The estate agent the work request is assigned to. allOf: - $ref: '#/components/schemas/AgentSummary' new_build: type: boolean urgent: type: boolean client_journey: type: string nullable: true handled_by_agent: type: boolean description: True when contractors contact the agent rather than the client. pay_via_works_order: type: boolean close_reason: type: string nullable: true address: $ref: '#/components/schemas/SubmissionAddress' client: $ref: '#/components/schemas/Contact' created_by: $ref: '#/components/schemas/SubmissionCreator' categories: type: array items: $ref: '#/components/schemas/SubmissionCategory' documents: type: array items: $ref: '#/components/schemas/Document' images: type: array items: type: object properties: url: type: string format: uri tenant_reports: type: array items: type: object properties: uuid: type: string format: uuid name: type: string created_at: type: string format: date-time updated_at: type: string format: date-time example: uuid: 7311ed6e-2b67-4db3-8b21-6ea1f7a14f50 job_number: 23138844 status: requested agent_status: requested external_reference: CRM-88231 external_details_url: https://crm.example.com/jobs/88231 property_uuid: 563bc697-3fe9-45c3-821c-09c4e6fd1779 agent: uuid: 4c1fa938-9abe-4eb9-85ba-3b93594332ae name: Jane Walker email: jane@agency.co.uk new_build: false urgent: false client_journey: null handled_by_agent: true pay_via_works_order: false close_reason: null address: line_1: 123 Example Street line_2: null city: London postcode: SW1A 1AA county: Greater London country: United Kingdom client: uuid: null name: null email: null phone_number: null created_by: uuid: 3b2ff3b7-f357-4850-ad89-e99acc5dfffc name: Example Lettings Ltd email: office@agency.co.uk phone_number: 020 7946 0000 type: company categories: - id: 13 name: Plumbing date_required: '2026-10-04' max_quotes: 3 description: Boiler not firing, no hot water. questions_answers: null additional_information: [] documents: [] images: [] tenant_reports: [] created_at: '2026-09-27T15:53:44+01:00' updated_at: '2026-09-27T15:53:44+01:00' SubmissionEnvelope: type: object properties: data: $ref: '#/components/schemas/Submission' SubmissionList: type: object properties: data: type: array items: $ref: '#/components/schemas/Submission' pagination: $ref: '#/components/schemas/Pagination' Contractor: type: object description: The contractor company that responded, and the user who responded for it. properties: uuid: type: string format: uuid name: type: string email: type: string nullable: true phone_number: type: string nullable: true website: type: string nullable: true user: $ref: '#/components/schemas/Contact' LineItem: type: object properties: type: type: string enum: [labour, materials, other] description: type: string nullable: true quantity: type: integer unit_price: type: string description: Unit price in pounds sterling, as a string with two decimal places. example: '150.00' total_price: type: string description: quantity × unit_price in pounds sterling, as a string with two decimal places. example: '300.00' SubmissionResponse: type: object properties: uuid: type: string format: uuid job_number: type: integer example: 67382434 submission_uuid: type: string format: uuid submission_job_number: type: integer example: 23138844 contractor: $ref: '#/components/schemas/Contractor' type: $ref: '#/components/schemas/SubmissionResponseType' status: $ref: '#/components/schemas/SubmissionResponseStatus' price: type: number nullable: true description: Quoted price in pounds sterling (not pence), e.g. 450.5 is £450.50. Null when there is no price, such as on a decline. example: 450.5 has_line_items: type: boolean line_items: type: array description: The quote's itemised breakdown, in order. Empty when the quote is not itemised. items: $ref: '#/components/schemas/LineItem' currency: type: string enum: [GBP] message: type: string nullable: true categories: type: array items: $ref: '#/components/schemas/IdName' terms: type: array items: $ref: '#/components/schemas/Document' documents: type: array items: $ref: '#/components/schemas/Document' created_at: type: string format: date-time updated_at: type: string format: date-time estimated_start_date: type: string format: date-time nullable: true completion_date: type: string format: date-time nullable: true example: uuid: c5ec4b1c-5bca-31e3-9716-a6211b12f0dd job_number: 67382434 submission_uuid: 7311ed6e-2b67-4db3-8b21-6ea1f7a14f50 submission_job_number: 23138844 contractor: uuid: b3ff408e-e6ef-4b86-9766-dd3062ad1c73 name: ABC Contractors Ltd email: contact@abccontractors.co.uk phone_number: 020 7946 0123 website: https://abccontractors.co.uk user: uuid: 3e5132a9-323c-4fd0-9b06-8a5b4f4d667c name: John Smith email: john@abccontractors.co.uk phone_number: 07700 900123 type: offer status: offered price: 450.5 has_line_items: true line_items: - type: labour description: Labour quantity: 2 unit_price: '150.00' total_price: '300.00' - type: materials description: Replacement valve quantity: 1 unit_price: '150.50' total_price: '150.50' currency: GBP message: We can attend on Tuesday morning. categories: - id: 13 name: Plumbing terms: [] documents: [] created_at: '2026-09-27T15:53:44+01:00' updated_at: '2026-09-27T15:53:44+01:00' estimated_start_date: '2026-10-04T09:00:00+01:00' completion_date: null SubmissionResponseList: type: object properties: data: type: array items: $ref: '#/components/schemas/SubmissionResponse' pagination: $ref: '#/components/schemas/Pagination' QuestionAnswer: type: object properties: question: type: string answer: type: string nullable: true TenantReportAddress: type: object properties: line_1: type: string nullable: true line_2: type: string nullable: true city: type: string nullable: true county: type: string nullable: true postcode: type: string nullable: true MouldAssessment: type: object description: AI analysis of mould in the report's photos. properties: mould_detected: type: boolean severity_score: type: integer minimum: 1 maximum: 5 nullable: true description: 1 (minor) to 5 (severe); null when no mould was detected. estimated_affected_area: type: string nullable: true location_type: type: string nullable: true awaabs_law_priority: type: string enum: [emergency, urgent, standard, monitor, not_applicable] recommended_trade: type: string nullable: true tenant_advice: type: string nullable: true investigation_deadline_at: type: string format: date-time nullable: true remediation_deadline_at: type: string format: date-time nullable: true analysed_at: type: string format: date-time nullable: true flagged_for_manual_review: type: boolean TenantReport: type: object properties: uuid: type: string format: uuid job_number: type: integer example: 69650541 submission_uuid: type: string format: uuid nullable: true description: The work request raised from this report, if any. property: type: object description: Every field is null when the report is not linked to a property. properties: uuid: type: string format: uuid nullable: true type: type: string nullable: true formatted_address: type: string nullable: true tenant: $ref: '#/components/schemas/Contact' assignee: $ref: '#/components/schemas/Contact' managing_agent: $ref: '#/components/schemas/Contact' name: type: string description: type: string nullable: true status: $ref: '#/components/schemas/TenantReportStatus' severity_score: type: integer minimum: 0 maximum: 100 nullable: true description: AI-assessed severity from 0 to 100; null until assessed. tenancy_id: type: string nullable: true description: The CRM tenancy the report is linked to, if any. mri_action_id: type: string nullable: true description: The MRI Software action created for this report, if any. questions_and_answers: type: array items: $ref: '#/components/schemas/QuestionAnswer' category: $ref: '#/components/schemas/IdName' sub_category: $ref: '#/components/schemas/IdName' address: $ref: '#/components/schemas/TenantReportAddress' documents: type: array items: $ref: '#/components/schemas/Document' mould_assessment: type: object nullable: true allOf: - $ref: '#/components/schemas/MouldAssessment' accepted_at: type: string format: date-time nullable: true completed_at: type: string format: date-time nullable: true created_at: type: string format: date-time updated_at: type: string format: date-time example: uuid: f3504db4-349e-4351-8216-cb3ca9294755 job_number: 69650541 submission_uuid: 7311ed6e-2b67-4db3-8b21-6ea1f7a14f50 property: uuid: 563bc697-3fe9-45c3-821c-09c4e6fd1779 type: house formatted_address: 123 Example Street, London, SW1A 1AA tenant: uuid: 0113d58e-0d38-4ba8-b6a5-17c326e1c662 name: Sam Taylor email: sam.taylor@example.com phone_number: 07700 900456 assignee: uuid: 4c1fa938-9abe-4eb9-85ba-3b93594332ae name: Jane Walker email: jane@agency.co.uk phone_number: null managing_agent: uuid: 4c1fa938-9abe-4eb9-85ba-3b93594332ae name: Jane Walker email: jane@agency.co.uk phone_number: 020 7946 0000 name: Leaking tap in kitchen description: The kitchen tap is leaking continuously. status: accepted severity_score: 25 tenancy_id: null mri_action_id: null questions_and_answers: - question: When did you first notice the leak? answer: 3 days ago category: id: 1 name: Plumbing sub_category: id: 5 name: Taps and fixtures address: line_1: 123 Example Street line_2: null city: London county: Greater London postcode: SW1A 1AA documents: [] mould_assessment: null accepted_at: '2026-09-27T15:53:45+01:00' completed_at: null created_at: '2026-09-27T15:53:45+01:00' updated_at: '2026-09-27T15:53:45+01:00' TenantReportList: type: object properties: data: type: array items: $ref: '#/components/schemas/TenantReport' pagination: $ref: '#/components/schemas/Pagination' Tenant: type: object properties: uuid: type: string format: uuid name: type: string email: type: string nullable: true phone_number: type: string nullable: true managing_agent: $ref: '#/components/schemas/AgentSummary' example: uuid: 0113d58e-0d38-4ba8-b6a5-17c326e1c662 name: Sam Taylor email: sam.taylor@example.com phone_number: 07700 900456 managing_agent: uuid: 4c1fa938-9abe-4eb9-85ba-3b93594332ae name: Jane Walker email: jane@agency.co.uk TenantList: type: object properties: data: type: array items: $ref: '#/components/schemas/Tenant' pagination: $ref: '#/components/schemas/Pagination' User: type: object properties: uuid: type: string format: uuid name: type: string email: type: string nullable: true phone_number: type: string nullable: true type: type: string description: The user type, such as `estate_agent` (your staff) or `client` (a tenant). example: estate_agent client_type: type: string nullable: true description: For clients, `tenant` or `mover_landlord`; null otherwise. active: type: integer enum: [0, 1] description: 1 when the account is active, 0 when it is not. revoked: type: boolean description: True when the user's access has been revoked. A revoked estate agent cannot be used as a work request's agent_uuid. referrer: $ref: '#/components/schemas/Contact' created_at: type: string format: date-time nullable: true updated_at: type: string format: date-time nullable: true example: uuid: 4c1fa938-9abe-4eb9-85ba-3b93594332ae name: Jane Walker email: jane@agency.co.uk phone_number: 020 7946 0000 type: estate_agent client_type: null active: 1 revoked: false referrer: uuid: null name: null email: null phone_number: null created_at: '2026-09-27T15:53:44+01:00' updated_at: '2026-09-27T15:53:44+01:00' UserList: type: object properties: data: type: array items: $ref: '#/components/schemas/User' pagination: $ref: '#/components/schemas/Pagination' ApiTokenAbility: type: string enum: [read:submission-responses, write:submissions] ApiToken: type: object properties: id: type: integer name: type: string abilities: type: array items: type: string description: Tokens created before abilities were validated may show `*`, which can read but cannot create work requests. created_at: type: string format: date-time last_used_at: type: string format: date-time nullable: true expires_at: type: string format: date-time nullable: true description: Null for a token that never expires. created_by: type: object nullable: true description: The admin who created the token; null for tokens created before this was recorded. properties: uuid: type: string format: uuid name: type: string ApiTokenCreate: type: object required: [name] properties: name: type: string maxLength: 255 abilities: type: array items: $ref: '#/components/schemas/ApiTokenAbility' description: Defaults to `[read:submission-responses]`. expires_in_days: type: integer enum: [30, 90, 180, 365, null] nullable: true description: Omit or send null for a token that never expires. ApiTokenCreated: type: object properties: message: type: string example: API token created successfully. data: allOf: - $ref: '#/components/schemas/ApiToken' - type: object properties: token: type: string description: The plain-text token. Shown only once. example: 42|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ApiTokenList: type: object properties: data: type: array items: $ref: '#/components/schemas/ApiToken' meta: type: object properties: write_enabled: type: boolean description: Whether PropServ has enabled work request creation for your company. max_tokens: type: integer example: 10 externalDocs: description: PropServ External API guide url: https://propserv.co/docs/api