openapi: 3.1.0
info:
  title: Upfort Home Partners API
  version: 1.0.0
  description: |
    The Upfort Home Partners API enables approved partners to create, terminate,
    and check the current status of Upfort Home consumer subscriptions.

    This document is a partner-review contract for the Home subscription lifecycle.
    It is intentionally limited to the endpoints needed for partner integrations and
    may change before implementation.

    ## Getting started

    1. Obtain your API key from your Upfort account manager.
    2. Include the key in every request via the `X-API-Key` header.
    3. Send and receive JSON (`application/json`).
    4. Use snake_case for all request and response fields.

    ## Partner configuration

    API keys identify the partner and the partner-specific configuration Upfort has
    set up internally. Partners do not send billing configuration, retail price, or
    internal subscription template IDs in API requests.

    ## Permanent IDs

    `permanent_id` is the partner's stable identifier for the subscriber, policy, or
    membership record. It is required because `email` may be unavailable and must not
    be used as the subscription lifecycle key.

    The same `permanent_id` must be used for create, terminate, and status requests
    for the same subscription. Upfort scopes `permanent_id` to the authenticated
    partner from the API key.

    ## Group IDs

    `group_id` is an optional partner-defined subgroup identifier. Partners can use
    it to represent their own account groups, books of business, agencies, employer
    groups, properties, or other internal segments. Upfort stores it for attribution,
    reporting, partner operations, and future group-level management.

    ## SKU codes

    `sku_code` is the partner-facing product code for the subscription. It can be a
    partner-specific SKU alias or a canonical Home SKU if Upfort has configured that
    code for the partner. Upfort maps `sku_code` to its internal subscription
    architecture. Partners do not send price, and price is not returned by the API.

    ## Base URL

    | Environment | URL |
    |-------------|-----|
    | Production | `https://api.consumer.upfort.com/api` |
    | Staging | `https://api.consumer.staging.upfort.com/api` |

    ## Error format

    Errors follow a consistent shape:

    ```json
    {
      "error": "Human-readable message",
      "code": "MACHINE_CODE",
      "details": []
    }
    ```
servers:
  - url: https://api.consumer.upfort.com/api
    description: Production
  - url: https://api.consumer.staging.upfort.com/api
    description: Staging

security:
  - apiKey: []

tags:
  - name: Subscriptions
    description: Partner-facing Home subscription lifecycle endpoints.

paths:
  /partner/subscriptions:
    post:
      operationId: createSubscription
      summary: Create subscription
      description: |
        Creates a new Upfort Home subscription for the authenticated partner.

        This endpoint is keyed by `permanent_id`, not email. If `email` is present,
        Upfort may use it for communications or account linking, but it is not
        required and is not the deduplication key.

        `sku_code` is resolved through Upfort's partner configuration and maps to
        the appropriate internal subscription template or product configuration.
      tags:
        - Subscriptions
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateSubscriptionRequest"
            examples:
              createSubscription:
                summary: Create subscription
                value:
                  permanent_id: MSI-POL-100001
                  group_id: MSI-AGENCY-42
                  sku_code: PARTNER_CONFIGURED_SKU
                  first_name: Jane
                  last_name: Doe
                  email: jane@example.com
                  zip_code: "90210"
                  start_date: "2025-08-01"
                  auto_renew: true
                  address_line_1: "123 Main St"
                  unit: "4B"
                  city: Los Angeles
                  state: CA
                  country: US
              createWithoutEmail:
                summary: Create subscription without email
                value:
                  permanent_id: MSI-POL-100002
                  group_id: MSI-AGENCY-42
                  sku_code: PARTNER_CONFIGURED_SKU
                  first_name: David
                  last_name: Park
                  zip_code: "60614"
                  start_date: "2025-08-15"
                  phone: "+14155550123"
      responses:
        "201":
          description: Subscription created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SubscriptionResponseEnvelope"
              example:
                success: true
                data:
                  subscription_id: "b47ac10b-58cc-4372-a567-0e02b2c3d479"
                  permanent_id: MSI-POL-100001
                  group_id: MSI-AGENCY-42
                  sku_code: PARTNER_CONFIGURED_SKU
                  status: active
                  auto_renew: true
                  current_period_start: "2025-08-01"
                  current_period_end: null
                  created_at: "2026-05-01T16:00:00.000Z"
                timestamp: "2026-05-01T16:00:00.000Z"
        "400":
          description: Validation error, invalid date, missing zip code, or unknown SKU.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              examples:
                validationFailed:
                  summary: Validation failed
                  value:
                    error: Validation failed
                    code: VALIDATION_FAILED
                    details:
                      - field: permanent_id
                        message: permanent_id is required
                unknownSku:
                  summary: Unknown SKU
                  value:
                    error: sku_code is not configured for this partner
                    code: UNKNOWN_SKU
                missingZipCode:
                  summary: Home+ zip code required
                  value:
                    error: zip_code is required for this SKU
                    code: VALIDATION_FAILED
                    details:
                      - field: zip_code
                        message: Required for Home+ subscriptions
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Invalid or inactive API key
                code: UNAUTHORIZED
        "409":
          description: A conflicting subscription already exists for this permanent ID.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Active subscription already exists for this permanent_id
                code: DUPLICATE_CONFLICT

  /partner/subscriptions/{permanent_id}/terminate:
    post:
      operationId: terminateSubscription
      summary: Terminate subscription
      description: |
        Terminates the current subscription for the authenticated partner and
        `permanent_id`.

        If `termination_date` is omitted, Upfort uses the request processing date.
        Future-dated termination behavior is subject to final implementation design.
      tags:
        - Subscriptions
      parameters:
        - name: permanent_id
          in: path
          required: true
          description: Partner's stable identifier for the subscriber, policy, or membership record.
          schema:
            type: string
            maxLength: 255
          example: MSI-POL-100001
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TerminateSubscriptionRequest"
            examples:
              immediate:
                summary: Immediate termination
                value:
                  reason: Policy cancelled
              withDate:
                summary: Termination with effective date
                value:
                  termination_date: "2025-10-31"
                  reason: Partner requested non-renewal
      responses:
        "200":
          description: Subscription terminated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SubscriptionResponseEnvelope"
              example:
                success: true
                data:
                  subscription_id: "b47ac10b-58cc-4372-a567-0e02b2c3d479"
                  permanent_id: MSI-POL-100001
                  group_id: MSI-AGENCY-42
                  sku_code: PARTNER_CONFIGURED_SKU
                  status: cancelled
                  auto_renew: false
                  current_period_start: "2025-08-01"
                  current_period_end: "2025-10-31"
                  terminated_at: "2025-10-31T00:00:00.000Z"
                timestamp: "2026-05-01T16:00:00.000Z"
        "400":
          description: Validation error or invalid termination date.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: termination_date must use YYYY-MM-DD format
                code: INVALID_DATE_FORMAT
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: No current subscription found for this partner and permanent ID.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Subscription not found
                code: SUBSCRIPTION_NOT_FOUND
        "409":
          description: Subscription is already terminated or otherwise cannot be terminated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Subscription is already cancelled
                code: DUPLICATE_CONFLICT

  /partner/subscriptions/{permanent_id}/status:
    get:
      operationId: getSubscriptionStatus
      summary: Get current status for subscription
      description: |
        Returns the current subscription status for the authenticated partner and
        `permanent_id`.
      tags:
        - Subscriptions
      parameters:
        - name: permanent_id
          in: path
          required: true
          description: Partner's stable identifier for the subscriber, policy, or membership record.
          schema:
            type: string
            maxLength: 255
          example: MSI-POL-100001
      responses:
        "200":
          description: Current subscription status found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SubscriptionResponseEnvelope"
              example:
                success: true
                data:
                  subscription_id: "b47ac10b-58cc-4372-a567-0e02b2c3d479"
                  permanent_id: MSI-POL-100001
                  group_id: MSI-AGENCY-42
                  sku_code: PARTNER_CONFIGURED_SKU
                  status: active
                  auto_renew: true
                  current_period_start: "2025-08-01"
                  current_period_end: null
                  created_at: "2026-05-01T16:00:00.000Z"
                timestamp: "2026-05-01T16:00:00.000Z"
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: No current subscription found for this partner and permanent ID.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: Subscription not found
                code: SUBSCRIPTION_NOT_FOUND

components:
  securitySchemes:
    apiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: Partner API key provided by Upfort.

  schemas:
    ErrorResponse:
      type: object
      additionalProperties: false
      required:
        - error
        - code
      properties:
        error:
          type: string
          description: Human-readable error message.
        code:
          type: string
          description: Machine-readable error code.
        details:
          type: array
          items:
            type: object
          description: Validation error details when applicable.

    CreateSubscriptionRequest:
      type: object
      additionalProperties: false
      required:
        - permanent_id
        - sku_code
        - first_name
        - last_name
        - start_date
      properties:
        permanent_id:
          type: string
          maxLength: 255
          description: Partner's stable identifier for the subscriber, policy, or membership record.
        group_id:
          type: string
          maxLength: 255
          description: Partner-defined subgroup identifier for attribution, reporting, and partner-side account grouping.
        sku_code:
          type: string
          maxLength: 100
          description: Partner SKU alias or configured canonical Home SKU.
        auto_renew:
          type: boolean
          description: Optional partner preference for whether the subscription should renew automatically. Final behavior may depend on partner configuration.
        first_name:
          type: string
          maxLength: 100
          description: Subscriber's first name.
        last_name:
          type: string
          maxLength: 100
          description: Subscriber's last name.
        email:
          type: string
          format: email
          maxLength: 255
          description: Subscriber's email address, when available. Not required and not used as the lifecycle key.
        zip_code:
          type: string
          pattern: '^\d{5}$'
          description: 5-digit US zip code.
        start_date:
          type: string
          format: date
          description: Subscription effective start date in YYYY-MM-DD format.
        phone:
          type: string
          maxLength: 32
          description: Subscriber's phone number, when available.
        address_line_1:
          type: string
          maxLength: 255
          description: Subscriber street address, when available.
        address_line_2:
          type: string
          maxLength: 255
          description: Additional address details, when available.
        unit:
          type: string
          maxLength: 100
          description: Apartment, unit, suite, or similar address designator.
        city:
          type: string
          maxLength: 100
          description: Subscriber city, when available.
        state:
          type: string
          minLength: 2
          maxLength: 2
          description: Two-letter US state code, when available.
        country:
          type: string
          minLength: 2
          maxLength: 2
          default: US
          description: ISO 3166-1 alpha-2 country code. Defaults to US when omitted.

    TerminateSubscriptionRequest:
      type: object
      additionalProperties: false
      properties:
        termination_date:
          type: string
          format: date
          description: Termination effective date in YYYY-MM-DD format. Defaults to the processing date when omitted.
        reason:
          type: string
          maxLength: 500
          description: Optional freeform termination reason stored for audit.

    SubscriptionStatus:
      type: string
      enum:
        - active
        - cancelled
        - expired
        - paused
      description: Current subscription status.

    SubscriptionResponse:
      type: object
      additionalProperties: false
      required:
        - subscription_id
        - permanent_id
        - sku_code
        - status
        - current_period_start
      properties:
        subscription_id:
          type: string
          description: Upfort subscription identifier.
        permanent_id:
          type: string
          description: Partner's stable identifier for the subscriber, policy, or membership record.
        group_id:
          type: string
          nullable: true
          description: Partner-defined subgroup identifier, when provided.
        sku_code:
          type: string
          description: Resolved SKU code for the current subscription.
        status:
          $ref: "#/components/schemas/SubscriptionStatus"
        auto_renew:
          type: boolean
          nullable: true
          description: Whether the subscription is configured to renew automatically, when known.
        current_period_start:
          type: string
          format: date
          description: Current subscription period start date.
        current_period_end:
          type: string
          format: date
          nullable: true
          description: Current subscription period end date, when applicable.
        created_at:
          type: string
          format: date-time
          description: Timestamp when Upfort created the subscription record.
        terminated_at:
          type: string
          format: date-time
          nullable: true
          description: Timestamp when Upfort terminated the subscription, when applicable.

    SubscriptionResponseEnvelope:
      type: object
      additionalProperties: false
      required:
        - success
        - data
        - timestamp
      properties:
        success:
          type: boolean
          enum:
            - true
        data:
          $ref: "#/components/schemas/SubscriptionResponse"
        timestamp:
          type: string
          format: date-time
