openapi: 3.1.0
info:
  title: RBS API
  version: 0.1.0-draft
  description: |
    Retail Bus Service (RBS).

    Contract notes:
    - API Gateway base path: `/rbs`
    - Org-gated for data routes: supply `x-orgcode`. Health (`GET /stat`) does not require `x-orgcode`.
    - Auth: either `x-session-guid` (user session) or `x-api-key` (org-bound service account).
    - Owner-only: non-owner calls are rejected.
    - Request context is required for write operations and captured for auditability.
    - Oversize delivery payloads are stored in S3 and delivered via signed pointers.
    - Delivery auth prefers assume-role with external ID; queue ownership is validated via `GetQueueAttributes` and a verification token.
servers:
  - url: https://api.g3nretailstack.com/rbs
security:
  - sessionAuth: []
  - apiKeyAuth: []
components:
  securitySchemes:
    sessionAuth:
      type: apiKey
      in: header
      name: x-session-guid
    apiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
  parameters:
    OrgHeader:
      in: header
      name: x-orgcode
      required: true
      schema: { type: string }
    CostCentreHeader:
      in: header
      name: x-cccode
      required: false
      schema: { type: string }
  schemas:
    BuildMeta:
      type: object
      properties:
        build_major: { type: string }
        build_minor: { type: string }
        build_id: { type: string }
      required: [build_major, build_minor, build_id]
    Stats:
      type: object
      properties:
        call: { type: string }
        service: { type: string, enum: [rbs] }
        request_id: { type: string }
        timestamp_utc: { type: string, format: date-time }
        actor: { type: string }
        context_source:
          type: string
          enum: [session, api_key, operator, system]
        session_fingerprint:
          type: string
          description: Non-reversible SHA-256 fingerprint of the caller session for correlation.
        api_key_fingerprint:
          type: string
          description: Non-reversible SHA-256 fingerprint of the caller API key for correlation.
        orgcode: { type: string }
        cccode: { type: string }
        logical_guid: { type: string }
        channel_code: { type: string }
        roles:
          type: array
          items: { type: string }
        build:
          $ref: '#/components/schemas/BuildMeta'
      required: [call, service, timestamp_utc, build]
    ErrorMajor:
      type: object
      properties:
        tag: { type: string }
        message:
          type: object
          properties:
            en_US: { type: string }
      required: [tag, message]
    ErrorEnvelope:
      type: object
      properties:
        error_code: { type: string }
        http_status: { type: integer }
        retryable: { type: boolean }
        request_id: { type: string }
        trace_id: { type: string }
        major: { $ref: '#/components/schemas/ErrorMajor' }
        minor: { $ref: '#/components/schemas/ErrorMajor' }
        details: { type: object }
        conflict_snapshot: { type: object }
      required: [major]
    Envelope:
      type: object
      properties:
        success: { type: boolean }
        data: { type: object }
        error: { $ref: '#/components/schemas/ErrorEnvelope' }
        stats: { $ref: '#/components/schemas/Stats' }
        revision: { type: string }
      required: [success]
    SourceRef:
      type: object
      properties:
        kind: { type: string }
        id: { type: string }
      required: [kind, id]
    SourceRefs:
      type: array
      items: { $ref: '#/components/schemas/SourceRef' }
    RequestContext:
      type: object
      properties:
        orgcode: { type: string }
        logical_guid: { type: string }
        channel_code: { type: string }
        cccode: { type: string }
        actor: { type: string }
        roles:
          type: array
          items: { type: string }
        context_source:
          type: string
          enum: [session, api_key, operator, system]
        session_fingerprint: { type: string }
        api_key_fingerprint: { type: string }
      required: [orgcode, actor, context_source]
    SubscriptionFilter:
      type: object
      properties:
        service: { type: string }
        actions:
          type: array
          items: { type: string }
        channel_codes:
          type: array
          items: { type: string }
        logical_guids:
          type: array
          items: { type: string }
      required: [service]
    Subscription:
      type: object
      properties:
        subscription_id: { type: string }
        orgcode: { type: string }
        caption: { type: string }
        queue_url: { type: string }
        queue_arn: { type: string }
        queue_region: { type: string }
        queue_owner_account_id: { type: string }
        queue_kms_key_id: { type: string }
        auth_mode:
          type: string
          enum: [assume_role, static]
        role_arn: { type: string }
        access_key_last4: { type: string }
        secret_last4: { type: string }
        external_id_last4: { type: string }
        status: { type: string, enum: [pending_verify, active, paused, revoked] }
        verification_expires_at: { type: string, format: date-time }
        event_filters:
          type: array
          items: { $ref: '#/components/schemas/SubscriptionFilter' }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        revision: { type: string }
      required: [subscription_id, orgcode, queue_url, queue_region, status, created_at, updated_at, revision]
    SubscriptionRegisterRequest:
      type: object
      x-idempotency-scope: orgcode+route+idempotency_key
      x-idempotency-retention-hours: 24
      x-idempotency-replay: return_original_response
      properties:
        request_context: { $ref: '#/components/schemas/RequestContext' }
        reason: { type: string }
        source_refs: { $ref: '#/components/schemas/SourceRefs' }
        subscription_id: { type: string }
        caption: { type: string }
        queue_url: { type: string }
        queue_arn: { type: string }
        queue_region: { type: string }
        queue_owner_account_id: { type: string }
        auth_mode:
          type: string
          enum: [assume_role, static]
        role_arn: { type: string }
        external_id: { type: string }
        access_key_id: { type: string }
        secret_access_key: { type: string }
        event_filters:
          type: array
          items: { $ref: '#/components/schemas/SubscriptionFilter' }
        idempotency_key: { type: string }
      required: [request_context, reason, source_refs, queue_url]
    SubscriptionRegisterResponse:
      allOf:
        - { $ref: '#/components/schemas/Subscription' }
        - type: object
          properties:
            verification_required: { type: boolean }
            verification_expires_at: { type: string, format: date-time }
    SubscriptionVerifyRequest:
      type: object
      properties:
        request_context: { $ref: '#/components/schemas/RequestContext' }
        reason: { type: string }
        source_refs: { $ref: '#/components/schemas/SourceRefs' }
        subscription_id: { type: string }
        verification_token: { type: string }
      required: [request_context, reason, source_refs, subscription_id, verification_token]
    SubscriptionUpdateRequest:
      type: object
      properties:
        request_context: { $ref: '#/components/schemas/RequestContext' }
        reason: { type: string }
        source_refs: { $ref: '#/components/schemas/SourceRefs' }
        subscription_id: { type: string }
        expected_revision: { type: string }
        caption: { type: string }
        event_filters:
          type: array
          items: { $ref: '#/components/schemas/SubscriptionFilter' }
        queue_url: { type: string }
        queue_arn: { type: string }
        queue_region: { type: string }
        queue_owner_account_id: { type: string }
        auth_mode:
          type: string
          enum: [assume_role, static]
        role_arn: { type: string }
        external_id: { type: string }
        access_key_id: { type: string }
        secret_access_key: { type: string }
      required: [request_context, reason, source_refs, subscription_id, expected_revision]
    SubscriptionStatusSetRequest:
      type: object
      properties:
        request_context: { $ref: '#/components/schemas/RequestContext' }
        reason: { type: string }
        source_refs: { $ref: '#/components/schemas/SourceRefs' }
        subscription_id: { type: string }
        status: { type: string, enum: [active, paused, revoked] }
        expected_revision: { type: string }
      required: [request_context, reason, source_refs, subscription_id, status, expected_revision]
    SubscriptionUnregisterRequest:
      type: object
      properties:
        request_context: { $ref: '#/components/schemas/RequestContext' }
        reason: { type: string }
        source_refs: { $ref: '#/components/schemas/SourceRefs' }
        subscription_id: { type: string }
        expected_revision: { type: string }
      required: [request_context, reason, source_refs, subscription_id, expected_revision]
    SubscriptionTestRequest:
      type: object
      properties:
        request_context: { $ref: '#/components/schemas/RequestContext' }
        reason: { type: string }
        source_refs: { $ref: '#/components/schemas/SourceRefs' }
        subscription_id: { type: string }
      required: [request_context, reason, source_refs, subscription_id]
    SubscriptionGetRequest:
      type: object
      properties:
        subscription_id: { type: string }
      required: [subscription_id]
    SubscriptionListRequest:
      type: object
      properties:
        limit: { type: integer }
        next_token: { type: object }
    SubscriptionListResponse:
      type: object
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/Subscription' }
        next_token: { type: object }
paths:
  /stat:
    get:
      summary: Health check
      description: |
        Health check. Tenant routes require x-orgcode; /stat health checks do not. Org-scoped reads may
        return 404 for non-associated callers (anti-enumeration). Route class Tier A (p95 500ms).
      x-route-class: Tier A
      x-qps-target: 5
      x-concurrency-target: 2
      x-latency-p95-ms: 500
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/Envelope' }
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          service: { type: string }
                          status: { type: string }
  /subscription/register:
    post:
      summary: Register a subscription
      description: |
        Register a subscription. Tenant routes require x-orgcode; /stat health checks do not. Route class
        Tier D (p95 2000ms, p99 4000ms).
      x-route-class: Tier D
      x-qps-target: 10
      x-concurrency-target: 5
      x-latency-p95-ms: 2000
      x-latency-p99-ms: 4000
      parameters:
        - $ref: '#/components/parameters/OrgHeader'
        - $ref: '#/components/parameters/CostCentreHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SubscriptionRegisterRequest' }
      responses:
        '200':
          description: Subscription registered
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/Envelope' }
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/SubscriptionRegisterResponse'
  /subscription/verify:
    post:
      summary: Verify subscription token
      description: |
        Verify subscription token. Tenant routes require x-orgcode; /stat health checks do not. Route class
        Tier D (p95 2000ms, p99 4000ms).
      x-route-class: Tier D
      x-qps-target: 10
      x-concurrency-target: 5
      x-latency-p95-ms: 2000
      x-latency-p99-ms: 4000
      parameters:
        - $ref: '#/components/parameters/OrgHeader'
        - $ref: '#/components/parameters/CostCentreHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SubscriptionVerifyRequest' }
      responses:
        '200':
          description: Subscription verified
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/Envelope' }
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Subscription' }
  /subscription/update:
    post:
      summary: Update subscription
      description: |
        Update subscription. Tenant routes require x-orgcode; /stat health checks do not. If this updates a
        revisioned record, expected_revision is required (428 if missing; 409 on mismatch). Route class Tier
        D (p95 2000ms, p99 4000ms).
      x-route-class: Tier D
      x-qps-target: 10
      x-concurrency-target: 5
      x-latency-p95-ms: 2000
      x-latency-p99-ms: 4000
      parameters:
        - $ref: '#/components/parameters/OrgHeader'
        - $ref: '#/components/parameters/CostCentreHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SubscriptionUpdateRequest' }
      responses:
        '200':
          description: Subscription updated
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/Envelope' }
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Subscription' }
  /subscription/status/set:
    post:
      summary: Update subscription status
      description: |
        Update subscription status. Tenant routes require x-orgcode; /stat health checks do not. If this
        updates a revisioned record, expected_revision is required (428 if missing; 409 on mismatch). Route
        class Tier D (p95 1500ms, p99 3000ms).
      x-route-class: Tier D
      x-qps-target: 10
      x-concurrency-target: 5
      x-latency-p95-ms: 1500
      x-latency-p99-ms: 3000
      parameters:
        - $ref: '#/components/parameters/OrgHeader'
        - $ref: '#/components/parameters/CostCentreHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SubscriptionStatusSetRequest' }
      responses:
        '200':
          description: Subscription status updated
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/Envelope' }
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Subscription' }
  /subscription/unregister:
    post:
      summary: Revoke subscription
      description: |
        Revoke subscription. Tenant routes require x-orgcode; /stat health checks do not. Route class Tier D
        (p95 1500ms, p99 3000ms).
      x-route-class: Tier D
      x-qps-target: 10
      x-concurrency-target: 5
      x-latency-p95-ms: 1500
      x-latency-p99-ms: 3000
      parameters:
        - $ref: '#/components/parameters/OrgHeader'
        - $ref: '#/components/parameters/CostCentreHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SubscriptionUnregisterRequest' }
      responses:
        '200':
          description: Subscription revoked
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/Envelope' }
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Subscription' }
  /subscription/test:
    post:
      summary: Send a test delivery
      description: |
        Send a test delivery. Tenant routes require x-orgcode; /stat health checks do not. Route class Tier
        D (p95 2000ms, p99 4000ms).
      x-route-class: Tier D
      x-qps-target: 10
      x-concurrency-target: 5
      x-latency-p95-ms: 2000
      x-latency-p99-ms: 4000
      parameters:
        - $ref: '#/components/parameters/OrgHeader'
        - $ref: '#/components/parameters/CostCentreHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SubscriptionTestRequest' }
      responses:
        '200':
          description: Test delivery sent
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/Envelope' }
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          subscription_id: { type: string }
                          status: { type: string }
  /subscription/get:
    post:
      summary: Get subscription
      description: |
        Get subscription. Tenant routes require x-orgcode; /stat health checks do not. Org-scoped reads may
        return 404 for non-associated callers (anti-enumeration). Route class Tier B (p95 400ms).
      x-route-class: Tier B
      x-qps-target: 30
      x-concurrency-target: 10
      x-latency-p95-ms: 400
      parameters:
        - $ref: '#/components/parameters/OrgHeader'
        - $ref: '#/components/parameters/CostCentreHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SubscriptionGetRequest' }
      responses:
        '200':
          description: Subscription record
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/Envelope' }
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Subscription' }
  /subscription/list:
    post:
      summary: List subscriptions
      description: |
        List subscriptions. Tenant routes require x-orgcode; /stat health checks do not. Paginated with
        limit/next_token (default 8; clamp 1–256). Route class Tier B (p95 400ms).
      x-route-class: Tier B
      x-qps-target: 30
      x-concurrency-target: 10
      x-latency-p95-ms: 400
      parameters:
        - $ref: '#/components/parameters/OrgHeader'
        - $ref: '#/components/parameters/CostCentreHeader'
      requestBody:
        required: false
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SubscriptionListRequest' }
      responses:
        '200':
          description: Subscription list
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/Envelope' }
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/SubscriptionListResponse' }
  /notification/template/set:
    post:
      operationId: notificationTemplateSet
      summary: Create or update a notification template (G92)
      x-route-class: Tier A
      x-qps-target: 50
      x-concurrency-target: 200
      x-latency-p95-ms: 500
      parameters:
        - $ref: '#/components/parameters/OrgHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                template_id: { type: string }
                event_type: { type: string }
                channel: { type: string }
                subject_template: { type: string }
                body_template: { type: string }
                status: { type: string }
                priority: { type: string }
      responses:
        '200':
          description: Template created or updated
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/Envelope' }
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          template_id: { type: string }
                          status: { type: string }
  /notification/template/get:
    post:
      operationId: notificationTemplateGet
      summary: Get a notification template by ID
      x-route-class: Tier B
      x-qps-target: 200
      x-concurrency-target: 500
      x-latency-p95-ms: 300
      parameters:
        - $ref: '#/components/parameters/OrgHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [template_id]
              properties:
                template_id: { type: string }
      responses:
        '200':
          description: Template record
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/Envelope' }
                  - type: object
                    properties:
                      data:
                        type: object
  /notification/template/list:
    post:
      operationId: notificationTemplateList
      summary: List notification templates
      x-route-class: Tier B
      x-qps-target: 100
      x-concurrency-target: 500
      x-latency-p95-ms: 500
      parameters:
        - $ref: '#/components/parameters/OrgHeader'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                limit: { type: integer }
                next_token: { type: object }
      responses:
        '200':
          description: Template list
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/Envelope' }
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          items: { type: array, items: { type: object } }
                          next_token: { type: object, nullable: true }
  /notification/template/status/set:
    post:
      operationId: notificationTemplateStatusSet
      summary: Update notification template status
      x-route-class: Tier A
      x-qps-target: 50
      x-concurrency-target: 200
      x-latency-p95-ms: 500
      parameters:
        - $ref: '#/components/parameters/OrgHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [template_id, status]
              properties:
                template_id: { type: string }
                status: { type: string }
                expected_revision: { type: string }
      responses:
        '200':
          description: Template status updated
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/Envelope' }
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          template_id: { type: string }
                          status: { type: string }
  /notification/send:
    post:
      operationId: notificationSend
      summary: Queue a notification for delivery (G92)
      x-route-class: Tier A
      x-qps-target: 100
      x-concurrency-target: 500
      x-latency-p95-ms: 500
      parameters:
        - $ref: '#/components/parameters/OrgHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                template_id: { type: string }
                event_type: { type: string }
                channel: { type: string }
                recipient: { type: object }
                data: { type: object }
      responses:
        '200':
          description: Notification queued
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/Envelope' }
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          notification_id: { type: string }
                          status: { type: string }
  /notification/log/list:
    post:
      operationId: notificationLogList
      summary: List notification delivery logs
      x-route-class: Tier B
      x-qps-target: 100
      x-concurrency-target: 500
      x-latency-p95-ms: 500
      parameters:
        - $ref: '#/components/parameters/OrgHeader'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                event_type: { type: string }
                limit: { type: integer }
                next_token: { type: object }
      responses:
        '200':
          description: Notification log list
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/Envelope' }
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          items: { type: array, items: { type: object } }
                          next_token: { type: object, nullable: true }
