openapi: 3.0.3 info: title: PropServ External API version: 1.1.0 description: | The PropServ External API gives estate agent companies programmatic access to their PropServ data: work requests (submissions), contractor responses, tenant reports, tenants and users — plus the ability to create work requests directly from your own systems. ## Authentication All endpoints require a **company API token** sent as a Bearer token. Admin users can create and manage tokens in the agent portal under **Profile → API**. ## Write access Creating work requests (`POST /external/submissions`) is not enabled automatically. Write access is granted per company on request — contact your PropServ account manager or email info@propserv.co to have it enabled. Your API token must also carry the `write:submissions` permission, chosen when the token is created. All other endpoints are read-only and available to every estate agent company. contact: name: PropServ Support email: info@propserv.co license: name: Proprietary servers: - url: https://api.propserv.co/api description: Production server - url: https://dev.propserv.co/api description: Development server security: - CompanyBearerAuth: [] paths: /external/submissions: post: summary: Create work request description: | Creates a work request (submission) and releases it to the contractor market. **Access must be requested** — this endpoint is not enabled automatically. Two things are required, on top of a valid company token: 1. Your API token must carry the `write:submissions` permission (chosen when the token is created under **Profile → API**). 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 permission, or a company without write access, receives a `403` naming the failed gate. **Idempotency:** pass your own `external_reference` (unique per company). If a work request with that reference already exists, it is returned unchanged with `200` instead of `201` — nothing is created and no notifications are re-sent. Check the status code to tell the two apart. Use `GET /external/properties` to resolve the `property_uuid` (the property must have an address on file), `GET /external/users` for the `agent_uuid`, and `GET /external/categories` for valid category IDs. **Rate limit:** 30 requests per minute per API token, counted separately from read traffic. tags: - Submissions requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/WorkRequestCreate' responses: '201': description: Work request created content: application/json: schema: type: object properties: data: $ref: '#/components/schemas/Submission' '200': description: Existing work request with this external_reference returned unchanged (idempotent replay) content: application/json: schema: type: object properties: data: $ref: '#/components/schemas/Submission' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/WriteForbidden' '422': $ref: '#/components/responses/ValidationError' '429': $ref: '#/components/responses/RateLimited' get: summary: List submissions description: Get paginated list of submissions for your estate agent company tags: - Submissions parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' - name: status in: query description: Filter by submission status required: false schema: type: string enum: [requested, in_progress, closed] - name: external_reference in: query description: Find a work request by the reference your own system supplied when creating it via the API. Only matches work requests your company created through the API. required: false schema: type: string maxLength: 255 - name: urgent in: query description: Filter by urgent flag required: false schema: type: string enum: ['0', '1', 'true', 'false'] - name: new_build in: query description: Filter by new build flag required: false schema: type: string enum: ['0', '1', 'true', 'false'] - name: handled_by_agent in: query description: Filter by whether handled by agent required: false schema: type: string enum: ['0', '1', 'true', 'false'] - name: pay_via_works_order in: query description: Filter by payment method required: false schema: type: string enum: ['0', '1', 'true', 'false'] - name: created_by_uuid in: query description: Filter by creator UUID required: false schema: type: string format: uuid responses: '200': description: List of submissions content: application/json: schema: allOf: - $ref: '#/components/schemas/PaginatedResponse' - type: object properties: data: type: array items: $ref: '#/components/schemas/Submission' '401': $ref: '#/components/responses/Unauthorized' '422': $ref: '#/components/responses/ValidationError' /external/submissions/{uuid}: get: summary: Get submission by UUID description: Get a specific submission by its UUID tags: - Submissions parameters: - name: uuid in: path required: true description: Submission UUID schema: type: string format: uuid responses: '200': description: Submission details content: application/json: schema: type: object properties: data: $ref: '#/components/schemas/Submission' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' /external/submissions/{uuid}/responses: get: summary: List responses for submission description: Get all responses for a specific submission tags: - Submissions parameters: - name: uuid in: path required: true description: Submission 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: type: string enum: [offered, callout_requested, callout_complete, declined, no_longer_required, accepted, ntu, paid, deposit_paid, paid_by_estate_agent, deposit_paid_by_estate_agent, works_order_submitted, complete] - name: type in: query description: Filter by response type required: false schema: type: string enum: [offer, decline, callout, deposit] responses: '200': description: List of submission responses content: application/json: schema: allOf: - $ref: '#/components/schemas/PaginatedResponse' - type: object properties: data: type: array items: $ref: '#/components/schemas/SubmissionResponse' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/ValidationError' /external/submission-responses: get: summary: List submission responses description: Get paginated list of submission responses for your estate agent company tags: - Submission Responses parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' - name: status in: query description: Filter by response status required: false schema: type: string enum: [offered, callout_requested, callout_complete, declined, no_longer_required, accepted, ntu, paid, deposit_paid, paid_by_estate_agent, deposit_paid_by_estate_agent, works_order_submitted, complete] - name: type in: query description: Filter by response type required: false schema: type: string enum: [offer, decline, callout, deposit] - name: submission_uuid in: query description: Filter by submission UUID required: false schema: type: string format: uuid responses: '200': description: List of submission responses content: application/json: schema: allOf: - $ref: '#/components/schemas/PaginatedResponse' - type: object properties: data: type: array items: $ref: '#/components/schemas/SubmissionResponse' '401': $ref: '#/components/responses/Unauthorized' '422': $ref: '#/components/responses/ValidationError' /external/submission-responses/{uuid}: get: summary: Get submission response by UUID description: Get a specific submission response by its UUID tags: - Submission Responses parameters: - name: uuid in: path required: true description: Submission response UUID schema: type: string format: uuid responses: '200': description: Submission response details content: application/json: schema: type: object properties: data: $ref: '#/components/schemas/SubmissionResponse' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' /external/tenant-reports: get: summary: List tenant reports description: Get paginated list of tenant reports for your estate agent company 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: type: string enum: [requested, accepted, awaiting_completion, complete, no_longer_required] responses: '200': description: List of tenant reports content: application/json: schema: allOf: - $ref: '#/components/schemas/PaginatedResponse' - type: object properties: data: type: array items: $ref: '#/components/schemas/TenantReport' '401': $ref: '#/components/responses/Unauthorized' '422': $ref: '#/components/responses/ValidationError' /external/tenant-reports/{uuid}: get: summary: Get tenant report by UUID description: Get a specific tenant report by its UUID tags: - Tenant Reports parameters: - name: uuid in: path required: true description: Tenant report UUID schema: type: string format: uuid responses: '200': description: Tenant report details content: application/json: schema: type: object properties: data: $ref: '#/components/schemas/TenantReport' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' /external/tenants: get: summary: List tenants description: Get paginated list of tenants for your estate agent company tags: - Tenants parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' - name: search in: query description: Search tenants by name or email required: false schema: type: string maxLength: 255 responses: '200': description: List of tenants content: application/json: schema: allOf: - $ref: '#/components/schemas/PaginatedResponse' - type: object properties: data: type: array items: $ref: '#/components/schemas/Tenant' '401': $ref: '#/components/responses/Unauthorized' '422': $ref: '#/components/responses/ValidationError' /external/users: get: summary: List users description: Get paginated list of users for your company tags: - Users parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' - name: search in: query description: Search users by name, email or phone number required: false schema: type: string maxLength: 255 - name: active in: query description: Filter by active status required: false schema: type: boolean responses: '200': description: List of users content: application/json: schema: allOf: - $ref: '#/components/schemas/PaginatedResponse' - type: object properties: data: type: array items: $ref: '#/components/schemas/User' '401': $ref: '#/components/responses/Unauthorized' '422': $ref: '#/components/responses/ValidationError' /external/users/{uuid}: get: summary: Get user by UUID description: Get a specific user by their UUID tags: - Users parameters: - name: uuid in: path required: true description: User UUID schema: type: string format: uuid responses: '200': description: User details content: application/json: schema: type: object properties: data: $ref: '#/components/schemas/User' '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' /external/categories: get: summary: List contractor categories description: | Lists the contractor categories your company can raise work in. 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: List of contractor categories content: application/json: schema: type: object properties: data: type: array items: $ref: '#/components/schemas/ContractorCategory' '401': $ref: '#/components/responses/Unauthorized' /external/properties: get: summary: List properties description: | Lists properties belonging to your company, so you can map your own address records onto a PropServ `property_uuid` when creating work requests. A property must have an address on file to be usable in `POST /external/submissions`. tags: - Reference Data parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/PerPageParam' - name: search in: query description: Matches address line 1/2, city and postcode required: false schema: type: string maxLength: 255 - name: active in: query description: Filter by active flag required: false schema: type: boolean responses: '200': description: List of properties content: application/json: schema: allOf: - $ref: '#/components/schemas/PaginatedResponse' - type: object properties: data: type: array items: $ref: '#/components/schemas/Property' '401': $ref: '#/components/responses/Unauthorized' '422': $ref: '#/components/responses/ValidationError' components: securitySchemes: CompanyBearerAuth: type: http scheme: bearer bearerFormat: Laravel Sanctum Company Token description: Company API token. Create tokens via the token management endpoints using your user authentication. parameters: PageParam: name: page in: query description: Page number (default 1) required: false schema: type: integer minimum: 1 default: 1 PerPageParam: name: per_page in: query description: Items per page (default 50, max 100) required: false schema: type: integer minimum: 1 maximum: 100 default: 50 responses: Unauthorized: description: Unauthorized content: application/json: schema: type: object properties: message: type: string example: "Unauthenticated." NotFound: description: Resource not found content: application/json: schema: type: object properties: message: type: string example: "Resource not found." ValidationError: description: Validation error content: application/json: schema: type: object properties: message: type: string example: "The given data was invalid." errors: type: object additionalProperties: type: array items: type: string WriteForbidden: description: | Write access denied. Either the token lacks the `write:submissions` permission, or write access has not been enabled for your company. The message names the failed gate. Contact your PropServ account manager or info@propserv.co to request write access. content: application/json: schema: type: object properties: message: type: string example: "Write access is not enabled for your company." RateLimited: description: Rate limit exceeded content: application/json: schema: type: object properties: message: type: string example: "Too Many Attempts." schemas: PaginatedResponse: type: object properties: pagination: type: object properties: current_page: type: integer last_page: type: integer per_page: type: integer total: type: integer from: type: integer nullable: true to: type: integer nullable: true has_more_pages: type: boolean Address: 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 country: type: string nullable: true Document: type: object properties: name: type: string url: type: string format: uri Category: type: object properties: id: type: integer name: type: string ContractorCategory: type: object properties: id: type: integer name: type: string key: type: string example: "plumbing" Property: type: object properties: uuid: type: string format: uuid property_type: type: string nullable: true example: "house" active: type: boolean address: $ref: '#/components/schemas/Address' WorkRequestCreate: type: object required: - agent_uuid - property_uuid - categories properties: agent_uuid: type: string format: uuid description: Must be an active estate agent in your company. Resolve via GET /external/users. property_uuid: type: string format: uuid description: Must be a property owned by your company with an address on file. Resolve via GET /external/properties. external_reference: type: string maxLength: 255 description: Your own reference, unique per company. Makes the call idempotent — retrying the same reference returns the existing work request with 200 instead of creating a duplicate. external_details_url: type: string format: uri maxLength: 2048 description: Deep link back into your own system, shown against the work request. categories: type: array minItems: 1 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, per category max_quotes: type: integer minimum: 1 maximum: 5 description: Caps how many contractor quotes are accepted date_required: type: string format: date description: YYYY-MM-DD, today or later SubmissionCategory: allOf: - $ref: '#/components/schemas/Category' - type: object properties: date_required: type: string format: date nullable: true max_quotes: type: integer nullable: true description: "Maximum number of quotes allowed for this category. Only applies to 'offer' and 'deposit' response types; 'callout' responses do not count towards this limit. Null means unlimited." description: type: string nullable: true questions_answers: type: object nullable: true additional_information: type: array items: type: object properties: name: type: string key: type: string label: type: string value: type: string nullable: true User: type: object properties: uuid: type: string format: uuid name: type: string email: type: string format: email phone_number: type: string nullable: true Company: type: object properties: uuid: type: string format: uuid name: type: string email: type: string format: email nullable: true phone_number: type: string nullable: true website: type: string format: uri nullable: true Contractor: allOf: - $ref: '#/components/schemas/Company' - type: object properties: user: $ref: '#/components/schemas/User' Submission: type: object properties: uuid: type: string format: uuid job_number: type: string nullable: true example: "PS-10023" status: type: string enum: [requested, in_progress, closed] new_build: type: boolean urgent: type: boolean client_journey: type: string nullable: true handled_by_agent: type: boolean pay_via_works_order: type: boolean close_reason: type: string nullable: true address: $ref: '#/components/schemas/Address' client: allOf: - $ref: '#/components/schemas/User' - type: object nullable: true created_by: allOf: - $ref: '#/components/schemas/User' - type: object nullable: true 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 SubmissionResponse: type: object properties: uuid: type: string format: uuid submission_uuid: type: string format: uuid contractor: $ref: '#/components/schemas/Contractor' type: type: string enum: [offer, decline, callout, deposit] status: type: string enum: [offered, callout_requested, callout_complete, declined, no_longer_required, accepted, ntu, paid, deposit_paid, paid_by_estate_agent, deposit_paid_by_estate_agent, works_order_submitted, complete] price: type: number format: float nullable: true currency: type: string default: "GBP" message: type: string nullable: true categories: type: array items: $ref: '#/components/schemas/Category' terms: type: array items: $ref: '#/components/schemas/Document' documents: type: array items: $ref: '#/components/schemas/Document' estimated_start_date: type: string format: date-time nullable: true created_at: type: string format: date-time updated_at: type: string format: date-time QuestionAnswer: type: object properties: question: type: string answer: type: string TenantReport: type: object properties: uuid: type: string format: uuid submission_uuid: type: string format: uuid nullable: true tenant: $ref: '#/components/schemas/User' managing_agent: allOf: - $ref: '#/components/schemas/User' - type: object nullable: true name: type: string description: type: string status: type: string enum: [requested, accepted, awaiting_completion, complete, no_longer_required] questions_and_answers: type: array items: $ref: '#/components/schemas/QuestionAnswer' category: allOf: - $ref: '#/components/schemas/Category' - type: object nullable: true sub_category: allOf: - $ref: '#/components/schemas/Category' - type: object nullable: true address: $ref: '#/components/schemas/Address' documents: type: array items: $ref: '#/components/schemas/Document' created_at: type: string format: date-time updated_at: type: string format: date-time Tenant: type: object properties: uuid: type: string format: uuid name: type: string email: type: string format: email phone_number: type: string nullable: true active: type: boolean approved_by_company: type: boolean tenant_reporting_enabled: type: boolean created_at: type: string format: date-time updated_at: type: string format: date-time referrer: type: object nullable: true properties: uuid: type: string format: uuid nullable: true name: type: string nullable: true email: type: string format: email nullable: true tags: - name: Submissions description: Work requests — read for all companies; creation requires write access, granted on request - name: Submission Responses description: Contractor responses to submissions - name: Tenant Reports description: Tenant reporting system - name: Tenants description: Tenant management - name: Users description: User management - name: Reference Data description: Categories and properties, used to build work request create payloads externalDocs: description: PropServ API Documentation url: https://propserv.co/docs/api