openapi: 3.0.3
info:
  title: UAEProjects Commercial API
  version: 1.0.0
  description: Published UAE real-estate project data with commercial access controls.
servers:
- url: https://api.uaeprojects.com/v1
- url: http://testuaeprojects.com/api/v1
security:
- bearerApiKey: []
tags:
- name: Projects
- name: Catalogs
- name: Webhooks
paths:
  /health:
    get:
      security: []
      operationId: health
      responses:
        '200':
          description: API health
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Success'
  /projects:
    get:
      tags:
      - Projects
      operationId: listProjects
      parameters:
      - $ref: '#/components/parameters/IfNoneMatch'
      - $ref: '#/components/parameters/Page'
      - $ref: '#/components/parameters/PerPage'
      - name: q
        in: query
        schema:
          type: string
      - name: developer
        in: query
        schema:
          type: string
      - name: community
        in: query
        schema:
          type: string
      - name: state
        in: query
        schema:
          type: string
      - name: nature
        in: query
        schema:
          type: string
      - name: min_price
        in: query
        schema:
          type: integer
          minimum: 0
      - name: max_price
        in: query
        schema:
          type: integer
          minimum: 0
      - name: updated_since
        in: query
        schema:
          type: string
          format: date-time
      - name: ids
        in: query
        description: Comma-separated unique numeric project IDs, up to 100.
        schema:
          type: string
          example: 101,102,103
      - name: view
        in: query
        description: Keep summary for the existing compact response. Detail requires ids and returns the individual-project representation for every accessible match. One quota unit is consumed per 10 requested IDs, rounded up.
        schema:
          type: string
          enum:
          - summary
          - detail
          default: summary
      - name: sort
        in: query
        schema:
          type: string
          enum:
          - updated_at
          - -updated_at
          - starting_from
          - -starting_from
          - name
          - -name
      responses:
        '304':
          $ref: '#/components/responses/NotModified'
        '200':
          $ref: '#/components/responses/ProjectList'
        '401':
          $ref: '#/components/responses/Error'
        '403':
          $ref: '#/components/responses/Error'
        '429':
          $ref: '#/components/responses/Error'
  /projects/changes:
    get:
      tags:
      - Projects
      operationId: changedProjects
      parameters:
      - $ref: '#/components/parameters/IfNoneMatch'
      - name: updated_since
        in: query
        required: true
        schema:
          type: string
          format: date-time
      - $ref: '#/components/parameters/Page'
      - $ref: '#/components/parameters/PerPage'
      responses:
        '304':
          $ref: '#/components/responses/NotModified'
        '200':
          $ref: '#/components/responses/ProjectList'
        '422':
          $ref: '#/components/responses/Error'
  /projects/resources:
    get:
      tags:
      - Projects
      operationId: batchProjectRelatedResources
      description: >-
        Fetches selected related collections for up to 25 unique project IDs. Every
        requested collection keeps its individual endpoint scope and entitlement;
        the whole request returns 403 if any requested collection is not allowed.
        Catalogue restrictions are applied before related data is loaded. The request
        consumes one quota unit per five project-resource pairs, rounded up.
      parameters:
      - $ref: '#/components/parameters/IfNoneMatch'
      - name: ids
        in: query
        required: true
        description: Comma-separated unique numeric project IDs, maximum 25.
        schema:
          type: string
          pattern: '^\d+(,\d+){0,24}$'
          example: 101,102,103
      - name: include
        in: query
        required: true
        description: >-
          Unique comma-separated related resources. Hyphenated aliases are accepted
          and normalized to underscores.
        schema:
          type: string
          example: media,floor_plans,amenities,payment_plans,payment_methods
      responses:
        '304':
          $ref: '#/components/responses/NotModified'
        '200':
          description: Permission-filtered related resources grouped by project.
          headers:
            X-Quota-Units:
              description: Weighted quota units charged for this batch.
              schema:
                type: integer
                minimum: 1
                maximum: 25
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectRelatedResourceBatchResponse'
        '401':
          $ref: '#/components/responses/Error'
        '403':
          $ref: '#/components/responses/Error'
        '422':
          $ref: '#/components/responses/Error'
        '429':
          $ref: '#/components/responses/Error'
  /projects/{project}:
    get:
      tags:
      - Projects
      operationId: getProject
      parameters:
      - $ref: '#/components/parameters/Project'
      - $ref: '#/components/parameters/IfNoneMatch'
      responses:
        '304':
          $ref: '#/components/responses/NotModified'
        '200':
          description: Project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectResponse'
        '404':
          $ref: '#/components/responses/Error'
  /projects/{project}/pdf:
    post:
      tags:
      - Projects
      operationId: generateProjectPdf
      description: 'Generates or reuses a client-safe project PDF. New generation is subject to the project-pdf:generate scope,
        project_pdf entitlement, and PDF-generation quotas. Cached responses do not consume PDF-generation quota.


        Pass agent_profile_id to print an approved agent''s contact details on the brochure — list the available ids with
        GET /agent-profiles. Omit template to use the account default, or pass a key returned by GET /pdf-templates to select
        another design enabled for this account.


        Caching: the cached copy is keyed by the project, its last-updated time, the selected layout, the attribution setting
        and the agent profile. Changing any of those — including switching layout or attaching a different agent — produces
        a new PDF automatically on the next call. There is no cache-invalidation endpoint because none is needed.

        '
      parameters:
      - $ref: '#/components/parameters/Project'
      - name: Idempotency-Key
        in: header
        description: Replay-safe retry key. Repeating the same key returns the original result without generating a new PDF
          or consuming quota.
        schema:
          type: string
      requestBody:
        required: false
        description: Optional. May also be sent as a query-string parameter.
        content:
          application/json:
            schema:
              type: object
              properties:
                agent_profile_id:
                  type: integer
                  description: 'Id of an approved agent profile belonging to the calling client, from GET /agent-profiles.
                    Its contact details are printed on the brochure. Returns 422 if the profile does not exist, is not approved,
                    or belongs to another client.

                    '
                  example: 1
                template:
                  type: string
                  description: 'Key of a PDF design enabled for this account, from GET /pdf-templates. Omit to use the account
                    default. Returns 422 if the design is not enabled for this account.

                    '
                  example: editorial
            example:
              template: editorial
              agent_profile_id: 1
      responses:
        '200':
          description: Signed PDF download
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectPdfResponse'
        '401':
          $ref: '#/components/responses/Error'
        '403':
          $ref: '#/components/responses/Error'
        '404':
          $ref: '#/components/responses/Error'
        '422':
          description: The requested agent profile or PDF template is not available for this account.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/Error'
        '503':
          $ref: '#/components/responses/Error'
  /agent-profiles:
    get:
      tags:
      - Projects
      operationId: listAgentProfiles
      description: 'Lists this client''s approved agent profiles. Use an id from here as agent_profile_id on POST /projects/{project}/pdf.
        Profiles that are pending, rejected or deactivated are not returned. Requires the same project-pdf:generate scope
        and project_pdf entitlement as PDF generation.

        '
      responses:
        '304':
          $ref: '#/components/responses/NotModified'
        '200':
          description: Approved agent profiles
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/AgentProfile'
              example:
                data:
                - id: 1
                  agent_name: First Agent
                  company_name: First Choice Properties
                  email: agent@fcprop.net
                  phone: '+971500000000'
                  whatsapp: '+971500000000'
                  image_url: https://api.uaeprojects.com/uploads/agent-profiles/1-ab12cd34.jpg
        '401':
          $ref: '#/components/responses/Error'
        '403':
          $ref: '#/components/responses/Error'
      parameters:
      - $ref: '#/components/parameters/IfNoneMatch'
  /pdf-templates:
    get:
      tags:
      - Projects
      operationId: listPdfTemplates
      description: 'Lists the PDF designs this account may pass as `template` when generating a project PDF, including which
        one is the default. Build a design picker from this rather than hard-coding keys — the set is per-account and changes
        when the client enables a design in the portal. Requires the same project-pdf:generate scope and project_pdf entitlement
        as PDF generation.

        '
      responses:
        '304':
          $ref: '#/components/responses/NotModified'
        '200':
          description: Designs available to this account
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/PdfTemplateOption'
              example:
                data:
                - template: classic
                  name: Classic
                  description: The original full layout — every section, in the standard order.
                  is_default: true
                - template: editorial
                  name: Editorial
                  description: Magazine-style brochure — cream pages, serif headlines, full-bleed photography and a dark cover.
                  is_default: false
        '401':
          $ref: '#/components/responses/Error'
        '403':
          $ref: '#/components/responses/Error'
      parameters:
      - $ref: '#/components/parameters/IfNoneMatch'
  /projects/{project}/floor-plans:
    get:
      tags:
      - Projects
      parameters:
      - $ref: '#/components/parameters/Project'
      - $ref: '#/components/parameters/IfNoneMatch'
      responses:
        '200':
          $ref: '#/components/responses/Success'
        '403':
          $ref: '#/components/responses/Error'
        '304':
          $ref: '#/components/responses/NotModified'
  /projects/{project}/payment-plans:
    get:
      tags:
      - Projects
      parameters:
      - $ref: '#/components/parameters/Project'
      - $ref: '#/components/parameters/IfNoneMatch'
      responses:
        '200':
          $ref: '#/components/responses/Success'
        '403':
          $ref: '#/components/responses/Error'
        '304':
          $ref: '#/components/responses/NotModified'
  /projects/{project}/media:
    get:
      tags:
      - Projects
      parameters:
      - $ref: '#/components/parameters/Project'
      - $ref: '#/components/parameters/IfNoneMatch'
      responses:
        '200':
          $ref: '#/components/responses/Success'
        '403':
          $ref: '#/components/responses/Error'
        '304':
          $ref: '#/components/responses/NotModified'
  /projects/{project}/video:
    get:
      tags:
      - Projects
      description: Returns an external video URL or a short-lived signed file_url, whichever the project has. 404 with VIDEO_NOT_AVAILABLE
        when neither exists.
      parameters:
      - $ref: '#/components/parameters/Project'
      - $ref: '#/components/parameters/IfNoneMatch'
      responses:
        '200':
          $ref: '#/components/responses/Success'
        '403':
          $ref: '#/components/responses/Error'
        '404':
          $ref: '#/components/responses/Error'
        '304':
          $ref: '#/components/responses/NotModified'
  /projects/{project}/brochure:
    get:
      tags:
      - Projects
      parameters:
      - $ref: '#/components/parameters/Project'
      - $ref: '#/components/parameters/IfNoneMatch'
      responses:
        '200':
          $ref: '#/components/responses/Success'
        '403':
          $ref: '#/components/responses/Error'
        '404':
          $ref: '#/components/responses/Error'
        '304':
          $ref: '#/components/responses/NotModified'
  /projects/{project}/related:
    get:
      tags:
      - Projects
      parameters:
      - $ref: '#/components/parameters/Project'
      - $ref: '#/components/parameters/IfNoneMatch'
      responses:
        '200':
          $ref: '#/components/responses/ProjectList'
        '304':
          $ref: '#/components/responses/NotModified'
  /projects/{project}/amenities:
    get:
      tags:
      - Projects
      parameters:
      - $ref: '#/components/parameters/Project'
      - $ref: '#/components/parameters/IfNoneMatch'
      responses:
        '200':
          $ref: '#/components/responses/Success'
        '403':
          $ref: '#/components/responses/Error'
        '304':
          $ref: '#/components/responses/NotModified'
  /projects/{project}/nearby:
    get:
      tags:
      - Projects
      parameters:
      - $ref: '#/components/parameters/Project'
      - $ref: '#/components/parameters/IfNoneMatch'
      responses:
        '200':
          $ref: '#/components/responses/Success'
        '403':
          $ref: '#/components/responses/Error'
        '304':
          $ref: '#/components/responses/NotModified'
  /projects/{project}/payment-methods:
    get:
      tags:
      - Projects
      parameters:
      - $ref: '#/components/parameters/Project'
      - $ref: '#/components/parameters/IfNoneMatch'
      responses:
        '200':
          $ref: '#/components/responses/Success'
        '403':
          $ref: '#/components/responses/Error'
        '304':
          $ref: '#/components/responses/NotModified'
  /projects/{project}/faqs:
    get:
      tags:
      - Projects
      parameters:
      - $ref: '#/components/parameters/Project'
      - $ref: '#/components/parameters/IfNoneMatch'
      responses:
        '200':
          $ref: '#/components/responses/Success'
        '403':
          $ref: '#/components/responses/Error'
        '304':
          $ref: '#/components/responses/NotModified'
  /projects/{project}/virtual-tours:
    get:
      tags:
      - Projects
      description: Requires the virtual_tours entitlement.
      parameters:
      - $ref: '#/components/parameters/Project'
      - $ref: '#/components/parameters/IfNoneMatch'
      responses:
        '200':
          $ref: '#/components/responses/Success'
        '403':
          $ref: '#/components/responses/Error'
        '304':
          $ref: '#/components/responses/NotModified'
  /projects/{project}/floor-plates:
    get:
      tags:
      - Projects
      description: Requires the floorplates entitlement.
      parameters:
      - $ref: '#/components/parameters/Project'
      - $ref: '#/components/parameters/IfNoneMatch'
      responses:
        '200':
          $ref: '#/components/responses/Success'
        '403':
          $ref: '#/components/responses/Error'
        '304':
          $ref: '#/components/responses/NotModified'
  /states/{state}:
    get:
      tags:
      - Catalogs
      parameters:
      - name: state
        in: path
        required: true
        schema:
          type: string
        description: Numeric ID or slug
      - $ref: '#/components/parameters/IfNoneMatch'
      responses:
        '200':
          $ref: '#/components/responses/Success'
        '404':
          $ref: '#/components/responses/Error'
        '304':
          $ref: '#/components/responses/NotModified'
  /sub-communities:
    get:
      tags:
      - Catalogs
      parameters:
      - $ref: '#/components/parameters/IfNoneMatch'
      - $ref: '#/components/parameters/Page'
      - $ref: '#/components/parameters/PerPage'
      - name: community
        in: query
        schema:
          type: string
      responses:
        '200':
          $ref: '#/components/responses/Success'
        '304':
          $ref: '#/components/responses/NotModified'
  /sub-communities/{subCommunity}:
    get:
      tags:
      - Catalogs
      parameters:
      - name: subCommunity
        in: path
        required: true
        schema:
          type: string
        description: Numeric ID or slug
      - $ref: '#/components/parameters/IfNoneMatch'
      responses:
        '200':
          $ref: '#/components/responses/Success'
        '404':
          $ref: '#/components/responses/Error'
        '304':
          $ref: '#/components/responses/NotModified'
  /webhooks:
    get:
      tags:
      - Webhooks
      description: Requires the webhooks:manage scope and webhooks entitlement.
      responses:
        '200':
          $ref: '#/components/responses/Success'
        '403':
          $ref: '#/components/responses/Error'
        '304':
          $ref: '#/components/responses/NotModified'
      parameters:
      - $ref: '#/components/parameters/IfNoneMatch'
    post:
      tags:
      - Webhooks
      description: Creates a webhook for the authenticated client. The signing secret is returned only once.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - name
              - url
              - events
              properties:
                name:
                  type: string
                url:
                  type: string
                  format: uri
                  description: Must be HTTPS and not resolve to a private/reserved address.
                events:
                  type: array
                  items:
                    type: string
                  description: e.g. project.created, project.updated, project.status_changed, project.deleted, developer.updated,
                    community.updated
                status:
                  type: string
                  enum:
                  - active
                  - disabled
                  default: active
      responses:
        '201':
          $ref: '#/components/responses/Success'
        '422':
          $ref: '#/components/responses/Error'
  /webhooks/{webhook}:
    put:
      tags:
      - Webhooks
      description: Partial update; omit fields you do not want to change.
      parameters:
      - name: webhook
        in: path
        required: true
        schema:
          type: string
        description: The webhook's id from the create/list response.
      responses:
        '200':
          $ref: '#/components/responses/Success'
        '404':
          $ref: '#/components/responses/Error'
    delete:
      tags:
      - Webhooks
      parameters:
      - name: webhook
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          $ref: '#/components/responses/Success'
        '404':
          $ref: '#/components/responses/Error'
  /webhooks/{webhook}/rotate-secret:
    post:
      tags:
      - Webhooks
      description: Issues a new signing secret, returned only once. The previous secret stops working immediately.
      parameters:
      - name: webhook
        in: path
        required: true
        schema:
          type: string
      responses:
        '200':
          $ref: '#/components/responses/Success'
        '404':
          $ref: '#/components/responses/Error'
  /developers:
    get:
      tags:
      - Catalogs
      operationId: listDevelopers
      parameters:
      - $ref: '#/components/parameters/Page'
      - $ref: '#/components/parameters/PerPage'
      - $ref: '#/components/parameters/IfNoneMatch'
      responses:
        '200':
          $ref: '#/components/responses/Success'
        '304':
          $ref: '#/components/responses/NotModified'
  /developers/{developer}:
    get:
      tags:
      - Catalogs
      parameters:
      - name: developer
        in: path
        required: true
        schema:
          type: string
      - $ref: '#/components/parameters/IfNoneMatch'
      responses:
        '200':
          $ref: '#/components/responses/Success'
        '404':
          $ref: '#/components/responses/Error'
        '304':
          $ref: '#/components/responses/NotModified'
  /communities:
    get:
      tags:
      - Catalogs
      parameters:
      - $ref: '#/components/parameters/Page'
      - $ref: '#/components/parameters/PerPage'
      - name: state
        in: query
        schema:
          type: string
      - $ref: '#/components/parameters/IfNoneMatch'
      responses:
        '200':
          $ref: '#/components/responses/Success'
        '304':
          $ref: '#/components/responses/NotModified'
  /communities/{community}:
    get:
      tags:
      - Catalogs
      parameters:
      - name: community
        in: path
        required: true
        schema:
          type: string
      - $ref: '#/components/parameters/IfNoneMatch'
      responses:
        '200':
          $ref: '#/components/responses/Success'
        '404':
          $ref: '#/components/responses/Error'
        '304':
          $ref: '#/components/responses/NotModified'
  /catalog/metadata:
    get:
      tags:
      - Catalogs
      responses:
        '200':
          $ref: '#/components/responses/Success'
        '304':
          $ref: '#/components/responses/NotModified'
      parameters:
      - $ref: '#/components/parameters/IfNoneMatch'
components:
  securitySchemes:
    bearerApiKey:
      type: http
      scheme: bearer
      bearerFormat: opaque API key
  parameters:
    Project:
      name: project
      in: path
      required: true
      schema:
        type: string
      description: Numeric ID or slug
    IfNoneMatch:
      name: If-None-Match
      in: header
      required: false
      schema:
        type: string
      description: ETag from a previous response. If it still matches, the API replies 304 Not Modified with no body and the
        call does NOT count against your monthly allowance.
    Page:
      name: page
      in: query
      schema:
        type: integer
        minimum: 1
        default: 1
    PerPage:
      name: per_page
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
  responses:
    NotModified:
      description: Not Modified - your cached copy is still current. No body is returned and no quota is consumed.
    Success:
      description: Success
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Success'
    ProjectList:
      description: Paginated projects
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ProjectListResponse'
    Error:
      description: Error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  schemas:
    Meta:
      type: object
      required:
      - request_id
      - generated_at
      properties:
        request_id:
          type: string
        generated_at:
          type: string
          format: date-time
        representation:
          type: string
          enum:
          - summary
          - detail
          - related_resources
        quota_units:
          type: integer
          minimum: 1
        pagination:
          type: object
          properties:
            page:
              type: integer
            per_page:
              type: integer
            total:
              type: integer
            last_page:
              type: integer
    Project:
      type: object
      required:
      - id
      - slug
      - name
      - created_at
      - updated_at
      properties:
        id:
          type: integer
        slug:
          type: string
        name:
          type: string
        title:
          type: string
          nullable: true
        summary:
          type: string
          nullable: true
        nature:
          type: string
          nullable: true
        starting_price:
          type: object
          nullable: true
          properties:
            amount:
              type: integer
            currency:
              type: string
              example: AED
        bedrooms:
          type: array
          nullable: true
          items:
            type: integer
        bathrooms:
          type: string
          nullable: true
        handover:
          type: string
          nullable: true
        ownership:
          type: string
          nullable: true
        area:
          type: object
          properties:
            from:
              type: number
              nullable: true
            to:
              type: number
              nullable: true
            unit:
              type: string
              nullable: true
        location:
          type: object
          properties:
            label:
              type: string
              nullable: true
            latitude:
              type: number
              format: double
              nullable: true
            longitude:
              type: number
              format: double
              nullable: true
        map_embed_html:
          type: string
          nullable: true
          description: Detail endpoint only. Fallback map embed markup to show when latitude/longitude are null.
        developer:
          type: object
          nullable: true
        community:
          type: object
          nullable: true
        created_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
    Success:
      type: object
      required:
      - data
      - meta
      properties:
        data: {}
        meta:
          $ref: '#/components/schemas/Meta'
    ProjectResponse:
      allOf:
      - $ref: '#/components/schemas/Success'
      - type: object
        properties:
          data:
            $ref: '#/components/schemas/ProjectDetail'
    ProjectDetail:
      allOf:
      - $ref: '#/components/schemas/Project'
      - type: object
        properties:
          property_no:
            type: string
            nullable: true
          license_no:
            type: string
            nullable: true
          project_types:
            type: array
            items:
              type: object
          unit_type_codes:
            type: array
            items:
              type: string
          overview:
            type: string
            nullable: true
          description:
            type: string
            nullable: true
          master_plan:
            type: string
            nullable: true
          video_description:
            type: string
            nullable: true
          map_embed_html:
            type: string
            nullable: true
          down_payment_percent:
            oneOf:
            - type: number
            - type: string
            nullable: true
          service_charges:
            type: string
            nullable: true
    ProjectListResponse:
      allOf:
      - $ref: '#/components/schemas/Success'
      - type: object
        properties:
          data:
            type: array
            items:
              oneOf:
              - $ref: '#/components/schemas/Project'
              - $ref: '#/components/schemas/ProjectDetail'
    ProjectRelatedResourceBatchResponse:
      allOf:
      - $ref: '#/components/schemas/Success'
      - type: object
        properties:
          data:
            type: array
            items:
              $ref: '#/components/schemas/ProjectRelatedResources'
          meta:
            allOf:
            - $ref: '#/components/schemas/Meta'
            - type: object
              properties:
                requested_project_count:
                  type: integer
                  minimum: 1
                  maximum: 25
                returned_project_count:
                  type: integer
                  minimum: 0
                  maximum: 25
                omitted_project_ids:
                  type: array
                  items:
                    type: integer
                included_resources:
                  type: array
                  items:
                    type: string
                    enum:
                    - media
                    - floor_plans
                    - amenities
                    - payment_plans
                    - payment_methods
    ProjectRelatedResources:
      type: object
      required:
      - project_id
      - resources
      properties:
        project_id:
          type: integer
        resources:
          type: object
          description: Contains only the collections requested through include.
          properties:
            media:
              type: object
              properties:
                items:
                  type: array
                  items:
                    type: object
                master_plan_document:
                  type: string
                  nullable: true
            floor_plans:
              type: object
              properties:
                floor_plans:
                  type: array
                  items:
                    type: object
                document:
                  type: string
                  nullable: true
            amenities:
              type: object
              properties:
                items:
                  type: array
                  items:
                    type: object
            payment_plans:
              type: object
              properties:
                payment_plans:
                  type: array
                  items:
                    type: object
            payment_methods:
              type: object
              properties:
                items:
                  type: array
                  items:
                    type: object
    AgentProfile:
      type: object
      required:
      - id
      - agent_name
      properties:
        id:
          type: integer
          description: Use as agent_profile_id when generating a PDF.
          example: 1
        agent_name:
          type: string
          example: First Agent
        company_name:
          type: string
          nullable: true
          example: First Choice Properties
        email:
          type: string
          nullable: true
          format: email
        phone:
          type: string
          nullable: true
        whatsapp:
          type: string
          nullable: true
        image_url:
          type: string
          nullable: true
          description: Absolute URL
          or null when no photo was uploaded.: null
    PdfTemplateOption:
      type: object
      required:
      - template
      - name
      - is_default
      properties:
        template:
          type: string
          description: Pass this as the `template` field when generating a PDF.
          example: editorial
        name:
          type: string
          example: Editorial
        description:
          type: string
          nullable: true
        is_default:
          type: boolean
          description: True for the design used when a request omits `template`.
    ProjectPdfResponse:
      allOf:
      - $ref: '#/components/schemas/Success'
      - type: object
        properties:
          data:
            type: object
            required:
            - project_id
            - cached
            - download_url
            - expires_at
            properties:
              project_id:
                type: integer
              cached:
                type: boolean
                description: True when an identical approved PDF was reused without consuming PDF-generation quota.
              download_url:
                type: string
                format: uri
                description: Short-lived signed URL; the caller must continue to send its bearer key.
              expires_at:
                type: string
                format: date-time
    ErrorResponse:
      type: object
      required:
      - error
      - meta
      properties:
        error:
          type: object
          required:
          - code
          - message
          properties:
            code:
              type: string
            message:
              type: string
            details:
              nullable: true
        meta:
          $ref: '#/components/schemas/Meta'
