openapi: 3.0.3
info:
  title: NASEBANAL Account API
  description: Centralized user and billing management API for NASEBANAL applications. Manages user profiles, per-service subscriptions, Stripe billing, and Personal Access Tokens (PAT).
  version: 1.6.0
  contact:
    name: NASEBANAL
    url: https://www.nasebanal.com
servers:
  - url: http://localhost:8788
    description: Local dev (wrangler dev)
  - url: https://api.account.nasebanal.com
    description: Production
security:
  - BearerAuth: []
paths:
  /health:
    get:
      tags:
        - Health
      summary: Health check
      description: Returns the health status of the API.
      security: []
      responses:
        "200":
          description: API is healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok
                  timestamp:
                    type: string
                    format: date-time
  /api/v1/me:
    get:
      tags:
        - User
      summary: Get current user profile
      description: Returns the authenticated user's profile information including subscription details.
      responses:
        "200":
          description: User profile
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
    delete:
      tags:
        - User
      summary: Delete account
      description: |-
        Permanently deletes the authenticated user's account and every service's data for it: Recorder records, tags and media, Target goals, shares on both sides, organization memberships, and API tokens. An active personal subscription is cancelled at Stripe as part of the same call.

        Irreversible — there is no soft-delete or reactivation. Callers must confirm intent before invoking, and must send the user through logout afterwards: their access token stays cryptographically valid but no longer resolves to an account, so a subsequent authenticated request would provision a brand-new empty user.

        Refused with `409` while the caller is the only administrator of an organization that still has other members; the organization must be handed over or deleted first. `502` means nothing was deleted (a downstream service could not be purged) and the request can safely be retried.
      responses:
        "200":
          description: Account deleted
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    type: object
                    required:
                      - deleted
                      - user_id
                    properties:
                      deleted:
                        type: boolean
                        enum:
                          - true
                      user_id:
                        type: integer
                      organizations_deleted:
                        type: integer
                        description: Organizations removed because the deleted user was their only member.
                      subscriptions_cancelled:
                        type: integer
                      auth0_identity_deleted:
                        type: boolean
                        description: Whether the Auth0 identity was removed as well. False when the API has no Management API credentials configured — the account data is still fully deleted, but signing in again creates a new empty account.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: Service accounts cannot be deleted through this endpoint
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: The caller is the last administrator of an organization that still has members
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "502":
          description: A downstream service could not be purged — nothing was deleted
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /api/v1/me/profile:
    put:
      tags:
        - User
      summary: Update profile
      description: Updates the authenticated user's profile information.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateProfileInput"
      responses:
        "200":
          description: Profile updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /api/v1/me/accept-terms:
    post:
      tags:
        - User
      summary: Accept terms of service
      description: Records the user's acceptance of the terms of service.
      responses:
        "200":
          description: Terms accepted
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    type: object
                    properties:
                      tos_accepted_at:
                        type: string
                        format: date-time
        "401":
          $ref: "#/components/responses/Unauthorized"
  /api/v1/me/subscriptions:
    get:
      tags:
        - Subscriptions
      summary: Get all per-service subscriptions
      description: Returns all per-service subscriptions for the authenticated user.
      responses:
        "200":
          description: List of service subscriptions
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/ServiceSubscription"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /api/v1/me/subscriptions/{serviceId}:
    get:
      tags:
        - Subscriptions
      summary: Get subscription for a service
      description: Returns the subscription for a specific service.
      parameters:
        - name: serviceId
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Service subscription
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    $ref: "#/components/schemas/ServiceSubscription"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/me/tokens:
    get:
      tags:
        - API Tokens
      summary: List API tokens
      description: Returns all Personal Access Tokens for the authenticated user.
      responses:
        "200":
          description: List of API tokens
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/ApiToken"
                      total:
                        type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags:
        - API Tokens
      summary: Create API token
      description: Creates a new Personal Access Token. The raw token is returned only once in the response.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateApiTokenInput"
      responses:
        "201":
          description: Token created
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    allOf:
                      - $ref: "#/components/schemas/ApiToken"
                      - type: object
                        properties:
                          token:
                            type: string
                            description: The raw token (only returned on creation)
                            example: nbpat_a1b2c3d4e5f6g7h8i9j0...
        "401":
          $ref: "#/components/responses/Unauthorized"
  /api/v1/me/tokens/{id}:
    delete:
      tags:
        - API Tokens
      summary: Delete API token
      description: Deletes a Personal Access Token by ID.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: Token deleted
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    type: object
                    properties:
                      success:
                        type: boolean
                        example: true
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/me/notifications:
    get:
      tags:
        - Notifications
      summary: List notifications
      description: Returns the caller's in-app notifications, newest first, with the total unread count. Read rows are deleted 30 days after they were read and unread rows 90 days after their last update.
      parameters:
        - name: limit
          in: query
          required: false
          description: Maximum rows to return.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 30
        - name: unread_only
          in: query
          required: false
          description: Return only unread rows.
          schema:
            type: boolean
            default: false
      responses:
        "200":
          description: Notifications and the unread count
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    $ref: "#/components/schemas/NotificationInbox"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /api/v1/me/notifications/unread-count:
    get:
      tags:
        - Notifications
      summary: Unread notification count
      description: Returns only the number of unread notifications. Cheap enough to poll for a badge; use the list endpoint to read the rows.
      responses:
        "200":
          description: Unread count
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    type: object
                    properties:
                      unread_count:
                        type: integer
                        minimum: 0
                        example: 3
        "401":
          $ref: "#/components/responses/Unauthorized"
  /api/v1/me/notifications/read:
    post:
      tags:
        - Notifications
      summary: Mark notifications as read
      description: "Marks the given rows (`ids`, at most 100) or every unread row (`all: true`) as read. Ids that do not exist or belong to someone else are ignored."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MarkNotificationsReadInput"
      responses:
        "200":
          description: Marked as read
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    type: object
                    properties:
                      updated:
                        type: boolean
                        example: true
        "401":
          $ref: "#/components/responses/Unauthorized"
  /api/v1/me/device-tokens:
    post:
      tags:
        - Notifications
      summary: Register a device for push
      description: Sends this device's Expo push token to the signed-in account. Registering a token that belongs to another account moves it to the caller, so a shared phone follows whoever is signed in. At most a handful of devices are kept per user (the newest).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RegisterDeviceTokenInput"
      responses:
        "200":
          description: Registered
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    type: object
                    properties:
                      registered:
                        type: boolean
                        example: true
        "401":
          $ref: "#/components/responses/Unauthorized"
    delete:
      tags:
        - Notifications
      summary: Unregister a device
      description: "Detaches a push token from the caller (on sign-out). Scoped to the caller: another account's token is left alone."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - token
              properties:
                token:
                  type: string
                  example: ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]
      responses:
        "200":
          description: Unregistered
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    type: object
                    properties:
                      unregistered:
                        type: boolean
                        example: true
        "401":
          $ref: "#/components/responses/Unauthorized"
  /api/v1/organizations/{orgId}/service-accounts:
    get:
      tags:
        - Service Accounts
      summary: List service accounts
      description: Returns all service accounts owned by the given organization. Requires the caller to be an Org Admin.
      parameters:
        - name: orgId
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: List of service accounts
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/ServiceAccount"
                      total:
                        type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    post:
      tags:
        - Service Accounts
      summary: Create a service account
      description: Creates a new service account in the organization. Requires the caller to be an Org Admin AND the organization to hold an active Enterprise subscription.
      parameters:
        - name: orgId
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateServiceAccountInput"
      responses:
        "201":
          description: Service account created
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    $ref: "#/components/schemas/ServiceAccount"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
  /api/v1/organizations/{orgId}/service-accounts/{saId}:
    put:
      tags:
        - Service Accounts
      summary: Rename a service account
      parameters:
        - name: orgId
          in: path
          required: true
          schema:
            type: integer
        - name: saId
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateServiceAccountInput"
      responses:
        "200":
          description: Service account updated
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    $ref: "#/components/schemas/ServiceAccount"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      tags:
        - Service Accounts
      summary: Delete a service account
      description: Deletes the service account and cascades to all of its API tokens. Returns the cascade count for audit reference.
      parameters:
        - name: orgId
          in: path
          required: true
          schema:
            type: integer
        - name: saId
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: Service account deleted
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    type: object
                    properties:
                      success:
                        type: boolean
                      cascade_token_count:
                        type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/organizations/{orgId}/service-accounts/{saId}/tokens:
    get:
      tags:
        - Service Accounts
      summary: List service-account tokens
      parameters:
        - name: orgId
          in: path
          required: true
          schema:
            type: integer
        - name: saId
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: List of tokens
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/ApiToken"
                      total:
                        type: integer
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    post:
      tags:
        - Service Accounts
      summary: Issue a new token for a service account
      description: Creates a new API token under the service account. The raw `nbpat_...` token is returned exactly once; it is not stored in plaintext and cannot be retrieved later.
      parameters:
        - name: orgId
          in: path
          required: true
          schema:
            type: integer
        - name: saId
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateApiTokenInput"
      responses:
        "201":
          description: Token issued
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    allOf:
                      - $ref: "#/components/schemas/ApiToken"
                      - type: object
                        properties:
                          token:
                            type: string
                            description: The raw token value (returned once on creation)
                            example: nbpat_a1b2c3d4...
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/organizations/{orgId}/service-accounts/{saId}/tokens/{tokenId}:
    delete:
      tags:
        - Service Accounts
      summary: Revoke a service-account token
      parameters:
        - name: orgId
          in: path
          required: true
          schema:
            type: integer
        - name: saId
          in: path
          required: true
          schema:
            type: integer
        - name: tokenId
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: Token revoked
          content:
            application/json:
              schema:
                type: object
                required:
                  - success
                  - data
                properties:
                  success:
                    type: boolean
                    enum:
                      - true
                  data:
                    type: object
                    properties:
                      success:
                        type: boolean
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT or PAT
      description: Use either an Auth0 JWT token or a Personal Access Token (PAT) starting with `nbpat_`.
  schemas:
    User:
      type: object
      properties:
        id:
          type: integer
        email:
          type: string
          format: email
        name:
          type: string
        language:
          type: string
          enum:
            - ja
            - en
        timezone:
          type: string
          description: IANA time zone name (e.g. `Asia/Tokyo`), or `auto` to follow the device the client runs on. Defaults to `auto`.
          example: auto
        plan:
          type: string
          enum:
            - free
            - pro
            - family
            - enterprise
        subscription_status:
          type: string
          enum:
            - active
            - canceled
            - past_due
            - trialing
            - null
          nullable: true
        subscription_expires_at:
          type: string
          format: date-time
          nullable: true
        subscriptions:
          type: array
          items:
            $ref: "#/components/schemas/ServiceSubscription"
        tos_accepted_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
    UserResponse:
      type: object
      required:
        - success
        - data
      properties:
        success:
          type: boolean
          enum:
            - true
        data:
          $ref: "#/components/schemas/User"
    UpdateProfileInput:
      type: object
      properties:
        name:
          type: string
        language:
          type: string
          enum:
            - ja
            - en
        timezone:
          type: string
          description: IANA time zone name, or `auto` to follow the device.
    ServiceSubscription:
      type: object
      properties:
        id:
          type: integer
        user_id:
          type: integer
        service_id:
          type: string
        plan:
          type: string
          enum:
            - free
            - pro
            - family
            - enterprise
        subscription_id:
          type: string
          nullable: true
        subscription_status:
          type: string
          enum:
            - active
            - canceled
            - past_due
            - trialing
            - null
          nullable: true
        subscription_expires_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    ApiToken:
      type: object
      properties:
        id:
          type: integer
        user_id:
          type: integer
        name:
          type: string
        last_used_at:
          type: string
          format: date-time
          nullable: true
        expires_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    CreateApiTokenInput:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
          example: My API Token
        expires_at:
          type: string
          format: date-time
          description: Optional expiration date for the token.
    Notification:
      type: object
      description: One inbox row. Repeats of the same event on the same entity collapse into a single unread row (`count` says how many).
      properties:
        id:
          type: integer
        type:
          type: string
          description: Event kind, e.g. `share.invited`, `share.accepted`, `share.rejected`, `share.revoked`, `ownership.transfer_requested`, `target.updated`, `target.deleted`, `recorder.tag_updated`. New kinds may appear; clients should fall back to a generic sentence for one they do not know.
          example: recorder.tag_updated
        service_id:
          type: string
          description: Which API raised it (`target`, `recorder`, `account`); tells a client which app the reference belongs to.
          example: recorder
        ref_id:
          type: string
          nullable: true
          description: "The entity to open: a target id, a tag id or a share id, depending on `type`."
        params:
          type: object
          additionalProperties: true
          description: "Display data, never rendered into a push: `actor_name`, `owner_name`, `recipient_name`, `target_title`, `tag_name`, and for `recorder.tag_updated` the latest `change` plus up to five `changes` (newest first), each with `action`, `record_id`, `value`, `unit`, `recorded_at`, `comment`, `media_type` (kind only, never a URL) and for goal edits `kind: goal`, `goal_value`, `direction`."
        count:
          type: integer
          minimum: 1
        read_at:
          type: string
          nullable: true
          description: UTC, `YYYY-MM-DD HH:MM:SS`. Null while unread.
        created_at:
          type: string
          description: UTC, `YYYY-MM-DD HH:MM:SS`.
        updated_at:
          type: string
          description: UTC, `YYYY-MM-DD HH:MM:SS`. Moves forward when a repeat collapses into the row.
    NotificationInbox:
      type: object
      properties:
        notifications:
          type: array
          items:
            $ref: "#/components/schemas/Notification"
        unread_count:
          type: integer
          minimum: 0
          description: Total unread rows, not just those in this page.
    MarkNotificationsReadInput:
      type: object
      description: "Send either `all: true` or a non-empty `ids` array."
      properties:
        all:
          type: boolean
          enum:
            - true
        ids:
          type: array
          minItems: 1
          maxItems: 100
          items:
            type: integer
          example:
            - 12
            - 13
    RegisterDeviceTokenInput:
      type: object
      required:
        - token
        - platform
      properties:
        token:
          type: string
          description: An Expo push token.
          example: ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]
        platform:
          type: string
          enum:
            - ios
            - android
    ServiceAccount:
      type: object
      description: A non-human users row scoped to a single organization. SAs hold API tokens via the existing api_tokens table (user_id = SA.id). They never appear in member lists; verifyToken rejects any JWT bearing the synthetic 'service-account|' sub.
      properties:
        id:
          type: integer
        name:
          type: string
        organization_id:
          type: integer
        is_service_account:
          type: integer
          enum:
            - 1
          description: Always 1 for SA rows; included for parity with the underlying users schema.
        auth0_sub:
          type: string
          description: Synthetic, prefixed 'service-account|' followed by a UUID. Has no real Auth0 identity behind it.
        email:
          type: string
          nullable: true
          description: Auto-generated 'sa-<uuid>@nasebanal.com'. Not a real mailbox.
        token_count:
          type: integer
          description: Number of api_tokens currently owned by this SA. Returned only on list and create responses.
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    CreateServiceAccountInput:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
          example: Acme Metrics Bot
    Error:
      type: object
      required:
        - success
        - error
      properties:
        success:
          type: boolean
          enum:
            - false
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
            message:
              type: string
            details:
              type: object
              additionalProperties: true
  responses:
    Unauthorized:
      description: Authentication required or invalid token
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            success: false
            error:
              code: AUTH_ERROR
              message: No authentication token provided
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            success: false
            error:
              code: NOT_FOUND
              message: Resource not found
    Forbidden:
      description: Caller is authenticated but lacks the required role or plan
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            success: false
            error:
              code: PLAN_LIMIT_EXCEEDED
              message: Service Accounts require an Enterprise plan for this organization.
tags:
  - name: Health
    description: Health check endpoints
  - name: User
    description: User profile endpoints
  - name: Subscriptions
    description: Subscription management endpoints
  - name: API Tokens
    description: Personal Access Token management
  - name: Service Accounts
    description: Org-scoped service accounts for Enterprise organizations and their API tokens. Org Admin only.
  - name: Notifications
    description: In-app notification inbox and mobile push device registration
