openapi: 3.0.3
info:
  title: RhysTalk Partner API
  version: 0.1.3
  description: |
    Partner-facing contract for the API Gateway REST API managed by this repository.

    The public surface is the API Gateway/Lambda facade, not the internal Portal
    ECS routes that some Lambdas call behind the scenes. All operations are
    JSON POST endpoints under the deployed API Gateway stage.

    **Flow Blocks V2 post-call outcomes (RTALK-197 / RTALK-219):** declarative IVR
    recipes may declare `emit_post_call_leaf`; partner-visible HTTPS callbacks and
    payload envelopes are **not** part of this Gateway contract. See
    `docs/RTALK197_POST_CALL_INTENT_V2_PARTNER_DELIVERY.md` and
    `docs/RTALK219_POST_CALL_INTENT_AUDIT_AND_JOINT_PROOF.md` in the Management Portal
    repository for the off-call delivery shape, redaction rules, and operator audit
    surfaces (tenant session Portal — not Gateway).

    Production connection details:
    - API base URL: https://hbgxb4yrq5.execute-api.eu-west-2.amazonaws.com/prod
    - AWS region: eu-west-2
    - Cognito User Pool ID: eu-west-2_c3m6uFFDm
    - Partner API Cognito app client ID: e5qfla5ih1g9el16k026kirlc
x-rhystalk-production:
  apiBaseUrl: https://hbgxb4yrq5.execute-api.eu-west-2.amazonaws.com/prod
  awsRegion: eu-west-2
  cognitoUserPoolId: eu-west-2_c3m6uFFDm
  cognitoIssuer: https://cognito-idp.eu-west-2.amazonaws.com/eu-west-2_c3m6uFFDm
  partnerApiUserPoolClientId: e5qfla5ih1g9el16k026kirlc
servers:
  - url: https://hbgxb4yrq5.execute-api.eu-west-2.amazonaws.com/prod
    description: Production API Gateway REST API stage.
  - url: https://{apiId}.execute-api.{region}.amazonaws.com/{stage}
    description: AWS API Gateway REST API stage.
    variables:
      apiId:
        default: hbgxb4yrq5
        description: API Gateway REST API id from the production stack output RestApiId.
      region:
        default: eu-west-2
      stage:
        default: prod
tags:
  - name: Balance
    description: Balance read/write operations.
  - name: Usage
    description: Read-only usage and billed call history.
  - name: Accounts
    description: Account lifecycle and caller ID management.
  - name: Calls
    description: Call setup operations.
security:
  - cognitoBearer: []
paths:
  /get_balance:
    post:
      tags: [Balance]
      operationId: getBalance
      summary: Get account balance
      description: |
        Reads the current CGRateS monetary balances for a 12-digit account code.
        Tenant, product, and currency context are derived from the authenticated
        partner credential and authorizer claims.
      parameters:
        - $ref: '#/components/parameters/CorrelationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GetBalanceRequest'
            examples:
              basic:
                value:
                  account: "123456789012"
                  product_id: prepaid_uk
                  currency: GBP
      responses:
        "200":
          description: Balance found.
          headers:
            X-Correlation-Id:
              $ref: '#/components/headers/CorrelationId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GetBalanceResponse'
        "400":
          $ref: '#/components/responses/BadRequest'
        "403":
          $ref: '#/components/responses/Forbidden'
        "404":
          $ref: '#/components/responses/NotFound'
        "500":
          $ref: '#/components/responses/ServerError'

  /call_history:
    post:
      tags: [Usage]
      operationId: callHistory
      summary: Recent billed call history for an account
      description: |
        Returns up to **10** most recent CGRateS `cdrs` rows for the account within the **last 30 days**
        (UTC). Each row exposes only **called_number** (`destination`), **duration_seconds** (from `usage`),
        and **amount_charged** (`cost`). Tenant/product/currency scope matches other partner balance APIs:
        the account must exist in portal `accounts_ext` for the authenticated tenant/product/currency or the
        API returns **403**. Empty history returns **200** with an empty `calls` array.
      parameters:
        - $ref: '#/components/parameters/CorrelationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CallHistoryRequest'
            examples:
              basic:
                value:
                  account: "123456789012"
                  product_id: prepaid_uk
                  currency: GBP
      responses:
        "200":
          description: History retrieved (possibly empty).
          headers:
            X-Correlation-Id:
              $ref: '#/components/headers/CorrelationId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CallHistoryResponse'
        "400":
          $ref: '#/components/responses/BadRequest'
        "403":
          $ref: '#/components/responses/Forbidden'
        "500":
          $ref: '#/components/responses/ServerError'

  /topup:
    post:
      tags: [Balance]
      operationId: topup
      summary: Add monetary balance
      description: |
        Adds credit to an existing account. This operation is idempotent by
        tenant, operation, and Idempotency-Key.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/CorrelationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BalanceMutationRequest'
            examples:
              topup:
                value:
                  account: "123456789012"
                  amount: 10.0
                  product_id: prepaid_uk
                  currency: GBP
      responses:
        "200":
          description: Balance added.
          headers:
            X-Correlation-Id:
              $ref: '#/components/headers/CorrelationId'
            X-Transaction-Id:
              $ref: '#/components/headers/TransactionId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BalanceMutationResponse'
        "400":
          $ref: '#/components/responses/BadRequest'
        "403":
          $ref: '#/components/responses/Forbidden'
        "404":
          $ref: '#/components/responses/NotFound'
        "409":
          $ref: '#/components/responses/IdempotencyConflict'
        "500":
          $ref: '#/components/responses/ServerError'

  /debit_balance:
    post:
      tags: [Balance]
      operationId: debitBalance
      summary: Debit monetary balance
      description: |
        Debits credit from an existing account. This operation is idempotent by
        tenant, operation, and Idempotency-Key.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/CorrelationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BalanceMutationRequest'
            examples:
              debit:
                value:
                  account: "123456789012"
                  amount: 1.25
                  product_id: prepaid_uk
                  currency: GBP
      responses:
        "200":
          description: Balance debited.
          headers:
            X-Correlation-Id:
              $ref: '#/components/headers/CorrelationId'
            X-Transaction-Id:
              $ref: '#/components/headers/TransactionId'
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/BalanceMutationResponse'
                  - type: object
                    properties:
                      cgrid:
                        type: string
                        description: CGRateS event id for the debit.
        "400":
          $ref: '#/components/responses/BadRequest'
        "403":
          $ref: '#/components/responses/Forbidden'
        "409":
          $ref: '#/components/responses/IdempotencyConflict'
        "500":
          $ref: '#/components/responses/ServerError'

  /create_account:
    post:
      tags: [Accounts]
      operationId: createAccount
      summary: Create account
      description: |
        Creates a portal account and provisions the billing state through the
        Portal internal route. The Lambda facade owns partner idempotency.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/CorrelationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateAccountRequest'
            examples:
              with_cli:
                value:
                  product_id: prepaid_uk
                  initial_balance: 0
                  requires_pin: true
                  external_user_id: "+441234567890"
      responses:
        "201":
          description: Account created.
          headers:
            X-Correlation-Id:
              $ref: '#/components/headers/CorrelationId'
            X-Transaction-Id:
              $ref: '#/components/headers/TransactionId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateAccountResponse'
        "400":
          $ref: '#/components/responses/BadRequest'
        "403":
          $ref: '#/components/responses/Forbidden'
        "409":
          $ref: '#/components/responses/Conflict'
        "502":
          $ref: '#/components/responses/UpstreamError'
        "503":
          $ref: '#/components/responses/ServiceUnavailable'

  /delete_account:
    post:
      tags: [Accounts]
      operationId: deleteAccount
      summary: Deactivate account
      description: |
        Soft-deletes or deactivates an account through the Portal internal
        route. This does not hard-delete account rows.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/CorrelationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DeleteAccountRequest'
            examples:
              deactivate:
                value:
                  account: "123456789012"
                  product_id: prepaid_uk
                  reason: customer requested closure
      responses:
        "200":
          description: Account deactivated, or already inactive.
          headers:
            X-Correlation-Id:
              $ref: '#/components/headers/CorrelationId'
            X-Transaction-Id:
              $ref: '#/components/headers/TransactionId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeleteAccountResponse'
        "400":
          $ref: '#/components/responses/BadRequest'
        "403":
          $ref: '#/components/responses/Forbidden'
        "404":
          $ref: '#/components/responses/NotFound'
        "409":
          $ref: '#/components/responses/IdempotencyConflict'
        "502":
          $ref: '#/components/responses/UpstreamError'
        "503":
          $ref: '#/components/responses/ServiceUnavailable'

  /change_cli:
    post:
      tags: [Accounts]
      operationId: changeCli
      summary: Change account caller ID
      description: |
        Updates the account external_user_id, also referred to as CLI or caller
        id. Clients may send either external_user_id or cli. If both are sent,
        they must match.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/CorrelationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChangeCliRequest'
            examples:
              change_cli:
                value:
                  account: "123456789012"
                  product_id: prepaid_uk
                  cli: "+441234567890"
      responses:
        "200":
          description: CLI updated.
          headers:
            X-Correlation-Id:
              $ref: '#/components/headers/CorrelationId'
            X-Transaction-Id:
              $ref: '#/components/headers/TransactionId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChangeCliResponse'
        "400":
          $ref: '#/components/responses/BadRequest'
        "403":
          $ref: '#/components/responses/Forbidden'
        "404":
          $ref: '#/components/responses/NotFound'
        "409":
          $ref: '#/components/responses/Conflict'
        "502":
          $ref: '#/components/responses/UpstreamError'
        "503":
          $ref: '#/components/responses/ServiceUnavailable'

  /setup_call:
    post:
      tags: [Calls]
      operationId: setupCall
      summary: Store number translation for a call
      description: |
        Stores a short-lived number translation for the calling path. The default
        partner contract is Cognito authorization plus Lambda partner RBAC. An
        API Gateway key gate is not part of the default partner contract.
      security:
        - cognitoBearer: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/CorrelationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SetupCallRequest'
            examples:
              setup:
                value:
                  a_number: "+441234567890"
                  b_number: "+442071234567"
                  access_number: "+442038355820"
                  account_number: "123456789012"
                  ttl: 300
                  product_id: prepaid_uk
                  currency: GBP
      responses:
        "200":
          description: Call setup saved.
          headers:
            X-Correlation-Id:
              $ref: '#/components/headers/CorrelationId'
            X-Transaction-Id:
              $ref: '#/components/headers/TransactionId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SetupCallResponse'
        "400":
          $ref: '#/components/responses/BadRequest'
        "403":
          $ref: '#/components/responses/Forbidden'
        "409":
          $ref: '#/components/responses/IdempotencyConflict'
        "500":
          $ref: '#/components/responses/ServerError'

components:
  securitySchemes:
    cognitoBearer:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        Cognito JWT accepted by API Gateway.

        Production Cognito details:
        - User Pool ID: eu-west-2_c3m6uFFDm
        - Issuer: https://cognito-idp.eu-west-2.amazonaws.com/eu-west-2_c3m6uFFDm
        - Partner API app client ID: e5qfla5ih1g9el16k026kirlc

        Use an ID token unless RhysTalk approves a different token type for the
        integration. Token claims derive tenant scope; the token must include
        custom:tenant_id for partner access.
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema:
        type: string
        minLength: 1
        maxLength: 128
        pattern: '^[\x20-\x7E]+$'
      description: Printable ASCII key used to replay the same response for the same request body.
    CorrelationId:
      name: X-Correlation-Id
      in: header
      required: false
      schema:
        type: string
        maxLength: 128
      description: Optional caller-provided correlation id for logs and responses.
  headers:
    CorrelationId:
      description: Correlation id used for request tracing.
      schema:
        type: string
    TransactionId:
      description: Transaction id, normally the Idempotency-Key for idempotent writes.
      schema:
        type: string

  schemas:
    AccountCode12:
      type: string
      description: Account code. Digits are normalized to a left-padded 12-digit value by the Lambda.
      example: "123456789012"
    MoneyAmount:
      type: number
      minimum: 0
      exclusiveMinimum: true
      example: 10.0
    Currency:
      type: string
      pattern: '^[A-Z]{3}$'
      example: GBP
    ErrorResponse:
      type: object
      additionalProperties: true
      properties:
        error:
          oneOf:
            - type: string
            - type: object
              additionalProperties: true
        message:
          type: string
        detail:
          type: string
        correlation_id:
          type: string
        transaction_id:
          type: string
    GetBalanceRequest:
      type: object
      additionalProperties: false
      required: [account]
      properties:
        account:
          $ref: '#/components/schemas/AccountCode12'
        product_id:
          type: string
          maxLength: 64
          description: Optional when product context is supplied by token claims.
          example: prepaid_uk
        currency:
          $ref: '#/components/schemas/Currency'
    CallHistoryRequest:
      description: Same shape as get_balance; identifies the account under tenant/product/currency scope.
      allOf:
        - $ref: '#/components/schemas/GetBalanceRequest'
    CallHistoryRow:
      type: object
      additionalProperties: false
      required: [called_number, duration_seconds, amount_charged]
      properties:
        called_number:
          type: string
          description: Dialed destination from CGRateS `cdrs.destination`.
        duration_seconds:
          type: integer
          minimum: 0
          description: Billable duration in whole seconds (derived from `usage`).
        amount_charged:
          type: number
          description: Monetary cost from `cdrs.cost` for the rated session.
    CallHistoryResponse:
      type: object
      additionalProperties: false
      required: [tenant_id, product_id, currency, account_code_12, calls]
      properties:
        tenant_id:
          type: string
        product_id:
          type: string
        currency:
          $ref: '#/components/schemas/Currency'
        account_code_12:
          $ref: '#/components/schemas/AccountCode12'
        calls:
          type: array
          maxItems: 10
          items:
            $ref: '#/components/schemas/CallHistoryRow'
    BalanceMutationRequest:
      type: object
      additionalProperties: false
      required: [account, amount]
      properties:
        account:
          $ref: '#/components/schemas/AccountCode12'
        amount:
          $ref: '#/components/schemas/MoneyAmount'
        product_id:
          type: string
          maxLength: 64
          description: Optional when product context is supplied by token claims.
          example: prepaid_uk
        currency:
          $ref: '#/components/schemas/Currency'
    GetBalanceResponse:
      type: object
      additionalProperties: true
      required: [tenant_id, product_id, currency, account_code_12, total_balance]
      properties:
        tenant_id:
          type: string
          example: rhysatalk
        product_id:
          type: string
          example: prepaid_uk
        currency:
          $ref: '#/components/schemas/Currency'
        account:
          type: string
        account_code_12:
          $ref: '#/components/schemas/AccountCode12'
        tenant:
          type: string
        total_balance:
          type: number
          example: 12.5
        balances:
          type: array
          items:
            type: object
            additionalProperties: true
            properties:
              id:
                type: string
              value:
                type: number
              weight:
                type: number
        balance_map:
          type: object
          additionalProperties: true
    BalanceMutationResponse:
      type: object
      additionalProperties: true
      properties:
        id:
          type: integer
        result:
          nullable: true
        error:
          nullable: true
        tenant_id:
          type: string
        product_id:
          type: string
        currency:
          $ref: '#/components/schemas/Currency'
        account_code_12:
          $ref: '#/components/schemas/AccountCode12'
        balance_before:
          type: number
        balance_after:
          type: number
        ledger_logged:
          type: boolean
        transaction_id:
          type: string
        correlation_id:
          type: string
    CreateAccountRequest:
      type: object
      additionalProperties: false
      required: [product_id]
      properties:
        product_id:
          type: string
          maxLength: 64
          example: prepaid_uk
        initial_balance:
          type: number
          minimum: 0
          default: 0
        requires_pin:
          type: boolean
          default: true
        external_user_id:
          type: string
          nullable: true
          maxLength: 255
          description: Optional caller id or partner user identifier.
        cli:
          type: string
          nullable: true
          maxLength: 255
          deprecated: true
          description: Alias for external_user_id when supported by the Lambda.
    CreateAccountResponse:
      type: object
      additionalProperties: true
      properties:
        tenant_id:
          type: string
        product_id:
          type: string
        currency:
          $ref: '#/components/schemas/Currency'
        account_code_12:
          $ref: '#/components/schemas/AccountCode12'
        initial_balance:
          type: number
        requires_pin:
          type: boolean
        external_user_id:
          type: string
          nullable: true
        pin_12:
          type: string
          nullable: true
        correlation_id:
          type: string
        transaction_id:
          type: string
    DeleteAccountRequest:
      type: object
      additionalProperties: false
      required: [account]
      properties:
        account:
          $ref: '#/components/schemas/AccountCode12'
        product_id:
          type: string
          nullable: true
          maxLength: 64
        reason:
          type: string
          nullable: true
          maxLength: 500
    DeleteAccountResponse:
      type: object
      additionalProperties: true
      properties:
        tenant_id:
          type: string
        account_code_12:
          $ref: '#/components/schemas/AccountCode12'
        product_id:
          type: string
          nullable: true
        status:
          type: string
          example: deactivated
        deactivated_at:
          type: string
          format: date-time
        reason:
          type: string
          nullable: true
        correlation_id:
          type: string
        transaction_id:
          type: string
    ChangeCliRequest:
      type: object
      additionalProperties: false
      required: [account]
      properties:
        account:
          $ref: '#/components/schemas/AccountCode12'
        product_id:
          type: string
          nullable: true
          maxLength: 64
        external_user_id:
          type: string
          maxLength: 255
          description: New caller id or external user identifier.
        cli:
          type: string
          maxLength: 255
          description: Alias for external_user_id. If both are sent, they must match.
    ChangeCliResponse:
      type: object
      additionalProperties: true
      properties:
        tenant_id:
          type: string
        account_code_12:
          $ref: '#/components/schemas/AccountCode12'
        product_id:
          type: string
          nullable: true
        external_user_id:
          type: string
        correlation_id:
          type: string
        transaction_id:
          type: string
    SetupCallRequest:
      type: object
      additionalProperties: false
      required: [a_number, b_number, access_number, account_number, ttl]
      properties:
        a_number:
          type: string
          description: Caller number. Digits are required; a leading plus is preserved.
          example: "+441234567890"
        b_number:
          type: string
          description: Destination number.
          example: "+442071234567"
        access_number:
          type: string
          description: Access number used for the call.
          example: "+442038355820"
        account_number:
          $ref: '#/components/schemas/AccountCode12'
        ttl:
          type: integer
          minimum: 1
          maximum: 86400
          description: Translation lifetime in seconds.
          example: 300
        product_id:
          type: string
          maxLength: 64
          description: Optional when product context is supplied by token claims.
          example: prepaid_uk
        currency:
          $ref: '#/components/schemas/Currency'
    SetupCallResponse:
      type: object
      additionalProperties: true
      properties:
        message:
          type: string
          example: Call setup saved successfully
        insertId:
          type: integer
        tenant_id:
          type: string
        product_id:
          type: string
        currency:
          $ref: '#/components/schemas/Currency'
        account_code_12:
          $ref: '#/components/schemas/AccountCode12'
        correlation_id:
          type: string
        transaction_id:
          type: string

  responses:
    BadRequest:
      description: Invalid request body, invalid idempotency key, tenant_id in body, or validation error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Forbidden:
      description: Missing or invalid authorization, inactive membership, tenant inactive, or insufficient route permission.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    NotFound:
      description: Requested account or resource was not found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Conflict:
      description: Business conflict such as duplicate external_user_id, or idempotency conflict.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    IdempotencyConflict:
      description: Idempotency-Key has already been used with a different request body.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: Idempotency key already used with a different request
    UpstreamError:
      description: The Lambda could not reach or complete the Portal internal request.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    ServiceUnavailable:
      description: A required internal dependency or Lambda configuration is unavailable.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    ServerError:
      description: Unexpected server error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
