> ## Documentation Index
> Fetch the complete documentation index at: https://docs.prov.ae/llms.txt
> Use this file to discover all available pages before exploring further.

# Search developers

> Matches the term against developer name, slug and description, ranking exact-name matches first. Falls back to the plain list when no term is given. CHANGED 2026-08: this endpoint moved from `page`/`limit` to `limit`/`offset`, and `meta` from `{page, limit, total, totalPages}` to `{limit, offset, total}`. `page` is no longer accepted — sending it returns 422, it is not ignored. Relevance-ranked against the search term (name, slug, description and id); falls back to the plain list when no term is given.



## OpenAPI

````yaml /api-reference/openapi.json get /public/developers/search
openapi: 3.0.3
info:
  title: Provident — Lead Intake API
  version: 1.4.0
  description: >-
    Server-to-server API for pushing leads into the Provident CRM.


    Core endpoints:

    1. `POST /oauth2/token` — exchange client credentials for a bearer token.

    2. `POST /public/leads` — create a CRM lead.

    3. `PUT /public/leads/{id}` — add details you did not have when you created
    it (quiz answers the customer finished afterwards, a budget, an area). Send
    only what changed; phones and emails are appended, never replaced, and the
    lead's stage, status, owner and funnel are not writable.


    Optional read-only reference data, using the same token: locations,
    developers, projects and project statuses. Use them to send values that
    match ours, or to show project content on your own pages. They share the
    lead endpoint's rate limit, so cache them.


    Authentication is OAuth 2.0 Client Credentials; there is no user login. Note
    that the `/v2` prefix is part of the server URL and is mandatory.


    The intake is deliberately forgiving: name-based lookup fields that cannot
    be matched do NOT fail the request — the lead is still created and the
    unmatched values are returned in `intakeUnresolved`.


    The endpoint is not idempotent at the transport level, but intake hygiene
    runs before the lead is created: a second enquiry from the same phone/email
    within the merge window (~24h) is MERGED into the existing lead and returns
    a different, much shorter 201 body — `{ id, merged: true, matchedOn }` —
    with no contactId, intakeUnresolved, needsIntakeReview or createdAt. Always
    branch on `merged` before reading anything else. Enquiries from
    property-portal sources (Property Finder, Bayut, Dubizzle) are exempt and
    never merge. Retry only on network timeouts, 429 and 5xx.
  contact:
    name: Provident API support
servers:
  - url: https://devapi.prov.ae/v2
    description: Staging / development — build and test here
  - url: https://prodapi.prov.ae/v2
    description: Production — live leads
security: []
tags:
  - name: Authentication
    description: OAuth 2.0 client-credentials token issuance
  - name: Lead intake
    description: Public lead creation
  - name: Reference data
    description: Read-only lookups — locations, developers, projects, project statuses
  - name: Lead follow-up
    description: >-
      Reading back who a lead was assigned to, and recording the agent response
      time
  - name: Webhooks
    description: >-
      Calls Provident makes to YOUR server. Documented here for the payload
      shape; these are not endpoints you call.
paths:
  /public/developers/search:
    get:
      tags:
        - Reference data
      summary: Search developers
      description: >-
        Matches the term against developer name, slug and description, ranking
        exact-name matches first. Falls back to the plain list when no term is
        given. CHANGED 2026-08: this endpoint moved from `page`/`limit` to
        `limit`/`offset`, and `meta` from `{page, limit, total, totalPages}` to
        `{limit, offset, total}`. `page` is no longer accepted — sending it
        returns 422, it is not ignored. Relevance-ranked against the search term
        (name, slug, description and id); falls back to the plain list when no
        term is given.
      operationId: searchDevelopers
      parameters:
        - $ref: '#/components/parameters/LimitCapped'
        - $ref: '#/components/parameters/Offset'
        - $ref: '#/components/parameters/Search'
        - $ref: '#/components/parameters/SearchAliasQ'
        - $ref: '#/components/parameters/SearchAliasQuery'
        - $ref: '#/components/parameters/Sort'
        - $ref: '#/components/parameters/Order'
        - $ref: '#/components/parameters/Popular'
      responses:
        '200':
          description: Developers, offset-paged.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeveloperListResponse'
              example:
                data:
                  - id: 11111111-1111-1111-1111-111111111111
                    name: Emaar
                    slug: emaar
                    logoUrl: https://media.prov.ae/public/developers/emaar-logo.png
                    imageUrl: https://media.prov.ae/public/developers/emaar-cover.jpg
                    description: Master developer.
                    enabled: true
                    popular: true
                    contactId: null
                    createdAt: '2026-07-21T10:09:32.393Z'
                    updatedAt: '2026-07-21T10:09:32.393Z'
                meta:
                  limit: 50
                  offset: 0
                  total: 244
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/RateLimited'
      security:
        - bearerAuth: []
components:
  parameters:
    LimitCapped:
      name: limit
      in: query
      required: false
      description: >-
        Page size, default 50, clamped to 200. `meta.limit` echoes the value
        actually applied, not what was asked for.
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 50
    Offset:
      name: offset
      in: query
      required: false
      description: Records to skip.
      schema:
        type: integer
        minimum: 0
        default: 0
    Search:
      name: search
      in: query
      required: false
      description: Search term. Aliases `q` and `query` are accepted for the same value.
      schema:
        type: string
    SearchAliasQ:
      name: q
      in: query
      required: false
      description: Alias for `search`.
      schema:
        type: string
    SearchAliasQuery:
      name: query
      in: query
      required: false
      description: Alias for `search`.
      schema:
        type: string
    Sort:
      name: sort
      in: query
      required: false
      description: >-
        CSV of sort keys, most significant first; each may carry its own
        direction (`name:desc`, `-name`). Unknown keys are ignored.
      schema:
        type: string
        example: popular:desc,name:asc
    Order:
      name: order
      in: query
      required: false
      description: Default direction for sort keys that do not carry one.
      schema:
        type: string
        enum:
          - asc
          - desc
        default: asc
    Popular:
      name: popular
      in: query
      required: false
      description: >-
        Omit for all enabled developers; `true` / `false` filters by the popular
        flag.
      schema:
        type: string
        enum:
          - 'true'
          - 'false'
  schemas:
    DeveloperListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Developer'
        meta:
          $ref: '#/components/schemas/OffsetMeta'
    Developer:
      type: object
      description: >-
        An enabled developer. Additional fields may be present; only these are
        contractual.
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          example: Emaar
        slug:
          type: string
          example: emaar
        logoUrl:
          type: string
          nullable: true
        imageUrl:
          type: string
          nullable: true
        description:
          type: string
          nullable: true
        popular:
          type: boolean
        enabled:
          type: boolean
          description: Always true — only enabled developers are returned.
        contactId:
          type: string
          format: uuid
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      additionalProperties: true
    OffsetMeta:
      type: object
      description: >-
        Offset paging envelope. `limit` echoes the value actually applied after
        clamping, not what was asked for.
      properties:
        limit:
          type: integer
          example: 50
        offset:
          type: integer
          example: 0
        total:
          type: integer
          example: 244
          description: Unpaginated row count for the query.
    ApiError:
      type: object
      description: Common error envelope used by every endpoint.
      properties:
        statusCode:
          type: integer
          example: 400
        code:
          type: string
          description: >-
            Stable machine-readable error code — branch on this, not on
            `message`.
          example: BAD_REQUEST
        message:
          type: string
          description: Human-readable description.
        errors:
          type: array
          items:
            type: string
          description: 'Present on 422 only: one entry per field that failed validation.'
        details:
          type: object
          additionalProperties: true
          description: Optional structured extras attached by the failing handler.
        path:
          type: string
          example: /v2/public/leads
        timestamp:
          type: string
          format: date-time
  responses:
    Unauthorized:
      description: Missing, invalid, revoked or expired bearer token.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
          example:
            statusCode: 401
            code: OAUTH_TOKEN_EXPIRED
            message: OAuth access token expired
            path: /v2/public/projects/filters
            timestamp: '2026-07-27T06:31:04.512Z'
    ValidationFailed:
      description: >-
        A parameter or field failed validation. Note that an UNRECOGNISED query
        parameter is rejected, not ignored — this is how a call written against
        the old `page`-based developers endpoint now fails. Fix the request;
        never retry it unchanged.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
          example:
            statusCode: 422
            code: VALIDATION_ERROR
            message: Validation failed
            errors:
              - property page should not exist
            path: /v2/public/developers?page=1&limit=2
            timestamp: '2026-08-19T07:20:28.507Z'
    RateLimited:
      description: >-
        Request quota for this client exceeded. Wait `Retry-After` seconds, then
        resume.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
        X-RateLimit-Limit:
          $ref: '#/components/headers/XRateLimitLimit'
        X-RateLimit-Remaining:
          $ref: '#/components/headers/XRateLimitRemaining'
        X-RateLimit-Reset:
          $ref: '#/components/headers/XRateLimitReset'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiError'
          example:
            statusCode: 429
            code: RATE_LIMITED
            message: Too Many Requests
            path: /v2/public/leads
            timestamp: '2026-07-27T06:31:04.512Z'
  headers:
    XRateLimitLimit:
      description: Requests allowed in the current window.
      schema:
        type: integer
    XRateLimitRemaining:
      description: Requests remaining in the current window.
      schema:
        type: integer
    XRateLimitReset:
      description: Seconds until the counter resets (end of the UTC day).
      schema:
        type: integer
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: Opaque
      description: >-
        The `access_token` returned by `POST /oauth2/token`, sent as
        `Authorization: Bearer <access_token>`. It is an opaque string, not a
        JWT.

````