openapi: 3.1.0
info:
  title: CrimeScore API
  version: "1.0"
  description: |
    CrimeScore is a crime data API, crime score API, and neighborhood safety
    scoring API for US locations. It returns a risk score (0-100), letter grade,
    resolved geography, and optional component breakdowns based on your
    subscription tier.

    Use the API for address-level crime scores, real estate safety context,
    PropTech location intelligence, embeddable crime maps, and bounded
    24-hour recent activity context.

    ## Authentication
    All requests require an `x-api-key` header. Create API keys in the
    [dashboard](/dashboard/api-keys).

    **Key Types:**
    - `cs_secret_*` — Server-side only. Use in your backend.
    - `cs_pub_*` — Publishable. Use in the embed widget. Domain-restricted.

    ## Embed Widget
    Embed an interactive crime map or neighborhood safety map on your website:
    ```html
    <iframe src="https://crimescore.io/embed?key=cs_pub_YOUR_KEY"
      width="600" height="400" style="border:0;border-radius:8px;">
    </iframe>
    ```
    Configure allowed domains in the [dashboard](/dashboard/api-keys).

    ## Tiers
    | Tier | Monthly Requests | Response Detail |
    |---|---|---|
    | Starter | 10,000 | Score + grade + watermarked map embed |
    | Pro | 50,000 | + component breakdown and unwatermarked map embed |
    | Scale | 250,000 | + top contributing factors and aggregate rollups |

    ## Rate Limits
    Requests are tracked monthly per organization. When your quota is exceeded,
    the API returns `403 Forbidden`.
  contact:
    name: CrimeScore Support
    url: https://crimescore.io
  license:
    name: Proprietary

servers:
  - url: https://api.crimescore.io/v1
    description: Production
  - url: https://api.dev.crimescore.io/v1
    description: Development

security:
  - ApiKeyAuth: []

paths:
  /score:
    get:
      operationId: getScore
      summary: Get safety score for a location
      description: |
        Returns the crime risk score for the census block group containing the
        given coordinates. The response detail depends on your subscription tier.
      parameters:
        - name: lat
          in: query
          required: true
          description: Latitude (-90 to 90)
          schema:
            type: number
            format: float
            minimum: -90
            maximum: 90
          example: 40.7128
        - name: lng
          in: query
          required: true
          description: Longitude (-180 to 180)
          schema:
            type: number
            format: float
            minimum: -180
            maximum: 180
          example: -74.006
      responses:
        "200":
          description: Score found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ScoreResponse"
              examples:
                starter:
                  summary: Starter tier response
                  value:
                    geoid: "360610031002"
                    resolved:
                      lat: 40.7128
                      lng: -74.006
                      state: "NY"
                      county_fips: "061"
                    risk_score: 72
                    risk_grade: "D"
                    data:
                      census_vintage: "2024"
                      model_version: "2.0"
                pro:
                  summary: Pro tier response (includes components)
                  value:
                    geoid: "360610031002"
                    resolved:
                      lat: 40.7128
                      lng: -74.006
                      state: "NY"
                      county_fips: "061"
                    risk_score: 72
                    risk_grade: "D"
                    components:
                      overall: 68.5
                      violent_crime: 75.2
                      property_crime: 62.1
                      disorder: 70.8
                    data:
                      census_vintage: "2024"
                      model_version: "2.0"
                scale:
                  summary: Scale tier response (includes components + factors)
                  value:
                    geoid: "360610031002"
                    resolved:
                      lat: 40.7128
                      lng: -74.006
                      state: "NY"
                      county_fips: "061"
                    risk_score: 72
                    risk_grade: "D"
                    components:
                      overall: 68.5
                      violent_crime: 75.2
                      property_crime: 62.1
                      disorder: 70.8
                    top_contributors:
                      - label: "Population density"
                        direction: "risk"
                        strength: "strong"
                      - label: "Median household income"
                        direction: "protective"
                        strength: "moderate"
                      - label: "Rental vacancy rate"
                        direction: "risk"
                        strength: "moderate"
                    data:
                      census_vintage: "2024"
                      model_version: "2.0"
        "400":
          description: Invalid parameters
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "INVALID_PARAMETERS"
                message: "Invalid lat/lng parameters"
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "UNAUTHORIZED"
                message: "Unauthorized"
        "403":
          description: Monthly quota exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "QUOTA_EXCEEDED"
                message: "Monthly API quota exceeded"
        "404":
          description: No data for location
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                no_coverage:
                  summary: Coordinates outside US census boundaries
                  value:
                    error: "NO_COVERAGE"
                    message: "No census block group found for coordinates"
                no_score:
                  summary: Block group found but no score data
                  value:
                    error: "NO_SCORE_DATA"
                    message: "No score data for block group"
                    geoid: "360610031002"
        "500":
          description: Server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "INTERNAL_ERROR"
                message: "Internal server error"

  /incidents/trending:
    get:
      operationId: getTrendingIncidents
      summary: Get recent incident activity near a location
      description: |
        Returns sanitized, normalized recent incident activity within a city-scale
        radius around the provided coordinates. Results are limited to a rolling
        24-hour window and do not include raw source payloads, media, user data,
        livestream details, update text, or internal source identifiers. This is
        a contextual recent-activity endpoint, not a bulk export API.
      parameters:
        - name: lat
          in: query
          required: true
          description: Center latitude (-90 to 90)
          schema:
            type: number
            format: float
            minimum: -90
            maximum: 90
          example: 40.7128
        - name: lng
          in: query
          required: true
          description: Center longitude (-180 to 180)
          schema:
            type: number
            format: float
            minimum: -180
            maximum: 180
          example: -74.006
        - name: radius_m
          in: query
          required: false
          description: Search radius in meters. Capped to city-scale local queries.
          schema:
            type: integer
            default: 5000
            minimum: 100
            maximum: 16000
          example: 5000
        - name: hours
          in: query
          required: false
          description: Rolling lookback window in hours. Currently capped at 24.
          schema:
            type: integer
            default: 24
            minimum: 1
            maximum: 24
          example: 24
        - name: limit
          in: query
          required: false
          description: Maximum number of incidents to return. Pagination is not available in v1.
          schema:
            type: integer
            default: 25
            minimum: 1
            maximum: 100
          example: 25
      responses:
        "200":
          description: Recent incidents found near the requested location
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IncidentTrendingResponse"
              example:
                capturedAt: "2026-05-07T16:00:00.000Z"
                center:
                  lat: 40.7128
                  lng: -74.006
                radius_m: 5000
                hours: 24
                limit: 25
                count: 2
                scannedCells: 4
                incidents:
                  - id: "citizen_abc123"
                    label: "Fire activity · NYC area"
                    categories: ["fire"]
                    latitude: 40.718
                    longitude: -73.996
                    cityCode: "nyc"
                    reportedAt: "2026-05-07T15:42:00.000Z"
                    severity: 3
                    level: 2
                    rank: 1
                    distance_m: 1284
                  - id: "citizen_def456"
                    label: "Traffic incident · NYC area"
                    categories: ["traffic"]
                    latitude: 40.689
                    longitude: -74.014
                    cityCode: "nyc"
                    reportedAt: "2026-05-07T15:25:00.000Z"
                    severity: 1
                    level: 1
                    rank: 2
                    distance_m: 2760
        "400":
          description: Invalid parameters or search radius too broad
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                invalid:
                  summary: Invalid coordinates
                  value:
                    error: "INVALID_PARAMETERS"
                    message: "lat and lng are required"
                radius_too_large:
                  summary: Query would scan too many geohash cells
                  value:
                    error: "RADIUS_TOO_LARGE"
                    message: "radius_m is too large for this query"
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "UNAUTHORIZED"
                message: "Unauthorized"
        "403":
          description: Monthly quota exceeded or plan does not include live incidents
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "QUOTA_EXCEEDED"
                message: "Monthly API quota exceeded"
        "500":
          description: Server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: "INTERNAL_ERROR"
                message: "Internal server error"

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: Your CrimeScore API key (`cs_secret_*` for server-side, `cs_pub_*` for embeds)

  schemas:
    ScoreResponse:
      type: object
      required:
        - geoid
        - resolved
        - risk_score
        - risk_grade
        - data
      properties:
        geoid:
          type: string
          description: Census block group FIPS code (12 digits)
          example: "360610031002"
        resolved:
          type: object
          description: Resolved location metadata
          required: [lat, lng, state, county_fips]
          properties:
            lat:
              type: number
              description: Input latitude
            lng:
              type: number
              description: Input longitude
            state:
              type: string
              description: 2-digit state FIPS code
              example: "NY"
            county_fips:
              type: string
              description: 3-digit county FIPS code
              example: "061"
        risk_score:
          type: integer
          minimum: 0
          maximum: 100
          description: "Crime risk score: 0 (safest) to 100 (most dangerous)"
        risk_grade:
          type: string
          enum: [A, B, C, D, F]
          description: "Letter grade: A (safest) through F (most dangerous)"
        components:
          type: object
          description: "Sub-score breakdown (Pro tier and above)"
          additionalProperties:
            type: number
          example:
            overall: 68.5
            violent_crime: 75.2
            property_crime: 62.1
            disorder: 70.8
        top_contributors:
          type: array
          description: "Census factors driving the score (Scale tier only)"
          items:
            type: object
            required: [label, direction, strength]
            properties:
              label:
                type: string
                description: Human-readable factor name
              direction:
                type: string
                enum: [risk, protective]
                description: Whether this factor increases or decreases crime risk
              strength:
                type: string
                enum: [strong, moderate, weak]
                description: Relative impact strength
        data:
          type: object
          required: [census_vintage, model_version]
          properties:
            census_vintage:
              type: string
              description: ACS census data year
              example: "2024"
            model_version:
              type: string
              description: Scoring model version
              example: "2.0"

    IncidentTrendingResponse:
      type: object
      required:
        - capturedAt
        - center
        - radius_m
        - hours
        - limit
        - count
        - scannedCells
        - incidents
      properties:
        capturedAt:
          type: string
          format: date-time
          description: Time this API response was generated
        center:
          type: object
          required: [lat, lng]
          properties:
            lat:
              type: number
              format: float
            lng:
              type: number
              format: float
        radius_m:
          type: integer
          description: Exact radius used for final distance filtering
        hours:
          type: integer
          description: Rolling recent-activity window
        limit:
          type: integer
          description: Maximum incidents requested
        count:
          type: integer
          description: Number of returned incidents
        scannedCells:
          type: integer
          description: Number of broad geohash cells queried before exact filtering
        incidents:
          type: array
          items:
            $ref: "#/components/schemas/RecentIncident"

    RecentIncident:
      type: object
      required:
        - id
        - label
        - latitude
        - longitude
        - reportedAt
        - distance_m
      properties:
        id:
          type: string
          description: Stable normalized incident identifier
        label:
          type: string
          description: Sanitized, generic incident label
        categories:
          type: array
          items:
            type: string
          description: Normalized incident categories
        latitude:
          type: number
          format: float
        longitude:
          type: number
          format: float
        cityCode:
          type: string
          description: Citizen market/city code when available
        reportedAt:
          type: string
          format: date-time
        severity:
          type: integer
          nullable: true
        level:
          type: integer
          nullable: true
        rank:
          type: integer
          nullable: true
        distance_m:
          type: integer
          description: Distance from the requested center point in meters

    ErrorResponse:
      type: object
      required: [error, message]
      properties:
        error:
          type: string
          description: Machine-readable error code
        message:
          type: string
          description: Human-readable error description
        geoid:
          type: string
          description: Block group ID (present in some error responses)
