
openapi: 3.1.0
info:
  title: FX Platform API
  version: "1.0.0"
  description: >
    Sprint 1 REST API for core auth, KYC, wallets, funding, trading fills, P2P skeleton,
    admin stubs, and mock PSP. All timestamps are UTC with microseconds.
servers:
  - url: http://127.0.0.1:8000/api/v1
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: Sanctum
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema: { type: string }
      description: Unique key per write to guarantee idempotency
  schemas:
    ApiMessage:
      type: object
      properties:
        message: { type: string }
    User:
      type: object
      properties:
        id: { type: integer }
        email: { type: string, format: email }
        surname: { type: string }
        othernames: { type: string }
        phone: { type: string }
        address: { type: string }
        kyc_status: { type: string, enum: [not_submitted, submitted, validating, verified, declined] }
    Wallet:
      type: object
      properties:
        id: { type: integer }
        user_id: { type: integer }
        currency: { type: string, enum: [USD, GBP, EUR, KES, SAR, AED] }
        balance: { type: string }
        available: { type: string }
        status: { type: string }
    LedgerEntry:
      type: object
      properties:
        id: { type: integer }
        wallet_id: { type: integer }
        ref_type: { type: string }
        ref_id: { type: string }
        debit: { type: string }
        credit: { type: string }
        balance_after: { type: string }
        currency: { type: string }
        description: { type: string }
        idempotency_key: { type: string }
        created_at: { type: string, format: date-time }
    Deposit:
      type: object
      properties:
        id: { type: integer }
        wallet_id: { type: integer }
        method: { type: string, enum: [card, mobile_money, bank_wire] }
        amount: { type: string }
        currency: { type: string }
        status: { type: string }
        provider: { type: string }
        provider_ref: { type: string }
        idempotency_key: { type: string }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    Withdrawal:
      type: object
      properties:
        id: { type: integer }
        wallet_id: { type: integer }
        method: { type: string, enum: [bank, mobile_money] }
        amount: { type: string }
        currency: { type: string }
        status: { type: string }
        destination_details_json: { type: object }
        idempotency_key: { type: string }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    Order:
      type: object
      properties:
        id: { type: integer }
        user_id: { type: integer }
        wallet_id: { type: integer }
        symbol: { type: string }
        side: { type: string, enum: [buy, sell] }
        type: { type: string, enum: [market, limit] }
        qty: { type: string }
        price: { type: string, nullable: true }
        status: { type: string }
        client_order_id: { type: string, nullable: true }
        ts_in: { type: string, format: date-time, nullable: true }
        ts_out: { type: string, format: date-time, nullable: true }
    Trade:
      type: object
      properties:
        id: { type: integer }
        order_id: { type: integer }
        fill_id: { type: string }
        symbol: { type: string }
        side: { type: string }
        qty: { type: string }
        price: { type: string }
        fee: { type: string }
        ts: { type: string, format: date-time }
    NewsItem:
      type: object
      properties:
        id: { type: integer }
        title: { type: string }
        source: { type: string }
        url: { type: string }
        body: { type: string }
        symbols_json:
          type: array
          items: { type: string }
        published_at: { type: string, format: date-time }
paths:
  /auth/email/request-code:
    post:
      summary: Request email verification code
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email: { type: string, format: email }
              required: [email]
      responses:
        "200":
          description: Code sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  expires_in_minutes: { type: integer }
  /auth/email/verify-code:
    post:
      summary: Verify email with 6 digit code and get registration token
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email: { type: string, format: email }
                code: { type: string }
              required: [email, code]
      responses:
        "200":
          description: Verified
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  email: { type: string, format: email }
                  registration_token: { type: string }
                  token_expires_in_minutes: { type: integer }
  /auth/register:
    post:
      summary: Register user with verified token
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                registration_token: { type: string }
                email: { type: string, format: email }
                surname: { type: string }
                othernames: { type: string }
                phone: { type: string }
                address: { type: string }
                password: { type: string }
                password_confirmation: { type: string }
              required: [registration_token, email, surname, othernames, phone, password, password_confirmation]
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  token: { type: string }
                  user: { $ref: "#/components/schemas/User" }
  /auth/login:
    post:
      summary: Login and get token
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email: { type: string, format: email }
                password: { type: string }
              required: [email, password]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  token: { type: string }
                  user: { $ref: "#/components/schemas/User" }
  /me:
    get:
      security: [{ bearerAuth: [] }]
      summary: Current user profile
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/User" }
  /wallets:
    get:
      security: [{ bearerAuth: [] }]
      summary: List wallets
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Wallet" }
    post:
      security: [{ bearerAuth: [] }]
      summary: Create wallet
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                currency: { type: string }
              required: [currency]
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Wallet" }
  /wallets/{id}/ledger:
    get:
      security: [{ bearerAuth: [] }]
      summary: Wallet ledger, paginated
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: OK
  /deposits/card/intent:
    post:
      security: [{ bearerAuth: [] }]
      summary: Create card deposit intent
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                wallet_id: { type: integer }
                amount: { type: number }
                currency: { type: string }
              required: [wallet_id, amount, currency]
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  deposit_id: { type: integer }
                  client_secret: { type: string }
  /deposits/mobile/init:
    post:
      security: [{ bearerAuth: [] }]
      summary: Init mobile money deposit
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                wallet_id: { type: integer }
                amount: { type: number }
                currency: { type: string }
                msisdn: { type: string }
              required: [wallet_id, amount, currency, msisdn]
      responses:
        "201":
          description: Created
  /deposits/{id}:
    get:
      security: [{ bearerAuth: [] }]
      summary: Get deposit
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Deposit" }
  /funding/psp/webhook:
    post:
      summary: PSP webhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                provider: { type: string }
                provider_ref: { type: string }
                method: { type: string }
                amount: { type: number }
                currency: { type: string }
                status: { type: string, enum: [succeeded, failed] }
                deposit_id: { type: integer }
              required: [provider, provider_ref, method, amount, currency, status]
      responses:
        "200":
          description: OK
  /withdrawals:
    post:
      security: [{ bearerAuth: [] }]
      summary: Request withdrawal
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                wallet_id: { type: integer }
                amount: { type: number }
                currency: { type: string }
                method: { type: string }
                destination: { type: object }
              required: [wallet_id, amount, currency, method, destination]
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  withdrawal_id: { type: integer }
                  status: { type: string }
  /withdrawals/{id}:
    get:
      security: [{ bearerAuth: [] }]
      summary: Get withdrawal
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Withdrawal" }
  /kyc/init:
    post:
      security: [{ bearerAuth: [] }]
      summary: Start KYC
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                doc_type: { type: string }
                doc_number: { type: string }
                expiry: { type: string, format: date }
              required: [doc_type, doc_number]
      responses:
        "200":
          description: OK
  /kyc/upload:
    post:
      security: [{ bearerAuth: [] }]
      summary: Upload KYC images
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                front: { type: string, format: binary }
                back: { type: string, format: binary }
                selfie: { type: string, format: binary }
      responses:
        "200":
          description: OK
  /kyc/status:
    get:
      security: [{ bearerAuth: [] }]
      summary: Get KYC status
      responses:
        "200":
          description: OK
  /kyc/provider/webhook:
    post:
      summary: KYC provider webhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                user_id: { type: integer }
                status: { type: string, enum: [validating, verified, declined] }
                face_match_score: { type: number }
                provider_ref: { type: string }
              required: [user_id, status]
      responses:
        "200":
          description: OK
  /trading/fills/callback:
    post:
      summary: Trading fill callback, HMAC signed
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                orderId: { type: string }
                clientOrderId: { type: string }
                userId: { type: integer }
                walletId: { type: integer }
                symbol: { type: string }
                side: { type: string, enum: [buy, sell] }
                qty: { type: number }
                price: { type: number }
                fee: { type: number }
                ts: { type: string, format: date-time }
              required: [orderId, userId, walletId, symbol, side, qty, price, ts]
      responses:
        "200":
          description: OK
  /mock-psp/card/pay:
    post:
      summary: Mock PSP card payment
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                deposit_id: { type: integer }
                outcome: { type: string, enum: [succeeded, failed] }
              required: [deposit_id, outcome]
      responses:
        "200":
          description: OK
  /mock-psp/mobile/pay:
    post:
      summary: Mock PSP mobile payment
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                deposit_id: { type: integer }
                outcome: { type: string, enum: [succeeded, failed] }
              required: [deposit_id, outcome]
      responses:
        "200":
          description: OK
  /mock-psp/withdrawals/settle:
    post:
      summary: Mock PSP settle withdrawal
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                withdrawal_id: { type: integer }
                outcome: { type: string, enum: [succeeded, failed] }
              required: [withdrawal_id, outcome]
      responses:
        "200":
          description: OK
