openapi: 3.1.0
info:
  title: LocalPrice API
  version: 1.0.0-early-access
  license:
    name: Proprietary API specification
    identifier: LicenseRef-Proprietary
  description: >-
    Direct FX conversion or deterministic India regional-pricing
    recommendations,

    both with secure quote verification.

    LocalPrice does not process payments. Client backends must verify a quote
    and use

    the returned stored amount when creating checkout with a payment provider.

    All pricing endpoints require a server-side LocalPrice API key and are
    isolated

    by project and TEST/LIVE mode.

    Secret `lp_test_...` and `lp_live_...` keys are for trusted server
    environments

    only and must never be embedded in browser or mobile code. Every response
    includes

    `X-Request-Id`. Protected endpoints are limited per project and API mode.


    Early access is currently available to selected early users while we are
    validating the pricing engine. New organizations are PENDING; API key
    creation and use require operator approval. No subscription is required.
    Successful creates/verifications consume finite organization and project
    monthly quotas shared across TEST and LIVE. PostgreSQL commits durable
    idempotency with the quote and quota; Redis caching is optional for
    completed responses. FX quotes use Frankfurter's no-key v2 pair endpoint;
    `FX_API_BASE_URL` defaults to `https://api.frankfurter.dev`.
servers:
  - url: /
    description: Same origin as the API explorer; use your LocalPrice API origin.
tags:
  - name: System
    description: Public liveness and dependency-readiness endpoints.
  - name: Quotes
    description: Create, retrieve, and verify project-scoped regional price quotes.
externalDocs:
  description: LocalPrice developer guides
  url: /docs/developer/
security:
  - ApiKeyAuth: []
paths:
  /health:
    get:
      tags:
        - System
      security: []
      summary: Check API health
      operationId: getHealth
      responses:
        "200":
          description: API is running
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                properties:
                  status:
                    type: string
                    const: ok
  /ready:
    get:
      tags:
        - System
      security: []
      summary: Check whether critical dependencies can serve business traffic
      operationId: getReadiness
      responses:
        "200":
          description: PostgreSQL and Redis are available
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
        "503":
          description: At least one critical dependency is unavailable
  /v1/prices/quote:
    post:
      tags:
        - Quotes
      summary: Create an FX or localized price quote
      description: >
        Supply `Idempotency-Key` to make retries safe. The key is scoped to
        project,

        TEST/LIVE mode, and this endpoint. Reusing it with the same normalized
        request

        replays the original response; reusing it with a different request
        returns 409.

        Completed records are durable in PostgreSQL and do not expire automatically.
        The configured TTL applies only to the optional Redis response cache.
        Replays do not consume monthly quota. Use a new key for a new quote.

        `localized` (the default) uses the LocalPrice regional pricing engine.
        `fx` uses

        only the current FX rate and normal two-decimal INR rounding.
      operationId: createQuote
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/RequestId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateQuoteRequest"
            example:
              amount: 49
              currency: USD
              targetMarket: IN
              productType: saas
              pricingMode: localized
      responses:
        "201":
          description: Quote created
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
            Idempotency-Replayed:
              description: true when a stored idempotent response was replayed.
              schema:
                type: string
                enum:
                  - "true"
                  - "false"
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Quote"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/AuthenticationError"
        "403":
          $ref: "#/components/responses/AccessRequired"
        "409":
          description: Idempotency key conflict or an identical request is still processing
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "413":
          $ref: "#/components/responses/RequestTooLarge"
        "422":
          description: Seller bounds cannot be satisfied at INR precision, or the FX
            result rounds below INR 0.01
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          $ref: "#/components/responses/RateLimitExceeded"
        "502":
          description: Current FX data could not be obtained or validated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
  /v1/prices/quotes/{quoteId}:
    parameters:
      - $ref: "#/components/parameters/QuoteId"
    get:
      tags:
        - Quotes
      summary: Get the current state of a quote
      description: An ACTIVE stored quote is represented as EXPIRED when server time
        is later than expiresAt.
      operationId: getQuote
      parameters:
        - $ref: "#/components/parameters/RequestId"
      responses:
        "200":
          description: Current stored quote and pricing audit metadata
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/Quote"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/AuthenticationError"
        "403":
          $ref: "#/components/responses/AccessRequired"
        "404":
          $ref: "#/components/responses/QuoteNotFound"
        "429":
          $ref: "#/components/responses/RateLimitExceeded"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
  /v1/prices/quotes/{quoteId}/verify:
    parameters:
      - $ref: "#/components/parameters/QuoteId"
    post:
      tags:
        - Quotes
      summary: Verify a stored quote for checkout
      description: >
        Accepts only the public quote ID from the path. The checkout amount and
        currency

        are loaded from the stored quote. Any request-body amount is not trusted
        or used.
      operationId: verifyQuote
      parameters:
        - $ref: "#/components/parameters/RequestId"
      responses:
        "200":
          description: Quote is currently valid for checkout
          headers:
            X-Request-Id:
              $ref: "#/components/headers/RequestId"
            RateLimit-Limit:
              $ref: "#/components/headers/RateLimitLimit"
            RateLimit-Remaining:
              $ref: "#/components/headers/RateLimitRemaining"
            RateLimit-Reset:
              $ref: "#/components/headers/RateLimitReset"
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                properties:
                  data:
                    $ref: "#/components/schemas/VerifiedQuote"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/AuthenticationError"
        "403":
          $ref: "#/components/responses/AccessRequired"
        "404":
          $ref: "#/components/responses/QuoteNotFound"
        "409":
          description: Quote has been invalidated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "410":
          description: Quote has expired
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "422":
          description: Stored quote failed checkout integrity validation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "429":
          $ref: "#/components/responses/RateLimitExceeded"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
components:
  headers:
    RequestId:
      description: Correlation ID for logs and support.
      schema:
        type: string
    RateLimitLimit:
      description: Maximum requests in the current fixed window.
      schema:
        type: integer
    RateLimitRemaining:
      description: Requests remaining in the current window.
      schema:
        type: integer
    RateLimitReset:
      description: Unix timestamp when the current window resets.
      schema:
        type: integer
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: LocalPrice API key
      description: >
        Server-side secret key such as lp_test_... or lp_live_.... Store it in
        an

        environment variable or secret manager. Never commit it or expose it in
        frontend code.
  parameters:
    RequestId:
      name: X-Request-Id
      in: header
      required: false
      description: Optional caller correlation ID using 1–64 letters, digits, dots,
        underscores, or hyphens.
      schema:
        type: string
        maxLength: 64
        pattern: ^[A-Za-z0-9._-]+$
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: Client-generated retry key (1–128 safe ASCII characters). Store and
        reuse it for one logical quote creation.
      schema:
        type: string
        minLength: 1
        maxLength: 128
        pattern: ^[A-Za-z0-9._:-]+$
    QuoteId:
      name: quoteId
      in: path
      required: true
      description: Cryptographically random, non-sequential LocalPrice public quote ID.
      schema:
        type: string
        pattern: ^quote_[A-Za-z0-9_-]{24,32}$
        example: quote_kN0cRjTQY0wJ8A6wE5b5FDzQ
  responses:
    AuthenticationError:
      description: API key is missing, malformed, invalid, or revoked
      headers:
        X-Request-Id:
          $ref: "#/components/headers/RequestId"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    ValidationError:
      description: Request validation failed
      headers:
        X-Request-Id:
          $ref: "#/components/headers/RequestId"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    QuoteNotFound:
      description: No quote exists for the supplied public ID
      headers:
        X-Request-Id:
          $ref: "#/components/headers/RequestId"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    RequestTooLarge:
      description: JSON body exceeds the configured request-size limit
      headers:
        X-Request-Id:
          $ref: "#/components/headers/RequestId"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    ServiceUnavailable:
      description: A critical dependency or request guarantee is temporarily unavailable
      headers:
        X-Request-Id:
          $ref: "#/components/headers/RequestId"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    RateLimitExceeded:
      description: Short-term RATE_LIMIT_EXCEEDED, or QUOTA_EXCEEDED on business
        operations. Monthly quota details include scope, limit, used, remaining
        and resetsAt. Quota exhaustion does not use short-term Retry-After.
      headers:
        X-Request-Id:
          $ref: "#/components/headers/RequestId"
        RateLimit-Limit:
          schema:
            type: integer
        RateLimit-Remaining:
          schema:
            type: integer
        RateLimit-Reset:
          description: Unix timestamp when the current bucket resets.
          schema:
            type: integer
        Retry-After:
          description: Seconds until another request should be attempted.
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
    AccessRequired:
      description: "403 EARLY_ACCESS_REQUIRED: organization is not approved, or
        project is disabled."
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
  schemas:
    CreateQuoteRequest:
      type: object
      additionalProperties: false
      required:
        - amount
        - currency
        - targetMarket
      properties:
        amount:
          type: number
          exclusiveMinimum: 0
          maximum: 999999999999
          multipleOf: 0.0001
          description: Positive source amount with at most four decimal places.
          example: 49
        currency:
          type: string
          enum:
            - USD
            - EUR
            - JPY
            - KRW
            - AED
        targetMarket:
          type: string
          const: IN
        pricingMode:
          type: string
          enum:
            - localized
            - fx
          default: localized
          description: LocalPrice regional recommendation, or direct market-rate FX
            conversion.
        productType:
          type: string
          enum:
            - saas
            - ai_tool
            - developer_tool
            - creator_tool
            - online_course
            - gaming
          description: Required in localized mode unless the project has a default;
            optional metadata in FX mode.
        sellerConstraints:
          $ref: "#/components/schemas/SellerConstraints"
          description: Localized mode only. Supplying this in FX mode returns
            CONSTRAINT_NOT_SUPPORTED_FOR_FX_MODE.
    SellerConstraints:
      type: object
      additionalProperties: false
      properties:
        maximumRegionalDiscountPercent:
          type: number
          minimum: 0
          maximum: 100
        minimumRevenueRetentionPercent:
          type: number
          minimum: 0
          maximum: 100
          description: Minimum retained percentage of direct FX value; not accounting
            profit margin.
        minimumLocalizedPriceINR:
          type: number
          exclusiveMinimum: 0
        maximumLocalizedPriceINR:
          type: number
          exclusiveMinimum: 0
        sellerCostAmount:
          type: number
          exclusiveMinimum: 0
        sellerCostCurrency:
          type: string
          enum:
            - USD
            - EUR
            - JPY
            - KRW
            - AED
            - INR
        minimumGrossMarginPercent:
          type: number
          minimum: 0
          maximum: 95
    Quote:
      type: object
      required:
        - quoteId
        - status
        - mode
        - pricingMode
        - sourceAmount
        - sourceCurrency
        - localizedAmount
        - localizedCurrency
        - targetMarket
        - pricingModelVersion
        - recommendation
        - createdAt
        - expiresAt
      properties:
        quoteId:
          type: string
          example: quote_kN0cRjTQY0wJ8A6wE5b5FDzQ
        status:
          $ref: "#/components/schemas/QuoteStatus"
        mode:
          type: string
          enum:
            - TEST
            - LIVE
          description: Mode inherited from the API key that created the quote.
        pricingMode:
          type: string
          enum:
            - localized
            - fx
        sourceAmount:
          type: string
          example: "49.0000"
        sourceCurrency:
          type: string
          example: USD
        localizedAmount:
          type: string
          example: "1499.00"
        localizedCurrency:
          type: string
          const: INR
        targetMarket:
          type: string
          const: IN
        productType:
          type:
            - string
            - "null"
          example: saas
          description: Null when an FX quote did not include product metadata.
        pricingModelVersion:
          type: string
          example: v1
        fx:
          type: object
          additionalProperties: true
        adjustments:
          type: object
          additionalProperties: true
        constraints:
          type: object
          additionalProperties: true
        recommendation:
          type: object
          required:
            - amount
            - currency
            - range
          properties:
            amount:
              type: string
              example: "1499.00"
            currency:
              type: string
              const: INR
            range:
              type: object
              required:
                - min
                - max
              properties:
                min:
                  type:
                    - string
                    - "null"
                max:
                  type:
                    - string
                    - "null"
        rounding:
          type: object
          additionalProperties: true
        createdAt:
          type: string
          format: date-time
        expiresAt:
          type: string
          format: date-time
    VerifiedQuote:
      type: object
      additionalProperties: false
      required:
        - valid
        - quoteId
        - status
        - pricingMode
        - checkout
        - expiresAt
      properties:
        valid:
          type: boolean
          const: true
        quoteId:
          type: string
        status:
          type: string
          const: ACTIVE
        pricingMode:
          type: string
          enum:
            - localized
            - fx
        checkout:
          type: object
          additionalProperties: false
          required:
            - amount
            - currency
          properties:
            amount:
              type: string
              description: Exact stored INR amount the client backend should use for checkout.
              example: "1499.00"
            currency:
              type: string
              const: INR
        expiresAt:
          type: string
          format: date-time
    QuoteStatus:
      type: string
      enum:
        - ACTIVE
        - EXPIRED
        - INVALIDATED
    ErrorResponse:
      type: object
      required:
        - requestId
        - error
      properties:
        requestId:
          type: string
          description: Correlation ID also returned in the X-Request-Id response header.
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              examples:
                - API_KEY_MISSING
                - API_KEY_INVALID
                - API_KEY_REVOKED
                - RESOURCE_NOT_FOUND
                - QUOTE_EXPIRED
                - QUOTE_INVALIDATED
                - QUOTE_NOT_VALID_FOR_CHECKOUT
                - FX_PROVIDER_UNAVAILABLE
                - REDIS_UNAVAILABLE
                - IDEMPOTENCY_KEY_CONFLICT
                - IDEMPOTENCY_RECOVERY_REQUIRED
                - IDEMPOTENCY_REQUEST_IN_PROGRESS
                - RATE_LIMIT_EXCEEDED
                - DATABASE_UNAVAILABLE
                - REQUEST_TOO_LARGE
            message:
              type: string
            details:
              description: Optional safe structured context. Shape depends on the error code.
