openapi: 3.0.3
info:
  title: Payylo API
  description: |
    API for creating and managing group payments with flexible slot distribution.

    ## Getting an API key
    API keys are created from the [**Payylo dashboard**](https://dashboard.payylo.com)
    under **Integration → API Keys** (the secret is shown only once at creation).
    There is no public endpoint to create keys.

    ## Before you start
    The merchant must have a payment provider (e.g. Stripe) connected in the dashboard,
    otherwise created group payments cannot be settled.
  version: 1.0.0

servers:
  - url: https://api.payylo.com
    description: Payylo API

tags:
  - name: Group Payments
    description: Create and manage group payments with flexible slot distribution
  - name: Refunds
    description: Refund captured payments (full group or specific slots)
  - name: Webhooks
    description: Register endpoints to receive signed event notifications

security:
  - ApiKeyAuth: []

paths:
  /v1/group-payments:
    get:
      tags:
        - Group Payments
      summary: List group payments
      description: |
        List the authenticated merchant's group payments, most recent first
        (ordered by `created_at` descending).

        Supports pagination (`limit`/`offset`) and optional filtering by state
        and creation date — useful for a booking engine reconciling payments in
        bulk. Slots are omitted for efficiency; use
        `GET /v1/group-payments/{id}` for the full detail of one group payment.
      operationId: listGroupPayments
      security:
        - ApiKeyAuth: []
      parameters:
        - name: limit
          in: query
          description: Maximum number of items to return (1–100). Defaults to 20 if omitted.
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
        - name: offset
          in: query
          description: Number of items to skip (for pagination). Defaults to 0 if omitted.
          required: false
          schema:
            type: integer
            minimum: 0
        - name: state
          in: query
          description: Filter by group payment state.
          required: false
          schema:
            type: string
            enum:
              - draft
              - expired
              - cancelled
              - open
              - partially_preauthorised
              - preauthorised
              - capturing
              - partially_paid
              - paid
              - capture_failed
              - refunding
              - partially_refunded
              - refunded
              - refund_failed
        - name: created_after
          in: query
          description: Only group payments created at or after this ISO 8601 datetime (inclusive).
          required: false
          schema:
            type: string
            format: date-time
        - name: created_before
          in: query
          description: Only group payments created at or before this ISO 8601 datetime (inclusive).
          required: false
          schema:
            type: string
            format: date-time
      responses:
        '200':
          description: Paginated list of group payments
          content:
            application/json:
              schema:
                type: object
                properties:
                  group_payments:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          format: uuid
                        merchant_id:
                          type: string
                          format: uuid
                        group_name:
                          type: string
                        merchant_order_id:
                          type: string
                        total_amount:
                          type: number
                          description: Amount in currency units (not cents).
                        currency:
                          type: string
                        expires_at:
                          type: string
                          format: date-time
                        state:
                          type: string
                        metadata:
                          type: object
                          additionalProperties: true
                        created_at:
                          type: string
                          format: date-time
                        updated_at:
                          type: string
                          format: date-time
                        checkout_url:
                          type: string
                  pagination:
                    type: object
                    properties:
                      limit:
                        type: integer
                      offset:
                        type: integer
                      total:
                        type: integer
                        description: Total group payments matching the filter.
                      has_more:
                        type: boolean
        '400':
          description: Invalid query parameters
        '401':
          description: Missing or invalid API key
    post:
      tags:
        - Group Payments
      summary: Create a group payment
      description: |
        Create a new group payment with automatic payment slot creation.

        Supports two distribution modes:
        - **equally**: Automatically divides the total amount equally among slots
        - **custom**: Allows specifying exact amounts for each slot

        The response includes a checkout URL that can be shared with customers.
        The `expires_at` value must be in the future and cannot exceed 7 days from the creation time.
      operationId: createGroupPayment
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateGroupPaymentRequest'
            examples:
              equally_simple:
                summary: '1. Equally - Team Dinner (5 slots)'
                value:
                  group_name: 'Team Dinner'
                  total_amount: 150.00
                  currency: 'EUR'
                  expires_at: '2025-11-14T23:59:59Z'
                  distribution:
                    type: 'equally'
                    number_of_slots: 5
                    slot_metadata:
                      - metadata:
                          participant: 'Alice'
                      - metadata:
                          participant: 'Bob'
                      - metadata:
                          participant: 'Charlie'
                      - metadata:
                          participant: 'Diana'
                      - metadata:
                          participant: 'Eve'
              custom_participants:
                summary: '2. Custom - Project Payment (3 slots)'
                value:
                  group_name: 'Project Payment'
                  total_amount: 1000.00
                  currency: 'EUR'
                  expires_at: '2025-11-13T20:00:00Z'
                  distribution:
                    type: 'custom'
                    slots:
                      - amount: 500.00
                        metadata:
                          participant: 'Alice'
                      - amount: 300.00
                        metadata:
                          participant: 'Bob'
                      - amount: 200.00
                        metadata:
                          participant: 'Charlie'
              birthday_gift:
                summary: '3. Equally with Metadata - Birthday Gift (10 slots)'
                value:
                  group_name: 'Birthday Gift'
                  total_amount: 250.00
                  currency: 'EUR'
                  expires_at: '2025-11-12T18:30:00Z'
                  metadata:
                    event: 'birthday'
                    person: 'John'
                  distribution:
                    type: 'equally'
                    number_of_slots: 10
      responses:
        '201':
          description: Group payment created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateGroupPaymentResponse'
              examples:
                equally_response:
                  summary: 'Equally - 5 slots of €30 each'
                  value:
                    group_payment:
                      id: 550e8400-e29b-41d4-a716-446655440000
                      merchant_id: merchant_123
                      group_name: 'Team Dinner'
                      merchant_order_id: '550e8400-e29b-41d4-a716-446655440000'
                      total_amount: 150.00
                      currency: 'EUR'
                      expires_at: '2025-11-14T23:59:59Z'
                      state: 'open'
                      metadata: {}
                      created_at: '2024-11-04T10:00:00Z'
                      updated_at: '2024-11-04T10:00:00Z'
                    payment_slots:
                      - id: slot_1
                        merchant_id: merchant_123
                        group_payment_id: 550e8400-e29b-41d4-a716-446655440000
                        sequence_no: 1
                        amount: 30.00
                        currency: EUR
                        state: unassigned
                        metadata: {}
                        created_at: '2024-11-04T10:00:00Z'
                        updated_at: '2024-11-04T10:00:00Z'
                      - id: slot_2
                        merchant_id: merchant_123
                        group_payment_id: 550e8400-e29b-41d4-a716-446655440000
                        sequence_no: 2
                        amount: 30.00
                        currency: EUR
                        state: unassigned
                        metadata: {}
                        created_at: '2024-11-04T10:00:00Z'
                        updated_at: '2024-11-04T10:00:00Z'
                      - id: slot_3
                        merchant_id: merchant_123
                        group_payment_id: 550e8400-e29b-41d4-a716-446655440000
                        sequence_no: 3
                        amount: 30.00
                        currency: EUR
                        state: unassigned
                        metadata: {}
                        created_at: '2024-11-04T10:00:00Z'
                        updated_at: '2024-11-04T10:00:00Z'
                      - id: slot_4
                        merchant_id: merchant_123
                        group_payment_id: 550e8400-e29b-41d4-a716-446655440000
                        sequence_no: 4
                        amount: 30.00
                        currency: EUR
                        state: unassigned
                        metadata: {}
                        created_at: '2024-11-04T10:00:00Z'
                        updated_at: '2024-11-04T10:00:00Z'
                      - id: slot_5
                        merchant_id: merchant_123
                        group_payment_id: 550e8400-e29b-41d4-a716-446655440000
                        sequence_no: 5
                        amount: 30.00
                        currency: EUR
                        state: unassigned
                        metadata: {}
                        created_at: '2024-11-04T10:00:00Z'
                        updated_at: '2024-11-04T10:00:00Z'
                    checkout_url: 'http://localhost:3001/g/550e8400-e29b-41d4-a716-446655440000'
                custom_response:
                  summary: 'Custom - 3 slots with different amounts'
                  value:
                    group_payment:
                      id: 660e8400-e29b-41d4-a716-446655440001
                      merchant_id: merchant_123
                      group_name: 'Project Payment'
                      merchant_order_id: '660e8400-e29b-41d4-a716-446655440001'
                      total_amount: 1000.00
                      currency: 'EUR'
                      expires_at: '2025-11-13T20:00:00Z'
                      state: 'open'
                      metadata: {}
                      created_at: '2024-11-04T10:00:00Z'
                      updated_at: '2024-11-04T10:00:00Z'
                    payment_slots:
                      - id: slot_1
                        merchant_id: merchant_123
                        group_payment_id: 660e8400-e29b-41d4-a716-446655440001
                        sequence_no: 1
                        amount: 500.00
                        currency: EUR
                        state: unassigned
                        metadata:
                          participant: 'Alice'
                        created_at: '2024-11-04T10:00:00Z'
                        updated_at: '2024-11-04T10:00:00Z'
                      - id: slot_2
                        merchant_id: merchant_123
                        group_payment_id: 660e8400-e29b-41d4-a716-446655440001
                        sequence_no: 2
                        amount: 300.00
                        currency: EUR
                        state: unassigned
                        metadata:
                          participant: 'Bob'
                        created_at: '2024-11-04T10:00:00Z'
                        updated_at: '2024-11-04T10:00:00Z'
                      - id: slot_3
                        merchant_id: merchant_123
                        group_payment_id: 660e8400-e29b-41d4-a716-446655440001
                        sequence_no: 3
                        amount: 200.00
                        currency: EUR
                        state: unassigned
                        metadata:
                          participant: 'Charlie'
                        created_at: '2024-11-04T10:00:00Z'
                        updated_at: '2024-11-04T10:00:00Z'
                    checkout_url: 'http://localhost:3002/g/660e8400-e29b-41d4-a716-446655440001'
        '400':
          description: Bad Request - Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                validation_error:
                  summary: Validation error
                  value:
                    error: 'Validation error: total_amount: Amount must be greater than 0'
                invalid_expiry:
                  summary: Invalid expiry date (in the past)
                  value:
                    error: 'Expiry date must be in the future'
                expiry_exceeds_limit:
                  summary: Invalid expiry date (beyond 7-day limit)
                  value:
                    error: 'Expiry date cannot be more than 7 days in the future'
                sum_mismatch:
                  summary: Custom slots sum mismatch
                  value:
                    error: 'Sum of slot amounts (95.00) must equal total amount (100.00)'
        '401':
          description: Unauthorized - Missing or invalid authentication
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: Merchant ID is required
        '409':
          description: Conflict - Duplicate merchant_order_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: A group payment with this merchant_order_id already exists
        '500':
          description: Internal Server Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: Internal server error

  /v1/group-payments/{id}:
    get:
      tags:
        - Group Payments
      summary: Get a group payment
      description: |
        Retrieve a group payment and its payment slots by id.
        Only returns the group payment if it belongs to the authenticated merchant.
        Use this to poll the `state` of the group payment and each slot after creation.
      operationId: getGroupPayment
      security:
        - ApiKeyAuth: []
      parameters:
        - name: id
          in: path
          required: true
          description: The group payment id (UUID)
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Group payment found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateGroupPaymentResponse'
        '401':
          description: Unauthorized - Missing or invalid authentication
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: Merchant ID is required
        '404':
          description: Not Found - Group payment does not exist for this merchant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: Group payment not found
        '500':
          description: Internal Server Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: Internal server error

  /v1/group-payments/{id}/pay:
    post:
      tags:
        - Group Payments
      summary: Initiate a payment
      description: |
        Start a payment for one or more slots of a group payment. The response tells
        you which gateway to render via `psp_code` — branch your checkout UI on it:

        - **`stripe`** — render Stripe Elements with `client_secret`,
          `publishable_key` and `stripe_account_id`, and confirm the card with
          Stripe.js.
        - **`rede`** — render the Evervault card field with `evervault_team_id` /
          `evervault_app_id`, then send the encrypted card to
          `POST /v1/group-payments/{id}/confirm`.
        - **`redsys`** — POST the returned `redsys_form` (`url` + `body`) to Redsys;
          the shopper completes payment on the bank's page and is returned to your
          site.

        If `slot_ids` is omitted, all unassigned slots are paid.
      operationId: payGroupPayment
      security:
        - ApiKeyAuth: []
      parameters:
        - name: id
          in: path
          required: true
          description: The group payment id (UUID)
          schema:
            type: string
            format: uuid
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PayRequest'
            examples:
              pay_all_remaining:
                summary: Pay all unassigned slots
                value:
                  customer_email: 'buyer@example.com'
                  customer_name: 'Jordi Pujol'
              pay_specific_slots:
                summary: Pay specific slots
                value:
                  customer_email: 'buyer@example.com'
                  customer_name: 'Jordi Pujol'
                  slot_ids:
                    - 7b2e8f10-3c4d-4e5f-9a1b-2c3d4e5f6a7b
      responses:
        '201':
          description: |
            Payment initiated. Which fields are present depends on `psp_code`
            (see the per-gateway examples).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PayResponse'
              examples:
                stripe:
                  summary: 'psp_code = stripe (embedded)'
                  value:
                    psp_code: stripe
                    payment_intent_id: 9c8b7a65-1234-4abc-9def-0123456789ab
                    client_secret: pi_3Q1abc2eZvKYlo2C1abc_secret_AbC123
                    stripe_account_id: acct_1Q0abcEXAMPLE
                    publishable_key: pk_test_51SoKbEXAMPLE
                    amount: 89.00
                    currency: EUR
                    slot_ids:
                      - 7b2e8f10-3c4d-4e5f-9a1b-2c3d4e5f6a7b
                    is_test_mode: true
                rede:
                  summary: 'psp_code = rede (embedded)'
                  value:
                    psp_code: rede
                    payment_intent_id: 9c8b7a65-1234-4abc-9def-0123456789ab
                    evervault_team_id: team_f7a30c6828d2
                    evervault_app_id: app_73e245cac3bc
                    amount: 89.00
                    currency: EUR
                    slot_ids:
                      - 7b2e8f10-3c4d-4e5f-9a1b-2c3d4e5f6a7b
                    is_test_mode: true
                redsys:
                  summary: 'psp_code = redsys (hosted redirect)'
                  value:
                    psp_code: redsys
                    payment_intent_id: 9c8b7a65-1234-4abc-9def-0123456789ab
                    redsys_form:
                      url: 'https://sis-t.redsys.es:25443/sis/realizarPago'
                      body:
                        Ds_SignatureVersion: HMAC_SHA256_V1
                        Ds_MerchantParameters: eyJEU19NRVJDSEFOVF9BTU9VTlQiOiI4OTAwIiwi...
                        Ds_Signature: K2x9eXamPLEsignatureBase64==
                    amount: 89.00
                    currency: EUR
                    slot_ids:
                      - 7b2e8f10-3c4d-4e5f-9a1b-2c3d4e5f6a7b
                    is_test_mode: true
        '400':
          description: Validation error (e.g. invalid slot_ids)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized - Missing or invalid authentication
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not Found - Group payment does not exist for this merchant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: |
            Conflict - the group payment is expired/not payable, there are no
            payable slots, or the merchant has no active payment gateway.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: Conflict
                message: 'Merchant has no active payment gateway. Connect one in the dashboard first.'
                version: v1
        '500':
          description: Internal Server Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/group-payments/{id}/confirm:
    post:
      tags:
        - Group Payments
      summary: '(Rede only) Confirm a card payment'
      description: |
        ⚠️ **Rede only** — this endpoint applies exclusively to `psp_code = rede`.
        Do not call it for `stripe` or `redsys`.

        Confirm a payment started by `/pay` when `psp_code = rede`. Send the card
        encrypted by the Evervault field on your page; on success the slot is
        authorised (`status: preauthorised`).

        **Which gateway am I on?** You don't detect it here — the `/pay` response
        tells you via `psp_code`. Call this endpoint **only when `psp_code = rede`**.
        Use the `payment_intent_id` from that same `/pay` response (the group payment
        id in the path is just routing; the payment is identified by
        `payment_intent_id`).

        `stripe` is confirmed client-side with Stripe.js, and `redsys` needs no
        confirm call (the bank's page completes the payment).
      operationId: confirmGroupPayment
      security:
        - ApiKeyAuth: []
      parameters:
        - name: id
          in: path
          required: true
          description: The group payment id (UUID)
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConfirmRequest'
      responses:
        '200':
          description: Card authorized; slot pre-authorised
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConfirmResponse'
              example:
                status: preauthorised
        '400':
          description: Validation error or card declined
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized - Missing or invalid authentication
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not Found - Payment intent does not exist for this merchant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal Server Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/group-payments/{id}/refund:
    post:
      tags:
        - Refunds
      summary: Refund a group payment (full or by slots)
      description: |
        Refund captured payments of a group payment. With **no body** (or an empty
        one) the whole group is refunded; pass `slot_ids` to refund only the
        captured payment intents covering those slots.

        Refunds are per captured payment intent (full-intent refunds). Supported
        for `rede` and `stripe`; `redsys` refunds via the API are not yet
        available. Send an `Idempotency-Key` header to make retries safe.

        State machine: the group moves to `refunding` while processing, then to
        `refunded` (all captured intents refunded), `partially_refunded` (some
        remain captured), or `refund_failed` (a PSP refund failed). A
        `group_payment.refunded` / `group_payment.partially_refunded` webhook event
        is emitted on success.
      operationId: refundGroupPayment
      security:
        - ApiKeyAuth: []
      parameters:
        - name: id
          in: path
          required: true
          description: The group payment id (UUID)
          schema:
            type: string
            format: uuid
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RefundRequest'
      responses:
        '200':
          description: Refund processed (see `failed` for any per-intent errors)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RefundResponse'
        '409':
          description: Nothing to refund / requested slots not captured
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized - Missing or invalid authentication
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Not Found - Group payment does not exist for this merchant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '502':
          description: All refunds were rejected by the payment provider
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RefundResponse'

  /v1/webhooks:
    get:
      tags:
        - Webhooks
      summary: List webhook endpoints
      operationId: listWebhookEndpoints
      security:
        - ApiKeyAuth: []
      responses:
        '200':
          description: The merchant's webhook endpoints (secret omitted)
          content:
            application/json:
              schema:
                type: object
                properties:
                  webhook_endpoints:
                    type: array
                    items:
                      $ref: '#/components/schemas/WebhookEndpoint'
    post:
      tags:
        - Webhooks
      summary: Register a webhook endpoint
      description: |
        Register an https endpoint to receive event notifications. The signing
        **secret is returned only once**, in this response — store it to verify
        deliveries.

        Each delivery is signed: `Payylo-Signature: t=<unix>,v1=<hmacSHA256>` over
        `"{t}.{rawBody}"` using your endpoint secret. Also sent:
        `Payylo-Event-Id` and `Payylo-Event-Type`. Respond `2xx` to acknowledge;
        non-2xx (or timeout) is retried with exponential backoff.
      operationId: createWebhookEndpoint
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateWebhookRequest'
            example:
              url: https://booking-engine.example.com/payylo/webhooks
              description: Reservation confirmations
              event_types:
                - group_payment.paid
                - group_payment.refunded
      responses:
        '201':
          description: Endpoint created (secret returned once)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookEndpointWithSecret'
        '400':
          description: Validation error (e.g. non-https url)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized - Missing or invalid authentication
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/webhooks/{id}:
    get:
      tags:
        - Webhooks
      summary: Get a webhook endpoint
      operationId: getWebhookEndpoint
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/WebhookId'
      responses:
        '200':
          description: The webhook endpoint
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookEndpoint'
        '404':
          description: Not Found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    patch:
      tags:
        - Webhooks
      summary: Update a webhook endpoint
      description: Update any of `url`, `description`, `enabled`, `event_types`.
      operationId: updateWebhookEndpoint
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/WebhookId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateWebhookRequest'
            example:
              enabled: false
      responses:
        '200':
          description: The updated webhook endpoint
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookEndpoint'
        '404':
          description: Not Found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    delete:
      tags:
        - Webhooks
      summary: Delete a webhook endpoint
      operationId: deleteWebhookEndpoint
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/WebhookId'
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  deleted:
                    type: boolean
                    example: true
        '404':
          description: Not Found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

components:
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: |
        A unique key (e.g. a UUID) that makes this POST safe to retry. Replaying
        the same key returns the original response instead of performing the
        action again. Reusing a key with a different body returns `422`.
      schema:
        type: string
        maxLength: 255
    WebhookId:
      name: id
      in: path
      required: true
      description: The webhook endpoint id (UUID)
      schema:
        type: string
        format: uuid

  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: API Key
      description: |
        API key authentication for server-to-server communication.
        Use format: `Authorization: Bearer sk_test_xxx` or `Authorization: Bearer sk_live_xxx`

  schemas:
    CreateGroupPaymentRequest:
      type: object
      required:
        - group_name
        - total_amount
        - currency
        - expires_at
        - distribution
      properties:
        group_name:
          type: string
          minLength: 1
          maxLength: 32
          description: Name of the group payment
        merchant_order_id:
          type: string
          readOnly: true
          description: Unique merchant order identifier generated automatically.
        total_amount:
          type: number
          format: double
          minimum: 0.01
          description: Total amount in currency units (e.g., 100.50 for €100.50)
        currency:
          type: string
          minLength: 3
          maxLength: 3
          pattern: '^[A-Z]{3}$'
          description: ISO 4217 currency code
        expires_at:
          type: string
          format: date-time
          description: ISO 8601 datetime when the payment expires (must be in the future and within 7 days)
        metadata:
          type: object
          additionalProperties: true
          description: Optional metadata as key-value pairs
        distribution:
          oneOf:
            - $ref: '#/components/schemas/EquallyDistribution'
            - $ref: '#/components/schemas/CustomDistribution'
          discriminator:
            propertyName: type
            mapping:
              equally: '#/components/schemas/EquallyDistribution'
              custom: '#/components/schemas/CustomDistribution'

    EquallyDistribution:
      type: object
      required:
        - type
        - number_of_slots
      properties:
        type:
          type: string
          enum: [equally]
          description: Distribution type
        number_of_slots:
          type: integer
          minimum: 1
          maximum: 30
          description: Number of slots to create with equal amounts
        slot_metadata:
          type: array
          description: Optional metadata for each slot (must have the same length as number_of_slots when provided)
          items:
            type: object
            properties:
              metadata:
                type: object
                additionalProperties: true
                description: Metadata for this slot

    CustomDistribution:
      type: object
      required:
        - type
        - slots
      properties:
        type:
          type: string
          enum: [custom]
          description: Distribution type
        slots:
          type: array
          minItems: 1
          maxItems: 30
          description: Array of slot configurations with specific amounts
          items:
            type: object
            required:
              - amount
            properties:
              amount:
                type: number
                format: double
                minimum: 0.01
                description: Amount for this slot in currency units
              metadata:
                type: object
                additionalProperties: true
                description: Optional metadata for this slot

    CreateGroupPaymentResponse:
      type: object
      required:
        - group_payment
        - payment_slots
        - checkout_url
      properties:
        group_payment:
          $ref: '#/components/schemas/GroupPayment'
        payment_slots:
          type: array
          items:
            $ref: '#/components/schemas/PaymentSlot'
        checkout_url:
          type: string
          format: uri
          description: Public URL for customers to complete the payment
          example: https://checkout.payylo.com/g/550e8400-e29b-41d4-a716-446655440000

    GroupPayment:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for the group payment
          example: 550e8400-e29b-41d4-a716-446655440000
        merchant_id:
          type: string
          description: Merchant identifier
          example: merchant_123
        group_name:
          type: string
          description: Name of the group payment
          example: Team Dinner
        merchant_order_id:
          type: string
          description: Merchant's order identifier
          example: ORDER-2024-001
        total_amount:
          type: number
          format: double
          description: Total amount in currency units
          example: 150.00
        currency:
          type: string
          description: ISO 4217 currency code
          example: EUR
        expires_at:
          type: string
          format: date-time
          description: Expiration datetime
          example: '2024-12-31T23:59:59Z'
        state:
          type: string
          enum:
            - draft
            - expired
            - cancelled
            - open
            - partially_preauthorised
            - preauthorised
            - capturing
            - partially_paid
            - paid
            - capture_failed
            - refunding
            - partially_refunded
            - refunded
            - refund_failed
          description: Current state of the group payment
          example: draft
        metadata:
          type: object
          additionalProperties: true
          description: Custom metadata
          example: {}
        created_at:
          type: string
          format: date-time
          description: Creation timestamp
          example: '2024-11-04T10:00:00Z'
        updated_at:
          type: string
          format: date-time
          description: Last update timestamp
          example: '2024-11-04T10:00:00Z'

    PaymentSlot:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for the slot
          example: slot_1
        merchant_id:
          type: string
          description: Merchant identifier
          example: merchant_123
        group_payment_id:
          type: string
          format: uuid
          description: Parent group payment ID
          example: 550e8400-e29b-41d4-a716-446655440000
        sequence_no:
          type: integer
          description: Slot sequence number (1-based)
          example: 1
        assigned_customer_id:
          type: string
          format: uuid
          nullable: true
          description: Customer assigned to this slot, or null if unassigned
          example: null
        amount:
          type: number
          format: double
          description: Slot amount in currency units
          example: 30.00
        currency:
          type: string
          description: ISO 4217 currency code
          example: EUR
        state:
          type: string
          enum:
            - unassigned
            - assigned
            - expired
            - preauthorised
            - captured
            - refunded
            - cancelled
          description: Current state of the slot
          example: unassigned
        metadata:
          type: object
          additionalProperties: true
          description: Custom metadata for this slot
          example: {}
        created_at:
          type: string
          format: date-time
          description: Creation timestamp
          example: '2024-11-04T10:00:00Z'
        updated_at:
          type: string
          format: date-time
          description: Last update timestamp
          example: '2024-11-04T10:00:00Z'

    ErrorResponse:
      type: object
      required:
        - error
        - message
        - version
      properties:
        error:
          type: string
          description: Short status text (e.g. "Bad Request", "Not Found")
          example: Bad Request
        message:
          type: string
          description: Human-readable error detail
          example: 'Validation error: total_amount: Amount must be greater than 0'
        version:
          type: string
          description: API version
          example: v1

    PayRequest:
      type: object
      required:
        - customer_email
      properties:
        customer_email:
          type: string
          format: email
          description: Payer identity. A customer is created or reused for the merchant.
        customer_name:
          type: string
          maxLength: 100
          description: Optional display name for the payer.
        slot_ids:
          type: array
          description: |
            Which slots to pay. If omitted, all currently unassigned slots are used
            ("pay everything that's left"). 1–30 items.
          minItems: 1
          maxItems: 30
          items:
            type: string
            format: uuid

    PayResponse:
      type: object
      description: |
        Gateway-agnostic payment-initiation response. `psp_code` tells you which
        gateway was selected; the gateway-specific fields below are only present for
        that gateway.
      required:
        - psp_code
        - payment_intent_id
        - amount
        - currency
      properties:
        psp_code:
          type: string
          enum: [stripe, rede, redsys]
          description: The active gateway selected for this merchant.
        payment_intent_id:
          type: string
          format: uuid
          description: Payylo payment intent id (used by /confirm and for tracking).
        amount:
          type: number
          description: Amount to pay, in major units (e.g. EUR).
          example: 89.00
        currency:
          type: string
          example: EUR
        slot_ids:
          type: array
          items:
            type: string
            format: uuid
        is_test_mode:
          type: boolean
        client_secret:
          type: string
          description: '`stripe` only — Stripe PaymentIntent client secret.'
        stripe_account_id:
          type: string
          description: '`stripe` only — connected account id (direct charges).'
        publishable_key:
          type: string
          description: '`stripe` only — platform publishable key.'
        evervault_team_id:
          type: string
          description: '`rede` only — Payylo public Evervault team id.'
        evervault_app_id:
          type: string
          description: '`rede` only — Payylo public Evervault app id.'
        redsys_form:
          type: object
          description: '`redsys` only — signed form to POST to the Redsys page.'
          properties:
            url:
              type: string
              description: Redsys endpoint to POST the form to.
              example: 'https://sis-t.redsys.es:25443/sis/realizarPago'
            body:
              type: object
              description: Hidden form fields (rendered as inputs and POSTed).
              properties:
                Ds_SignatureVersion:
                  type: string
                  example: HMAC_SHA256_V1
                Ds_MerchantParameters:
                  type: string
                  description: Base64-encoded payment parameters.
                Ds_Signature:
                  type: string
                  description: HMAC-SHA256 signature of the parameters.

    ConfirmRequest:
      type: object
      required:
        - payment_intent_id
        - card
      properties:
        payment_intent_id:
          type: string
          format: uuid
          description: The payment intent returned by /pay (psp_code = rede).
        card:
          type: object
          description: Card collected by Evervault. PAN and CVC arrive encrypted.
          required:
            - cardholder_name
            - encrypted_card_number
            - expiration_month
            - expiration_year
            - encrypted_security_code
          properties:
            cardholder_name:
              type: string
              minLength: 1
              maxLength: 100
            encrypted_card_number:
              type: string
              description: Evervault-encrypted PAN (ev:...).
            expiration_month:
              type: integer
              minimum: 1
              maximum: 12
            expiration_year:
              type: integer
              minimum: 2000
              maximum: 2100
            encrypted_security_code:
              type: string
              description: Evervault-encrypted CVC (ev:...).

    ConfirmResponse:
      type: object
      required:
        - status
      properties:
        status:
          type: string
          enum: [preauthorised]
          description: Slot state after a successful authorization.
          example: preauthorised

    RefundRequest:
      type: object
      description: |
        Optional. Omit (or send an empty body) to refund the whole group.
      properties:
        slot_ids:
          type: array
          description: Refund only the captured intents covering these slots.
          items:
            type: string
            format: uuid
          minItems: 1
          maxItems: 30

    RefundResponse:
      type: object
      properties:
        refunded_payment_intents:
          type: array
          items:
            type: string
            format: uuid
        refunded_slot_ids:
          type: array
          items:
            type: string
            format: uuid
        failed:
          type: array
          description: Per-intent failures (empty on full success).
          items:
            type: object
            properties:
              payment_intent_id:
                type: string
                format: uuid
              psp_code:
                type: string
                example: redsys
              error:
                type: string
                example: Redsys refunds are not yet supported via the API
        group_state:
          type: string
          enum: [refunded, partially_refunded, refund_failed]
          example: refunded

    WebhookEndpoint:
      type: object
      properties:
        id:
          type: string
          format: uuid
        url:
          type: string
          format: uri
          example: https://booking-engine.example.com/payylo/webhooks
        description:
          type: string
          nullable: true
        enabled:
          type: boolean
          example: true
        event_types:
          type: array
          description: Subscribed events. Empty = all events.
          items:
            type: string
          example:
            - group_payment.paid
            - group_payment.refunded
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    WebhookEndpointWithSecret:
      allOf:
        - $ref: '#/components/schemas/WebhookEndpoint'
        - type: object
          properties:
            secret:
              type: string
              description: Signing secret, returned ONLY at creation.
              example: whsec_3f9a...e21

    CreateWebhookRequest:
      type: object
      required:
        - url
      properties:
        url:
          type: string
          format: uri
          description: https endpoint (http allowed only for localhost).
        description:
          type: string
          maxLength: 255
        event_types:
          type: array
          description: Subset of supported events. Omit/empty = subscribe to all.
          items:
            type: string
            enum:
              - group_payment.partially_paid
              - group_payment.paid
              - group_payment.partially_refunded
              - group_payment.refunded
              - payment_intent.preauthorised
              - payment_intent.captured
              - payment_intent.refunded
              - payment_intent.failed

    UpdateWebhookRequest:
      type: object
      description: At least one field must be provided.
      properties:
        url:
          type: string
          format: uri
        description:
          type: string
          maxLength: 255
          nullable: true
        enabled:
          type: boolean
        event_types:
          type: array
          items:
            type: string
