openapi: 3.1.0
info:
  title: Arcalotl Public API
  version: "2026-07"
  description: >
    Community-scoped public REST API. Authenticate with a bearer API key
    (`Authorization: Bearer arclt_live_...`). Every resource is scoped to the
    key's community; a valid id owned by another community answers 404. Read
    endpoints only (PUBLIC_API_PLAN.md WP2). Additive-only within this
    api_version.
servers:
  - url: https://api.arcalotl.com
security:
  - apiKey: []
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: arclt_live_
  parameters:
    cursor:
      name: cursor
      in: query
      required: false
      schema:
        type: string
      description: Opaque keyset cursor from a previous response's next_cursor.
    limit:
      name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
      description: Page size, capped at 100.
    idempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
        minLength: 1
        maxLength: 255
      description: >-
        Opaque client-generated key (1-255 chars) that makes a write safe to
        retry for 24h. A replay returns the original response with an
        Idempotent-Replayed:true header; reuse with a different body returns 422;
        an in-flight duplicate returns 409.
  responses:
    Problem:
      description: RFC 9457 problem response.
      content:
        application/problem+json:
          schema:
            type: object
            properties:
              type: { type: string }
              title: { type: string }
              status: { type: integer }
              detail: { type: string }
              code: { type: string }
paths:
  /v1/subscriptions:
    get:
      summary: List subscriptions
      operationId: listSubscriptions
      security:
        - apiKey: []
      parameters:
        - $ref: '#/components/parameters/cursor'
        - $ref: '#/components/parameters/limit'
        - name: status
          in: query
          schema: { type: string }
        - name: plan_id
          in: query
          schema: { type: string }
        - name: tier_id
          in: query
          schema: { type: string }
        - name: member_id
          in: query
          schema: { type: string }
      responses:
        '200': { description: A page of subscriptions. }
        '401': { $ref: '#/components/responses/Problem' }
        '403': { $ref: '#/components/responses/Problem' }
        '429': { $ref: '#/components/responses/Problem' }
  /v1/subscriptions/{id}:
    get:
      summary: Get a subscription
      operationId: getSubscription
      security:
        - apiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200': { description: The subscription. }
        '401': { $ref: '#/components/responses/Problem' }
        '403': { $ref: '#/components/responses/Problem' }
        '404': { $ref: '#/components/responses/Problem' }
  /v1/subscriptions/{id}/cancel:
    post:
      summary: Cancel a subscription at period end
      description: >
        Schedules a period-end cancellation. The member keeps access until the
        current period ends. Idempotent: a subscription already cancelling
        returns 202 without another provider call. Supply an Idempotency-Key
        header to make retries safe.
      operationId: cancelSubscription
      security:
        - apiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
        - $ref: '#/components/parameters/idempotencyKey'
      responses:
        '202':
          description: Cancellation scheduled.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: cancelling }
        '401': { $ref: '#/components/responses/Problem' }
        '403': { $ref: '#/components/responses/Problem' }
        '404': { $ref: '#/components/responses/Problem' }
        '409': { $ref: '#/components/responses/Problem' }
        '422': { $ref: '#/components/responses/Problem' }
  /v1/plans:
    get:
      summary: List plan tiers
      operationId: listPlans
      security:
        - apiKey: []
      responses:
        '200': { description: The community's plan tiers with nested billing plans. }
        '401': { $ref: '#/components/responses/Problem' }
        '403': { $ref: '#/components/responses/Problem' }
  /v1/plans/{tierId}:
    get:
      summary: Get a plan tier
      operationId: getPlanTier
      security:
        - apiKey: []
      parameters:
        - name: tierId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200': { description: The plan tier. }
        '401': { $ref: '#/components/responses/Problem' }
        '403': { $ref: '#/components/responses/Problem' }
        '404': { $ref: '#/components/responses/Problem' }
  /v1/members:
    get:
      summary: List members
      operationId: listMembers
      security:
        - apiKey: []
      parameters:
        - $ref: '#/components/parameters/cursor'
        - $ref: '#/components/parameters/limit'
      responses:
        '200': { description: A page of members. }
        '401': { $ref: '#/components/responses/Problem' }
        '403': { $ref: '#/components/responses/Problem' }
  /v1/members/lookup:
    get:
      summary: Look up a member by platform identity
      operationId: lookupMember
      security:
        - apiKey: []
      parameters:
        - name: platform
          in: query
          required: true
          schema: { type: string }
        - name: platform_uid
          in: query
          required: true
          schema: { type: string }
      responses:
        '200': { description: The member. }
        '400': { $ref: '#/components/responses/Problem' }
        '401': { $ref: '#/components/responses/Problem' }
        '403': { $ref: '#/components/responses/Problem' }
        '404': { $ref: '#/components/responses/Problem' }
  /v1/members/{id}:
    get:
      summary: Get a member
      operationId: getMember
      security:
        - apiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200': { description: The member with platform identities. }
        '401': { $ref: '#/components/responses/Problem' }
        '403': { $ref: '#/components/responses/Problem' }
        '404': { $ref: '#/components/responses/Problem' }
  /v1/members/{id}/entitlements:
    get:
      summary: List a member's entitlements
      operationId: getMemberEntitlements
      security:
        - apiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200': { description: The member's active entitlements. }
        '401': { $ref: '#/components/responses/Problem' }
        '403': { $ref: '#/components/responses/Problem' }
        '404': { $ref: '#/components/responses/Problem' }
  /v1/entitlements/check:
    get:
      summary: Check an entitlement
      operationId: checkEntitlement
      security:
        - apiKey: []
      parameters:
        - name: platform
          in: query
          required: true
          schema: { type: string }
        - name: platform_uid
          in: query
          required: true
          schema: { type: string }
        - name: tier_id
          in: query
          required: false
          schema: { type: string }
      responses:
        '200': { description: Whether the identity is entitled, and to what. }
        '400': { $ref: '#/components/responses/Problem' }
        '401': { $ref: '#/components/responses/Problem' }
        '403': { $ref: '#/components/responses/Problem' }
  /v1/purchases:
    get:
      summary: List purchases
      operationId: listPurchases
      security:
        - apiKey: []
      parameters:
        - $ref: '#/components/parameters/cursor'
        - $ref: '#/components/parameters/limit'
      responses:
        '200': { description: A page of one-time purchases. }
        '401': { $ref: '#/components/responses/Problem' }
        '403': { $ref: '#/components/responses/Problem' }
  /v1/purchases/{id}:
    get:
      summary: Get a purchase
      operationId: getPurchase
      security:
        - apiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200': { description: The purchase. }
        '401': { $ref: '#/components/responses/Problem' }
        '403': { $ref: '#/components/responses/Problem' }
        '404': { $ref: '#/components/responses/Problem' }
  /v1/analytics/summary:
    get:
      summary: Get the analytics summary
      operationId: getAnalyticsSummary
      security:
        - apiKey: []
      responses:
        '200': { description: MRR, active subscribers, and recent signups/cancels. }
        '401': { $ref: '#/components/responses/Problem' }
        '403': { $ref: '#/components/responses/Problem' }
  /v1/checkout-links:
    post:
      summary: Create a hosted checkout link
      description: >
        Creates a real hosted checkout session for a buyer identified by a
        platform identity, and returns its URL. The tier must exist and be
        active within the community, the community must have an active payment
        provider and a configuration for the buyer's platform, and the buyer
        must be eligible (not already subscribed to the tier). plan_id may be
        omitted when the tier has exactly one active plan; otherwise 422
        plan_required. expires_at is present only when the provider reports a
        session expiry; Stripe hosted sessions otherwise expire per Stripe's
        defaults (24h). Supply an Idempotency-Key header to make retries safe.
      operationId: createCheckoutLink
      security:
        - apiKey: []
      parameters:
        - $ref: '#/components/parameters/idempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tier_id, platform, platform_uid]
              properties:
                tier_id: { type: string }
                plan_id: { type: string }
                platform:
                  type: string
                  enum: [discord, stoat, fluxer]
                platform_uid: { type: string }
      responses:
        '201':
          description: The hosted checkout URL (and expiry when reported).
          content:
            application/json:
              schema:
                type: object
                required: [url]
                properties:
                  url: { type: string, format: uri }
                  expires_at: { type: string, format: date-time }
        '400': { $ref: '#/components/responses/Problem' }
        '401': { $ref: '#/components/responses/Problem' }
        '403': { $ref: '#/components/responses/Problem' }
        '404': { $ref: '#/components/responses/Problem' }
        '409': { $ref: '#/components/responses/Problem' }
        '422': { $ref: '#/components/responses/Problem' }
  /v1/events:
    get:
      summary: List projected public events
      description: >-
        Polling and reconciliation feed. Ordered newest-first with keyset
        pagination. Requires the events:read scope.
      operationId: listEvents
      security:
        - apiKey: []
      parameters:
        - name: type
          in: query
          required: false
          description: >-
            One or more public event types. Repeat the parameter or provide a
            comma-separated list.
          schema:
            type: array
            items: { type: string }
        - name: after_id
          in: query
          required: false
          schema: { type: string }
          description: Return only events created after the given event id.
        - $ref: '#/components/parameters/cursor'
        - $ref: '#/components/parameters/limit'
      responses:
        '200': { description: A keyset page of public event envelopes. }
        '401': { $ref: '#/components/responses/Problem' }
        '403': { $ref: '#/components/responses/Problem' }
  /v1/events/{id}:
    get:
      summary: Get one public event
      operationId: getEvent
      security:
        - apiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200': { description: The public event envelope. }
        '401': { $ref: '#/components/responses/Problem' }
        '403': { $ref: '#/components/responses/Problem' }
        '404': { $ref: '#/components/responses/Problem' }
  /v1/webhooks/event-types:
    get:
      summary: List the machine-readable event-type catalog
      description: >-
        The authoritative catalog of public event types and descriptions.
        Available to any authenticated key.
      operationId: listEventTypes
      security:
        - apiKey: []
      responses:
        '200': { description: The event-type catalog. }
        '401': { $ref: '#/components/responses/Problem' }
  /v1/webhooks/endpoints:
    get:
      summary: List webhook endpoints
      operationId: listWebhookEndpoints
      security:
        - apiKey: []
      responses:
        '200': { description: The community's webhook endpoints (no secrets). }
        '401': { $ref: '#/components/responses/Problem' }
        '403': { $ref: '#/components/responses/Problem' }
        '503': { $ref: '#/components/responses/Problem' }
    post:
      summary: Create a webhook endpoint
      description: >-
        Registers an endpoint and returns the whsec_ signing secret exactly
        once. Requires webhooks:write. Max 5 endpoints per community.
      operationId: createWebhookEndpoint
      security:
        - apiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, event_types]
              properties:
                url: { type: string }
                description: { type: string }
                event_types:
                  type: array
                  items: { type: string }
      responses:
        '201': { description: The created endpoint plus the one-time plaintext secret. }
        '401': { $ref: '#/components/responses/Problem' }
        '403': { $ref: '#/components/responses/Problem' }
        '422': { $ref: '#/components/responses/Problem' }
        '503': { $ref: '#/components/responses/Problem' }
  /v1/webhooks/endpoints/{id}:
    get:
      summary: Get one webhook endpoint
      operationId: getWebhookEndpoint
      security:
        - apiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200': { description: The webhook endpoint. }
        '401': { $ref: '#/components/responses/Problem' }
        '403': { $ref: '#/components/responses/Problem' }
        '404': { $ref: '#/components/responses/Problem' }
        '503': { $ref: '#/components/responses/Problem' }
    patch:
      summary: Update a webhook endpoint
      description: Patch url, description, event_types, or enabled. Requires webhooks:write.
      operationId: updateWebhookEndpoint
      security:
        - apiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                url: { type: string }
                description: { type: string }
                event_types:
                  type: array
                  items: { type: string }
                enabled: { type: boolean }
      responses:
        '200': { description: The updated endpoint. }
        '401': { $ref: '#/components/responses/Problem' }
        '403': { $ref: '#/components/responses/Problem' }
        '404': { $ref: '#/components/responses/Problem' }
        '422': { $ref: '#/components/responses/Problem' }
        '503': { $ref: '#/components/responses/Problem' }
    delete:
      summary: Delete a webhook endpoint
      operationId: deleteWebhookEndpoint
      security:
        - apiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '204': { description: Endpoint deleted. }
        '401': { $ref: '#/components/responses/Problem' }
        '403': { $ref: '#/components/responses/Problem' }
        '404': { $ref: '#/components/responses/Problem' }
        '503': { $ref: '#/components/responses/Problem' }
  /v1/webhooks/endpoints/{id}/rotate-secret:
    post:
      summary: Rotate a webhook endpoint's signing secret
      description: Returns a new whsec_ secret exactly once. Requires webhooks:write.
      operationId: rotateWebhookSecret
      security:
        - apiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200': { description: The new one-time plaintext secret. }
        '401': { $ref: '#/components/responses/Problem' }
        '403': { $ref: '#/components/responses/Problem' }
        '404': { $ref: '#/components/responses/Problem' }
        '503': { $ref: '#/components/responses/Problem' }
  /v1/webhooks/endpoints/{id}/deliveries:
    get:
      summary: List an endpoint's delivery attempts
      operationId: listWebhookDeliveries
      security:
        - apiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
        - $ref: '#/components/parameters/cursor'
        - $ref: '#/components/parameters/limit'
      responses:
        '200': { description: A keyset page of delivery attempt logs. }
        '401': { $ref: '#/components/responses/Problem' }
        '403': { $ref: '#/components/responses/Problem' }
        '404': { $ref: '#/components/responses/Problem' }
        '503': { $ref: '#/components/responses/Problem' }
  /v1/webhooks/endpoints/{id}/deliveries/{deliveryId}/redeliver:
    post:
      summary: Requeue a delivery for retry
      operationId: redeliverWebhookDelivery
      security:
        - apiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
        - name: deliveryId
          in: path
          required: true
          schema: { type: string }
      responses:
        '202': { description: Delivery requeued. }
        '401': { $ref: '#/components/responses/Problem' }
        '403': { $ref: '#/components/responses/Problem' }
        '404': { $ref: '#/components/responses/Problem' }
        '503': { $ref: '#/components/responses/Problem' }
  /v1/webhooks/endpoints/{id}/test:
    post:
      summary: Send a signed test ping
      description: >-
        Synchronously delivers a signed ping envelope and returns the HTTP
        status code. Rate-limited to 10/min per endpoint. Requires webhooks:write.
      operationId: testWebhookEndpoint
      security:
        - apiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200': { description: The endpoint's HTTP status code for the ping. }
        '401': { $ref: '#/components/responses/Problem' }
        '403': { $ref: '#/components/responses/Problem' }
        '404': { $ref: '#/components/responses/Problem' }
        '429': { $ref: '#/components/responses/Problem' }
        '503': { $ref: '#/components/responses/Problem' }
