openapi: 3.1.0
info:
  title: Member Center API
  version: 0.1.0
  description: OAuth2/OIDC + Newsletter Subscription API
servers:
  - url: /
security:
  - BearerAuth: []

paths:
  /oauth/authorize:
    get:
      summary: OAuth2 Authorization Endpoint
      description: Authorization Code + PKCE flow
      security: []
      parameters:
        - in: query
          name: client_id
          required: true
          schema: { type: string }
        - in: query
          name: redirect_uri
          required: true
          schema: { type: string }
        - in: query
          name: response_type
          required: true
          schema: { type: string, enum: [code] }
        - in: query
          name: scope
          required: true
          schema: { type: string }
        - in: query
          name: code_challenge
          required: true
          schema: { type: string }
        - in: query
          name: code_challenge_method
          required: true
          schema: { type: string, enum: [S256] }
        - in: query
          name: state
          required: false
          schema: { type: string }
      responses:
        '302':
          description: Redirect to client with code

  /oauth/token:
    post:
      summary: OAuth2 Token Endpoint
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              oneOf:
                - $ref: '#/components/schemas/AuthorizationCodeTokenRequest'
                - $ref: '#/components/schemas/RefreshTokenRequest'
                - $ref: '#/components/schemas/ClientCredentialsTokenRequest'
      responses:
        '200':
          description: Token response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'

  /.well-known/openid-configuration:
    get:
      summary: OIDC Discovery
      responses:
        '200':
          description: OIDC discovery document

  /.well-known/jwks:
    get:
      summary: JWKS
      responses:
        '200':
          description: JSON Web Key Set

  /auth/register:
    post:
      summary: Register user
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterRequest'
      responses:
        '200':
          description: Registered
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserProfile'

  /auth/login:
    post:
      summary: API login
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PasswordTokenRequest'
      responses:
        '200':
          description: Token response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'

  /auth/refresh:
    post:
      summary: Refresh token
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/RefreshTokenRequest'
      responses:
        '200':
          description: Token response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'

  /auth/logout:
    post:
      summary: Logout (revoke refresh token)
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                refresh_token:
                  type: string
      responses:
        '204':
          description: Logged out

  /auth/password/forgot:
    post:
      summary: Request password reset
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ForgotPasswordRequest'
      responses:
        '204':
          description: Email sent

  /auth/password/reset:
    post:
      summary: Reset password
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ResetPasswordRequest'
      responses:
        '204':
          description: Password reset

  /auth/email/verify:
    get:
      summary: Verify email
      security: []
      parameters:
        - in: query
          name: token
          required: true
          schema: { type: string }
        - in: query
          name: email
          required: true
          schema: { type: string, format: email }
      responses:
        '200':
          description: Email verified

  /user/profile:
    get:
      summary: Get current user profile
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Profile
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserProfile'

  /newsletter/subscribe:
    post:
      summary: Subscribe (unauthenticated allowed)
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SubscribeRequest'
      responses:
        '200':
          description: Pending subscription
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PendingSubscriptionResponse'
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /newsletter/confirm:
    get:
      summary: Confirm subscription (double opt-in)
      security: []
      parameters:
        - in: query
          name: token
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Active subscription
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Subscription'
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /newsletter/unsubscribe:
    post:
      summary: Unsubscribe single list
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token]
              properties:
                token: { type: string }
      responses:
        '200':
          description: Unsubscribed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Subscription'
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /newsletter/unsubscribe-token:
    post:
      summary: Issue unsubscribe token
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [list_id, email]
              properties:
                list_id: { type: string }
                email: { type: string, format: email }
      responses:
        '200':
          description: Unsubscribe token issued
          content:
            application/json:
              schema:
                type: object
                properties:
                  unsubscribe_token: { type: string }
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /newsletter/one-click-unsubscribe-token:
    post:
      summary: Issue one-click unsubscribe token (Send Engine pre-send)
      security: [{ BearerAuth: [] }]
      description: Requires scope `newsletter:events.write` or `newsletter:events.write.global`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IssueOneClickUnsubscribeTokenRequest'
      responses:
        '200':
          description: Unsubscribe token issued
          content:
            application/json:
              schema:
                type: object
                properties:
                  unsubscribe_token: { type: string }
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /newsletter/one-click-unsubscribe-tokens:
    post:
      summary: Issue one-click unsubscribe tokens in batch (Send Engine pre-send)
      security: [{ BearerAuth: [] }]
      description: Requires scope `newsletter:events.write` or `newsletter:events.write.global`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IssueOneClickUnsubscribeTokensRequest'
      responses:
        '200':
          description: Unsubscribe tokens issued
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/OneClickUnsubscribeTokenItem'
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden

  /newsletter/preferences:
    get:
      summary: Get preferences
      parameters:
        - in: query
          name: list_id
          required: true
          schema: { type: string }
        - in: query
          name: email
          required: true
          schema: { type: string, format: email }
      responses:
        '200':
          description: Preferences
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Subscription'
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

    post:
      summary: Update preferences
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [list_id, email, preferences]
              properties:
                list_id: { type: string }
                email: { type: string, format: email }
                preferences: { type: object }
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Subscription'
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /newsletter/subscriptions:
    get:
      summary: List subscriptions by list
      security: [{ BearerAuth: [] }]
      description: Requires scope `newsletter:list.read`.
      parameters:
        - in: query
          name: list_id
          required: true
          schema: { type: string }
      responses:
        '200':
          description: List
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Subscription'
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /subscriptions/disable:
    post:
      summary: Disable subscription / blacklist by reason
      security: [{ BearerAuth: [] }]
      description: |
        Requires scope `newsletter:events.write` (tenant client) or `newsletter:events.write.global` (platform client).
        Request must include `tenant_id`, `subscriber_id`, and `list_id` for tenant-boundary validation.
        Reason handling:
        - `hard_bounce`, `soft_bounce_threshold`, `suppression`: unsubscribe all subscriptions for the email across tenants + add to global blacklist.
        - `complaint`: unsubscribe only the target subscription, no blacklist.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DisableSubscriptionRequest'
      responses:
        '200':
          description: Disabled
        '400':
          description: Bad request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /integrations/send-engine/webhook-clients/upsert:
    post:
      summary: Upsert Send Engine webhook client mapping for tenant
      security: [{ BearerAuth: [] }]
      description: Requires scope `newsletter:events.write.global`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpsertSendEngineWebhookClientRequest'
      responses:
        '200':
          description: Updated
        '400':
          description: Bad request
        '401':
          description: Unauthorized
        '403':
          description: Forbidden
        '404':
          description: Tenant not found

  /webhooks/subscriptions:
    post:
      summary: (NOTE) Member Center -> Send Engine subscription events webhook
      description: This endpoint is implemented by Send Engine; listed here as an integration note. Required headers are X-Signature, X-Timestamp, X-Nonce, X-Client-Id. X-Client-Id must be Send Engine `auth_clients.id` (UUID), tenant-bound, no fallback client id.
      security: []
      parameters:
        - in: header
          name: X-Signature
          required: true
          schema: { type: string }
        - in: header
          name: X-Timestamp
          required: true
          schema: { type: string }
        - in: header
          name: X-Nonce
          required: true
          schema: { type: string, format: uuid }
        - in: header
          name: X-Client-Id
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendEngineSubscriptionEvent'
      responses:
        '200':
          description: Accepted

  /webhooks/lists/full-sync:
    post:
      summary: (NOTE) Member Center -> Send Engine full list sync webhook
      description: This endpoint is implemented by Send Engine; listed here as an integration note. Required headers are X-Signature, X-Timestamp, X-Nonce, X-Client-Id. X-Client-Id must be Send Engine `auth_clients.id` (UUID), tenant-bound, no fallback client id.
      security: []
      parameters:
        - in: header
          name: X-Signature
          required: true
          schema: { type: string }
        - in: header
          name: X-Timestamp
          required: true
          schema: { type: string }
        - in: header
          name: X-Nonce
          required: true
          schema: { type: string, format: uuid }
        - in: header
          name: X-Client-Id
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendEngineListFullSync'
      responses:
        '200':
          description: Accepted

  /admin/tenants:
    get:
      summary: List tenants
      security: [{ BearerAuth: [] }]
      responses:
        '200':
          description: List
    post:
      summary: Create tenant
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Tenant'
      responses:
        '201':
          description: Created

  /admin/tenants/{id}:
    get:
      summary: Get tenant
      security: [{ BearerAuth: [] }]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Tenant
    put:
      summary: Update tenant
      security: [{ BearerAuth: [] }]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Tenant'
      responses:
        '200':
          description: Updated
    delete:
      summary: Delete tenant
      security: [{ BearerAuth: [] }]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        '204':
          description: Deleted

  /admin/newsletter-lists:
    get:
      summary: List newsletter lists
      security: [{ BearerAuth: [] }]
      responses:
        '200':
          description: List
    post:
      summary: Create list
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NewsletterList'
      responses:
        '201':
          description: Created

  /admin/newsletter-lists/{id}:
    get:
      summary: Get list
      security: [{ BearerAuth: [] }]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        '200':
          description: List
    put:
      summary: Update list
      security: [{ BearerAuth: [] }]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NewsletterList'
      responses:
        '200':
          description: Updated
    delete:
      summary: Delete list
      security: [{ BearerAuth: [] }]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        '204':
          description: Deleted

  /admin/oauth-clients:
    get:
      summary: List OAuth clients
      security: [{ BearerAuth: [] }]
      responses:
        '200':
          description: List
    post:
      summary: Create OAuth client
      security: [{ BearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OAuthClient'
      responses:
        '201':
          description: Created

  /admin/oauth-clients/{id}:
    get:
      summary: Get OAuth client
      security: [{ BearerAuth: [] }]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        '200':
          description: OAuth client
    put:
      summary: Update OAuth client
      security: [{ BearerAuth: [] }]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OAuthClient'
      responses:
        '200':
          description: Updated
    delete:
      summary: Delete OAuth client
      security: [{ BearerAuth: [] }]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      responses:
        '204':
          description: Deleted

components:
  securitySchemes:
    OAuth2:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: /oauth/authorize
          tokenUrl: /oauth/token
          scopes:
            openid: OpenID Connect
            email: Email
            profile: Basic profile
            newsletter:list.read: Read newsletter subscriptions by list
            newsletter:send.write: Create/send newsletter jobs
            newsletter:send.read: Read newsletter send status
            newsletter:events.read: Read newsletter events
            newsletter:events.write: Write newsletter events (tenant scoped)
            newsletter:events.write.global: Write newsletter events (platform scoped)
            files:upload.write: Upload files to file space via access agent
            files:download.read: Read file content via delegated short-lived token
            files:download.delegate: Delegate short-lived file download authorization
            files:delete: Delete files from file space
            files:metadata.read: Read file metadata
        clientCredentials:
          tokenUrl: /oauth/token
          scopes:
            newsletter:list.read: Read newsletter subscriptions by list
            newsletter:send.write: Create/send newsletter jobs
            newsletter:send.read: Read newsletter send status
            newsletter:events.read: Read newsletter events
            newsletter:events.write: Write newsletter events (tenant scoped)
            newsletter:events.write.global: Write newsletter events (platform scoped)
            files:upload.write: Upload files to file space via access agent
            files:download.read: Read file content via delegated short-lived token
            files:download.delegate: Delegate short-lived file download authorization
            files:delete: Delete files from file space
            files:metadata.read: Read file metadata
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  schemas:
    TokenResponse:
      type: object
      properties:
        access_token: { type: string }
        refresh_token: { type: string }
        id_token: { type: string }
        token_type: { type: string, example: Bearer }
        expires_in: { type: integer }

    RegisterRequest:
      type: object
      required: [email, password]
      properties:
        email: { type: string, format: email }
        password: { type: string }

    PasswordTokenRequest:
      type: object
      required: [grant_type, username, password]
      properties:
        grant_type: { type: string, enum: [password] }
        username: { type: string, format: email }
        password: { type: string }
        scope: { type: string }
        client_id: { type: string }

    AuthorizationCodeTokenRequest:
      type: object
      required: [grant_type, code, redirect_uri, code_verifier]
      properties:
        grant_type: { type: string, enum: [authorization_code] }
        code: { type: string }
        redirect_uri: { type: string }
        code_verifier: { type: string }
        client_id: { type: string }

    RefreshRequest:
      type: object
      required: [refresh_token, client_id]
      properties:
        refresh_token: { type: string }
        client_id: { type: string }

    RefreshTokenRequest:
      type: object
      required: [grant_type, refresh_token]
      properties:
        grant_type: { type: string, enum: [refresh_token] }
        refresh_token: { type: string }
        client_id: { type: string }

    ClientCredentialsTokenRequest:
      type: object
      required: [grant_type, client_id, client_secret]
      properties:
        grant_type: { type: string, enum: [client_credentials] }
        client_id: { type: string }
        client_secret: { type: string }
        scope: { type: string }

    ForgotPasswordRequest:
      type: object
      required: [email]
      properties:
        email: { type: string, format: email }

    ResetPasswordRequest:
      type: object
      required: [email, token, new_password]
      properties:
        email: { type: string, format: email }
        token: { type: string }
        new_password: { type: string }

    UserProfile:
      type: object
      properties:
        id: { type: string }
        email: { type: string, format: email }
        email_verified: { type: boolean }
        created_at: { type: string, format: date-time }

    SubscribeRequest:
      type: object
      required: [list_id, email]
      properties:
        list_id: { type: string }
        email: { type: string, format: email }
        preferences: { type: object }
        source: { type: string }

    Subscription:
      type: object
      properties:
        id: { type: string }
        list_id: { type: string }
        email: { type: string, format: email }
        status: { type: string, enum: [pending, active, unsubscribed] }
        preferences: { type: object }
        created_at: { type: string, format: date-time }

    PendingSubscriptionResponse:
      allOf:
        - $ref: '#/components/schemas/Subscription'
        - type: object
          properties:
            confirm_token: { type: string }

    DisableSubscriptionRequest:
      type: object
      required: [tenant_id, subscriber_id, list_id, reason, disabled_by, occurred_at]
      properties:
        tenant_id: { type: string, format: uuid }
        subscriber_id: { type: string, format: uuid }
        list_id: { type: string, format: uuid }
        reason:
          type: string
          enum: [hard_bounce, soft_bounce_threshold, complaint, suppression]
        disabled_by: { type: string }
        occurred_at: { type: string, format: date-time }

    IssueOneClickUnsubscribeTokenRequest:
      type: object
      required: [tenant_id, list_id, subscriber_id]
      properties:
        tenant_id: { type: string, format: uuid }
        list_id: { type: string, format: uuid }
        subscriber_id: { type: string, format: uuid }

    IssueOneClickUnsubscribeTokensRequest:
      type: object
      required: [tenant_id, list_id, subscriber_ids]
      properties:
        tenant_id: { type: string, format: uuid }
        list_id: { type: string, format: uuid }
        subscriber_ids:
          type: array
          items: { type: string, format: uuid }

    OneClickUnsubscribeTokenItem:
      type: object
      properties:
        subscriber_id: { type: string, format: uuid }
        unsubscribe_token: { type: string, nullable: true }
        status: { type: string, enum: [issued, not_found, blacklisted] }

    ErrorResponse:
      type: object
      required: [error, message, request_id]
      properties:
        error: { type: string }
        message: { type: string }
        request_id: { type: string }

    SendEngineSubscriptionEvent:
      type: object
      required: [event_id, event_type, tenant_id, list_id, subscriber, occurred_at]
      properties:
        event_id: { type: string }
        event_type: { type: string, enum: [subscription.activated, subscription.unsubscribed, preferences.updated, subscription.linked_to_user] }
        tenant_id: { type: string }
        list_id: { type: string }
        subscriber:
          type: object
          required: [id, email, status]
          properties:
            id: { type: string }
            email: { type: string, format: email }
            status: { type: string, enum: [pending, active, unsubscribed] }
            preferences: { type: object }
        occurred_at: { type: string, format: date-time }

    SendEngineListFullSync:
      type: object
      required: [sync_id, batch_no, batch_total, tenant_id, list_id, subscribers, occurred_at]
      properties:
        sync_id: { type: string }
        batch_no: { type: integer }
        batch_total: { type: integer }
        tenant_id: { type: string }
        list_id: { type: string }
        subscribers:
          type: array
          items:
            type: object
            required: [id, email, status]
            properties:
              id: { type: string }
              email: { type: string, format: email }
              status: { type: string, enum: [pending, active, unsubscribed] }
              preferences: { type: object }
        occurred_at: { type: string, format: date-time }

    Tenant:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        domains: { type: array, items: { type: string } }
        status: { type: string }
        send_engine_webhook_client_id:
          type: string
          format: uuid
          nullable: true

    UpsertSendEngineWebhookClientRequest:
      type: object
      required: [tenant_id, webhook_client_id]
      properties:
        tenant_id: { type: string, format: uuid }
        webhook_client_id: { type: string, format: uuid }

    NewsletterList:
      type: object
      properties:
        id: { type: string }
        tenant_id: { type: string }
        name: { type: string }
        status: { type: string }

    OAuthClient:
      type: object
      properties:
        id: { type: string }
        tenant_id:
          type: string
          nullable: true
        name: { type: string }
        usage: { type: string, enum: [tenant_api, send_api, webhook_outbound, platform_service] }
        redirect_uris: { type: array, items: { type: string } }
        client_type: { type: string, enum: [public, confidential] }
