> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hashlock.markets/llms.txt
> Use this file to discover all available pages before exploring further.

# Bitcoin leg: build its claim (receiver) or refund (funder) to PRE-SIGN, at up to three fee levels

> `kind=claim` (default): only the leg's receiver, once its escrow is recorded. A claim sighash does not cover the witness, so signing it needs no secret: sign every `sighashHexes` entry of each level with the script's receiver key and POST them back. When the secret is revealed the keeper inserts it and broadcasts the level the fee market needs, so the claim is paid while you are offline. `kind=refund`: only the initiator's leg, by its funder, once its escrow is recorded and while the secret is not revealed and the leg not refunded — also after the deal failed; signed with the script's sender key, valid only after the timelock. If the deal cannot complete (the secret never revealed, the other leg refunded), the keeper broadcasts it once the median time past is beyond the timelock — never after a reveal. `to` (optional) is where the spend pays; default: a claim pays the leg's `claimTo` (a Bitcoin address sent to POST /v1/swaps/{id}/address — over /v1 one of this account's proven wallets), else the receiver's own wallet behind the signing key, else that key's P2WPKH address; a refund pays the signing key's own P2WPKH address. A claim `to` other than a set `claimTo` is refused with 409.



## OpenAPI

````yaml /api-reference/openapi.json get /v1/swaps/{id}/legs/{leg}/presign
openapi: 3.1.0
info:
  title: Hashlock Markets API
  version: 1.0.0
  description: >-
    Guides, concepts and a quickstart: https://docs.hashlock.markets


    Non-custodial cross-chain atomic swaps (BTC ↔ EVM / TRON / Solana) via
    sealed RFQ + HTLC. Makers and takers share these endpoints. Authenticate
    with an API key: `Authorization: Bearer hk_...`.


    **Wallets first.** An order that GIVES an asset requires a proven wallet on
    that asset's chain, and your key starts with only the address its account
    signed in with. `GET /v1/me` lists both what each chain's `login` address is
    and what you have `proven`; `POST /v1/wallets/{evm|tron|solana|bitcoin}`
    proves another by signature — Bitcoin included, on a BIP-322 one. A proof
    adds trading reach and nothing else: it never becomes the account's payout
    identity, which only a wallet session can change — so a key alone cannot
    redirect anyone's money. The message you sign for it must say `link this
    wallet` and carry a nonce from `GET /v1/wallets/nonce`; that nonce is not a
    login nonce, so the signature you collect is not a session for that wallet's
    account.


    **Rate limits.** Every response carries `RateLimit-Limit`,
    `RateLimit-Remaining` and `RateLimit-Reset` (seconds). Over the limit
    returns `429` with `Retry-After`.


    **Idempotency.** Send an `Idempotency-Key` header on any POST to make
    retries safe: the operation runs once and the same response is replayed
    (with `Idempotency-Replayed: true`). Reusing a key with a different body
    returns `422`. Keys are remembered for 24h — except a `504`, which is a read
    of ours running out of time: on the builders nothing happened, and on
    `/tx/broadcast` we cannot tell — so either way a retry with the same key
    really retries. The fund, claim and refund builders change nothing on the
    server, so they run on every call and nothing of theirs is remembered —
    retry them freely.


    **Custody-agnostic settlement.** The fund/claim/refund endpoints return
    UNSIGNED transactions; you sign with your own key/HSM and submit via
    `/v1/tx/broadcast`. The server never holds your keys.


    ---


    ## Maker feed (WebSocket)


    Polling `GET /v1/rfqs` will not win you flow. `wss://<host>/v1/ws` — the
    host you selected above — streams every request-for-quote you are eligible
    for, and takes your quotes back over the same socket.


    **Handshake.** Connect, then send your key as the first frame. Nothing else
    is accepted until you do:


    ```json

    { "apiKey": "hk_live_..." }

    ```


    The key needs the `maker` scope (`GET /v1/me` shows what yours has). You get
    a snapshot, then a readiness marker:


    ```json

    { "type": "snapshot", "rfqs": [ ... ] }

    { "type": "ready" }

    ```


    The snapshot is the **public** book only, and at most **100** entries,
    newest first. If you need more than that, page `GET /v1/rfqs?cursor=…` after
    connecting. An RFQ created while the snapshot was being built may appear
    both in it and in the stream that follows — frames carry the rfq id, so key
    on it and the duplicate is a no-op.


    **The stream.** Two kinds, and only two:


    ```json

    { "type": "rfq", "kind": "created",   "rfq": { ... } }

    { "type": "rfq", "kind": "cancelled", "rfq": { ... } }

    ```


    `cancelled` means the order left the book, not always that its owner
    cancelled it: when a listed token **closes** (it no longer passes the
    listing checks), every public order on it is sent as `cancelled` while its
    status is unchanged (`open` or `negotiating`); if the token **reopens**, the
    same orders come back as `created` with their original id and `createdAt`.
    Treat both as upserts keyed on the id.


    An RFQ also leaves the book when it **expires** or is **agreed** with
    another maker, and neither emits a frame. A book you maintain purely from
    the stream therefore drifts: it keeps orders nobody can take any more, and
    quoting into one is rejected. Reconcile with a fresh `snapshot` (reconnect)
    or check `GET /v1/rfqs/{id}` before committing to a price.


    **What reaches you.** Public RFQs, plus private ones addressed to an address
    on your account. Matching follows each encoding: EVM hex and bech32
    (`bc1…`/`tb1…`) compare case-insensitively, base58check (TRON, legacy
    Bitcoin) does not — there the case of a character is part of the address.
    Never your own orders — neither in the snapshot nor in the stream. Note the
    asymmetry: **private orders arrive only on the live stream.** They are
    link-only by design, so they are absent from the snapshot and from `GET
    /v1/rfqs`, and there is no endpoint that lists the ones addressed to you. A
    private order sent while your socket was down is not recoverable — if you
    trade private flow, stay connected.


    **Quoting.** Send the quote down the same socket; no HTTP round-trip, no
    second authentication. `quoteAmount` is in base units, as everywhere else in
    this API, with at most 8 decimals of its asset (fewer when the asset has
    fewer) — round a computed price down, or it is refused with "a price has at
    most N decimals":


    ```json

    { "quote": { "rfqId": "…", "quoteAmount": "1500000000" } }

    → { "type": "quoted", "rfqId": "…", "threadId": "…" }

    ```


    The `threadId` is a negotiation thread: `GET /v1/threads/{id}` and the
    propose/accept endpoints take it from there. `POST /v1/rfqs/{id}/quotes`
    does the same thing over REST if you prefer.


    **Errors** arrive as `{ "type": "error", "error": "…" }` and do not close
    the socket.


    **Operating it.** There is no heartbeat and no resume: detect a dead socket
    yourself and reconnect. The fresh `snapshot` is your recovery for the public
    book, with the two limits above — 100 entries, and no private orders.
    Nothing is replayed, so this socket is not a settlement log. For that
    register a webhook (`POST /v1/webhooks`): the feed tells you what to quote,
    webhooks tell you what happened to your swaps.


    **Webhook signature.** `x-hashlock-signature: sha256=<hex>` is HMAC-SHA256
    over `<timestamp>.<body>` with your webhook secret; the timestamp is in
    `x-hashlock-timestamp`. Verify before trusting a delivery, and reply 2xx.


    **Webhook events.** `quote.created`, `swap.agreed`, `swap.funded`,
    `secret.revealed`, `swap.settled`, `swap.refunded`, `swap.half_settled` (one
    leg claimed, the other refunded at its timelock) — subscribe to none and you
    get all of them. `POST /v1/webhooks/{id}/ping` also delivers a `ping` event,
    so handle an unknown `type` by returning 2xx rather than erroring.


    **Webhook delivery is best-effort, not durable.** A non-2xx or a timeout is
    retried after 15s, 30s, 60s, 120s and 240s — **6 attempts over 7m45s** — and
    is then dropped; there is no redelivery endpoint and no delivery log beyond
    `lastStatus` on `GET /v1/webhooks`. If your receiver is down longer than
    that you will lose events, so reconcile against `GET /v1/swaps/{id}` — that
    is the authoritative state — rather than treating the webhook as the record.
servers:
  - url: https://api.hashlock.markets
    description: production
security:
  - ApiKey: []
paths:
  /v1/swaps/{id}/legs/{leg}/presign:
    get:
      summary: >-
        Bitcoin leg: build its claim (receiver) or refund (funder) to PRE-SIGN,
        at up to three fee levels
      description: >-
        `kind=claim` (default): only the leg's receiver, once its escrow is
        recorded. A claim sighash does not cover the witness, so signing it
        needs no secret: sign every `sighashHexes` entry of each level with the
        script's receiver key and POST them back. When the secret is revealed
        the keeper inserts it and broadcasts the level the fee market needs, so
        the claim is paid while you are offline. `kind=refund`: only the
        initiator's leg, by its funder, once its escrow is recorded and while
        the secret is not revealed and the leg not refunded — also after the
        deal failed; signed with the script's sender key, valid only after the
        timelock. If the deal cannot complete (the secret never revealed, the
        other leg refunded), the keeper broadcasts it once the median time past
        is beyond the timelock — never after a reveal. `to` (optional) is where
        the spend pays; default: a claim pays the leg's `claimTo` (a Bitcoin
        address sent to POST /v1/swaps/{id}/address — over /v1 one of this
        account's proven wallets), else the receiver's own wallet behind the
        signing key, else that key's P2WPKH address; a refund pays the signing
        key's own P2WPKH address. A claim `to` other than a set `claimTo` is
        refused with 409.
      operationId: getSwapsByIdLegsByLegPresign
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: leg
          in: path
          required: true
          schema:
            type: string
            enum:
              - a
              - b
        - name: kind
          in: query
          required: false
          schema:
            type: string
            enum:
              - claim
              - refund
        - name: to
          in: query
          required: false
          schema:
            type: string
      responses:
        '200':
          description: the spend to sign at each fee level
          content:
            application/json:
              schema:
                type: object
                properties:
                  kind:
                    type: string
                    enum:
                      - claim
                      - refund
                  toAddress:
                    type: string
                  signerPubkeyHex:
                    type: string
                    description: the key that signs this branch
                  receiverPubkeyHex:
                    type: string
                    description: kind=claim only — the same key as signerPubkeyHex
                  inputs:
                    type: array
                    items:
                      type: object
                      properties:
                        txid:
                          type: string
                        vout:
                          type: integer
                        value:
                          type: integer
                  levels:
                    type: array
                    items:
                      type: object
                      properties:
                        feeSats:
                          type: integer
                        psbtBase64:
                          type: string
                        sighashHexes:
                          type: array
                          items:
                            type: string
        '400':
          description: error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: >-
            not a Bitcoin leg you receive (claim) or fund as the initiator
            (refund), the escrow is not recorded yet, (claim) the swap is over,
            or (refund) the secret is revealed or the leg refunded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: the Bitcoin explorer could not be read — retry
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
      required:
        - error
  securitySchemes:
    ApiKey:
      type: http
      scheme: bearer
      description: >-
        An API key (hk_live_… / hk_test_…) with read/taker/maker scopes. `read`
        is read-only: every write except webhooks needs `taker` or `maker` (403
        otherwise), and either of those can read too. Creating RFQs and listing
        tokens need `taker`; quoting needs `maker`.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.