openapi: 3.1.0
info:
  title: halfin API
  version: 1.0.0
  description: |-
    halfin merchant API for programmatic integrations.

    Use API keys in the X-API-Key header for merchant endpoints such as invoices, payouts, refunds, balances, and addresses. Rates, currencies, and webhook schemas are public reference surfaces.
  contact:
    name: halfin Engineering
    url: https://docs.halfin.xyz
  license:
    name: Proprietary
servers:
  - url: https://dashboard.halfin.xyz/api
    description: Production
tags:
  - name: merchant
    x-displayName: Merchant API
    description: Programmatic merchant API authenticated by API key.
  - name: public
    x-displayName: Public Data
    description: Unauthenticated public endpoints (rates, currencies).
  - name: webhooks
    x-displayName: Webhooks
    description: Merchant webhook event payloads.
security: []
paths:
  /v1/invoices:
    post:
      operationId: createInvoice
      tags:
        - merchant
      summary: Create a new payment invoice.
      description: |
        Creates an invoice with a deposit address for the specified currency.
        Returns a payment URL in `checkout_url` that the customer can visit
        to pay.

        Idempotency: when `idempotency_key` is supplied, a replay with the
        same key but a different request body returns `422` with code
        `idempotency_key_mismatch`. Replays with the same body are
        idempotent and return the original invoice.
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -X POST https://dashboard.halfin.xyz/api/v1/invoices \
              -H "Content-Type: application/json" \
              -H "X-API-Key: $HALFIN_API_KEY" \
              -d '{
                "currency": "BTC",
                "amount": "0.01000000",
                "description": "Order #0001",
                "idempotency_key": "order-0001"
              }'
        - lang: ts
          label: TypeScript
          source: |
            import { createHalfin, createInvoice } from '@halfin/sdk-merchant';

            const client = createHalfin({
              apiKey: process.env.HALFIN_API_KEY,
              baseUrl: 'https://dashboard.halfin.xyz/api',
            });
            const { data } = await createInvoice({
              client,
              body: {
                currency: 'BTC',
                amount: '0.01000000',
                description: 'Order #0001',
                idempotency_key: 'order-0001',
              },
            });
      security:
        - apiKeyAuth: []
      x-required-permission: invoices:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateInvoiceRequest"
      responses:
        "201":
          description: Invoice created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessEnvelope_InvoiceResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          description: |
            Idempotency replay rejected because the request body does not
            match the body stored on the original invoice. Error code:
            `idempotency_key_mismatch`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          $ref: "#/components/responses/GateOffline"
    get:
      operationId: listInvoices
      tags:
        - merchant
      summary: List invoices for the authenticated merchant.
      description: |
        Returns a paginated list of invoices scoped to the API key's
        environment. Supports filtering by status and currency.
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -G https://dashboard.halfin.xyz/api/v1/invoices \
              -H "X-API-Key: $HALFIN_API_KEY" \
              --data-urlencode "limit=20" \
              --data-urlencode "offset=0"
        - lang: ts
          label: TypeScript
          source: |
            import { createHalfin, listInvoices } from '@halfin/sdk-merchant';

            const client = createHalfin({
              apiKey: process.env.HALFIN_API_KEY,
              baseUrl: 'https://dashboard.halfin.xyz/api',
            });
            const { data } = await listInvoices({
              client,
              query: { limit: 20, offset: 0 },
            });
      security:
        - apiKeyAuth: []
      x-required-permission: invoices:read
      parameters:
        - $ref: "#/components/parameters/StatusFilter"
        - $ref: "#/components/parameters/CurrencyFilter"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Offset"
      responses:
        "200":
          description: Paginated invoice list.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedEnvelope_InvoiceResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/invoices/{invoiceID}:
    get:
      operationId: getInvoice
      tags:
        - merchant
      summary: Get a single invoice with payment details.
      description: |
        Returns the invoice and its associated on-chain payments. The
        invoice must belong to the authenticated merchant and match the
        API key's environment. Use this endpoint for server-side status
        checks before fulfillment.
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl https://dashboard.halfin.xyz/api/v1/invoices/00000000-0000-0000-0000-000000000001 \
              -H "X-API-Key: $HALFIN_API_KEY"
        - lang: ts
          label: TypeScript
          source: |
            import { createHalfin, getInvoice } from '@halfin/sdk-merchant';

            const client = createHalfin({
              apiKey: process.env.HALFIN_API_KEY,
              baseUrl: 'https://dashboard.halfin.xyz/api',
            });
            const { data } = await getInvoice({
              client,
              path: { invoiceID: '00000000-0000-0000-0000-000000000001' },
            });
      security:
        - apiKeyAuth: []
      x-required-permission: invoices:read
      parameters:
        - $ref: "#/components/parameters/InvoiceID"
      responses:
        "200":
          description: Invoice detail.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessEnvelope_InvoiceResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/invoices/{invoiceID}/cancel:
    post:
      operationId: cancelInvoice
      tags:
        - merchant
      summary: Cancel a pending invoice.
      description: |
        Cancels an invoice that has not yet received payment. Only invoices
        in `pending` status can be cancelled.
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -X POST https://dashboard.halfin.xyz/api/v1/invoices/00000000-0000-0000-0000-000000000001/cancel \
              -H "X-API-Key: $HALFIN_API_KEY"
        - lang: ts
          label: TypeScript
          source: |
            import { cancelInvoice, createHalfin } from '@halfin/sdk-merchant';

            const client = createHalfin({
              apiKey: process.env.HALFIN_API_KEY,
              baseUrl: 'https://dashboard.halfin.xyz/api',
            });
            const { data } = await cancelInvoice({
              client,
              path: { invoiceID: '00000000-0000-0000-0000-000000000001' },
            });
      security:
        - apiKeyAuth: []
      x-required-permission: invoices:write
      parameters:
        - $ref: "#/components/parameters/InvoiceID"
      responses:
        "200":
          description: Invoice cancelled.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessEnvelope_InvoiceResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/payouts:
    post:
      operationId: createPayout
      tags:
        - merchant
      summary: Create a new payout.
      description: |
        Initiates a withdrawal to the specified destination address. The
        payout enters `pending_approval` status and must be approved via
        the dashboard before execution.
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -X POST https://dashboard.halfin.xyz/api/v1/payouts \
              -H "Content-Type: application/json" \
              -H "X-API-Key: $HALFIN_API_KEY" \
              -d '{
                "currency": "BTC",
                "amount": "0.01000000",
                "destination": "bc1qexampleaddress000000000000000000000000",
                "idempotency_key": "payout-0001"
              }'
        - lang: ts
          label: TypeScript
          source: |
            import { createHalfin, createPayout } from '@halfin/sdk-merchant';

            const client = createHalfin({
              apiKey: process.env.HALFIN_API_KEY,
              baseUrl: 'https://dashboard.halfin.xyz/api',
            });
            const { data } = await createPayout({
              client,
              body: {
                currency: 'BTC',
                amount: '0.01000000',
                destination: 'bc1qexampleaddress000000000000000000000000',
                idempotency_key: 'payout-0001',
              },
            });
      security:
        - apiKeyAuth: []
      x-required-permission: payouts:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreatePayoutRequest"
      responses:
        "201":
          description: Payout created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessEnvelope_PayoutResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          $ref: "#/components/responses/GateOffline"
    get:
      operationId: listPayouts
      tags:
        - merchant
      summary: List payouts for the authenticated merchant.
      description: |
        Returns a paginated list of payouts scoped to the API key's environment.
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -G https://dashboard.halfin.xyz/api/v1/payouts \
              -H "X-API-Key: $HALFIN_API_KEY" \
              --data-urlencode "limit=20" \
              --data-urlencode "offset=0"
        - lang: ts
          label: TypeScript
          source: |
            import { createHalfin, listPayouts } from '@halfin/sdk-merchant';

            const client = createHalfin({
              apiKey: process.env.HALFIN_API_KEY,
              baseUrl: 'https://dashboard.halfin.xyz/api',
            });
            const { data } = await listPayouts({
              client,
              query: { limit: 20, offset: 0 },
            });
      security:
        - apiKeyAuth: []
      x-required-permission: payouts:read
      parameters:
        - $ref: "#/components/parameters/StatusFilter"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Offset"
      responses:
        "200":
          description: Paginated payout list.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedEnvelope_PayoutResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/refunds:
    post:
      operationId: createRefund
      tags:
        - merchant
      summary: Create a refund for a paid invoice.
      description: |
        Creates a merchant-funded refund to a supplied destination address
        using the existing payout rail. The refund amount is capped by the
        invoice's credited merchant balance for the same asset. The merchant
        is charged the refund amount, the configured refund fee, and the
        actual on-chain network fee reported when the linked payout completes.
      security:
        - apiKeyAuth: []
      x-required-permission: refunds:write
      parameters:
        - $ref: "#/components/parameters/RefundIdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateRefundRequest"
      responses:
        "201":
          description: Refund created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessEnvelope_RefundResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          $ref: "#/components/responses/GateOffline"
  /v1/payouts/{payoutID}:
    get:
      operationId: getPayout
      tags:
        - merchant
      summary: Get a single payout.
      description: |
        Returns the payout detail. The payout must belong to the authenticated
        merchant and match the API key's environment.
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl https://dashboard.halfin.xyz/api/v1/payouts/00000000-0000-0000-0000-000000000002 \
              -H "X-API-Key: $HALFIN_API_KEY"
        - lang: ts
          label: TypeScript
          source: |
            import { createHalfin, getPayout } from '@halfin/sdk-merchant';

            const client = createHalfin({
              apiKey: process.env.HALFIN_API_KEY,
              baseUrl: 'https://dashboard.halfin.xyz/api',
            });
            const { data } = await getPayout({
              client,
              path: { payoutID: '00000000-0000-0000-0000-000000000002' },
            });
      security:
        - apiKeyAuth: []
      x-required-permission: payouts:read
      parameters:
        - $ref: "#/components/parameters/PayoutID"
      responses:
        "200":
          description: Payout detail.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessEnvelope_PayoutResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/payouts/{payoutID}/cancel:
    post:
      operationId: cancelPayout
      tags:
        - merchant
      summary: Cancel a payout.
      description: |
        Cancels a payout in `pending_approval` or `approved` status. Payouts
        that are already processing or completed cannot be cancelled.
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -X POST https://dashboard.halfin.xyz/api/v1/payouts/00000000-0000-0000-0000-000000000002/cancel \
              -H "X-API-Key: $HALFIN_API_KEY"
        - lang: ts
          label: TypeScript
          source: |
            import { cancelPayout, createHalfin } from '@halfin/sdk-merchant';

            const client = createHalfin({
              apiKey: process.env.HALFIN_API_KEY,
              baseUrl: 'https://dashboard.halfin.xyz/api',
            });
            const { data } = await cancelPayout({
              client,
              path: { payoutID: '00000000-0000-0000-0000-000000000002' },
            });
      security:
        - apiKeyAuth: []
      x-required-permission: payouts:write
      parameters:
        - $ref: "#/components/parameters/PayoutID"
      responses:
        "200":
          description: Payout cancelled.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessEnvelope_PayoutResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/balances:
    get:
      operationId: listBalances
      tags:
        - merchant
      summary: List all currency balances for the authenticated merchant.
      description: |
        Returns one balance row per currency for the merchant tied to the
        authenticating API key. The environment is resolved from the API
        key that authenticated the request. Use balances and ledger entries
        to reconcile spendable funds after invoice status checks.
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl https://dashboard.halfin.xyz/api/v1/balances \
              -H "X-API-Key: $HALFIN_API_KEY"
        - lang: ts
          label: TypeScript
          source: |
            import { createHalfin, listBalances } from '@halfin/sdk-merchant';

            const client = createHalfin({
              apiKey: process.env.HALFIN_API_KEY,
              baseUrl: 'https://dashboard.halfin.xyz/api',
            });
            const { data } = await listBalances({ client });
      security:
        - apiKeyAuth: []
      x-required-permission: balances:read
      parameters:
        - name: environment
          in: query
          required: false
          description: |
            Reserved for forward compatibility with JWT-authenticated
            dashboard callers. For API-key callers the environment is
            determined by the key itself and this parameter is ignored.
          schema:
            type: string
            enum:
              - live
              - test
        - name: limit
          in: query
          required: false
          description: Reserved for forward compatibility.
          schema:
            type: integer
            minimum: 1
            maximum: 100
        - name: offset
          in: query
          required: false
          description: Reserved for forward compatibility.
          schema:
            type: integer
            minimum: 0
      responses:
        "200":
          description: Balance list.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessEnvelope_BalanceList"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/balances/{currency}/ledger:
    get:
      operationId: getLedger
      tags:
        - merchant
      summary: Get ledger history for a specific currency.
      description: |
        Returns a paginated list of ledger entries (deposits, fees, payouts)
        for the given currency. Supports filtering by entry type.
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -G https://dashboard.halfin.xyz/api/v1/balances/BTC/ledger \
              -H "X-API-Key: $HALFIN_API_KEY" \
              --data-urlencode "limit=20" \
              --data-urlencode "offset=0"
        - lang: ts
          label: TypeScript
          source: |
            import { createHalfin, getLedger } from '@halfin/sdk-merchant';

            const client = createHalfin({
              apiKey: process.env.HALFIN_API_KEY,
              baseUrl: 'https://dashboard.halfin.xyz/api',
            });
            const { data } = await getLedger({
              client,
              path: { currency: 'BTC' },
              query: { limit: 20, offset: 0 },
            });
      security:
        - apiKeyAuth: []
      x-required-permission: balances:read
      parameters:
        - $ref: "#/components/parameters/CurrencyPath"
        - name: entry_type
          in: query
          required: false
          description: Filter by ledger entry type.
          schema:
            type: string
            maxLength: 255
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Offset"
      responses:
        "200":
          description: Paginated ledger entries.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedEnvelope_LedgerEntryResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/addresses:
    post:
      operationId: createAddress
      tags:
        - merchant
      summary: Create a permanent static deposit address.
      description: |
        Creates a new static deposit address for the specified currency.
        Deposits to this address auto-create invoices. Static addresses are
        deposit rails for repeat deposits, not checkout sessions.
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -X POST https://dashboard.halfin.xyz/api/v1/addresses \
              -H "Content-Type: application/json" \
              -H "X-API-Key: $HALFIN_API_KEY" \
              -d '{
                "currency": "BTC",
                "label": "Account top-up"
              }'
        - lang: ts
          label: TypeScript
          source: |
            import { createAddress, createHalfin } from '@halfin/sdk-merchant';

            const client = createHalfin({
              apiKey: process.env.HALFIN_API_KEY,
              baseUrl: 'https://dashboard.halfin.xyz/api',
            });
            const { data } = await createAddress({
              client,
              body: { currency: 'BTC', label: 'Account top-up' },
            });
      security:
        - apiKeyAuth: []
      x-required-permission: addresses:write
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateAddressRequest"
      responses:
        "201":
          description: Static address created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessEnvelope_AddressResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "422":
          $ref: "#/components/responses/ValidationError"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          $ref: "#/components/responses/GateOffline"
    get:
      operationId: listAddresses
      tags:
        - merchant
      summary: List static deposit addresses.
      description: |
        Returns a paginated list of static addresses for the authenticated
        merchant. Supports filtering by currency.
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -G https://dashboard.halfin.xyz/api/v1/addresses \
              -H "X-API-Key: $HALFIN_API_KEY" \
              --data-urlencode "limit=20" \
              --data-urlencode "offset=0"
        - lang: ts
          label: TypeScript
          source: |
            import { createHalfin, listAddresses } from '@halfin/sdk-merchant';

            const client = createHalfin({
              apiKey: process.env.HALFIN_API_KEY,
              baseUrl: 'https://dashboard.halfin.xyz/api',
            });
            const { data } = await listAddresses({
              client,
              query: { limit: 20, offset: 0 },
            });
      security:
        - apiKeyAuth: []
      x-required-permission: addresses:read
      parameters:
        - $ref: "#/components/parameters/CurrencyFilter"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Offset"
      responses:
        "200":
          description: Paginated address list.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedEnvelope_AddressResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/addresses/{addressID}:
    get:
      operationId: getAddress
      tags:
        - merchant
      summary: Get a single static deposit address.
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl https://dashboard.halfin.xyz/api/v1/addresses/00000000-0000-0000-0000-000000000003 \
              -H "X-API-Key: $HALFIN_API_KEY"
        - lang: ts
          label: TypeScript
          source: |
            import { createHalfin, getAddress } from '@halfin/sdk-merchant';

            const client = createHalfin({
              apiKey: process.env.HALFIN_API_KEY,
              baseUrl: 'https://dashboard.halfin.xyz/api',
            });
            const { data } = await getAddress({
              client,
              path: { addressID: '00000000-0000-0000-0000-000000000003' },
            });
      security:
        - apiKeyAuth: []
      x-required-permission: addresses:read
      parameters:
        - $ref: "#/components/parameters/AddressID"
      responses:
        "200":
          description: Address detail.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessEnvelope_AddressResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/addresses/{addressID}/invoices:
    get:
      operationId: listAddressInvoices
      tags:
        - merchant
      summary: List invoices for a static address.
      description: |
        Returns a paginated list of invoices auto-created from deposits
        to the specified static address.
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -G https://dashboard.halfin.xyz/api/v1/addresses/00000000-0000-0000-0000-000000000003/invoices \
              -H "X-API-Key: $HALFIN_API_KEY" \
              --data-urlencode "limit=20" \
              --data-urlencode "offset=0"
        - lang: ts
          label: TypeScript
          source: |
            import { createHalfin, listAddressInvoices } from '@halfin/sdk-merchant';

            const client = createHalfin({
              apiKey: process.env.HALFIN_API_KEY,
              baseUrl: 'https://dashboard.halfin.xyz/api',
            });
            const { data } = await listAddressInvoices({
              client,
              path: { addressID: '00000000-0000-0000-0000-000000000003' },
              query: { limit: 20, offset: 0 },
            });
      security:
        - apiKeyAuth: []
      x-required-permission: addresses:read
      parameters:
        - $ref: "#/components/parameters/AddressID"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Offset"
      responses:
        "200":
          description: Paginated invoice list.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedEnvelope_InvoiceResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/rates:
    get:
      operationId: getRates
      tags:
        - public
      security: []
      summary: Get current exchange rates.
      description: |
        Returns USD exchange rates for all supported cryptocurrencies.
        Response is a map keyed by currency code. Use it for display and
        reconciliation context, not as a payment-status signal.
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl https://dashboard.halfin.xyz/api/v1/rates
        - lang: ts
          label: TypeScript
          source: |
            import { getRates } from '@halfin/sdk-merchant';

            const { data } = await getRates();
      responses:
        "200":
          description: Exchange rates.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessEnvelope_RatesResponse"
        "500":
          $ref: "#/components/responses/InternalError"
  /v1/currencies:
    get:
      operationId: getCurrencies
      tags:
        - public
      security: []
      summary: Get supported payment currencies.
      description: |
        Returns all available payment currencies with their network details
        and confirmation requirements. Use it to decide which currencies and
        networks can be offered for the selected environment.
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -G https://dashboard.halfin.xyz/api/v1/currencies \
              --data-urlencode "environment=test"
        - lang: ts
          label: TypeScript
          source: |
            import { getCurrencies } from '@halfin/sdk-merchant';

            const { data } = await getCurrencies({
              query: { environment: 'test' },
            });
      parameters:
        - name: environment
          in: query
          required: false
          schema:
            type: string
            enum:
              - live
              - test
            default: live
          description: Which environment's currencies to return. Defaults to live.
      responses:
        "200":
          description: Currency list.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessEnvelope_CurrencyList"
        "500":
          $ref: "#/components/responses/InternalError"
components:
  schemas:
    CreateInvoiceRequest:
      type: object
      additionalProperties: false
      properties:
        currency:
          type: string
          maxLength: 20
          description: |
            Cryptocurrency code (e.g. BTC, ETH, SOL). Required when
            deferred is false. Optional when deferred is true (payer
            chooses at activation time).
        network:
          type: string
          maxLength: 30
          description: |
            Blockchain network (e.g. ethereum, polygon, solana,
            tron, bsc, arbitrum). Required for multi-chain tokens
            whose network cannot be unambiguously inferred from the
            `currency` ticker (USDT, USDC, ETH). Optional for
            unambiguous tokens (BTC, SOL, TRX, etc.) — the backend
            fills it in via currency.InferNetwork when omitted.
            Sending an unsupported (currency, network) pair returns
            a 400 validation_error.
        amount:
          type: string
          maxLength: 50
          description: |
            Crypto-denominated amount the merchant wants to collect, as
            a decimal string in units of `currency`. Required unless
            `amount_fiat` (or the legacy `amount_usd`) is sent instead.
            Mutually exclusive with the fiat-amount fields — sending
            both is a validation error.
        amount_usd:
          type: string
          maxLength: 50
          description: |
            DEPRECATED — legacy alias for `amount_fiat` with
            `fiat_currency='USD'`. Kept accepted during the Phase 4
            backward-compat window (plan §C.2 rule 1 / §D). Migrate to
            `amount_fiat` + `fiat_currency`.

            Aliasing precedence rules (§C.2, checked in this order):
              1. `amount_usd` supplied alone (no `amount_fiat`, and
                 `fiat_currency` is empty or `"USD"`) → normalized to
                 `amount_fiat` + `fiat_currency='USD'`.
              2. BOTH `amount_usd` AND `amount_fiat` supplied
                 (`fiat_currency` implicit or explicit `"USD"`) →
                 `amount_fiat` wins, `amount_usd` is ignored for
                 pricing.
              3. `amount_usd` supplied together with a non-USD
                 `fiat_currency` → request is rejected with 400
                 `conflicting_amount_fields` REGARDLESS of whether
                 `amount_fiat` is also present. **Rule 3 overrides
                 Rule 2** — the conflict check runs before the
                 "amount_fiat wins" resolution, because silently
                 coercing a mixed `amount_usd` + non-USD
                 `fiat_currency` payload would mis-bill the merchant
                 by the USD/<fiat> cross rate (plan §C.2 rule 3 /
                 financial-ask #2).
        amount_fiat:
          type: string
          maxLength: 50
          description: |
            Fiat-denominated amount in the currency named by
            `fiat_currency` (default `USD`). The invoice is billed
            against the selected crypto at the activation-time
            exchange rate. Sent instead of `amount` to express
            "charge the customer $50 / €50 / £50 worth of BTC". When
            sent without `currency` the invoice behaves like the
            existing deferred "any crypto" flow. When sent alongside a
            specific `currency` + `network` the invoice is locked to
            that pair (no currency picker on checkout); this
            combination requires `deferred=true`. Mutually exclusive
            with `amount`.

            When BOTH `amount_fiat` and the legacy `amount_usd` are
            supplied, `amount_fiat` takes precedence (§C.2 rule 2).
            However, the conflict guard in §C.2 rule 3 (amount_usd +
            non-USD `fiat_currency` → 400 `conflicting_amount_fields`)
            is evaluated first and overrides rule 2 — see the
            `amount_usd` description for the full precedence list.
        fiat_currency:
          type: string
          maxLength: 3
          enum:
            - USD
            - EUR
            - GBP
            - JPY
            - AUD
            - CAD
            - CHF
            - NZD
            - SEK
            - NOK
            - DKK
            - SGD
            - HKD
            - INR
            - BRL
          description: |
            ISO 4217 code that denominates `amount_fiat`. Defaults to
            `USD` when omitted. Must be one of the server-side fiat
            allowlist (plan §C.2 rule 7 / appsec-ask #7). Sending a
            code outside the allowlist returns a 400
            `invalid_fiat_currency`.
        deferred:
          type: boolean
          default: false
          description: |
            If true, creates a draft invoice. The payment timer starts
            when the payer activates via the checkout page.
        description:
          type: string
          maxLength: 1000
          description: Human-readable invoice description.
        external_id:
          type: string
          maxLength: 255
          description: Merchant-supplied external reference ID.
        idempotency_key:
          type: string
          maxLength: 255
          description: Idempotency key to prevent duplicate invoices.
        metadata:
          type: object
          maxProperties: 50
          additionalProperties:
            type: string
            maxLength: 500
          description: Key-value metadata attached to the invoice.
        redirect_url:
          type: string
          format: uri
          maxLength: 2048
          description: URL to redirect the customer after payment.
        ttl_minutes:
          type: integer
          description: Invoice expiration time as a minute count (0 = default).
        underpayment_threshold:
          type: string
          maxLength: 50
          description: Decimal string. Payments below this tolerance are rejected.
        customer_email:
          type: string
          format: email
          maxLength: 320
          nullable: true
          description: |
            Optional customer email address. When present, ss-core
            dispatches an asynchronous INVOICE_PAYMENT_REQUESTED email
            to this address after the invoice row is committed
            (fire-and-forget goroutine; the response does not block on
            Resend). The value is also copied into `payer_email` so the
            manual `/send` endpoint can reuse it without re-entry.

            Dispatch is idempotent against a retried CreateInvoice with
            the same `idempotency_key`: a subsequent hit that resolves
            to the existing invoice does not re-send. Phase 5 extends
            the same auto-send pipeline to INVOICE_PAID and
            INVOICE_EXPIRED status transitions, also guarded by
            per-invoice idempotency timestamps.

            Future sends are suppressed if Resend reports a hard bounce
            for this address via the bounce webhook — once the row's
            `email_bounced` flag is set, no further emails are issued
            regardless of subsequent status transitions.
    SuccessEnvelope_InvoiceResponse:
      type: object
      additionalProperties: false
      required:
        - data
        - meta
      properties:
        data:
          $ref: "#/components/schemas/InvoiceResponse"
        meta:
          $ref: "#/components/schemas/Meta"
    ErrorEnvelope:
      type: object
      additionalProperties: false
      required:
        - error
        - meta
      properties:
        error:
          $ref: "#/components/schemas/ErrorBody"
        meta:
          $ref: "#/components/schemas/Meta"
    PaginatedEnvelope_InvoiceResponse:
      type: object
      additionalProperties: false
      required:
        - data
        - meta
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/InvoiceResponse"
        meta:
          $ref: "#/components/schemas/PaginatedMeta"
    CreatePayoutRequest:
      type: object
      additionalProperties: false
      required:
        - currency
        - amount
        - destination
      properties:
        currency:
          type: string
          maxLength: 20
          description: Cryptocurrency code.
        amount:
          type: string
          maxLength: 50
          description: Decimal string amount to send.
        destination:
          type: string
          maxLength: 256
          description: Destination blockchain address.
        destination_tag:
          type: string
          maxLength: 256
          description: Memo/tag for chains that require it.
        description:
          type: string
          maxLength: 1000
          description: Human-readable payout description.
        metadata:
          type: object
          maxProperties: 50
          additionalProperties:
            type: string
            maxLength: 500
          description: Key-value metadata.
        idempotency_key:
          type: string
          maxLength: 255
          description: Idempotency key to prevent duplicate payouts.
    SuccessEnvelope_PayoutResponse:
      type: object
      additionalProperties: false
      required:
        - data
        - meta
      properties:
        data:
          $ref: "#/components/schemas/PayoutResponse"
        meta:
          $ref: "#/components/schemas/Meta"
    PaginatedEnvelope_PayoutResponse:
      type: object
      additionalProperties: false
      required:
        - data
        - meta
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/PayoutResponse"
        meta:
          $ref: "#/components/schemas/PaginatedMeta"
    CreateRefundRequest:
      type: object
      additionalProperties: false
      required:
        - invoice_id
        - currency
        - amount
        - destination
      properties:
        invoice_id:
          type: string
          format: uuid
          description: Invoice UUID whose credited balance is being refunded.
        currency:
          type: string
          maxLength: 20
          description: Credited asset currency to refund.
        network:
          type: string
          maxLength: 64
          description: Credited asset network. Optional when the currency has an unambiguous network.
        amount:
          type: string
          maxLength: 50
          description: Decimal string amount to refund.
        destination:
          type: string
          maxLength: 256
          description: Customer-supplied destination address.
        destination_tag:
          type: string
          maxLength: 256
          description: Memo/tag for chains that require it.
        description:
          type: string
          maxLength: 1000
          description: Human-readable refund description.
        metadata:
          type: object
          maxProperties: 50
          additionalProperties:
            type: string
            maxLength: 500
          description: Key-value metadata.
    SuccessEnvelope_RefundResponse:
      type: object
      additionalProperties: false
      required:
        - data
        - meta
      properties:
        data:
          $ref: "#/components/schemas/RefundResponse"
        meta:
          $ref: "#/components/schemas/Meta"
    SuccessEnvelope_BalanceList:
      type: object
      additionalProperties: false
      required:
        - data
        - meta
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/BalanceResponse"
        meta:
          $ref: "#/components/schemas/Meta"
    PaginatedEnvelope_LedgerEntryResponse:
      type: object
      additionalProperties: false
      required:
        - data
        - meta
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/LedgerEntryResponse"
        meta:
          $ref: "#/components/schemas/PaginatedMeta"
    CreateAddressRequest:
      type: object
      additionalProperties: false
      required:
        - currency
      properties:
        currency:
          type: string
          maxLength: 20
          description: Cryptocurrency code for the static address.
        label:
          type: string
          maxLength: 1000
          description: Optional human-readable label.
    SuccessEnvelope_AddressResponse:
      type: object
      additionalProperties: false
      required:
        - data
        - meta
      properties:
        data:
          $ref: "#/components/schemas/AddressResponse"
        meta:
          $ref: "#/components/schemas/Meta"
    PaginatedEnvelope_AddressResponse:
      type: object
      additionalProperties: false
      required:
        - data
        - meta
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/AddressResponse"
        meta:
          $ref: "#/components/schemas/PaginatedMeta"
    SuccessEnvelope_RatesResponse:
      type: object
      additionalProperties: false
      required:
        - data
        - meta
      properties:
        data:
          $ref: "#/components/schemas/RatesResponse"
        meta:
          $ref: "#/components/schemas/Meta"
    SuccessEnvelope_CurrencyList:
      type: object
      additionalProperties: false
      required:
        - data
        - meta
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/CurrencyInfo"
        meta:
          $ref: "#/components/schemas/Meta"
    InvoiceWebhookEnvelope:
      type: object
      additionalProperties: false
      required:
        - event
        - created_at
        - data
      description: Typed envelope for invoice webhook events.
      properties:
        event:
          type: string
          description: Event type identifier.
          enum:
            - invoice.confirming
            - invoice.paid
            - invoice.overpaid
            - invoice.underpaid
            - invoice.expired
            - invoice.deposit_reversed
        created_at:
          type: string
          format: date-time
          description: Event creation timestamp.
        data:
          $ref: "#/components/schemas/InvoiceWebhookData"
    LateDepositWebhookEnvelope:
      type: object
      additionalProperties: false
      required:
        - event
        - created_at
        - data
      description: Typed envelope for late deposit webhook events.
      properties:
        event:
          type: string
          description: Event type identifier.
          enum:
            - invoice.late_deposit
        created_at:
          type: string
          format: date-time
          description: Event creation timestamp.
        data:
          $ref: "#/components/schemas/LateDepositWebhookData"
    BalanceCreditedWebhookEnvelope:
      type: object
      additionalProperties: false
      required:
        - event_id
        - event
        - created_at
        - data
      description: Typed envelope for balance.credited webhook events.
      properties:
        event_id:
          type: string
          format: uuid
          description: Stable event UUID for merchant-side idempotency.
        event:
          type: string
          description: Event type identifier.
          enum:
            - balance.credited
        created_at:
          type: string
          format: date-time
          description: Balance credit transaction timestamp.
        data:
          $ref: "#/components/schemas/BalanceCreditedWebhookData"
    PayoutCompletedWebhookEnvelope:
      type: object
      additionalProperties: false
      required:
        - event
        - created_at
        - data
      description: Typed envelope for payout completed webhook events.
      properties:
        event:
          type: string
          description: Event type identifier.
          enum:
            - payout.completed
        created_at:
          type: string
          format: date-time
          description: Event creation timestamp.
        data:
          $ref: "#/components/schemas/PayoutCompletedWebhookData"
    PayoutFailedWebhookEnvelope:
      type: object
      additionalProperties: false
      required:
        - event
        - created_at
        - data
      description: Typed envelope for payout failed webhook events.
      properties:
        event:
          type: string
          description: Event type identifier.
          enum:
            - payout.failed
        created_at:
          type: string
          format: date-time
          description: Event creation timestamp.
        data:
          $ref: "#/components/schemas/PayoutFailedWebhookData"
    RefundWebhookEnvelope:
      type: object
      additionalProperties: false
      required:
        - event
        - created_at
        - data
      description: Typed envelope for refund webhook events.
      properties:
        event:
          type: string
          description: Event type identifier.
          enum:
            - refund.created
            - refund.completed
            - refund.failed
        created_at:
          type: string
          format: date-time
          description: Event creation timestamp.
        data:
          $ref: "#/components/schemas/RefundWebhookData"
    InvoiceResponse:
      type: object
      additionalProperties: false
      required:
        - id
        - currency
        - network
        - amount_requested
        - amount_paid
        - amount_usd
        - amount_fiat
        - amount_in_base_currency
        - fiat_currency
        - status
        - environment
        - description
        - deposit_address_tag
        - external_id
        - idempotency_key
        - metadata
        - redirect_url
        - source
        - expires_at
        - paid_at
        - created_at
        - is_deferred
        - draft_expires_at
        - activated_at
        - activation_count
        - is_locked_fiat
      properties:
        id:
          type: string
          format: uuid
          description: Invoice UUID.
        merchant_id:
          type: string
          format: uuid
          description: Owning merchant UUID.
        currency:
          type: string
          description: Cryptocurrency code.
        network:
          type: string
          description: Blockchain network key for explorer and gate routing.
        amount_requested:
          type: string
          nullable: true
          description: |
            Decimal string of the crypto amount the payer must send.
            Null for draft locked-fiat invoices (the crypto amount is unknown
            until activation locks the exchange rate). Set on activation and
            preserved for the rest of the invoice lifecycle. Consumers must
            fall back to `amount_fiat` + `fiat_currency` for display when
            this field is null.
        amount_paid:
          type: string
          description: Decimal string of the total amount paid.
        amount_usd:
          type: string
          nullable: true
          description: |
            DEPRECATED — legacy mirror of `amount_fiat` emitted only when
            `fiat_currency='USD'`; `null` for all non-USD invoices so legacy
            consumers do not read a misleading non-USD figure from a field
            literally named "usd". Phase 4 emits both fields during the
            transition window (plan §D); migrate to `amount_fiat` +
            `fiat_currency`. Scheduled for removal in Phase 5+.
        amount_fiat:
          type: string
          nullable: true
          description: |
            Fiat-denominated invoice amount in the currency named by
            `fiat_currency`. Source of truth for draft locked-fiat
            invoices (those rows have a NULL `amount_requested` until
            activation). Null for pure crypto-denominated invoices
            whose `amount` was supplied in units of `currency`.
        amount_in_base_currency:
          type: string
          nullable: true
          description: |
            Invoice amount pre-computed at response time in the
            merchant's CURRENT `base_currency` setting (read from
            `core.merchants.base_currency`). The conversion is server
            -side via `rates.RateService` from `amount_fiat` +
            `fiat_currency` to `base_currency`. Null when the source
            amount is absent (draft locked-fiat invoices before
            activation), when the rate lookup fails or is stale, or
            when the conversion result is otherwise unsafe to display.
            For UI consumers — historical billing fidelity is preserved
            by `amount_fiat` + `fiat_currency`; this field is the
            current-base view that lets the merchant dashboard render
            all invoices in their chosen base_currency without per-row
            client-side FX math.
        fiat_currency:
          type: string
          description: |
            ISO 4217 code that denominates `amount_fiat`. Always set;
            defaults to `USD` for pre-Phase-4 rows and for invoices
            created via the legacy `amount_usd` request field. Must be
            one of the server-side fiat allowlist.
        status:
          type: string
          description: Invoice status.
          enum:
            - draft
            - pending
            - confirming
            - paid
            - overpaid
            - underpaid
            - expired
            - cancelled
            - invalid
        environment:
          type: string
          enum:
            - live
            - test
          description: Environment this invoice belongs to.
        deposit_address:
          type: string
          description: Blockchain deposit address.
        deposit_address_tag:
          type: string
          nullable: true
          description: Memo/tag for chains that require it (e.g. XRP, XLM).
        description:
          type: string
          description: Invoice description.
        external_id:
          type: string
          nullable: true
          description: Merchant-supplied external reference ID.
        idempotency_key:
          type: string
          nullable: true
          description: Idempotency key (only in creation response).
        metadata:
          type: object
          nullable: true
          additionalProperties: true
          description: Merchant-supplied metadata.
        redirect_url:
          type: string
          nullable: true
          description: Customer redirect URL.
        source:
          type: string
          description: How the invoice was created (api, dashboard, static_address).
        static_address_id:
          type: string
          format: uuid
          description: Static address UUID if created from a static address deposit.
        checkout_url:
          type: string
          description: |
            URL for the hosted checkout page. Omitted for legacy hash-only
            invoices created before raw checkout tokens were persisted.
        payments:
          type: array
          items:
            $ref: "#/components/schemas/InvoicePaymentInfo"
          description: On-chain payments detected for this invoice.
        expires_at:
          type: string
          format: date-time
          description: Expiration timestamp.
        paid_at:
          type: string
          nullable: true
          format: date-time
          description: Timestamp when the invoice was fully paid.
        created_at:
          type: string
          format: date-time
          description: Creation timestamp.
        is_deferred:
          type: boolean
          description: Whether this is a deferred invoice.
        draft_expires_at:
          type: string
          nullable: true
          format: date-time
          description: When the draft expires (7-day window). Null for instant invoices.
        activated_at:
          type: string
          nullable: true
          format: date-time
          description: When the payer activated the invoice. Null if not yet activated.
        activation_count:
          type: integer
          format: int32
          description: Number of times this invoice has been activated.
        is_locked_fiat:
          type: boolean
          description: |
            True when the invoice was created via the locked-fiat flow
            (AmountUSD + a specific crypto). When true and `status` is
            `draft`, consumers MUST render `amount_usd` + "USD" — the
            crypto `amount_requested` is intentionally null until
            activation locks the exchange rate.
        payer_email:
          type: string
          nullable: true
          description: Email address the invoice was sent to.
        sent_at:
          type: string
          nullable: true
          format: date-time
          description: Timestamp when the invoice email was sent.
    Meta:
      type: object
      additionalProperties: false
      required:
        - request_id
      properties:
        request_id:
          type: string
          description: Echo of the incoming X-Request-ID, or a generated UUID.
    ErrorBody:
      type: object
      additionalProperties: false
      required:
        - code
        - message
      properties:
        code:
          type: string
          description: Stable machine-readable error code.
        message:
          type: string
          description: Human-readable description of the error.
        details:
          description: Optional structured context (validation errors, etc.).
          type: array
          items:
            type: string
    PaginatedMeta:
      type: object
      additionalProperties: false
      required:
        - request_id
        - pagination
      properties:
        request_id:
          type: string
        pagination:
          $ref: "#/components/schemas/Pagination"
    PayoutResponse:
      type: object
      additionalProperties: false
      required:
        - id
        - currency
        - network
        - amount
        - amount_in_base_currency
        - network_fee
        - fee_amount
        - destination
        - destination_tag
        - status
        - environment
        - tx_hash
        - description
        - executable_at
        - completed_at
        - created_at
      properties:
        id:
          type: string
          format: uuid
          description: Payout UUID.
        currency:
          type: string
          description: Cryptocurrency code.
        network:
          type: string
          description: Blockchain network key for explorer and gate routing.
        amount:
          type: string
          description: Decimal string of the payout amount.
        amount_in_base_currency:
          type: string
          nullable: true
          description: |
            Payout amount pre-computed at response time in the
            merchant's CURRENT `base_currency` setting via
            `rates.RateService` (`amount` × `currency` → `base_currency`
            using current rates). Null when the rate lookup fails or
            the conversion is otherwise unsafe. Consumers that need
            the on-chain authoritative number must read `amount` +
            `currency`; this field is the merchant-dashboard display
            equivalent in their chosen base.
        network_fee:
          type: string
          description: Decimal string of the network fee.
        fee_amount:
          type: string
          description: Decimal string of the platform payout fee in payout currency.
        destination:
          type: string
          description: Destination blockchain address.
        destination_tag:
          type: string
          nullable: true
          description: Memo/tag if applicable.
        status:
          type: string
          description: Payout status.
          enum:
            - pending_approval
            - approved
            - processing
            - confirming
            - completed
            - failed
            - cancelled
        environment:
          type: string
          enum:
            - live
            - test
          description: Environment this payout belongs to.
        tx_hash:
          type: string
          nullable: true
          description: On-chain transaction hash (populated after broadcast).
        idempotency_key:
          type: string
          description: Idempotency key (only in creation response).
        description:
          type: string
          description: Payout description.
        executable_at:
          type: string
          nullable: true
          format: date-time
          description: Earliest time the payout can be executed.
        completed_at:
          type: string
          nullable: true
          format: date-time
          description: Completion timestamp.
        created_at:
          type: string
          format: date-time
          description: Creation timestamp.
    RefundResponse:
      type: object
      additionalProperties: false
      required:
        - id
        - invoice_id
        - payout_id
        - currency
        - network
        - amount
        - refund_fee
        - network_fee
        - destination
        - destination_tag
        - status
        - environment
        - tx_hash
        - failure_reason
        - description
        - metadata
        - completed_at
        - created_at
      properties:
        id:
          type: string
          format: uuid
          description: Refund UUID.
        invoice_id:
          type: string
          format: uuid
          description: Invoice UUID being refunded.
        payout_id:
          type: string
          format: uuid
          description: Linked payout UUID used for on-chain execution.
        currency:
          type: string
          description: Refunded asset currency.
        network:
          type: string
          description: Refunded asset network.
        amount:
          type: string
          description: Decimal string refund amount.
        refund_fee:
          type: string
          description: Decimal string platform refund fee in refund currency.
        network_fee:
          type: string
          description: Decimal string actual on-chain network fee after completion.
        destination:
          type: string
          description: Destination blockchain address.
        destination_tag:
          type: string
          nullable: true
          description: Memo/tag if applicable.
        status:
          type: string
          enum:
            - pending
            - completed
            - failed
          description: Refund execution status.
        environment:
          type: string
          enum:
            - live
            - test
          description: Environment this refund belongs to.
        tx_hash:
          type: string
          nullable: true
          description: On-chain transaction hash after completion.
        failure_reason:
          type: string
          nullable: true
          description: Processor failure reason when the refund fails.
        description:
          type: string
          nullable: true
          description: Refund description.
        metadata:
          type: object
          nullable: true
          additionalProperties: true
          description: Merchant-supplied metadata.
        completed_at:
          type: string
          nullable: true
          format: date-time
          description: Completion timestamp.
        created_at:
          type: string
          format: date-time
          description: Creation timestamp.
    BalanceResponse:
      type: object
      additionalProperties: false
      required:
        - currency
        - network
        - environment
        - available
        - pending
        - total_received
        - total_fees
        - total_paid_out
        - total_in_base_currency
      properties:
        currency:
          type: string
          description: Currency code.
          example: BTC
        network:
          type: string
          description: Blockchain network key.
          example: bitcoin
        environment:
          type: string
          enum:
            - live
            - test
          description: Environment.
        available:
          type: string
          description: Decimal string of available balance.
        pending:
          type: string
          description: Decimal string of pending balance.
        total_received:
          type: string
          description: Decimal string of total received.
        total_fees:
          type: string
          description: Decimal string of total fees collected.
        total_paid_out:
          type: string
          description: Decimal string of total paid out.
        total_in_base_currency:
          type: string
          nullable: true
          description: |
            (`available` + `pending`) pre-computed at response time in
            the merchant's CURRENT `base_currency` via
            `rates.RateService`. Null when the rate lookup fails or
            the conversion is otherwise unsafe. The dashboard hero
            grand-total sums this across rows; per-currency on-chain
            values stay in `available` / `pending` for chain-truth.
    LedgerEntryResponse:
      type: object
      additionalProperties: false
      required:
        - id
        - environment
        - entry_type
        - amount
        - direction
        - balance_after
        - reference_type
        - reference_id
        - description
        - created_at
      properties:
        id:
          type: integer
          format: int64
          description: Ledger entry ID.
        environment:
          type: string
          enum:
            - live
            - test
          description: Environment.
        entry_type:
          type: string
          description: Type of ledger entry (deposit, fee, payout, etc.).
        amount:
          type: string
          description: Decimal string amount.
        direction:
          type: string
          enum:
            - credit
            - debit
          description: Whether funds were added or removed.
        balance_after:
          type: string
          description: Decimal string balance after this entry.
        reference_type:
          type: string
          description: Type of the referenced resource (invoice, payout).
        reference_id:
          type: string
          nullable: true
          format: uuid
          description: UUID of the referenced resource.
        description:
          type: string
          description: Human-readable description.
        created_at:
          type: string
          format: date-time
          description: Entry timestamp.
    AddressResponse:
      type: object
      additionalProperties: false
      required:
        - id
        - currency
        - environment
        - gate_id
        - address
        - address_tag
        - label
        - total_received
        - invoice_count
        - created_at
      properties:
        id:
          type: string
          format: uuid
          description: Static address UUID.
        merchant_id:
          type: string
          format: uuid
          description: Owning merchant UUID.
        currency:
          type: string
          description: Currency code.
        environment:
          type: string
          enum:
            - live
            - test
          description: Environment.
        gate_id:
          type: string
          description: Payment gate identifier.
        address:
          type: string
          description: Blockchain deposit address.
        address_tag:
          type: string
          nullable: true
          description: Memo/tag for chains that require it.
        label:
          type: string
          description: Human-readable label.
        total_received:
          type: string
          description: Decimal string of total deposits received.
        invoice_count:
          type: integer
          description: Number of auto-created invoices.
        created_at:
          type: string
          format: date-time
          description: Creation timestamp.
    RatesResponse:
      type: object
      additionalProperties:
        $ref: "#/components/schemas/RateEntry"
      description: Map of currency code to rate entry.
    CurrencyInfo:
      type: object
      additionalProperties: false
      required:
        - id
        - name
        - network
        - decimals
        - confirmations_required
        - status
      properties:
        id:
          type: string
          description: Currency/gate identifier.
        name:
          type: string
          description: Human-readable currency name.
        network:
          type: string
          description: Blockchain network name.
        decimals:
          type: integer
          format: int32
          description: Number of decimal places.
        confirmations_required:
          type: integer
          format: int32
          description: Required blockchain confirmations.
        status:
          type: string
          description: Gate status (online, offline).
    InvoiceWebhookData:
      type: object
      additionalProperties: false
      required:
        - invoice_id
        - external_id
        - currency
        - environment
        - amount_requested
        - amount_paid
        - status
      description: |
        Payload data for invoice webhook events (invoice.confirming,
        invoice.paid, invoice.overpaid, invoice.underpaid, invoice.expired,
        invoice.deposit_reversed).

        Phase 4 (multi-fiat plan §D) adds `amount_fiat` + `fiat_currency`
        alongside the legacy `amount_usd`. Dual emission lets webhook
        consumers migrate on their own timeline; once the
        `ss_core_webhook_legacy_amount_usd_emitted_total` counter drops
        to zero for 7 consecutive days, Phase 5+ removes the legacy
        `amount_usd` field.
      properties:
        invoice_id:
          type: string
          format: uuid
          description: Invoice UUID.
        external_id:
          type: string
          nullable: true
          description: Merchant-supplied external ID (null if not set).
        currency:
          type: string
          description: Currency code.
        environment:
          type: string
          enum:
            - live
            - test
          description: Environment.
        amount_requested:
          type: string
          description: Decimal string of the requested amount.
        amount_paid:
          type: string
          description: Decimal string of the total amount paid.
        amount_usd:
          type: string
          nullable: true
          description: |
            DEPRECATED — legacy mirror of `amount_fiat` emitted only when
            `fiat_currency='USD'`. Absent for non-USD invoices so legacy
            consumers do not read a misleading non-USD figure.
            Phase 4 transition window; SDK consumers should migrate to
            `amount_fiat` + `fiat_currency`.
        amount_fiat:
          type: string
          description: |
            Fiat-denominated invoice amount in the currency named by
            `fiat_currency`. Emitted for fiat-denominated invoices only
            (absent when the invoice is crypto-denominated without a
            fiat leg).
        fiat_currency:
          type: string
          description: |
            ISO 4217 code that denominates `amount_fiat`. Emitted
            alongside `amount_fiat` whenever the invoice carries a fiat
            leg.
        status:
          type: string
          description: Invoice status at the time of the event.
        paid_at:
          type: string
          format: date-time
          description: Payment timestamp (only for invoice.paid and invoice.overpaid).
        expires_at:
          type: string
          format: date-time
          description: Expiration timestamp (only for invoice.expired).
        source:
          type: string
          description: Set to "static_address" for auto-created invoices.
        static_address_id:
          type: string
          format: uuid
          description: Static address UUID (only for static address invoices).
    LateDepositWebhookData:
      type: object
      additionalProperties: false
      required:
        - invoice_id
        - external_id
        - currency
        - environment
        - amount_requested
        - late_deposit_amount
        - status
      description: |
        Payload data for invoice.late_deposit events.

        Commit 2 of the rate-freshness + offerability refactor adds
        four OPTIONAL fields (`rate_applied`, `rate_updated_at`,
        `rate_age_ms`, `rate_stale`) that land ONLY on static-address
        credits. Invoice-based late deposits carry a slot-locked rate
        and omit these fields — the slot's `exchange_rate` is
        authoritative for the invoice-based case. Merchant SDK
        consumers that depend on `additionalProperties: false` are
        unaffected because the new keys are explicitly listed.
      properties:
        invoice_id:
          type: string
          format: uuid
          description: Invoice UUID.
        external_id:
          type: string
          nullable: true
          description: Merchant-supplied external ID (null if not set).
        currency:
          type: string
          description: Currency code.
        environment:
          type: string
          enum:
            - live
            - test
          description: Environment.
        amount_requested:
          type: string
          description: Decimal string of the originally requested amount.
        late_deposit_amount:
          type: string
          description: Decimal string of the late deposit amount.
        status:
          type: string
          description: Invoice status (always "expired").
        source:
          type: string
          description: Set to "static_address" for auto-created invoices.
        static_address_id:
          type: string
          format: uuid
          description: Static address UUID (only for static address invoices).
        rate_applied:
          type: string
          description: |
            Decimal string of the crypto→USD rate used to price the
            credit's fiat_amount. Present ONLY on static-address
            credits (Commit 2); invoice-based credits use the
            slot's locked exchange_rate and omit this field.
        rate_updated_at:
          type: string
          format: date-time
          description: |
            ISO-8601 timestamp of the exchange_rates row that fed
            `rate_applied`. Present ONLY on static-address credits.
        rate_age_ms:
          type: integer
          format: int64
          description: |
            Age of `rate_applied` in milliseconds at credit time.
            Redundant with `rate_updated_at` but included so
            merchant consumers can threshold staleness without a
            date-math step. Present ONLY on static-address credits.
        rate_stale:
          type: boolean
          description: |
            True when `rate_age_ms` exceeds the currency-kind
            freshness threshold (crypto: 5 min default; fiat: 4 h
            default). Merchants should treat the accompanying
            `fiat_amount` figure as best-effort when true — credits
            DO still proceed (the merchant wants the crypto
            regardless) but reconciliation may want to wait for a
            non-stale confirmation. Present ONLY on
            static-address credits.
    BalanceCreditedWebhookData:
      type: object
      additionalProperties: false
      required:
        - invoice_id
        - payment_id
        - outcome
        - source_amount
        - source_currency
        - source_network
        - credited_amount
        - credited_currency
        - credited_network
      description: Payload data for balance.credited events emitted only after spendable merchant balance is credited.
      properties:
        invoice_id:
          type: string
          format: uuid
          description: Invoice UUID.
        payment_id:
          type: string
          format: uuid
          description: Payment row UUID that produced the balance credit.
        external_id:
          type: string
          nullable: true
          description: Merchant-supplied invoice external ID when set.
        outcome:
          type: string
          description: Balance credit outcome.
        source_amount:
          type: string
          format: decimal
          pattern: ^\d+(\.\d+)?$
          description: Decimal string of the original source payment amount.
        source_currency:
          type: string
          description: Source payment currency code.
        source_network:
          type: string
          description: Source payment blockchain network.
        credited_amount:
          type: string
          format: decimal
          pattern: ^\d+(\.\d+)?$
          description: Decimal string credited to the merchant available balance.
        credited_currency:
          type: string
          description: Credited balance currency code.
        credited_network:
          type: string
          description: Credited balance blockchain network.
        effective_rate:
          type: string
          format: decimal
          pattern: ^\d+(\.\d+)?$
          description: Decimal string of credited amount per source unit when available.
        gross_amount:
          type: string
          format: decimal
          pattern: ^\d+(\.\d+)?$
          description: Decimal string of the gross credited asset amount before service fee when available.
        fee_amount:
          type: string
          format: decimal
          pattern: ^\d+(\.\d+)?$
          description: Decimal string of the service fee amount in credited asset when available.
        net_amount:
          type: string
          format: decimal
          pattern: ^\d+(\.\d+)?$
          description: Decimal string of the final net credited amount when available.
    PayoutCompletedWebhookData:
      type: object
      additionalProperties: false
      required:
        - payout_id
        - currency
        - network
        - environment
        - amount
        - destination
        - tx_hash
        - network_fee
        - fee_amount
        - status
      description: Payload data for payout.completed events.
      properties:
        payout_id:
          type: string
          format: uuid
          description: Payout UUID.
        currency:
          type: string
          description: Currency code.
        network:
          type: string
          description: Blockchain network code.
        environment:
          type: string
          enum:
            - live
            - test
          description: Environment.
        amount:
          type: string
          description: Decimal string of the payout amount.
        destination:
          type: string
          description: Destination blockchain address.
        tx_hash:
          type: string
          description: On-chain transaction hash.
        network_fee:
          type: string
          description: Decimal string of the network fee.
        fee_amount:
          type: string
          description: Decimal string of the platform payout fee in payout currency.
        status:
          type: string
          description: Always "completed".
    PayoutFailedWebhookData:
      type: object
      additionalProperties: false
      required:
        - payout_id
        - currency
        - network
        - environment
        - amount
        - destination
        - error_message
        - status
      description: Payload data for payout.failed events.
      properties:
        payout_id:
          type: string
          format: uuid
          description: Payout UUID.
        currency:
          type: string
          description: Currency code.
        network:
          type: string
          description: Blockchain network code.
        environment:
          type: string
          enum:
            - live
            - test
          description: Environment.
        amount:
          type: string
          description: Decimal string of the payout amount.
        destination:
          type: string
          description: Destination blockchain address.
        error_message:
          type: string
          description: Error description.
        status:
          type: string
          description: Always "failed".
    RefundWebhookData:
      type: object
      additionalProperties: false
      required:
        - refund_id
        - invoice_id
        - payout_id
        - currency
        - network
        - environment
        - amount
        - refund_fee
        - network_fee
        - destination
        - status
      description: Payload data for refund.created, refund.completed, and refund.failed events.
      properties:
        refund_id:
          type: string
          format: uuid
          description: Refund UUID.
        invoice_id:
          type: string
          format: uuid
          description: Invoice UUID being refunded.
        payout_id:
          type: string
          format: uuid
          description: Linked payout UUID.
        currency:
          type: string
          description: Refunded asset currency.
        network:
          type: string
          description: Refunded asset network.
        environment:
          type: string
          enum:
            - live
            - test
          description: Environment.
        amount:
          type: string
          description: Decimal string refund amount.
        refund_fee:
          type: string
          description: Decimal string platform refund fee.
        network_fee:
          type: string
          description: Decimal string actual on-chain network fee after completion.
        destination:
          type: string
          description: Destination blockchain address.
        destination_tag:
          type: string
          nullable: true
          description: Memo/tag if applicable.
        status:
          type: string
          enum:
            - pending
            - completed
            - failed
          description: Refund status.
        tx_hash:
          type: string
          nullable: true
          description: On-chain transaction hash after completion.
        failure_reason:
          type: string
          nullable: true
          description: Processor failure reason when the refund fails.
        error_message:
          type: string
          description: Error description for refund.failed.
    InvoicePaymentInfo:
      type: object
      additionalProperties: false
      required:
        - tx_hash
        - amount
        - confirmations
        - required_confirmations
        - status
        - detected_at
      properties:
        tx_hash:
          type: string
          description: Blockchain transaction hash.
        amount:
          type: string
          description: Decimal string of the source payment amount.
        confirmations:
          type: integer
          description: Number of blockchain confirmations.
        required_confirmations:
          type: integer
          description: Required blockchain confirmations for this payment.
        status:
          type: string
          description: Payment status (confirming, confirmed, dropped).
        detected_at:
          type: string
          format: date-time
          description: When the payment was first detected.
    Pagination:
      type: object
      additionalProperties: false
      required:
        - total
        - limit
        - offset
        - has_more
      properties:
        total:
          type: integer
          format: int32
          description: Total number of matching records.
        limit:
          type: integer
          format: int32
          description: Requested page size.
        offset:
          type: integer
          format: int32
          description: Requested offset.
        has_more:
          type: boolean
          description: Whether more records exist beyond this page.
    RateEntry:
      type: object
      additionalProperties: false
      required:
        - usd
      properties:
        usd:
          type: string
          description: USD exchange rate as decimal string.
        updated_at:
          type: string
          format: date-time
          description: Rate last updated timestamp.
  responses:
    ValidationError:
      description: The request is malformed or contains invalid fields. Fix the body or query parameters before retrying.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    Unauthorized:
      description: Missing, malformed, revoked, or wrong-environment credentials. Check the `X-API-Key` header before retrying.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    Forbidden:
      description: The credentials are valid but lack the required permission. Update the API key permissions or use a key with access to this operation.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    InternalError:
      description: Unexpected server error. Retry safely when the operation is idempotent; otherwise contact support with `meta.request_id`.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    GateOffline:
      description: The blockchain payment processor for the requested currency/network is temporarily unavailable. Retry with backoff or choose another supported currency.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    NotFound:
      description: The resource ID does not exist for this merchant/environment, or the caller cannot access it. Verify the ID and API key environment.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    Conflict:
      description: The resource is not in a state that allows this action. Refresh the resource and retry only if the state changed.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
  parameters:
    StatusFilter:
      name: status
      in: query
      required: false
      description: Filter by status.
      schema:
        type: string
        maxLength: 255
    CurrencyFilter:
      name: currency
      in: query
      required: false
      description: Filter by currency code.
      schema:
        type: string
        maxLength: 20
    Limit:
      name: limit
      in: query
      required: false
      description: Maximum number of items to return (default 20, max 100).
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20
    Offset:
      name: offset
      in: query
      required: false
      description: Number of items to skip.
      schema:
        type: integer
        minimum: 0
        default: 0
    InvoiceID:
      name: invoiceID
      in: path
      required: true
      description: Invoice UUID.
      schema:
        type: string
        format: uuid
    RefundIdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: Per-merchant idempotency key for refund POSTs. Replays return the previously recorded refund without duplicate side effects.
      schema:
        type: string
        minLength: 1
        maxLength: 255
    PayoutID:
      name: payoutID
      in: path
      required: true
      description: Payout UUID.
      schema:
        type: string
        format: uuid
    CurrencyPath:
      name: currency
      in: path
      required: true
      description: Currency code (e.g. BTC, ETH, SOL).
      schema:
        type: string
        maxLength: 20
    AddressID:
      name: addressID
      in: path
      required: true
      description: Static address UUID.
      schema:
        type: string
        format: uuid
  securitySchemes:
    apiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: |
        API key issued by the merchant dashboard. The key's environment
        (live/test) determines which data the caller can access. Permissions
        on the key control which endpoints are reachable.
webhooks:
  invoice.confirming:
    post:
      operationId: webhookInvoiceConfirming
      tags:
        - webhooks
      summary: Invoice payment detected, awaiting confirmations.
      description: |
        Fired when the first on-chain payment is detected for an invoice.

        **Signature:** `X-Halfin-Signature: t={timestamp},v1={hmac}`
        where `hmac = HMAC-SHA256(secret, "{timestamp}.{raw_body}")`.
        Reject if `abs(now - timestamp) > 300` seconds.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InvoiceWebhookEnvelope"
  invoice.paid:
    post:
      operationId: webhookInvoicePaid
      tags:
        - webhooks
      summary: Invoice fully paid.
      description: |
        Fired when the invoice amount is met and confirmations are sufficient.

        **Signature:** `X-Halfin-Signature: t={timestamp},v1={hmac}`
        where `hmac = HMAC-SHA256(secret, "{timestamp}.{raw_body}")`.
        Reject if `abs(now - timestamp) > 300` seconds.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InvoiceWebhookEnvelope"
  invoice.overpaid:
    post:
      operationId: webhookInvoiceOverpaid
      tags:
        - webhooks
      summary: Invoice received more than the requested amount.
      description: |
        Fired when the total paid exceeds the requested amount.

        **Signature:** `X-Halfin-Signature: t={timestamp},v1={hmac}`
        where `hmac = HMAC-SHA256(secret, "{timestamp}.{raw_body}")`.
        Reject if `abs(now - timestamp) > 300` seconds.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InvoiceWebhookEnvelope"
  invoice.underpaid:
    post:
      operationId: webhookInvoiceUnderpaid
      tags:
        - webhooks
      summary: Invoice expired with partial payment.
      description: |
        Fired when an invoice expires after receiving a partial payment
        below the underpayment threshold.

        **Signature:** `X-Halfin-Signature: t={timestamp},v1={hmac}`
        where `hmac = HMAC-SHA256(secret, "{timestamp}.{raw_body}")`.
        Reject if `abs(now - timestamp) > 300` seconds.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InvoiceWebhookEnvelope"
  invoice.expired:
    post:
      operationId: webhookInvoiceExpired
      tags:
        - webhooks
      summary: Invoice expired without sufficient payment.
      description: |
        Fired when an invoice reaches its TTL without being fully paid.

        **Signature:** `X-Halfin-Signature: t={timestamp},v1={hmac}`
        where `hmac = HMAC-SHA256(secret, "{timestamp}.{raw_body}")`.
        Reject if `abs(now - timestamp) > 300` seconds.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InvoiceWebhookEnvelope"
  invoice.late_deposit:
    post:
      operationId: webhookInvoiceLateDeposit
      tags:
        - webhooks
      summary: Late deposit received on an expired invoice.
      description: |
        Fired when a payment arrives after the invoice has already expired.
        The merchant can decide whether to credit the customer.

        **Signature:** `X-Halfin-Signature: t={timestamp},v1={hmac}`
        where `hmac = HMAC-SHA256(secret, "{timestamp}.{raw_body}")`.
        Reject if `abs(now - timestamp) > 300` seconds.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LateDepositWebhookEnvelope"
  invoice.deposit_reversed:
    post:
      operationId: webhookInvoiceDepositReversed
      tags:
        - webhooks
      summary: A previously confirmed deposit was reversed (e.g. chain reorg).
      description: |
        Fired when a confirmed payment is dropped due to a blockchain
        reorganization. The invoice's amount_paid is recalculated.

        **Signature:** `X-Halfin-Signature: t={timestamp},v1={hmac}`
        where `hmac = HMAC-SHA256(secret, "{timestamp}.{raw_body}")`.
        Reject if `abs(now - timestamp) > 300` seconds.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InvoiceWebhookEnvelope"
  balance.credited:
    post:
      operationId: webhookBalanceCredited
      tags:
        - webhooks
      summary: Merchant balance credited.
      description: |
        Fired only after the merchant's available balance is credited.
        Use this event as the fulfillment signal when balance
        availability matters.

        **Signature:** `X-Halfin-Signature: t={timestamp},v1={hmac}`
        where `hmac = HMAC-SHA256(secret, "{timestamp}.{raw_body}")`.
        Reject if `abs(now - timestamp) > 300` seconds.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BalanceCreditedWebhookEnvelope"
  payout.completed:
    post:
      operationId: webhookPayoutCompleted
      tags:
        - webhooks
      summary: Payout successfully broadcast and confirmed.
      description: |
        Fired when a payout is confirmed on-chain.

        **Signature:** `X-Halfin-Signature: t={timestamp},v1={hmac}`
        where `hmac = HMAC-SHA256(secret, "{timestamp}.{raw_body}")`.
        Reject if `abs(now - timestamp) > 300` seconds.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PayoutCompletedWebhookEnvelope"
  payout.failed:
    post:
      operationId: webhookPayoutFailed
      tags:
        - webhooks
      summary: Payout failed, funds released.
      description: |
        Fired when a payout fails. Reserved funds are released back to the
        merchant's available balance.

        **Signature:** `X-Halfin-Signature: t={timestamp},v1={hmac}`
        where `hmac = HMAC-SHA256(secret, "{timestamp}.{raw_body}")`.
        Reject if `abs(now - timestamp) > 300` seconds.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PayoutFailedWebhookEnvelope"
  refund.created:
    post:
      operationId: webhookRefundCreated
      tags:
        - webhooks
      summary: Refund created and funds reserved.
      description: |
        Fired when a refund is created and the linked payout is queued for
        execution.

        **Signature:** `X-Halfin-Signature: t={timestamp},v1={hmac}`
        where `hmac = HMAC-SHA256(secret, "{timestamp}.{raw_body}")`.
        Reject if `abs(now - timestamp) > 300` seconds.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RefundWebhookEnvelope"
  refund.completed:
    post:
      operationId: webhookRefundCompleted
      tags:
        - webhooks
      summary: Refund completed on-chain.
      description: |
        Fired when the linked payout completes. The payload includes the
        transaction hash and the actual network fee charged to the merchant.

        **Signature:** `X-Halfin-Signature: t={timestamp},v1={hmac}`
        where `hmac = HMAC-SHA256(secret, "{timestamp}.{raw_body}")`.
        Reject if `abs(now - timestamp) > 300` seconds.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RefundWebhookEnvelope"
  refund.failed:
    post:
      operationId: webhookRefundFailed
      tags:
        - webhooks
      summary: Refund failed, reserved funds released.
      description: |
        Fired when the linked payout fails and the refund reservation is
        released back to the merchant balance.

        **Signature:** `X-Halfin-Signature: t={timestamp},v1={hmac}`
        where `hmac = HMAC-SHA256(secret, "{timestamp}.{raw_body}")`.
        Reject if `abs(now - timestamp) > 300` seconds.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RefundWebhookEnvelope"
