{
  "openapi": "3.1.0",
  "info": {
    "title": "Bitwyre REST API",
    "version": "0.1.0",
    "summary": "Trade and custody endpoints for the Bitwyre exchange.",
    "description": "Authenticated endpoints require a `Authorization: Bearer <JWT>` header. JWTs are obtained by completing a challenge flow at `/auth/login` + `/auth/verify`. All amounts are decimal strings to avoid floating-point rounding. All timestamps are ISO 8601 UTC.",
    "contact": {
      "name": "Bitwyre Engineering",
      "url": "https://bitwyre.com"
    }
  },
  "servers": [
    {
      "url": "https://api.bitwyre.com",
      "description": "Production"
    },
    {
      "url": "https://api-sandbox.bitwyre.com",
      "description": "Sandbox"
    },
    {
      "url": "http://localhost:3001",
      "description": "Local"
    }
  ],
  "tags": [
    {
      "name": "auth",
      "description": "Registration, login, 2FA, sessions"
    },
    {
      "name": "kyc",
      "description": "Individual identity verification"
    },
    {
      "name": "kyb",
      "description": "Corporate onboarding"
    },
    {
      "name": "wallet",
      "description": "Balances, deposits, withdrawals"
    },
    {
      "name": "orders",
      "description": "Order lifecycle on the matching engine"
    },
    {
      "name": "markets",
      "description": "Public market data"
    },
    {
      "name": "admin",
      "description": "KYC/KYB review (requires admin role)"
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT"
      },
      "apiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "HMAC-signed API key auth for trading/wallet endpoints. Send three headers: `X-API-Key` (public key id, `bwk_...`), `X-API-Timestamp` (unix seconds, within \u00b130s of server time), and `X-API-Signature` = lowercase hex HMAC-SHA256(secret, `{timestamp}{METHOD}{path}{body}`) \u2014 e.g. `1752800000POST/api/v1/orders{\"symbol\":...}`. The secret is shown once at key creation. Keys carry a permission bitmask: 1=read, 2=trade, 4=withdraw."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error",
          "message"
        ],
        "properties": {
          "error": {
            "type": "string",
            "example": "bad_request"
          },
          "message": {
            "type": "string"
          }
        }
      },
      "RegisterRequest": {
        "type": "object",
        "required": [
          "email",
          "password"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          },
          "password": {
            "type": "string",
            "minLength": 10
          }
        }
      },
      "RegisterResponse": {
        "type": "object",
        "required": [
          "userId",
          "verificationSent",
          "isDryRun"
        ],
        "properties": {
          "userId": {
            "type": "integer",
            "format": "int64"
          },
          "verificationSent": {
            "type": "boolean"
          },
          "isDryRun": {
            "type": "boolean",
            "description": "True when RESEND_API_KEY is unset and the code was only logged."
          }
        }
      },
      "VerifyEmailRequest": {
        "type": "object",
        "required": [
          "email",
          "code"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          },
          "code": {
            "type": "string",
            "minLength": 6,
            "maxLength": 6
          }
        }
      },
      "LoginRequest": {
        "type": "object",
        "required": [
          "email",
          "password"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          },
          "password": {
            "type": "string"
          }
        }
      },
      "LoginResponse": {
        "type": "object",
        "required": [
          "challengeId",
          "requiredMethods"
        ],
        "properties": {
          "challengeId": {
            "type": "string"
          },
          "requiredMethods": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "totp",
                "passkey",
                "email"
              ]
            }
          }
        }
      },
      "VerifyRequest": {
        "type": "object",
        "required": [
          "challengeId",
          "method",
          "code"
        ],
        "properties": {
          "challengeId": {
            "type": "string"
          },
          "method": {
            "type": "string",
            "enum": [
              "totp",
              "passkey",
              "email"
            ]
          },
          "code": {
            "type": "string"
          }
        }
      },
      "AuthToken": {
        "type": "object",
        "required": [
          "accessToken",
          "refreshToken",
          "expiresIn"
        ],
        "properties": {
          "accessToken": {
            "type": "string"
          },
          "refreshToken": {
            "type": "string"
          },
          "expiresIn": {
            "type": "integer"
          }
        }
      },
      "IndividualProfile": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "not_started",
              "in_progress",
              "pending_review",
              "approved",
              "rejected",
              "info_requested"
            ]
          },
          "provider": {
            "type": "string",
            "enum": [
              "own",
              "sumsub"
            ]
          },
          "firstName": {
            "type": "string"
          },
          "middleName": {
            "type": "string"
          },
          "lastName": {
            "type": "string"
          },
          "dateOfBirth": {
            "type": "string",
            "format": "date"
          },
          "nationality": {
            "type": "string",
            "description": "ISO 3166-1 alpha-2"
          },
          "residenceCountry": {
            "type": "string",
            "description": "ISO 3166-1 alpha-2"
          },
          "idDocumentKind": {
            "type": "string",
            "enum": [
              "passport",
              "drivers_license",
              "national_id"
            ]
          },
          "idDocumentCountry": {
            "type": "string"
          }
        }
      },
      "DocumentRegisterRequest": {
        "type": "object",
        "required": [
          "kind",
          "objectKey",
          "mimeType",
          "sizeBytes",
          "sha256Hex"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "description": "See /docs for allowed values"
          },
          "objectKey": {
            "type": "string"
          },
          "mimeType": {
            "type": "string"
          },
          "sizeBytes": {
            "type": "integer",
            "format": "int64"
          },
          "sha256Hex": {
            "type": "string",
            "minLength": 64,
            "maxLength": 64
          }
        }
      },
      "CorporateProfile": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string"
          },
          "provider": {
            "type": "string",
            "enum": [
              "own",
              "sumsub"
            ]
          },
          "legalName": {
            "type": "string"
          },
          "tradingName": {
            "type": "string"
          },
          "registrationNumber": {
            "type": "string"
          },
          "jurisdiction": {
            "type": "string"
          },
          "incorporationDate": {
            "type": "string",
            "format": "date"
          },
          "lineOfBusiness": {
            "type": "string"
          },
          "incorporatedOver1y": {
            "type": "boolean"
          }
        }
      },
      "Shareholder": {
        "type": "object",
        "required": [
          "fullName"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "fullName": {
            "type": "string"
          },
          "dateOfBirth": {
            "type": "string",
            "format": "date"
          },
          "nationality": {
            "type": "string"
          },
          "residenceCountry": {
            "type": "string"
          },
          "sharePercent": {
            "type": "number"
          },
          "isUbo": {
            "type": "boolean"
          },
          "isDirector": {
            "type": "boolean"
          }
        }
      },
      "BalanceInfo": {
        "type": "object",
        "properties": {
          "currency": {
            "type": "string"
          },
          "available": {
            "type": "string"
          },
          "locked": {
            "type": "string"
          },
          "total": {
            "type": "string"
          }
        }
      },
      "SubmitOrderRequest": {
        "type": "object",
        "required": [
          "symbol",
          "side",
          "orderType",
          "quantity"
        ],
        "properties": {
          "symbol": {
            "type": "string",
            "example": "USDT_USD"
          },
          "side": {
            "type": "string",
            "enum": [
              "Buy",
              "Sell"
            ]
          },
          "orderType": {
            "type": "string",
            "enum": [
              "Market",
              "Limit",
              "Stop",
              "StopLimit"
            ]
          },
          "timeInForce": {
            "type": "string",
            "enum": [
              "GTC",
              "IOC",
              "FOK",
              "Day"
            ]
          },
          "quantity": {
            "type": "string",
            "example": "0.25"
          },
          "price": {
            "type": "string",
            "example": "78331.20"
          },
          "stopPrice": {
            "type": "string"
          },
          "postOnly": {
            "type": "boolean",
            "default": false,
            "description": "Maker-only: reject the order instead of letting it take liquidity (cross the spread) as a taker. A buy at or above the best ask, or a sell at or below the best bid, is rejected rather than filled."
          },
          "clientOrderId": {
            "type": "string"
          }
        }
      },
      "OrderResponse": {
        "type": "object",
        "description": "BREAKING CHANGE: this used to be `{\"orderId\": <integer>, \"status\": \"pending\"}`, where `orderId` actually held the *correlation* id minted at submit time — not the real exchange order id (order-manager assigns that asynchronously, over RabbitMQ, so it isn't known yet when this response is built). A client cancelling with that value was cancelling an id that didn't exist. There is no `orderId` in this response anymore; use `correlationId` (honestly named) plus GET /api/v1/orders/open, the ExecutionReport/OrderStatusUpdate frames on /ws/v1/private (both now carry correlationId), or DELETE /api/v1/orders/by-correlation/{correlationId} to resolve or act on the real order id.",
        "properties": {
          "correlationId": {
            "type": "string",
            "description": "The id minted at submit time (a nanosecond timestamp). Returned as a string, not a bare number — it can exceed Number.MAX_SAFE_INTEGER.",
            "example": "1785842370546517451"
          },
          "status": {
            "type": "string",
            "example": "pending"
          }
        }
      },
      "OpenOrder": {
        "type": "object",
        "properties": {
          "orderId": {
            "type": "integer",
            "format": "int64"
          },
          "correlationId": {
            "type": "string",
            "description": "The id this order was originally submitted with — the same value POST /api/v1/orders returned as correlationId. Lets a client map its own submit-time id to this order_id.",
            "example": "1785842370546517451"
          },
          "symbol": {
            "type": "string",
            "example": "USDT_USD"
          },
          "side": {
            "type": "string",
            "enum": [
              "Buy",
              "Sell"
            ]
          },
          "orderType": {
            "type": "string",
            "enum": [
              "Market",
              "Limit",
              "Stop",
              "StopLimit"
            ]
          },
          "timeInForce": {
            "type": "string",
            "enum": [
              "GTC",
              "IOC",
              "FOK",
              "Day"
            ]
          },
          "price": {
            "type": "string",
            "example": "78331.20000000"
          },
          "quantity": {
            "type": "string",
            "example": "0.25000000"
          },
          "filledQuantity": {
            "type": "string",
            "example": "0.00000000"
          },
          "status": {
            "type": "string",
            "enum": [
              "New",
              "PartialFill"
            ],
            "description": "Only orders still resting are returned; Filled/Cancelled/Rejected orders are not \"open\"."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CancelAllResponse": {
        "type": "object",
        "description": "Reports outcomes honestly: `cancelled` + `failed` always equal `requested`. A 200 response does not imply every order was cancelled.",
        "properties": {
          "requested": {
            "type": "integer"
          },
          "accepted": {
            "type": "integer"
          },
          "failed": {
            "type": "integer"
          },
          "failedOrderIds": {
            "type": "array",
            "items": {
              "type": "integer",
              "format": "int64"
            }
          }
        }
      },
      "Ticker": {
        "type": "object",
        "properties": {
          "symbol": {
            "type": "string"
          },
          "last": {
            "type": "string"
          },
          "bid": {
            "type": "string"
          },
          "ask": {
            "type": "string"
          },
          "volume24h": {
            "type": "string"
          },
          "change24h": {
            "type": "string"
          }
        }
      },
      "Candle": {
        "type": "object",
        "properties": {
          "openTime": {
            "type": "integer",
            "format": "int64",
            "description": "Nanoseconds since the Unix epoch, bucket start"
          },
          "open": {
            "type": "string"
          },
          "high": {
            "type": "string"
          },
          "low": {
            "type": "string"
          },
          "close": {
            "type": "string"
          },
          "volume": {
            "type": "string"
          },
          "quoteVolume": {
            "type": "string"
          },
          "tradeCount": {
            "type": "integer"
          },
          "source": {
            "type": "string",
            "description": "Venue that produced this candle — never blended with another source"
          }
        }
      },
      "CreateApiKeyRequest": {
        "type": "object",
        "required": [
          "label",
          "permissions"
        ],
        "properties": {
          "label": {
            "type": "string",
            "maxLength": 100
          },
          "permissions": {
            "type": "integer",
            "description": "Bitmask 1..7: 1=read, 2=trade, 4=withdraw"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "AdminCreateApiKeyRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/CreateApiKeyRequest"
          },
          {
            "type": "object",
            "required": [
              "ledgerAccountId"
            ],
            "properties": {
              "ledgerAccountId": {
                "type": "integer",
                "format": "int64",
                "description": "Ledger account embedded in orders signed by this key"
              }
            }
          }
        ]
      },
      "CreateApiKeyResponse": {
        "type": "object",
        "properties": {
          "keyId": {
            "type": "string",
            "example": "bwk_0011223344556677889900aa"
          },
          "secret": {
            "type": "string",
            "description": "HMAC signing secret. Returned exactly once \u2014 store it now."
          },
          "label": {
            "type": "string"
          },
          "permissions": {
            "type": "integer"
          }
        }
      },
      "ApiKeyInfo": {
        "type": "object",
        "properties": {
          "keyId": {
            "type": "string",
            "nullable": true
          },
          "label": {
            "type": "string"
          },
          "permissions": {
            "type": "integer"
          },
          "isActive": {
            "type": "boolean"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "lastUsedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      }
    }
  },
  "paths": {
    "/api/v1/auth/register": {
      "post": {
        "tags": [
          "auth"
        ],
        "summary": "Create a new user, email a 6-digit verification code",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RegisterRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "User created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegisterResponse"
                }
              }
            }
          },
          "400": {
            "description": "Email already registered or password too short",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/auth/email/verify": {
      "post": {
        "tags": [
          "auth"
        ],
        "summary": "Consume a registration verification code",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VerifyEmailRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Email verified"
          },
          "401": {
            "description": "Invalid or expired code"
          }
        }
      }
    },
    "/api/v1/auth/email/request": {
      "post": {
        "tags": [
          "auth"
        ],
        "summary": "Mid-login: issue a 6-digit email 2FA code against an active challenge",
        "responses": {
          "200": {
            "description": "Code emailed"
          },
          "404": {
            "description": "Challenge not found"
          }
        }
      }
    },
    "/api/v1/auth/login": {
      "post": {
        "tags": [
          "auth"
        ],
        "summary": "Start a login challenge \u2014 returns required 2FA methods",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LoginRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Challenge started",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LoginResponse"
                }
              }
            }
          },
          "401": {
            "description": "Invalid email or password"
          }
        }
      }
    },
    "/api/v1/auth/verify": {
      "post": {
        "tags": [
          "auth"
        ],
        "summary": "Complete one 2FA step; returns tokens when all methods verified",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VerifyRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tokens issued",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthToken"
                }
              }
            }
          },
          "401": {
            "description": "Invalid code"
          }
        }
      }
    },
    "/api/v1/kyc/individual": {
      "get": {
        "tags": [
          "kyc"
        ],
        "summary": "Read current KYC profile",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Profile",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IndividualProfile"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "kyc"
        ],
        "summary": "Upsert KYC profile (partial updates allowed via COALESCE)",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Updated profile"
          }
        }
      }
    },
    "/api/v1/kyc/individual/submit": {
      "post": {
        "tags": [
          "kyc"
        ],
        "summary": "Submit for review \u2014 gates on required fields + at least one ID document",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Status now pending_review"
          },
          "400": {
            "description": "Missing required field or document"
          }
        }
      }
    },
    "/api/v1/kyc/documents": {
      "get": {
        "tags": [
          "kyc"
        ],
        "summary": "List uploaded documents",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Documents"
          }
        }
      },
      "post": {
        "tags": [
          "kyc"
        ],
        "summary": "Register an already-uploaded object (DO Spaces object_key + sha256)",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DocumentRegisterRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Registered"
          }
        }
      }
    },
    "/api/v1/kyc/documents/upload-url": {
      "post": {
        "tags": [
          "kyc"
        ],
        "summary": "Request a presigned DO Spaces PUT URL",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "501": {
            "description": "Not implemented \u2014 Spaces credentials not wired yet"
          }
        }
      }
    },
    "/api/v1/kyb/corporate": {
      "get": {
        "tags": [
          "kyb"
        ],
        "summary": "Read corporate profile",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Profile",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CorporateProfile"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "kyb"
        ],
        "summary": "Upsert corporate profile",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Updated"
          }
        }
      }
    },
    "/api/v1/kyb/corporate/submit": {
      "post": {
        "tags": [
          "kyb"
        ],
        "summary": "Submit \u2014 requires legal name, reg number, jurisdiction, LOB, incorporation doc, and \u22651 UBO",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Status now pending_review"
          },
          "400": {
            "description": "Missing required field / document / UBO"
          }
        }
      }
    },
    "/api/v1/kyb/shareholders": {
      "get": {
        "tags": [
          "kyb"
        ],
        "summary": "List shareholders and UBOs",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Shareholders",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Shareholder"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "kyb"
        ],
        "summary": "Add a shareholder or UBO (locked once corporate is pending/approved)",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Added"
          },
          "400": {
            "description": "Corporate already submitted \u2014 cannot mutate"
          }
        }
      }
    },
    "/api/v1/kyb/shareholders/{id}": {
      "delete": {
        "tags": [
          "kyb"
        ],
        "summary": "Remove a shareholder (pre-submit only)",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "404": {
            "description": "Not found"
          }
        }
      }
    },
    "/api/v1/balances": {
      "get": {
        "tags": [
          "wallet"
        ],
        "summary": "List balances across all supported currencies",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Balances",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/BalanceInfo"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/wallet/withdraw": {
      "post": {
        "tags": [
          "wallet"
        ],
        "summary": "Submit a withdrawal \u2014 routed through risk-settlement + custodian",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Withdrawal queued"
          }
        }
      }
    },
    "/api/v1/wallet/deposits": {
      "get": {
        "tags": [
          "wallet"
        ],
        "summary": "Deposit history",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Deposits"
          }
        }
      }
    },
    "/api/v1/wallet/withdrawals": {
      "get": {
        "tags": [
          "wallet"
        ],
        "summary": "Withdrawal history",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Withdrawals"
          }
        }
      }
    },
    "/api/v1/orders": {
      "post": {
        "tags": [
          "orders"
        ],
        "summary": "Submit a new order",
        "description": "BREAKING CHANGE (see OrderResponse): the response no longer returns an `orderId` field. It returns `correlationId` (a string) instead — the real, exchange-assigned `orderId` is not known synchronously and is assigned asynchronously by order-manager over RabbitMQ. Resolve it via GET /api/v1/orders/open, the ExecutionReport/OrderStatusUpdate frames on /ws/v1/private, or cancel directly with DELETE /api/v1/orders/by-correlation/{correlationId}.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubmitOrderRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Order submitted (pending order_id assignment)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrderResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/orders/open": {
      "get": {
        "tags": [
          "orders"
        ],
        "summary": "List open orders",
        "description": "Returns the authenticated account's currently-open orders (status New or PartialFill). Registered before /api/v1/orders/{id} so the literal \"open\" segment is never captured by the {id} u64 path parameter.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Open orders",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/OpenOrder"
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "orders"
        ],
        "summary": "Cancel all open orders",
        "description": "Enumerates the account's open orders and cancels each one over the same RabbitMQ path as DELETE /api/v1/orders/{id}. Partial failure is reported, not hidden — check `failed`/`failedOrderIds` in the response even on a 200.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Cancel-all summary",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CancelAllResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/orders/{id}": {
      "delete": {
        "tags": [
          "orders"
        ],
        "summary": "Cancel an order",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cancel submitted"
          }
        }
      }
    },
    "/api/v1/orders/by-correlation/{correlationId}": {
      "delete": {
        "tags": [
          "orders"
        ],
        "summary": "Cancel an order by its submit-time correlation id",
        "description": "Cancel using the `correlationId` POST /api/v1/orders returned, before the client has learned the real `orderId` (from GET /api/v1/orders/open or a WS execution/status frame). Resolution happens in order-manager's in-memory correlation index, not by querying orders_db here, so there's no extra race window on top of the async order_id assignment. Registered before /api/v1/orders/{id} for the same reason /api/v1/orders/open is — a literal path segment must not fall through to the {id} u64 extractor.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "correlationId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            },
            "description": "The correlationId returned by POST /api/v1/orders (numeric on the wire path segment, even though the JSON body renders it as a string for precision)."
          }
        ],
        "responses": {
          "200": {
            "description": "Cancel submitted"
          }
        }
      }
    },
    "/api/v1/markets": {
      "get": {
        "tags": [
          "markets"
        ],
        "summary": "List all tradable markets",
        "responses": {
          "200": {
            "description": "Markets"
          }
        }
      }
    },
    "/api/v1/markets/tickers": {
      "get": {
        "tags": [
          "markets"
        ],
        "summary": "24h tickers across all markets",
        "responses": {
          "200": {
            "description": "Tickers",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Ticker"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/markets/{symbol}/orderbook": {
      "get": {
        "tags": [
          "markets"
        ],
        "summary": "Order book snapshot",
        "parameters": [
          {
            "name": "symbol",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Order book"
          }
        }
      }
    },
    "/api/v1/markets/{symbol}/trades": {
      "get": {
        "tags": [
          "markets"
        ],
        "summary": "Recent public trades",
        "parameters": [
          {
            "name": "symbol",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Trades"
          }
        }
      }
    },
    "/api/v1/markets/{symbol}/ticker": {
      "get": {
        "tags": [
          "markets"
        ],
        "summary": "Single ticker",
        "parameters": [
          {
            "name": "symbol",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Ticker",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ticker"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/markets/{symbol}/klines": {
      "get": {
        "tags": [
          "markets"
        ],
        "summary": "OHLCV candles (klines)",
        "description": "Returns candles for exactly one venue per request, oldest to newest. `source` is part of the `candles` primary key, so Bitwyre's own prints ('bitwyre') and reference-venue data (binance/bitfinex/kraken/coinbase) are never blended into one series.",
        "parameters": [
          {
            "name": "symbol",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "interval",
            "in": "query",
            "required": false,
            "description": "Candle width. Defaults to 1m.",
            "schema": {
              "type": "string",
              "enum": [
                "1m",
                "5m",
                "15m",
                "30m",
                "1h",
                "4h",
                "1d",
                "1w"
              ]
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Inclusive lower bound on open_time, nanoseconds since the Unix epoch. Defaults to 0.",
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Inclusive upper bound on open_time, nanoseconds since the Unix epoch. Defaults to unbounded.",
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum candles returned, clamped to [1, 5000]. Defaults to 500. Selects the most recent candles in range, then returns them oldest-to-newest.",
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          },
          {
            "name": "source",
            "in": "query",
            "required": false,
            "description": "Venue whose series to return. Defaults to bitwyre.",
            "schema": {
              "type": "string",
              "enum": [
                "bitwyre",
                "binance",
                "bitfinex",
                "kraken",
                "coinbase"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Candles, ordered by open_time ascending",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Candle"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unknown interval or source",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/account/api-keys": {
      "post": {
        "tags": [
          "api-keys"
        ],
        "summary": "Create an API key",
        "description": "JWT-only (an API key cannot mint more keys). The secret in the response is shown exactly once.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateApiKeyRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Key created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateApiKeyResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid label or permissions",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "api-keys"
        ],
        "summary": "List own API keys (no secrets)",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Keys",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ApiKeyInfo"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/account/api-keys/{keyId}": {
      "delete": {
        "tags": [
          "api-keys"
        ],
        "summary": "Revoke an API key",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "keyId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked"
          },
          "404": {
            "description": "Not found or not yours",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/admin/users/{userId}/api-keys": {
      "post": {
        "tags": [
          "admin"
        ],
        "summary": "Admin: issue an API key for a user",
        "description": "Audited. Takes the ledger account id explicitly, mirroring seed-MM token minting.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "userId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int64"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AdminCreateApiKeyRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Key created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateApiKeyResponse"
                }
              }
            }
          },
          "401": {
            "description": "Not an admin",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "User not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  }
}