> ## 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.

# Get an access token (client credentials)

> Exchanges your `client_id` and `client_secret` for a short-lived opaque bearer token. `client_credentials` is the only supported grant type.

Cache the token in memory for `expires_in` seconds (refresh ~60 seconds early) — token requests count against your rate limit.



## OpenAPI

````yaml /api-reference/openapi.json post /oauth2/token
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:
  /oauth2/token:
    post:
      tags:
        - Authentication
      summary: Get an access token (client credentials)
      description: >-
        Exchanges your `client_id` and `client_secret` for a short-lived opaque
        bearer token. `client_credentials` is the only supported grant type.


        Cache the token in memory for `expires_in` seconds (refresh ~60 seconds
        early) — token requests count against your rate limit.
      operationId: getAccessToken
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TokenRequest'
            example:
              grant_type: client_credentials
              client_id: YOUR_CLIENT_ID
              client_secret: YOUR_CLIENT_SECRET
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/TokenRequest'
      responses:
        '200':
          description: Token issued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'
              example:
                access_token: >-
                  9f2c1b7d4e8a3c5f6b0d2e4a8c1f3b5d7e9a0c2f4b6d8e0a2c4f6b8d0e2a4c6f
                token_type: Bearer
                expires_in: 3600
                scope: ''
        '401':
          description: >-
            Invalid client credentials, unknown client, or unsupported grant
            type
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
              example:
                statusCode: 401
                code: UNAUTHORIZED
                message: Invalid client credentials
                path: /v2/oauth2/token
                timestamp: '2026-07-27T06:28:41.703Z'
        '429':
          $ref: '#/components/responses/RateLimited'
      security: []
components:
  schemas:
    TokenRequest:
      type: object
      required:
        - grant_type
        - client_id
        - client_secret
      properties:
        grant_type:
          type: string
          enum:
            - client_credentials
          description: The only supported grant type.
        client_id:
          type: string
          description: Client id issued by Provident (per environment).
        client_secret:
          type: string
          description: >-
            Client secret issued by Provident. Server-side use only — never
            expose it in a browser or mobile app.
    TokenResponse:
      type: object
      properties:
        access_token:
          type: string
          description: 'Opaque bearer token. Send as `Authorization: Bearer <access_token>`.'
        token_type:
          type: string
          example: Bearer
        expires_in:
          type: integer
          description: Seconds until the token expires (typically 3600).
          example: 3600
        scope:
          type: string
          description: Space-separated scopes granted to the client. May be empty.
    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:
    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

````