> ## 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.

# Store the pre-signed Bitcoin claim or refund — body: { kind: "claim" | "refund", toAddress, levels: [{ feeSats, psbtBase64, signaturesHex }] }

> Each level is verified against the escrow's confirmed outputs and the script's key for that branch: one output to toAddress, one signature per input; a claim by the receiver key with locktime 0, a refund by the sender key with locktime = the leg's timelock and non-final inputs.



## OpenAPI

````yaml /api-reference/openapi.json post /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:
    post:
      summary: >-
        Store the pre-signed Bitcoin claim or refund — body: { kind: "claim" |
        "refund", toAddress, levels: [{ feeSats, psbtBase64, signaturesHex }] }
      description: >-
        Each level is verified against the escrow's confirmed outputs and the
        script's key for that branch: one output to toAddress, one signature per
        input; a claim by the receiver key with locktime 0, a refund by the
        sender key with locktime = the leg's timelock and non-final inputs.
      operationId: postSwapsByIdLegsByLegPresign
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: leg
          in: path
          required: true
          schema:
            type: string
            enum:
              - a
              - b
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - kind
                - toAddress
                - levels
              properties:
                kind:
                  type: string
                  enum:
                    - claim
                    - refund
                toAddress:
                  type: string
                levels:
                  type: array
                  minItems: 1
                  maxItems: 3
                  items:
                    type: object
                    properties:
                      feeSats:
                        type: integer
                      psbtBase64:
                        type: string
                      signaturesHex:
                        type: array
                        items:
                          type: string
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                type: object
                properties:
                  swap:
                    $ref: '#/components/schemas/Swap'
        '400':
          description: >-
            a level failed verification — the message names the level and the
            check
          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:
    Swap:
      type: object
      description: leg a = what the maker gives, leg b = what the maker wants
      required:
        - id
        - threadId
        - status
        - makerId
        - takerId
        - initiatorUserId
        - hashlock
        - aAssetId
        - aChain
        - aAmount
        - aTimelock
        - bAssetId
        - bChain
        - bAmount
        - bTimelock
      properties:
        id:
          type: string
        rfqId:
          type:
            - string
            - 'null'
        threadId:
          type: string
        status:
          type: string
        makerId:
          type: string
        takerId:
          type: string
        initiatorUserId:
          type: string
          description: funds the long leg first and holds the secret
        hashlock:
          type: string
          description: sha256(secret), hex
        secretCiphertext:
          type:
            - string
            - 'null'
          description: the secret once revealed on chain; null before
        meta:
          type:
            - object
            - 'null'
          additionalProperties: true
          description: >-
            server bookkeeping; `meta.fundIntent[leg].at` is a funding in flight
            (see fund-intent)
        feeBps:
          type: integer
        feeAmount:
          type: string
          description: base units of feeAssetId, paid by the taker
        feeAssetId:
          type:
            - string
            - 'null'
        aAssetId:
          type: string
        aChain:
          type: string
        aAmount:
          type: string
          description: base units
        aTimelock:
          type: string
          format: date-time
        aPayoutAddress:
          type:
            - string
            - 'null'
        aClaimTo:
          type:
            - string
            - 'null'
          description: >-
            Bitcoin: where this leg's claim pays (its receiver's wallet unless
            they named another address)
        aRefundAddress:
          type:
            - string
            - 'null'
        aHtlcAddress:
          type:
            - string
            - 'null'
        aFundTx:
          type:
            - string
            - 'null'
        aFundOutpoints:
          type:
            - array
            - 'null'
          items:
            $ref: '#/components/schemas/FundOutpoint'
          description: 'Bitcoin: the funding outputs a claim or refund spends'
        aClaimTx:
          type:
            - string
            - 'null'
        aRefundTx:
          type:
            - string
            - 'null'
        bAssetId:
          type: string
        bChain:
          type: string
        bAmount:
          type: string
          description: base units
        bTimelock:
          type: string
          format: date-time
        bPayoutAddress:
          type:
            - string
            - 'null'
        bClaimTo:
          type:
            - string
            - 'null'
          description: >-
            Bitcoin: where this leg's claim pays (its receiver's wallet unless
            they named another address)
        bRefundAddress:
          type:
            - string
            - 'null'
        bHtlcAddress:
          type:
            - string
            - 'null'
        bFundTx:
          type:
            - string
            - 'null'
        bFundOutpoints:
          type:
            - array
            - 'null'
          items:
            $ref: '#/components/schemas/FundOutpoint'
          description: 'Bitcoin: the funding outputs a claim or refund spends'
        bClaimTx:
          type:
            - string
            - 'null'
        bRefundTx:
          type:
            - string
            - 'null'
        timeline:
          type:
            - object
            - 'null'
          description: >-
            each party's turns as the settlement engine decided them — fundA
            (the maker's funding), fundB (the taker's), payoutA / payoutB
            (naming where leg a / leg b pays: the taker's / the maker's), reveal
            (the initiator's) — with ISO times, the first of each kept: what a
            Thread's counterpartyStats is counted from. Null on swaps from
            before it was recorded; any turn or time may be absent
          properties:
            fundA:
              type: object
              properties:
                openedAt:
                  type: string
                  format: date-time
                deadline:
                  type: string
                  format: date-time
                doneAt:
                  type: string
                  format: date-time
                sentAt:
                  type: string
                  format: date-time
                heldAt:
                  type: string
                  format: date-time
                shortAt:
                  type: string
                  format: date-time
                unverifiedAt:
                  type: string
                  format: date-time
            fundB:
              type: object
              properties:
                openedAt:
                  type: string
                  format: date-time
                deadline:
                  type: string
                  format: date-time
                doneAt:
                  type: string
                  format: date-time
                sentAt:
                  type: string
                  format: date-time
                heldAt:
                  type: string
                  format: date-time
                shortAt:
                  type: string
                  format: date-time
                unverifiedAt:
                  type: string
                  format: date-time
            reveal:
              type: object
              properties:
                openedAt:
                  type: string
                  format: date-time
                deadline:
                  type: string
                  format: date-time
                doneAt:
                  type: string
                  format: date-time
                sentAt:
                  type: string
                  format: date-time
                heldAt:
                  type: string
                  format: date-time
                shortAt:
                  type: string
                  format: date-time
                unverifiedAt:
                  type: string
                  format: date-time
            payoutA:
              type: object
              properties:
                openedAt:
                  type: string
                  format: date-time
                deadline:
                  type: string
                  format: date-time
                doneAt:
                  type: string
                  format: date-time
                sentAt:
                  type: string
                  format: date-time
                heldAt:
                  type: string
                  format: date-time
                shortAt:
                  type: string
                  format: date-time
                unverifiedAt:
                  type: string
                  format: date-time
            payoutB:
              type: object
              properties:
                openedAt:
                  type: string
                  format: date-time
                deadline:
                  type: string
                  format: date-time
                doneAt:
                  type: string
                  format: date-time
                sentAt:
                  type: string
                  format: date-time
                heldAt:
                  type: string
                  format: date-time
                shortAt:
                  type: string
                  format: date-time
                unverifiedAt:
                  type: string
                  format: date-time
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    Error:
      type: object
      properties:
        error:
          type: string
      required:
        - error
    FundOutpoint:
      type: object
      properties:
        txid:
          type: string
        vout:
          type: integer
        value:
          type: integer
          description: sats
  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.