openapi: 3.1.0
info:
  title: Frag Public API
  version: "1.0.0"
  description: |
    Accept one payment from many coins, tokens, and chains. Receive one clean settlement.
    All endpoints live under `/api/public/v1/*` and require a bearer secret key
    (`sk_test_…` or `sk_live_…`) except where noted.
  contact:
    name: Frag
    url: https://frag.dev
servers:
  - url: https://frag.dev
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Payment intents
  - name: Refunds
  - name: Payouts
  - name: Webhook endpoints
  - name: Tokens
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: sk_test_… / sk_live_…
  parameters:
    IdempotencyKey:
      in: header
      name: Idempotency-Key
      required: false
      schema: { type: string, maxLength: 255 }
      description: Safe-retry key. Replays within 24h return the original response.
  schemas:
    Error:
      type: object
      properties:
        error: { type: string }
        code: { type: string }
        details: { type: object, additionalProperties: true }
    PaymentIntent:
      type: object
      properties:
        id: { type: string, format: uuid }
        amount: { type: string }
        currency: { type: string }
        status:
          type: string
          enum: [created, requires_payment, quoted, awaiting_signature, executing, partially_filled, settled, failed, expired, cancelled, refunded]
        checkout_url: { type: string, format: uri }
        expires_at: { type: string, format: date-time }
        created_at: { type: string, format: date-time }
        metadata: { type: object, additionalProperties: true }
    Refund:
      type: object
      properties:
        id: { type: string, format: uuid }
        payment_intent_id: { type: string, format: uuid }
        amount_usd: { type: number }
        chain: { type: string }
        token: { type: string }
        destination: { type: string }
        reason: { type: string }
        status: { type: string, enum: [pending, paid, failed, cancelled] }
        tx_hash: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
        paid_at: { type: string, nullable: true }
    Payout:
      type: object
      properties:
        id: { type: string, format: uuid }
        payment_intent_id: { type: string, nullable: true }
        rail: { type: string }
        destination: { type: string }
        chain: { type: string }
        token: { type: string }
        amount: { type: string }
        currency: { type: string }
        status: { type: string, enum: [queued, pending, paid, failed, paused] }
        tx_hash: { type: string, nullable: true }
        failure_reason: { type: string, nullable: true }
        attempts: { type: integer }
        next_attempt_at: { type: string, nullable: true }
        paid_at: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
    WebhookEndpoint:
      type: object
      properties:
        id: { type: string }
        url: { type: string, format: uri }
        events: { type: array, items: { type: string } }
        enabled: { type: boolean }
        created_at: { type: string, format: date-time }
        signing_secret:
          type: string
          description: Returned only in create + rotate-secret responses. Store immediately.
    Token:
      type: object
      properties:
        chain: { type: string }
        address: { type: string }
        symbol: { type: string }
        name: { type: string }
        decimals: { type: integer }
        verified: { type: boolean }
paths:
  /api/public/v1/payment-intents:
    post:
      tags: [Payment intents]
      summary: Create a payment intent
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount]
              properties:
                amount: { type: number, minimum: 0.01, maximum: 1000000 }
                currency: { type: string, default: USD }
                settlement_chain: { type: string }
                settlement_token: { type: string }
                settlement_address: { type: string }
                customer_email: { type: string, format: email }
                expires_in_seconds: { type: integer, minimum: 60, maximum: 2592000 }
                metadata: { type: object, additionalProperties: true }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PaymentIntent' }
        '400': { description: Invalid input, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '401': { description: Unauthorized }
        '403': { description: forbidden }
        '409': { description: Idempotency key mismatch }
    get:
      tags: [Payment intents]
      summary: List payment intents
      parameters:
        - { in: query, name: limit, schema: { type: integer, maximum: 100, default: 20 } }
        - { in: query, name: status, schema: { type: string } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/PaymentIntent' } }
  /api/public/v1/payment-intents/{id}:
    parameters: [{ in: path, name: id, required: true, schema: { type: string, format: uuid } }]
    get:
      tags: [Payment intents]
      summary: Retrieve a payment intent
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/PaymentIntent' } } } }
        '404': { description: Not found }
  /api/public/v1/payment-intents/{id}/cancel:
    parameters: [{ in: path, name: id, required: true, schema: { type: string, format: uuid } }]
    post:
      tags: [Payment intents]
      summary: Cancel a payment intent
      responses:
        '200': { description: Cancelled, content: { application/json: { schema: { $ref: '#/components/schemas/PaymentIntent' } } } }
        '409': { description: Cannot cancel in current status }
  /api/public/v1/payment-intents/{id}/transactions:
    parameters: [{ in: path, name: id, required: true, schema: { type: string, format: uuid } }]
    get:
      tags: [Payment intents]
      summary: List transaction legs contributing to this intent
      responses:
        '200': { description: OK }
  /api/public/v1/payment-intents/{id}/route:
    parameters: [{ in: path, name: id, required: true, schema: { type: string, format: uuid } }]
    get:
      tags: [Payment intents]
      summary: List router candidates and the one chosen
      responses:
        '200': { description: OK }
  /api/public/v1/refunds:
    post:
      tags: [Refunds]
      summary: Create a refund
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [intent_id, amount_usd, destination]
              properties:
                intent_id: { type: string, format: uuid }
                amount_usd: { type: number }
                destination: { type: string }
                reason: { type: string }
      responses:
        '201': { description: Created, content: { application/json: { schema: { $ref: '#/components/schemas/Refund' } } } }
        '422': { description: Refund exceeds refundable balance }
    get:
      tags: [Refunds]
      summary: List refunds
      parameters:
        - { in: query, name: payment_intent_id, schema: { type: string, format: uuid } }
        - { in: query, name: limit, schema: { type: integer, maximum: 200, default: 50 } }
      responses:
        '200': { description: OK }
  /api/public/v1/refunds/{id}:
    parameters: [{ in: path, name: id, required: true, schema: { type: string, format: uuid } }]
    get:
      tags: [Refunds]
      summary: Retrieve a refund
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Refund' } } } }
  /api/public/v1/payouts:
    get:
      tags: [Payouts]
      summary: List payouts
      parameters:
        - { in: query, name: status, schema: { type: string } }
        - { in: query, name: payment_intent_id, schema: { type: string, format: uuid } }
        - { in: query, name: limit, schema: { type: integer, maximum: 200, default: 50 } }
      responses:
        '200': { description: OK }
  /api/public/v1/payouts/{id}:
    parameters: [{ in: path, name: id, required: true, schema: { type: string, format: uuid } }]
    get:
      tags: [Payouts]
      summary: Retrieve a payout
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Payout' } } } }
  /api/public/v1/payouts/{id}/replay:
    parameters: [{ in: path, name: id, required: true, schema: { type: string, format: uuid } }]
    post:
      tags: [Payouts]
      summary: Requeue a failed payout for another dispatch attempt
      responses:
        '200': { description: OK }
  /api/public/v1/webhook-endpoints:
    post:
      tags: [Webhook endpoints]
      summary: Create a webhook endpoint
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url: { type: string, format: uri }
                events: { type: array, items: { type: string } }
                enabled: { type: boolean, default: true }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WebhookEndpoint' }
    get:
      tags: [Webhook endpoints]
      summary: List webhook endpoints
      responses:
        '200': { description: OK }
  /api/public/v1/webhook-endpoints/{id}:
    parameters: [{ in: path, name: id, required: true, schema: { type: string } }]
    get: { tags: [Webhook endpoints], summary: Retrieve endpoint, responses: { '200': { description: OK } } }
    patch:
      tags: [Webhook endpoints]
      summary: Update an endpoint
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                url: { type: string, format: uri }
                events: { type: array, items: { type: string } }
                enabled: { type: boolean }
      responses: { '200': { description: OK } }
    delete: { tags: [Webhook endpoints], summary: Delete endpoint, responses: { '200': { description: OK } } }
  /api/public/v1/webhook-endpoints/{id}/rotate-secret:
    parameters: [{ in: path, name: id, required: true, schema: { type: string } }]
    post:
      tags: [Webhook endpoints]
      summary: Rotate the signing secret. Returned in plaintext exactly once.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WebhookEndpoint' }
  /api/public/v1/tokens:
    get:
      tags: [Tokens]
      summary: List supported tokens (unauthenticated)
      security: []
      parameters:
        - { in: query, name: chain, schema: { type: string } }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: '#/components/schemas/Token' } }
