---
openapi: 3.0.3
info:
  title: Mobile Joe API
  description: Read app content, register devices, and toggle Feature Plan votes using
    an app API key.
  version: 1.0.0
  contact:
    name: Mobile Joe Support
servers:
- url: https://mbj-api.com/v1
  description: Production API
security:
- tokenAuth: []
tags:
- name: Feature Plans
  description: Read feature plans and toggle votes
- name: Alerts
  description: Retrieve in-app alerts and notifications
- name: FAQs
  description: Retrieve frequently asked questions
- name: Identity
  description: Device registration and user identification
paths:
  "/feature_plans":
    get:
      tags:
      - Feature Plans
      summary: List feature plans
      description: Lists published plans of every status, including completed and
        closed; drafts are excluded. No debug filter. Localized by Preferred-Languages
        or Language-Code with original-content fallback. Search matches all query words
        as prefixes in the original or any translated title/body. Pagination returns a JSON
        array, with counts and navigation in response headers. Page 1 is valid for
        an empty list.
      operationId: listFeaturePlans
      parameters:
      - "$ref": "#/components/parameters/IdentityAnonymousID"
      - "$ref": "#/components/parameters/IdentityExternalID"
      - "$ref": "#/components/parameters/PreferredLanguages"
      - "$ref": "#/components/parameters/LanguageCode"
      - name: search
        in: query
        description: Full-text search on original and translated title and body
          (case-insensitive, partial matches)
        schema:
          type: string
        example: export pdf
      - name: statuses[]
        in: query
        description: Repeat the key, for example statuses[]=open&statuses[]=planned.
          Case-sensitive OR filter; unknown values match nothing. Omit for all statuses.
        schema:
          type: array
          items:
            "$ref": "#/components/schemas/FeaturePlanStatus"
        style: form
        explode: true
        example:
        - open
        - planned
      - name: sort
        in: query
        description: Defaults to updated_desc for absent or unknown values. created
          sorts newest first; score aliases score_desc. trending orders by votes
          from the last 30 days, then by score, then newest first; initial votes
          don't count. Status sorts alphabetically. roadmap orders in_progress, planned,
          under_review, then all others, with newest updates first within groups;
          it does not filter statuses.
        schema:
          type: string
          enum:
          - created
          - score
          - trending
          - title_asc
          - title_desc
          - status_asc
          - status_desc
          - score_asc
          - score_desc
          - updated_asc
          - updated_desc
          - roadmap
          default: updated_desc
      - name: limit
        in: query
        description: 'Items per page; converted to an integer and clamped to 1–100.
          Omitted: 20; nonnumeric values: 1.'
        schema:
          type: integer
          minimum: 1
          maximum: 100
          default: 20
      - name: page
        in: query
        description: One-based page number. Omission defaults to 1. Nonnumeric,
          fractional, and non-positive values return 400; pages beyond the last
          available page return 404. Page 1 supports an empty result set.
        schema:
          type: integer
          minimum: 1
          default: 1
        example: 1
      responses:
        '200':
          description: Successful response
          headers:
            current-page:
              schema:
                type: integer
              description: Current page number
            page-items:
              schema:
                type: integer
              description: Configured items per page
            total-count:
              schema:
                type: integer
              description: Total matching plans
            total-pages:
              schema:
                type: integer
              description: Total pages
            link:
              schema:
                type: string
              description: Pagination links with rel="first", "prev", "next", and
                "last" as applicable.
          content:
            application/json:
              schema:
                type: array
                items:
                  "$ref": "#/components/schemas/FeaturePlan"
        '400':
          "$ref": "#/components/responses/BadRequest"
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '422':
          "$ref": "#/components/responses/UnprocessableEntity"
        '403':
          "$ref": "#/components/responses/Forbidden"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
  "/feature_plans/{id}":
    get:
      tags:
      - Feature Plans
      summary: Get a feature plan
      description: Returns one published plan with voted for the supplied identity.
        Drafts return 404; no debug filter. Localized by Preferred-Languages or
        Language-Code with original-content fallback.
      operationId: getFeaturePlan
      parameters:
      - "$ref": "#/components/parameters/IdentityAnonymousID"
      - "$ref": "#/components/parameters/IdentityExternalID"
      - "$ref": "#/components/parameters/PreferredLanguages"
      - "$ref": "#/components/parameters/LanguageCode"
      - name: id
        in: path
        required: true
        description: Feature plan ID
        schema:
          type: integer
        example: 1
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/FeaturePlan"
        '400':
          "$ref": "#/components/responses/BadRequest"
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '422':
          "$ref": "#/components/responses/UnprocessableEntity"
        '403':
          "$ref": "#/components/responses/Forbidden"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
  "/feature_plans/{id}/vote":
    post:
      tags:
      - Feature Plans
      summary: Toggle vote on a feature plan
      description: 'Toggles the supplied identity’s vote in any published plan; drafts
        return 404. No body
        is required. Not idempotent: retrying can undo a vote. After an ambiguous
        timeout, GET the plan and inspect voted before deciding whether to toggle
        again. The response includes the resulting voted state and current score,
        localized like Get a feature plan.'
      operationId: voteFeaturePlan
      parameters:
      - "$ref": "#/components/parameters/IdentityAnonymousID"
      - "$ref": "#/components/parameters/IdentityExternalID"
      - "$ref": "#/components/parameters/PreferredLanguages"
      - "$ref": "#/components/parameters/LanguageCode"
      - name: id
        in: path
        required: true
        description: Feature plan ID
        schema:
          type: integer
        example: 1
      responses:
        '200':
          description: Vote toggled successfully
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/FeaturePlan"
        '400':
          "$ref": "#/components/responses/BadRequest"
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '404':
          "$ref": "#/components/responses/NotFound"
        '422':
          "$ref": "#/components/responses/UnprocessableEntity"
        '403':
          "$ref": "#/components/responses/Forbidden"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
  "/alerts":
    get:
      tags:
      - Alerts
      summary: List alerts
      description: 'Returns an unpaginated array; no ordering is guaranteed. Normally
        returns live alerts for all users or matching the device audience. Filtered
        audiences require a recognized OS and configured filters for that platform.
        Each configured app/OS version list must contain the corresponding header
        exactly; missing versions fail configured lists. Alerts without an audience
        are not normally delivered. Debug-Mode: true (case-insensitive) additionally
        includes every non-archived debug-marked alert, including drafts, bypassing
        audience filters. Archived alerts are always excluded. Localized by
        Preferred-Languages or Language-Code with original-content fallback. No identity
        headers required.'
      operationId: listAlerts
      parameters:
      - "$ref": "#/components/parameters/OSName"
      - "$ref": "#/components/parameters/AppVersion"
      - "$ref": "#/components/parameters/OSVersion"
      - "$ref": "#/components/parameters/PreferredLanguages"
      - "$ref": "#/components/parameters/LanguageCode"
      - name: Debug-Mode
        in: header
        description: Enable debug mode to include draft alerts marked for debugging
        schema:
          type: string
          enum:
          - 'true'
          - 'false'
        example: 'false'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: array
                items:
                  "$ref": "#/components/schemas/Alert"
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/Forbidden"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
  "/faqs":
    get:
      tags:
      - FAQs
      summary: List FAQs
      description: Returns live FAQs sorted by position, with no pagination. Search
        matches all query words as prefixes against the original title/body, not translations.
        Localized by Preferred-Languages or Language-Code with original-content fallback.
        No identity headers required.
      operationId: listFaqs
      parameters:
      - "$ref": "#/components/parameters/PreferredLanguages"
      - "$ref": "#/components/parameters/LanguageCode"
      - name: search
        in: query
        description: Full-text search on title and body
        schema:
          type: string
        example: password reset
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: array
                items:
                  "$ref": "#/components/schemas/Faq"
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/Forbidden"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
  "/identify":
    post:
      tags:
      - Identity
      summary: Register or update device
      description: Creates or updates a device for the resolved app user. Send a nonblank
        Identity-Device-ID header OR device_id in JSON or query parameters; the header
        takes precedence. Device metadata comes only from headers; omitted metadata
        is cleared. Identity may be created/merged before missing device data returns
        400. Successful response has no body.
      operationId: identifyDevice
      parameters:
      - "$ref": "#/components/parameters/IdentityAnonymousID"
      - "$ref": "#/components/parameters/IdentityExternalID"
      - name: Identity-Device-ID
        in: header
        required: false
        description: Required unless device_id is supplied in JSON or query. Nonblank
          device identifier, persisted across requests. Takes precedence over device_id.
        schema:
          type: string
        example: "$MBJDeviceID:cccccccccccccccccccccccccccccccc"
      - "$ref": "#/components/parameters/AppVersion"
      - name: App-Build-Version
        in: header
        description: Application build number
        schema:
          type: string
        example: '8796845'
      - "$ref": "#/components/parameters/OSName"
      - "$ref": "#/components/parameters/OSVersion"
      - "$ref": "#/components/parameters/PreferredLanguages"
      - "$ref": "#/components/parameters/LanguageCode"
      - name: Device-Model
        in: header
        description: Device model name
        schema:
          type: string
        example: iPhone 15 Pro
      - name: Framework-Version
        in: header
        description: Mobile Joe SDK version
        schema:
          type: string
        example: 2.1.0
      - name: device_id
        in: query
        description: Alternative to Identity-Device-ID or the JSON property; provide
          at least one device ID.
        schema:
          type: string
      requestBody:
        description: Optional alternative device ID. Other device metadata is read
          only from headers.
        content:
          application/json:
            schema:
              type: object
              properties:
                device_id:
                  type: string
                  description: Device ID (alternative to header)
            example:
              device_id: "$MBJDeviceID:cccccccccccccccccccccccccccccccc"
      responses:
        '204':
          description: Device registered/updated successfully
        '400':
          "$ref": "#/components/responses/BadRequest"
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '422':
          "$ref": "#/components/responses/UnprocessableEntity"
        '403':
          "$ref": "#/components/responses/Forbidden"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
  "/faqs/{id}":
    get:
      tags:
      - FAQs
      summary: Get an FAQ
      description: Returns one live FAQ. Draft, archived, missing, or other-app IDs
        return 404. Localized by Preferred-Languages or Language-Code with original-content
        fallback.
      operationId: getFaq
      parameters:
      - "$ref": "#/components/parameters/PreferredLanguages"
      - "$ref": "#/components/parameters/LanguageCode"
      - name: id
        in: path
        required: true
        description: Published FAQ ID from List FAQs.
        schema:
          type: integer
        example: 1
      responses:
        '200':
          description: Published FAQ
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Faq"
        '404':
          "$ref": "#/components/responses/NotFound"
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/Forbidden"
        '429':
          "$ref": "#/components/responses/TooManyRequests"
components:
  securitySchemes:
    tokenAuth:
      type: apiKey
      in: header
      name: Authorization
      description: 'App API key. Send Authorization: Token token=YOUR_API_KEY (or
        Bearer YOUR_API_KEY). Requires API access for the owning account.'
  parameters:
    IdentityAnonymousID:
      name: Identity-Anonymous-ID
      in: header
      required: true
      description: Required persisted anonymous transport identity, including when
        an external identity is present. See the introduction for generation and merging.
      schema:
        type: string
        pattern: "^\\$MBJAnonymousID:[a-z0-9]{32}$"
      example: "$MBJAnonymousID:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
    IdentityExternalID:
      name: Identity-External-ID
      in: header
      required: false
      description: Optional external transport identity. Use the prefixed truncated
        SHA-256 value, not a raw SDK-facing user ID. See the introduction.
      schema:
        type: string
        pattern: "^\\$MBJExternalID:[a-z0-9]{32}$"
      example: "$MBJExternalID:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
    OSName:
      name: OS-Name
      in: header
      description: Operating system. Alert filtering recognizes ios and android case-insensitively;
        unknown or absent values do not match filtered audiences.
      schema:
        type: string
      example: ios
    AppVersion:
      name: App-Version
      in: header
      description: App version string. Alert audience filters use exact string membership,
        not semantic version ranges.
      schema:
        type: string
      example: 1.5.0
    OSVersion:
      name: OS-Version
      in: header
      description: OS version string. Alert audience filters use exact string membership,
        not semantic version ranges.
      schema:
        type: string
      example: '17.5'
    PreferredLanguages:
      name: Preferred-Languages
      in: header
      description: Comma-separated BCP 47 language tags in the user's priority order.
        Takes precedence over Language-Code. Empty, invalid, and duplicate entries are
        ignored; at most five valid entries are used. FAQs, alerts and feature plans resolve each entry
        like Language-Code before trying the next one; if no entry matches, they return
        the original content. An entry that matches the app's source language returns
        the original content. Identify stores the first entry as device metadata.
      schema:
        type: string
      example: de-DE,en-GB
    LanguageCode:
      name: Language-Code
      in: header
      description: BCP 47 language tag, used when Preferred-Languages has no valid entry.
        FAQs, alerts and feature plans use the most specific localization (exact tag, language and script,
        language and region, then language). Values matching the app's source language,
        absent values, and unmatched values return the original content. No Accept-Language
        negotiation. Identify stores the normalized tag as device metadata.
      schema:
        type: string
      example: en
  schemas:
    FeaturePlan:
      type: object
      properties:
        id:
          type: integer
          description: Unique identifier
          example: 1
        title:
          type: string
          description: Feature plan title (localized for the requested languages)
          example: PDF Export
        body:
          type: string
          description: Detailed description of the feature plan (localized for the requested languages)
          example: Allow exporting data to PDF format for offline viewing
        score:
          type: integer
          description: Total score, including initial votes. The toggle response
            reflects the score after the vote change.
          example: 42
        status:
          "$ref": "#/components/schemas/FeaturePlanStatus"
        voted:
          type: boolean
          description: Whether the current user has voted for this plan
          example: false
        created_at:
          type: string
          format: date-time
          description: Creation timestamp
          example: '2024-01-08T12:00:00Z'
        updated_at:
          type: string
          format: date-time
          description: Last update timestamp
          example: '2024-01-08T12:00:00Z'
      required:
      - id
      - title
      - body
      - score
      - status
      - voted
      - created_at
      - updated_at
    FeaturePlanStatus:
      type: string
      description: Current status of the feature plan
      enum:
      - open
      - under_review
      - planned
      - in_progress
      - completed
      - closed
      example: open
    Alert:
      type: object
      properties:
        id:
          type: integer
          description: Unique identifier
          example: 1
        title:
          type: string
          description: Alert title (localized for the requested languages)
          example: Service Outage
        message:
          type: string
          description: Alert message body (localized for the requested languages)
          example: Some users may experience login issues
          nullable: true
        status:
          "$ref": "#/components/schemas/AlertStatus"
        kind:
          "$ref": "#/components/schemas/AlertKind"
        occurred_at:
          type: string
          format: date-time
          description: Timestamp when the alert event occurred
          example: '2024-01-08T00:00:00Z'
          nullable: true
        created_at:
          type: string
          format: date-time
          description: Creation timestamp
          example: '2024-01-08T12:00:00Z'
        updated_at:
          type: string
          format: date-time
          description: Last update timestamp
          example: '2024-01-08T12:00:00Z'
      required:
      - id
      - title
      - message
      - status
      - kind
      - created_at
      - updated_at
      - occurred_at
    AlertStatus:
      type: string
      description: Alert publication status
      enum:
      - draft
      - live
      - archived
      example: live
    AlertKind:
      type: string
      description: Type/severity of the alert
      enum:
      - error
      - warning
      example: error
    Faq:
      type: object
      properties:
        id:
          type: integer
          description: Unique identifier
          example: 1
        title:
          type: string
          description: FAQ question/title (localized for the requested languages)
          example: How do I reset my password?
        body:
          type: string
          description: FAQ answer/content (localized for the requested languages)
          example: Click the 'Forgot Password' link on the login page...
        position:
          type: integer
          description: Display order position
          example: 1
          nullable: true
        created_at:
          type: string
          format: date-time
          description: Creation timestamp
          example: '2024-01-08T12:00:00Z'
        updated_at:
          type: string
          format: date-time
          description: Last update timestamp
          example: '2024-01-08T12:00:00Z'
      required:
      - id
      - title
      - body
      - position
      - created_at
      - updated_at
    Error:
      type: object
      properties:
        error:
          type: string
          description: Error message
          example: Unauthorized
      required:
      - error
  responses:
    BadRequest:
      description: Missing or malformed request information
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/Error"
          example:
            error: Invalid request
    Unauthorized:
      description: Invalid or missing API token
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/Error"
          example:
            error: Unauthorized
    NotFound:
      description: Resource or page not found
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/Error"
          example:
            error: Resource not found
    UnprocessableEntity:
      description: Invalid request data or headers
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/Error"
          example:
            error: Invalid request
    Forbidden:
      description: The app owner does not currently have API access.
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/Error"
          example:
            error: Forbidden
    TooManyRequests:
      description: Request limit exceeded. Back off before retrying; no Retry-After
        header is promised.
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/Error"
          example:
            error: Rate limit exceeded
x-introduction:
- title: First request
  id: first-request
  text: 'Copy your app''s API key from App Settings in the dashboard and replace YOUR_API_KEY
    below. The base URL is https://mbj-api.com/v1, not the dashboard host. An empty
    array [] means the request succeeded and no FAQs are published.'
- title: Authentication
  id: authentication
  text: 'Every operation requires Authorization: Token token=YOUR_API_KEY; Bearer
    YOUR_API_KEY is also accepted. A key selects one app, so IDs of another app return
    404. Missing or invalid keys return 401; an account without a verified email or
    an active trial or subscription returns 403.'
- title: Identity without an SDK
  id: identity
  text: 'Feature Plan operations and device registration require Identity-Anonymous-ID:
    $MBJAnonymousID: followed by a random lowercase UUID without hyphens, persisted
    per installation. For a signed-in user, also send Identity-External-ID: $MBJExternalID:
    followed by the first 32 lowercase hexadecimal characters of SHA-256 over the
    UTF-8 bytes of your user ID; the server does not hash raw user IDs. The first
    request that pairs both identities links them and moves anonymous votes and devices
    to an existing account. Generate new anonymous and device identifiers on sign-out
    and before switching accounts, as the SDKs do. FAQs and Alerts do not use identity
    headers.'
- title: Device registration
  id: devices
  text: 'Persist a device identifier, $MBJDeviceID: followed by a lowercase UUID without
    hyphens, and send it to Register or update device. Repeating the registration
    updates the same device; omitted metadata headers clear their stored values.'
- title: Retries and limits
  id: retries
  text: 'Voting toggles: repeating a successful POST removes the vote, so after a timeout
    fetch the plan and inspect voted before retrying. Limits are 300 requests per
    minute, plus 30 per minute each for identify and voting, per API key and client
    IP. Back off after a 429; no Retry-After header is promised. Do not share identity-dependent
    responses between users.'
