openapi: 3.1.0
info:
  title: Karibu Data API
  version: "1.0.0"
  description: >
    Kenya's property & rental-payment data, as an API. Authenticated, read-only
    access to the Karibu Index, verified-listing comps, canonical property
    records, market intelligence, and de-identified customer aggregates.


    **Authentication.** Send your key as a bearer token:
    `Authorization: Bearer kapi_<keyId>_<secret>`. Generate keys in your
    developer console (https://karibuhome.world/data/console). Sandbox keys
    answer synthetic data on the same schema; production keys require approval.


    **Environments.** A `sandbox` key returns deterministic synthetic payloads
    (flagged with `"sandbox": true` and an `x-karibu-environment: sandbox`
    response header) so you can integrate end-to-end before going live. The
    schema is identical to production.


    **Privacy (DPA 2019).** Customer endpoints return aggregate, de-identified
    data only — never names, emails, or phone numbers.
  contact:
    name: Karibu Data API — support
    email: data@karibu.africa
  termsOfService: https://karibuhome.world/terms
servers:
  - url: https://karibuhome.world
    description: Production
security:
  - bearerKey: []
tags:
  - name: Market
    description: Index, comps, and market intelligence (scope market:read)
  - name: Property
    description: Canonical property records & inventory (scope listings:read)
  - name: Customers
    description: De-identified customer aggregates (scope customers:read)
  - name: Account
    description: Identify the calling key
paths:
  /api/v1/index:
    get:
      tags: [Market]
      operationId: getKaribuIndex
      summary: Karibu Index time series
      description: Karibu-original asking-price index by area × property type × sale/rent.
      security:
        - bearerKey: []
      parameters:
        - { name: area, in: query, schema: { type: string }, description: Neighbourhood name }
        - { name: city, in: query, schema: { type: string } }
        - { name: propertyType, in: query, schema: { $ref: '#/components/schemas/PropertyType' } }
        - { name: saleOrRent, in: query, schema: { $ref: '#/components/schemas/SaleOrRent' } }
        - { name: period, in: query, schema: { type: string, example: "2026-Q2" } }
      responses:
        "200":
          description: Index series
          headers:
            x-karibu-quota-remaining: { schema: { type: integer }, description: Remaining daily quota }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IndexResponse' }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "403": { $ref: '#/components/responses/Forbidden' }
        "429": { $ref: '#/components/responses/RateLimited' }
  /api/v1/market/comps:
    get:
      tags: [Market]
      operationId: getComps
      summary: Aggregate comps from verified listings
      description: Median price & KES/sqm per area × type × sale/rent, verified Karibu listings only.
      security:
        - bearerKey: []
      parameters:
        - { name: area, in: query, schema: { type: string } }
        - { name: city, in: query, schema: { type: string } }
        - { name: propertyType, in: query, schema: { $ref: '#/components/schemas/PropertyType' } }
        - { name: saleOrRent, in: query, schema: { $ref: '#/components/schemas/SaleOrRent' } }
      responses:
        "200":
          description: Comp aggregates
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CompsResponse' }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "403": { $ref: '#/components/responses/Forbidden' }
        "429": { $ref: '#/components/responses/RateLimited' }
  /api/v1/properties/{upi}:
    get:
      tags: [Property]
      operationId: getProperty
      summary: Canonical property record by Karibu UPI
      description: Canonical Property facts + attribute-level provenance for a Karibu UPI.
      security:
        - bearerKey: []
      parameters:
        - name: upi
          in: path
          required: true
          schema: { type: string }
          description: "Karibu UPI, e.g. urn:karibu:nairobi:plus-6GCRPR6C+8R (URL-encode it)"
      responses:
        "200":
          description: Property record + attributes
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PropertyResponse' }
        "404": { description: Unknown UPI, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "403": { $ref: '#/components/responses/Forbidden' }
  /api/partner/listings:
    get:
      tags: [Property]
      operationId: getListings
      summary: Market inventory (published listings)
      security:
        - bearerKey: []
      parameters:
        - { name: city, in: query, schema: { type: string } }
        - { name: neighborhood, in: query, schema: { type: string } }
        - { name: saleOrRent, in: query, schema: { $ref: '#/components/schemas/SaleOrRent' } }
        - { name: limit, in: query, schema: { type: integer, default: 50, maximum: 200 } }
        - { name: offset, in: query, schema: { type: integer, default: 0 } }
      responses:
        "200":
          description: Paginated listings
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ListingsResponse' }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "403": { $ref: '#/components/responses/Forbidden' }
  /api/partner/market:
    get:
      tags: [Market]
      operationId: getMarket
      summary: Source registry + inventory aggregates
      security:
        - bearerKey: []
      responses:
        "200":
          description: Market intelligence
          content: { application/json: { schema: { type: object } } }
        "401": { $ref: '#/components/responses/Unauthorized' }
  /api/partner/customers:
    get:
      tags: [Customers]
      operationId: getCustomers
      summary: De-identified customer aggregates
      description: Aggregate counts & segments only. No PII (names, emails, phones) is ever returned.
      security:
        - bearerKey: []
      responses:
        "200":
          description: De-identified aggregates
          content: { application/json: { schema: { type: object } } }
        "401": { $ref: '#/components/responses/Unauthorized' }
        "403": { $ref: '#/components/responses/Forbidden' }
  /api/partner/me:
    get:
      tags: [Account]
      operationId: getMe
      summary: Identify the calling key
      security:
        - bearerKey: []
      responses:
        "200":
          description: Key identity
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  keyId: { type: string }
                  environment: { type: string, enum: [sandbox, production] }
                  tier: { type: string, enum: [trial, pro, enterprise] }
                  scopes: { type: array, items: { type: string } }
                  status: { type: string }
        "401": { $ref: '#/components/responses/Unauthorized' }
components:
  securitySchemes:
    bearerKey:
      type: http
      scheme: bearer
      bearerFormat: kapi_<keyId>_<secret>
      description: API key as a bearer token. Scopes per key — listings:read, market:read, customers:read.
  responses:
    Unauthorized:
      description: Missing or invalid key
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Forbidden:
      description: Key lacks the required scope
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    RateLimited:
      description: Daily quota exceeded
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
  schemas:
    Error:
      type: object
      properties:
        error: { type: string, example: unauthorized }
    PropertyType:
      type: string
      enum: [APARTMENT, HOUSE, TOWNHOUSE, VILLA, LAND, COMMERCIAL, OFFICE, OTHER]
    SaleOrRent:
      type: string
      enum: [SALE, RENT]
    IndexResponse:
      type: object
      properties:
        ok: { type: boolean }
        source: { type: string }
        count: { type: integer }
        series:
          type: array
          items:
            type: object
            properties:
              name: { type: string }
              area: { type: string, nullable: true }
              city: { type: string, nullable: true }
              propertyType: { type: string, nullable: true }
              saleOrRent: { type: string, nullable: true }
              period: { type: string }
              valueKesPerSqm: { type: number, nullable: true }
              medianPriceKes: { type: number, nullable: true }
              changePct: { type: number, nullable: true }
              sampleSize: { type: integer }
              confidence: { type: number, nullable: true }
              thinSample: { type: boolean }
    CompsResponse:
      type: object
      properties:
        ok: { type: boolean }
        source: { type: string }
        count: { type: integer }
        comps:
          type: array
          items:
            type: object
            properties:
              area: { type: string }
              city: { type: string, nullable: true }
              propertyType: { type: string }
              saleOrRent: { type: string }
              medianPriceKes: { type: number, nullable: true }
              medianKesPerSqm: { type: number, nullable: true }
              sampleSize: { type: integer }
              confidence: { type: number }
              thinSample: { type: boolean }
    PropertyResponse:
      type: object
      properties:
        ok: { type: boolean }
        property:
          type: object
          properties:
            karibuUpi: { type: string }
            lrNumber: { type: string, nullable: true }
            plusCode: { type: string, nullable: true }
            lat: { type: number, nullable: true }
            lng: { type: number, nullable: true }
            county: { type: string, nullable: true }
            city: { type: string, nullable: true }
            neighborhood: { type: string, nullable: true }
            propertyType: { type: string, nullable: true }
            titleStatus: { type: string, nullable: true }
            confidence: { type: number, nullable: true }
        attributes:
          type: array
          items:
            type: object
            properties:
              name: { type: string }
              value: { type: string }
              source: { type: string, nullable: true }
              sourceClass: { type: string, nullable: true }
              confidence: { type: number, nullable: true }
              verified: { type: boolean }
    ListingsResponse:
      type: object
      properties:
        ok: { type: boolean }
        total: { type: integer }
        limit: { type: integer }
        offset: { type: integer }
        count: { type: integer }
        listings:
          type: array
          items:
            type: object
            properties:
              listingRef: { type: string }
              city: { type: string, nullable: true }
              neighborhood: { type: string, nullable: true }
              propertyType: { type: string }
              saleOrRent: { type: string }
              bedrooms: { type: integer, nullable: true }
              bathrooms: { type: integer, nullable: true }
              sqmBuilt: { type: number, nullable: true }
              askingPriceKes: { type: string, nullable: true }
              karibuVerified: { type: boolean }
              status: { type: string }
