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, 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 checkout URL 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',
              },
            });
        - lang: python
          label: Python
          source: |
            # Python SDK coming soon. Use the cURL example for now.
      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 },
            });
        - lang: python
          label: Python
          source: |
            # Python SDK coming soon. Use the cURL example for now.
      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.
      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' },
            });
        - lang: python
          label: Python
          source: |
            # Python SDK coming soon. Use the cURL example for now.
      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' },
            });
        - lang: python
          label: Python
          source: |
            # Python SDK coming soon. Use the cURL example for now.
      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',
              },
            });
        - lang: python
          label: Python
          source: |
            # Python SDK coming soon. Use the cURL example for now.
      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 },
            });
        - lang: python
          label: Python
          source: |
            # Python SDK coming soon. Use the cURL example for now.
      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/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' },
            });
        - lang: python
          label: Python
          source: |
            # Python SDK coming soon. Use the cURL example for now.
      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' },
            });
        - lang: python
          label: Python
          source: |
            # Python SDK coming soon. Use the cURL example for now.
      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.
      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 });
        - lang: python
          label: Python
          source: |
            # Python SDK coming soon. Use the cURL example for now.
      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 },
            });
        - lang: python
          label: Python
          source: |
            # Python SDK coming soon. Use the cURL example for now.
      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.
      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": "Donation page"
              }'
        - 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: 'Donation page' },
            });
        - lang: python
          label: Python
          source: |
            # Python SDK coming soon. Use the cURL example for now.
      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 },
            });
        - lang: python
          label: Python
          source: |
            # Python SDK coming soon. Use the cURL example for now.
      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' },
            });
        - lang: python
          label: Python
          source: |
            # Python SDK coming soon. Use the cURL example for now.
      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 },
            });
        - lang: python
          label: Python
          source: |
            # Python SDK coming soon. Use the cURL example for now.
      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.
      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();
        - lang: python
          label: Python
          source: |
            # Python SDK coming soon. Use the cURL example for now.
      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.
      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' },
            });
        - lang: python
          label: Python
          source: |
            # Python SDK coming soon. Use the cURL example for now.
      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"
    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.invalid
            - 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"
    InvoiceActivatedWebhookEnvelope:
      type: object
      additionalProperties: false
      required:
        - event
        - created_at
        - data
      description: Typed envelope for invoice activated webhook events.
      properties:
        event:
          type: string
          description: Event type identifier.
          enum:
            - invoice.activated
        created_at:
          type: string
          format: date-time
          description: Event creation timestamp.
        data:
          $ref: "#/components/schemas/InvoiceActivatedWebhookData"
    InvoiceDraftExpiredWebhookEnvelope:
      type: object
      additionalProperties: false
      required:
        - event
        - created_at
        - data
      description: Typed envelope for draft invoice expiry webhook events.
      properties:
        event:
          type: string
          description: Event type identifier.
          enum:
            - invoice.draft_expired
        created_at:
          type: string
          format: date-time
          description: Event creation timestamp.
        data:
          $ref: "#/components/schemas/InvoiceDraftExpiredWebhookData"
    InvoiceSentWebhookEnvelope:
      type: object
      additionalProperties: false
      required:
        - event
        - created_at
        - data
      description: Typed envelope for invoice sent webhook events.
      properties:
        event:
          type: string
          description: Event type identifier.
          enum:
            - invoice.sent
        created_at:
          type: string
          format: date-time
          description: Event creation timestamp.
        data:
          $ref: "#/components/schemas/InvoiceSentWebhookData"
    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"
    TestWebhookEnvelope:
      type: object
      additionalProperties: false
      required:
        - type
        - message
        - environment
        - created_at
      description: Payload sent by the dashboard test webhook action.
      properties:
        type:
          type: string
          enum:
            - test
          description: Test event type identifier.
        message:
          type: string
          description: Human-readable test message.
        environment:
          type: string
          enum:
            - live
            - test
          description: Environment selected for the test delivery.
        created_at:
          type: string
          format: date-time
          description: Event creation timestamp.
    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"
    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
        - target_settlement_asset
      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.
        target_settlement_asset:
          type: string
          enum:
            - passthrough
            - usdt_tron
            - usdt_ethereum
            - usdc_ethereum
          nullable: true
          description: Conversion target frozen at invoice creation. Null for legacy rows.
        settlement_target_currency:
          type: string
          nullable: true
          enum:
            - USDC
            - USDT
          description: Target settlement currency derived from `target_settlement_asset`.
        settlement_target_network:
          type: string
          nullable: true
          enum:
            - ethereum
            - tron
          description: Target settlement network derived from `target_settlement_asset`.
        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.
    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.invalid, 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.
    InvoiceActivatedWebhookData:
      type: object
      additionalProperties: false
      required:
        - invoice_id
        - external_id
        - currency
        - environment
        - amount_requested
        - deposit_address
        - status
        - activation_count
        - expires_at
      description: Payload data for invoice.activated events.
      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_usd:
          type: string
          nullable: true
          description: Deprecated USD fiat amount mirror when present.
        amount_fiat:
          type: string
          description: Fiat-denominated invoice amount when present.
        fiat_currency:
          type: string
          description: ISO 4217 code for `amount_fiat` when present.
        deposit_address:
          type: string
          description: Blockchain address assigned at activation.
        status:
          type: string
          description: Invoice status at activation time.
        activation_count:
          type: integer
          description: Number of activation attempts for the draft invoice.
        expires_at:
          type: string
          format: date-time
          description: Payment window expiration timestamp.
        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).
    InvoiceDraftExpiredWebhookData:
      type: object
      additionalProperties: false
      required:
        - invoice_id
        - external_id
        - currency
        - environment
        - status
      description: Payload data for invoice.draft_expired events.
      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_usd:
          type: string
          nullable: true
          description: Deprecated USD fiat amount mirror when present.
        amount_fiat:
          type: string
          description: Fiat-denominated invoice amount when present.
        fiat_currency:
          type: string
          description: ISO 4217 code for `amount_fiat` when present.
        status:
          type: string
          description: Invoice status at draft expiry time.
        draft_expires_at:
          type: string
          format: date-time
          description: Draft invoice expiration timestamp when present.
        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).
    InvoiceSentWebhookData:
      type: object
      additionalProperties: false
      required:
        - invoice_id
        - external_id
        - currency
        - environment
        - amount_requested
        - payer_email
        - status
      description: Payload data for invoice.sent events.
      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_usd:
          type: string
          nullable: true
          description: Deprecated USD fiat amount mirror when present.
        amount_fiat:
          type: string
          description: Fiat-denominated invoice amount when present.
        fiat_currency:
          type: string
          description: ISO 4217 code for `amount_fiat` when present.
        payer_email:
          type: string
          format: email
          description: Email address the payment request was sent to.
        status:
          type: string
          description: Invoice status when the email was sent.
        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).
    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
          enum:
            - target_credited
            - source_credited
          description: Whether the merchant balance was credited in the target settlement asset or terminal source asset.
        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".
    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.
        settlement_conversion:
          $ref: "#/components/schemas/SettlementConversionInfo"
    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.
    SettlementConversionInfo:
      type: object
      additionalProperties: false
      required:
        - target_currency
        - target_network
        - source_amount
        - target_gross_amount
        - fee_amount
        - net_amount
        - quote_status
        - execution_status
      properties:
        target_currency:
          type: string
          description: Merchant settlement asset credited for this payment. In passthrough mode this is the paid currency.
        target_network:
          type: string
          description: Merchant settlement network credited for this payment. In passthrough mode this is the paid network.
        source_amount:
          type: string
          description: Source payment amount used for settlement accounting.
        target_gross_amount:
          type: string
          description: Settlement target amount before invoice fees.
        fee_amount:
          type: string
          description: Fee deducted in the target settlement asset.
        net_amount:
          type: string
          description: Net target settlement amount credited to merchant balance.
        quote_status:
          type: string
          enum:
            - not_required
            - ready
            - quote_unavailable
            - route_unavailable
            - quote_stale
          description: Locked quote status copied from the selected payment slot.
        execution_status:
          type: string
          enum:
            - not_required
            - execution_pending
            - execution_completed
            - execution_failed
            - source_credited
            - reversed
          description: Async settlement execution status when a conversion execution exists.
        failure_class:
          type: string
          nullable: true
          description: Settlement quote or execution failure class when applicable.
        quoted_at:
          type: string
          nullable: true
          format: date-time
          description: Time the selected conversion quote was obtained.
        locked_until:
          type: string
          nullable: true
          format: date-time
          description: Time until which the selected settlement quote remains valid, when applicable.
  responses:
    ValidationError:
      description: Validation error.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    Unauthorized:
      description: Missing or invalid credentials.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    Forbidden:
      description: Insufficient permissions.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    InternalError:
      description: Internal server error.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    GateOffline:
      description: Payment processor unavailable.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    Conflict:
      description: Conflict (e.g. resource not in expected state).
      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
    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.invalid:
    post:
      operationId: webhookInvoiceInvalid
      tags:
        - webhooks
      summary: Invoice marked as invalid.
      description: |
        Fired when an invoice is marked invalid due to a processing error.

        **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"
  invoice.activated:
    post:
      operationId: webhookInvoiceActivated
      tags:
        - webhooks
      summary: Draft invoice activated for payment.
      description: |
        Fired when a deferred invoice is activated and receives a deposit
        address plus payment window.

        **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/InvoiceActivatedWebhookEnvelope"
  invoice.draft_expired:
    post:
      operationId: webhookInvoiceDraftExpired
      tags:
        - webhooks
      summary: Draft invoice expired before activation.
      description: |
        Fired when a deferred invoice's draft window expires before the
        payer activates a currency and payment address.

        **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/InvoiceDraftExpiredWebhookEnvelope"
  invoice.sent:
    post:
      operationId: webhookInvoiceSent
      tags:
        - webhooks
      summary: Invoice payment request email sent.
      description: |
        Fired after halfin queues a payment request email for an invoice
        payer.

        **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/InvoiceSentWebhookEnvelope"
  balance.credited:
    post:
      operationId: webhookBalanceCredited
      tags:
        - webhooks
      summary: Merchant balance credited.
      description: |
        Fired only after the merchant's available balance is credited.
        Converted payments emit `outcome=target_credited`; terminal
        unconverted fallback emits `outcome=source_credited`.

        **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"
  test:
    post:
      operationId: webhookTest
      tags:
        - webhooks
      summary: Test webhook delivery.
      description: |
        Fired when a merchant sends a test webhook from the dashboard to
        verify endpoint connectivity.

        **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/TestWebhookEnvelope"
  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"
