openapi: "3.1.0"
info:
  title: Lahja API
  version: 0.1.0
  description: |
    Arabic dialect phrases recorded and written by native-speaking teachers. No AI-generated content.

    Billing is prepaid credits. Searching is free. Unlocking a phrase costs credits once per customer;
    after that the phrase is yours and fetching it again is free.

    To get content recorded, file prompt requests: things to say, in the dialects you want, under your
    own keys. Prompts are shared between customers, so if someone already asked for the same meaning
    (same addressee and register) your request joins theirs, and anything already recorded is ready
    to unlock.
servers:
  - url: https://api.lahjaapi.com/v1
    description: Production
security:
  - apiKey: []
paths:
  /health:
    get:
      operationId: getHealth
      security: []
      responses:
        "200":
          description: The service is up.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Health"
  /account:
    get:
      operationId: getAccount
      responses:
        "200":
          description: The calling customer's account and credit balance.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Account"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /account/ledger:
    get:
      operationId: listLedgerEntries
      parameters:
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
      responses:
        "200":
          description: Most recent credit movements first.
          content:
            application/json:
              schema:
                type: object
                required: [entries]
                properties:
                  entries:
                    type: array
                    items:
                      $ref: "#/components/schemas/LedgerEntry"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /phrases/{phraseId}/unlock:
    post:
      operationId: unlockPhrase
      description: |
        Charges the phrase's credit cost the first time this customer unlocks it. Calling it again
        for a phrase the customer already owns is free and returns `charged: false`.
      parameters:
        - name: phraseId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: The customer owns the phrase.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Unlock"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: Not enough credits.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: No published phrase with this ID.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /requests:
    put:
      operationId: submitPromptRequests
      description: |
        Creates or updates your prompt requests. Re-submitting a key replaces its meaning and dialects.
        Returns the submitted requests with what has been published for them so far.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [requests]
              properties:
                requests:
                  type: array
                  minItems: 1
                  maxItems: 500
                  items:
                    $ref: "#/components/schemas/PromptRequestInput"
      responses:
        "200":
          description: The submitted requests.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PromptRequestList"
        "400":
          description: A request is invalid; nothing was saved.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
    get:
      operationId: listPromptRequests
      responses:
        "200":
          description: All your prompt requests, by key, with published phrases for your requested dialects.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PromptRequestList"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /contributor/agreement:
    get:
      operationId: getContributorAgreement
      description: The current contributor agreement. Public, so the teacher app can show it before sign-up.
      security: []
      responses:
        "200":
          description: The agreement contributors must accept.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ContributorAgreement"
  /contributor/me:
    get:
      operationId: getContributor
      security:
        - firebase: []
      responses:
        "200":
          description: The signed-in contributor.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Contributor"
        "401":
          $ref: "#/components/responses/ContributorUnauthorized"
        "404":
          $ref: "#/components/responses/NotAContributor"
    put:
      operationId: saveContributorProfile
      description: |
        Joins Lahja, or edits the signed-in contributor's profile. A new contributor has no dialects:
        they ask for them with `POST /contributor/me/dialect-requests` and an admin approves them.
      security:
        - firebase: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ContributorProfile"
      responses:
        "200":
          description: The saved contributor.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Contributor"
        "400":
          description: A field is invalid; nothing was saved.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/ContributorUnauthorized"
  /contributor/me/agreement:
    post:
      operationId: acceptContributorAgreement
      description: Accepts the agreement version the app showed. Only the current version can be accepted.
      security:
        - firebase: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [version]
              properties:
                version:
                  type: string
      responses:
        "200":
          description: The contributor, with the acceptance recorded.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Contributor"
        "401":
          $ref: "#/components/responses/ContributorUnauthorized"
        "404":
          $ref: "#/components/responses/NotAContributor"
        "409":
          description: The version isn't the current agreement. Fetch the agreement again and show it.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /contributor/me/dialect-requests:
    post:
      operationId: requestContributorDialects
      description: Asks to contribute in more dialects. An admin approves or declines each one.
      security:
        - firebase: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [dialects]
              properties:
                dialects:
                  type: array
                  minItems: 1
                  items:
                    $ref: "#/components/schemas/Dialect"
      responses:
        "200":
          description: The contributor, with the dialects waiting for an admin.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Contributor"
        "401":
          $ref: "#/components/responses/ContributorUnauthorized"
        "404":
          $ref: "#/components/responses/NotAContributor"
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: "An API key such as `lahja_live_...`, sent as `Authorization: Bearer <key>`."
    firebase:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: "A contributor's Firebase ID token (ArabicCompanion Firebase Auth), sent as `Authorization: Bearer <token>`."
  responses:
    Unauthorized:
      description: Missing, unknown or revoked API key.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    ContributorUnauthorized:
      description: Missing, expired or invalid Firebase ID token.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotAContributor:
      description: This account hasn't joined Lahja yet. Join with `PUT /contributor/me`.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
  schemas:
    Health:
      type: object
      required: [status]
      properties:
        status:
          type: string
    Account:
      type: object
      required: [id, name, balance]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        balance:
          type: integer
          description: Credits available. Can be negative after a refund or chargeback, which blocks unlocking.
    LedgerEntry:
      type: object
      required: [id, delta, reason, createdAt]
      properties:
        id:
          type: integer
          format: int64
        delta:
          type: integer
        reason:
          type: string
          enum: [signup_grant, purchase, unlock, refund, chargeback, adjustment]
        phraseId:
          type: string
          format: uuid
        createdAt:
          type: string
          format: date-time
    Unlock:
      type: object
      required: [phraseId, charged, creditsCharged, balance]
      properties:
        phraseId:
          type: string
          format: uuid
        charged:
          type: boolean
          description: False when the customer already owned the phrase.
        creditsCharged:
          type: integer
        balance:
          type: integer
    Dialect:
      type: string
      enum: [fusha, egyptian, levantine, gulf, iraqi, maghrebi, sudanese]
    Addressee:
      type: string
      description: Who the phrase is said to. `any` when the form doesn't matter.
      enum: [masculine, feminine, plural, any]
    Register:
      type: string
      enum: [intimate, casual, polite, formal]
    PromptRequestInput:
      type: object
      required: [key, english, meaning, addressee, dialects]
      properties:
        key:
          type: string
          description: Your own identifier, e.g. `simon_says_verb.jump`. Unique within your account.
          maxLength: 200
        english:
          type: string
          description: Short label shown to teachers, e.g. "jump".
          maxLength: 100
        meaning:
          type: string
          description: |
            The full request as a sentence, e.g. "telling one boy to jump, as a game command". Together
            with addressee and register, this decides whether your request joins an existing prompt.
          maxLength: 500
        addressee:
          $ref: "#/components/schemas/Addressee"
        register:
          $ref: "#/components/schemas/Register"
        fushaText:
          type: string
          description: Optional Modern Standard Arabic form, to help teachers.
          maxLength: 200
        guidance:
          type: string
          description: Optional notes for teachers.
          maxLength: 1000
        dialects:
          type: array
          minItems: 1
          items:
            $ref: "#/components/schemas/Dialect"
    PromptRequest:
      type: object
      required: [key, promptId, english, meaning, addressee, dialects, published]
      properties:
        key:
          type: string
        promptId:
          type: string
          format: uuid
          description: The shared prompt. Customers asking for the same thing get the same ID.
        english:
          type: string
        meaning:
          type: string
          description: The prompt's meaning, as first requested.
        addressee:
          $ref: "#/components/schemas/Addressee"
        register:
          $ref: "#/components/schemas/Register"
        dialects:
          type: array
          items:
            $ref: "#/components/schemas/Dialect"
        published:
          type: array
          description: Published phrases for each requested dialect that has any. Unlock them to get content.
          items:
            type: object
            required: [dialect, phraseIds]
            properties:
              dialect:
                $ref: "#/components/schemas/Dialect"
              phraseIds:
                type: array
                items:
                  type: string
                  format: uuid
    PromptRequestList:
      type: object
      required: [requests]
      properties:
        requests:
          type: array
          items:
            $ref: "#/components/schemas/PromptRequest"
    Error:
      type: object
      required: [error, message]
      properties:
        error:
          type: string
        message:
          type: string
    Place:
      type: object
      required: [country, city]
      properties:
        country:
          type: string
          description: ISO 3166-1 alpha-2, e.g. EG.
        city:
          type: string
    ContributorAgreement:
      type: object
      required: [version, title, body]
      properties:
        version:
          type: string
        title:
          type: string
        body:
          type: string
          description: Markdown.
    ContributorProfile:
      type: object
      required: [displayName, gender, birthYear, origin, residence]
      properties:
        displayName:
          type: string
          description: Shown to reviewers and admins only, never to customers.
        gender:
          type: string
          enum: [female, male]
        birthYear:
          type: integer
          description: Customers see an age range, never the year.
        origin:
          $ref: "#/components/schemas/Place"
          description: Where they grew up speaking Arabic.
        residence:
          $ref: "#/components/schemas/Place"
          description: Where they live now.
    Contributor:
      type: object
      required: [id, profile, dialects, requestedDialects, isAdmin, agreement]
      properties:
        id:
          type: string
          format: uuid
        profile:
          $ref: "#/components/schemas/ContributorProfile"
        dialects:
          type: array
          description: Approved by an admin; the only dialects they may contribute and review.
          items:
            $ref: "#/components/schemas/Dialect"
        requestedDialects:
          type: array
          description: Waiting for an admin.
          items:
            $ref: "#/components/schemas/Dialect"
        isAdmin:
          type: boolean
        agreement:
          type: object
          required: [currentVersion, needsAcceptance]
          properties:
            currentVersion:
              type: string
            needsAcceptance:
              type: boolean
              description: True until they accept the current version, including after it changes.
            acceptedVersion:
              type: string
            acceptedAt:
              type: string
              format: date-time
