openapi: 3.1.0
info:
  title: FlyPay Merchant API
  version: 1.0.0
  description: Merchant-facing IDR API. V1 order creation currently supports QRIS only. All request and response amounts are normal Rupiah values; minor units are internal and are never part of this contract. Use the branded API domain matching your merchant credentials.
servers:
  - url: https://api.flypay.asia
    description: Production merchant API
security:
  - MerchantApiKey: []
tags:
  - name: Health
  - name: Orders
  - name: Disbursements
  - name: Balance
  - name: Beneficiaries
  - name: Settlements
  - name: Master Data
  - name: Reports
  - name: Team
paths:
  /v1/brand:
    get:
      tags: [Health]
      summary: Resolve the brand for the current API domain
      security: []
      responses:
        "200": {description: Brand metadata.}
  /healthz:
    get:
      tags: [Health]
      summary: Merchant API health check
      security: []
      responses:
        "200":
          description: Service is healthy.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
  /v1/orders:
    get:
      tags: [Orders]
      summary: List merchant orders
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          $ref: "#/components/responses/OrderList"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      tags: [Orders]
      summary: Create QRIS payment order
      description: >-
        Use a stable merchant_order_id and Idempotency-Key with a byte-identical body on retry.
        The same key replays the original response; a different key with the same confirmed
        merchant_order_id returns the existing order (200). On timeout, VENDOR_ERROR,
        or ORDER_PENDING_CONFIRMATION, query GET /v1/orders?merchant_order_id=<id> and
        reconcile before requesting another QR. X-Signature is required when the merchant
        has a webhook secret configured.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/XSignature"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateOrderRequest"
      responses:
        "200":
          description: Existing confirmed order returned for the same merchant order ID.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrderResponseEnvelope"
        "201":
          description: Order created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrderResponseEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          description: ORDER_PENDING_CONFIRMATION; the existing vendor result is unresolved.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
  /v1/orders/{ref_code}:
    get:
      tags: [Orders]
      summary: Get order by reference code
      parameters:
        - $ref: "#/components/parameters/RefCode"
      responses:
        "200":
          description: Order found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OrderResponseEnvelope"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /v1/orders/{ref_code}/refund:
    post:
      tags: [Orders]
      summary: Create refund for a paid order
      parameters:
        - $ref: "#/components/parameters/RefCode"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateRefundRequest"
      responses:
        "201":
          description: Refund created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GenericObjectEnvelope"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
  /v1/orders/{ref_code}/events:
    get:
      tags: [Orders]
      summary: List the order status timeline
      parameters: [{$ref: "#/components/parameters/RefCode"}]
      responses:
        "200": {$ref: "#/components/responses/ObjectListNoMeta"}
        "404": {$ref: "#/components/responses/NotFound"}
  /v1/orders/{ref_code}/fetch-status:
    post:
      tags: [Orders]
      summary: Reconcile an order status with the selected gateway
      parameters: [{$ref: "#/components/parameters/RefCode"}]
      responses:
        "200": {description: Current order status.}
        "404": {$ref: "#/components/responses/NotFound"}
  /v1/disbursements:
    get:
      tags: [Disbursements]
      summary: List merchant disbursements
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          $ref: "#/components/responses/ObjectList"
    post:
      tags: [Disbursements]
      summary: Create disbursement
      description: Available only for merchants using WALLET settlement type.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateDisbursementRequest"
      responses:
        "201":
          description: Disbursement created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GenericObjectEnvelope"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"
  /v1/disbursements/{ref_code}:
    get:
      tags: [Disbursements]
      summary: Get disbursement by reference code
      parameters:
        - $ref: "#/components/parameters/RefCode"
      responses:
        "200":
          description: Disbursement found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GenericObjectEnvelope"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /v1/disbursements/import:
    post:
      tags: [Disbursements]
      summary: Import bank or e-wallet disbursements from CSV
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file, idempotency_prefix]
              properties:
                file: {type: string, format: binary}
                idempotency_prefix:
                  type: string
                  description: Stable prefix used to deduplicate every CSV row when an import is retried.
      responses:
        "200": {description: Import validation and queueing result.}
        "400": {$ref: "#/components/responses/BadRequest"}
  /v1/balance:
    get:
      tags: [Balance]
      summary: Get merchant wallet balance
      description: Available only for merchants using WALLET settlement type.
      responses:
        "200":
          description: Balance snapshot.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BalanceEnvelope"
        "403":
          $ref: "#/components/responses/Forbidden"
  /v1/balance-history:
    get:
      tags: [Balance]
      summary: List wallet balance movements
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200": {$ref: "#/components/responses/ObjectList"}
  /v1/balance-history/{id}:
    get:
      tags: [Balance]
      summary: Get a wallet balance movement
      parameters: [{$ref: "#/components/parameters/UUID"}]
      responses:
        "200": {description: Balance movement.}
        "404": {$ref: "#/components/responses/NotFound"}
  /v1/beneficiaries:
    get:
      tags: [Beneficiaries]
      summary: List beneficiaries
      responses:
        "200":
          $ref: "#/components/responses/ObjectListNoMeta"
    post:
      tags: [Beneficiaries]
      summary: Create beneficiary
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateBeneficiaryRequest"
      responses:
        "201":
          description: Beneficiary created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GenericObjectEnvelope"
  /v1/beneficiaries/{id}:
    get:
      tags: [Beneficiaries]
      summary: Get beneficiary by ID
      parameters:
        - $ref: "#/components/parameters/UUID"
      responses:
        "200":
          description: Beneficiary found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GenericObjectEnvelope"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      tags: [Beneficiaries]
      summary: Delete beneficiary
      parameters:
        - $ref: "#/components/parameters/UUID"
      responses:
        "200":
          description: Beneficiary deleted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DeleteEnvelope"
  /v1/settlements:
    get:
      tags: [Settlements]
      summary: List merchant settlements
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          $ref: "#/components/responses/ObjectList"
  /v1/settlements/{id}:
    get:
      tags: [Settlements]
      summary: Get settlement by ID
      parameters:
        - $ref: "#/components/parameters/UUID"
      responses:
        "200":
          description: Settlement found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GenericObjectEnvelope"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /v1/banks:
    get:
      tags: [Master Data]
      summary: List enabled bank destinations
      responses:
        "200": {$ref: "#/components/responses/ObjectListNoMeta"}
  /v1/ewallets:
    get:
      tags: [Master Data]
      summary: List enabled e-wallet destinations
      responses:
        "200": {$ref: "#/components/responses/ObjectListNoMeta"}
  /v1/reports/profits:
    get:
      tags: [Reports]
      summary: Get merchant-visible financial totals
      responses:
        "200": {description: Merchant financial summary.}
  /v1/reports/export:
    post:
      tags: [Reports]
      summary: Queue an asynchronous CSV export
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type]
              properties:
                type: {type: string, enum: [orders, settlements, disbursements, balance_history]}
                date_from: {type: string, format: date}
                date_to: {type: string, format: date}
      responses:
        "201": {description: Export queued.}
  /v1/reports/exports:
    get:
      tags: [Reports]
      summary: List export jobs
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200": {$ref: "#/components/responses/ObjectList"}
  /v1/reports/exports/{id}/download:
    get:
      tags: [Reports]
      summary: Download a completed export
      parameters: [{$ref: "#/components/parameters/UUID"}]
      responses:
        "200": {description: CSV export file.}
        "404": {$ref: "#/components/responses/NotFound"}
  /v1/my/users:
    get:
      tags: [Team]
      summary: List merchant users
      responses:
        "200": {$ref: "#/components/responses/ObjectListNoMeta"}
    post:
      tags: [Team]
      summary: Add or link a merchant user
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [full_name, email, role]
              properties:
                full_name: {type: string}
                email: {type: string, format: email}
                phone_number: {type: string}
                role: {type: string, enum: [owner, finance, ops, viewer]}
      responses:
        "201": {description: Merchant user linked.}
  /v1/my/users/{user_id}/role:
    patch:
      tags: [Team]
      summary: Change a merchant user's role
      parameters:
        - name: user_id
          in: path
          required: true
          schema: {type: string, format: uuid}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [role]
              properties:
                role: {type: string, enum: [owner, finance, ops, viewer]}
      responses:
        "200": {description: Role updated.}
    delete:
      tags: [Team]
      summary: Remove a merchant user
      parameters:
        - name: user_id
          in: path
          required: true
          schema: {type: string, format: uuid}
      responses:
        "200": {description: User removed.}
components:
  securitySchemes:
    MerchantApiKey:
      type: apiKey
      in: header
      name: X-API-Key
  parameters:
    Page:
      name: page
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        default: 1
    Limit:
      name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20
    RefCode:
      name: ref_code
      in: path
      required: true
      schema:
        type: string
        example: ORD-20260529-ABC123
    UUID:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
      description: Stable per intended order; reuse with the exact same raw body on retry.
    XSignature:
      name: X-Signature
      in: header
      required: false
      schema:
        type: string
      description: HMAC signature. Required when the merchant has a webhook secret configured.
  responses:
    BadRequest:
      description: Invalid request.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    Unauthorized:
      description: Authentication failed.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    Forbidden:
      description: Access denied.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    UnprocessableEntity:
      description: Request cannot be processed.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    ServiceUnavailable:
      description: Required vendor/service unavailable.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    OrderList:
      description: Paginated order list.
      content:
        application/json:
          schema:
            allOf:
              - $ref: "#/components/schemas/SuccessEnvelope"
              - type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/OrderResponse"
                  meta:
                    $ref: "#/components/schemas/Meta"
    ObjectList:
      description: Paginated object list.
      content:
        application/json:
          schema:
            allOf:
              - $ref: "#/components/schemas/SuccessEnvelope"
              - type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
                  meta:
                    $ref: "#/components/schemas/Meta"
    ObjectListNoMeta:
      description: Object list.
      content:
        application/json:
          schema:
            allOf:
              - $ref: "#/components/schemas/SuccessEnvelope"
              - type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
  schemas:
    HealthResponse:
      type: object
      properties:
        status:
          type: string
          example: ok
        service:
          type: string
          example: merchant-api
    SuccessEnvelope:
      type: object
      required: [success]
      properties:
        success:
          type: boolean
          example: true
    ErrorEnvelope:
      type: object
      required: [success, error]
      properties:
        success:
          type: boolean
          example: false
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
            message:
              type: string
    Meta:
      type: object
      properties:
        page:
          type: integer
        limit:
          type: integer
        total:
          type: integer
          format: int64
        total_pages:
          type: integer
    GenericObjectEnvelope:
      allOf:
        - $ref: "#/components/schemas/SuccessEnvelope"
        - type: object
          properties:
            data:
              type: object
              additionalProperties: true
    DeleteEnvelope:
      allOf:
        - $ref: "#/components/schemas/SuccessEnvelope"
        - type: object
          properties:
            data:
              type: object
              properties:
                deleted:
                  type: boolean
                  example: true
    CreateOrderRequest:
      type: object
      required: [merchant_order_id, amount, payment_method]
      properties:
        merchant_order_id:
          type: string
          example: order-test-001
        amount:
          type: integer
          format: int64
          minimum: 1
          description: Whole Rupiah amount.
          example: 100000
        payment_method:
          type: string
          enum: [QRIS]
          example: QRIS
        customer_name:
          type: string
        customer_email:
          type: string
          format: email
        customer_phone:
          type: string
        notify_url:
          type: string
          format: uri
        return_url:
          type: string
          format: uri
        expire_minutes:
          type: integer
          default: 60
        metadata:
          type: object
          additionalProperties: true
    OrderResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
        ref_code:
          type: string
        merchant_order_id:
          type: string
        amount:
          type: number
          description: Rupiah amount. Responses can contain up to two decimal places for fee calculations.
          example: 100000
        mdr_total:
          type: number
          description: Total MDR in Rupiah.
        merchant_net:
          type: number
          description: Merchant net amount in Rupiah.
        status:
          type: string
          example: PENDING
        qris_string:
          type: string
          nullable: true
        qr_url:
          type: string
          nullable: true
        expires_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
    OrderResponseEnvelope:
      allOf:
        - $ref: "#/components/schemas/SuccessEnvelope"
        - type: object
          properties:
            data:
              $ref: "#/components/schemas/OrderResponse"
    CreateRefundRequest:
      type: object
      required: [amount, idempotency_key]
      properties:
        amount:
          type: integer
          format: int64
          minimum: 1
          description: Whole Rupiah amount.
        reason:
          type: string
        reason_detail:
          type: string
        idempotency_key:
          type: string
    CreateDisbursementRequest:
      type: object
      required: [idempotency_key, channel, amount]
      properties:
        idempotency_key:
          type: string
        channel:
          type: string
          enum: [BANK, EWALLET]
        bank_short_code:
          type: string
          example: BCA
        account_number:
          type: string
        account_name:
          type: string
        ewallet_provider:
          type: string
        ewallet_phone:
          type: string
        beneficiary_id:
          type: string
          format: uuid
        amount:
          type: integer
          format: int64
          minimum: 1
          description: Whole Rupiah amount.
        notify_url:
          type: string
          format: uri
    BalanceEnvelope:
      allOf:
        - $ref: "#/components/schemas/SuccessEnvelope"
        - type: object
          properties:
            data:
              type: object
              properties:
                merchant_id:
                  type: string
                  format: uuid
                balance:
                  type: number
                  description: Available balance in Rupiah.
    CreateBeneficiaryRequest:
      type: object
      required: [channel]
      properties:
        label:
          type: string
        channel:
          type: string
          enum: [BANK, EWALLET]
        bank_short_code:
          type: string
        account_number:
          type: string
        ewallet_provider:
          type: string
        ewallet_phone:
          type: string
        account_name:
          type: string
        is_default:
          type: boolean
