{
  "openapi": "3.0.4",
  "info": {
    "title": "send21 API",
    "description": "Non-custodial Bitcoin payment drafts for humans and AI agents. send21 never holds keys or funds; it prepares multi-output payment templates that the sender's own PSBT-enabled wallet (Bitcoin Core, Sparrow, Electrum) funds, reviews, and signs. AI agents authenticate with scoped API keys (Authorization: Bearer s21_...), get idempotent draft creation, batch operations, recurring templates, and HMAC-signed webhooks. Machine-readable overview: /llms.txt. Integration questions: integration@send21.io — account or payment issues: support@send21.io",
    "contact": {
      "name": "send21 integration support",
      "email": "integration@send21.io"
    },
    "version": "v1"
  },
  "paths": {
    "/api/v1/address-book": {
      "get": {
        "tags": [
          "AddressBook"
        ],
        "summary": "List saved receiver addresses, ordered by label.",
        "responses": {
          "200": {
            "description": "All entries for the caller."
          }
        }
      },
      "post": {
        "tags": [
          "AddressBook"
        ],
        "summary": "Save a receiver address. The address is validated against the selected network.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddressBookUpsertRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/AddressBookUpsertRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/AddressBookUpsertRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "201": {
            "description": "Entry created."
          },
          "400": {
            "description": "Address invalid for the network."
          },
          "409": {
            "description": "Label already in use."
          }
        }
      }
    },
    "/api/v1/address-book/{id}": {
      "put": {
        "tags": [
          "AddressBook"
        ],
        "summary": "Update a saved address. Changing address or network clears its verified status.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddressBookUpsertRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/AddressBookUpsertRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/AddressBookUpsertRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated entry."
          },
          "400": {
            "description": "Address invalid for the network."
          },
          "404": {
            "description": "Not found or owned by another user."
          }
        }
      },
      "delete": {
        "tags": [
          "AddressBook"
        ],
        "summary": "Delete a saved address.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "204": {
            "description": "Deleted."
          },
          "404": {
            "description": "Not found or owned by another user."
          }
        }
      }
    },
    "/api/v1/api-keys": {
      "get": {
        "tags": [
          "ApiKeys"
        ],
        "summary": "List the caller's API keys (hashes only; plaintext keys are never stored).",
        "responses": {
          "200": {
            "description": "All keys, newest first."
          }
        }
      },
      "post": {
        "tags": [
          "ApiKeys"
        ],
        "summary": "Create a scoped API key. The plaintext key is returned once and never again.",
        "description": "Store the key immediately; only its SHA-256 hash is kept. Grant the minimum scopes\n            needed and set an expiry for agent keys. Use as \"Authorization: Bearer s21_...\".",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateApiKeyRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateApiKeyRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/CreateApiKeyRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new key, shown exactly once."
          },
          "400": {
            "description": "Unknown scope requested."
          }
        }
      }
    },
    "/api/v1/api-keys/{id}": {
      "delete": {
        "tags": [
          "ApiKeys"
        ],
        "summary": "Revoke an API key immediately. Revocation is permanent.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "204": {
            "description": "Revoked (idempotent)."
          },
          "404": {
            "description": "Not found or owned by another user."
          }
        }
      }
    },
    "/api/v1/auth/register/begin": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Start passkey (WebAuthn) registration.",
        "description": "Returns WebAuthn creation options and a ceremony id (valid 5 minutes).\n            Pass the options to navigator.credentials.create(), then call register/complete.\n            Passkeys are phishing-resistant; the private key never leaves the user's device.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RegisterBeginRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/RegisterBeginRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/RegisterBeginRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Ceremony id and WebAuthn creation options."
          },
          "403": {
            "description": "Registration refused: an account on this network has blocked accrued fees."
          },
          "409": {
            "description": "Email already registered."
          }
        }
      }
    },
    "/api/v1/auth/register/complete": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Complete passkey registration and sign in.",
        "description": "Verifies the authenticator's attestation, creates the account, and sets the session cookie.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RegisterCompleteRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/RegisterCompleteRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/RegisterCompleteRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Account created; session established. Returns the user profile."
          },
          "400": {
            "description": "Unknown/expired ceremony or attestation verification failed."
          },
          "409": {
            "description": "Email already registered."
          }
        }
      }
    },
    "/api/v1/auth/login/begin": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Start passkey sign-in (usernameless).",
        "description": "Returns WebAuthn assertion options and a ceremony id (valid 5 minutes).\n            Pass the options to navigator.credentials.get(); the authenticator picks the account.",
        "responses": {
          "200": {
            "description": "Ceremony id and WebAuthn assertion options."
          }
        }
      }
    },
    "/api/v1/auth/login/complete": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Complete passkey sign-in and establish the session.",
        "description": "If the account has TOTP enabled, totpCode must also be supplied (401 with requiresTotp=true otherwise).",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LoginCompleteRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/LoginCompleteRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/LoginCompleteRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signed in. Returns the user profile."
          },
          "400": {
            "description": "Unknown or expired ceremony."
          },
          "401": {
            "description": "Verification failed, unknown credential, or TOTP required/invalid."
          }
        }
      }
    },
    "/api/v1/auth/logout": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Sign out and clear the session cookie.",
        "responses": {
          "200": {
            "description": "OK"
          },
          "204": {
            "description": "Session ended."
          }
        }
      }
    },
    "/api/v1/auth/password/register": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Start email + password registration.",
        "description": "Emails a 6-digit verification code (valid 5 minutes, max 5 attempts).\n            The account is only created after register/complete verifies the code.\n            The password is stored as a PBKDF2-SHA256 hash; consider a passkey instead for phishing resistance.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PasswordRegisterRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/PasswordRegisterRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/PasswordRegisterRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Ceremony id; verification code sent to the email address."
          },
          "403": {
            "description": "Registration refused: an account on this network has blocked accrued fees."
          },
          "409": {
            "description": "Email already registered."
          }
        }
      }
    },
    "/api/v1/auth/password/register/complete": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Verify the emailed code, create the account, and sign in.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailCodeCompleteRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailCodeCompleteRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/EmailCodeCompleteRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Account created with verified email; session established. Returns the user profile."
          },
          "400": {
            "description": "Invalid, expired, or exhausted verification code."
          },
          "409": {
            "description": "Email already registered."
          }
        }
      }
    },
    "/api/v1/auth/password/change": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Change the signed-in account's password. Requires the current password.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PasswordChangeRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/PasswordChangeRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/PasswordChangeRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "204": {
            "description": "Password changed."
          },
          "400": {
            "description": "Account has no password (passkey-only)."
          },
          "401": {
            "description": "No valid session, or the current password is incorrect."
          }
        }
      }
    },
    "/api/v1/auth/password/forgot": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Start a password reset. Emails a 6-digit code if the address belongs to a password account.",
        "description": "Always returns a ceremony id, whether or not the account exists (no email enumeration).\n            The code is valid 5 minutes with max 5 attempts. Accounts with TOTP enabled must also\n            supply a current authenticator code at forgot/complete.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PasswordForgotRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/PasswordForgotRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/PasswordForgotRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Ceremony id; a verification code was sent if the account exists."
          }
        }
      }
    },
    "/api/v1/auth/password/forgot/complete": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Verify the emailed reset code, set the new password, and sign in.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PasswordForgotCompleteRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/PasswordForgotCompleteRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/PasswordForgotCompleteRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Password changed; session established. Returns the user profile."
          },
          "400": {
            "description": "Invalid, expired, or exhausted reset code."
          },
          "401": {
            "description": "TOTP code required or invalid (requiresTotp=true)."
          }
        }
      }
    },
    "/api/v1/auth/password/login": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Sign in with email + password. A second factor is always required.",
        "description": "If TOTP is enabled, supply totpCode in this request and the session is established directly.\n            Otherwise a 6-digit code is emailed and the response contains a ceremony id for login/complete.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PasswordLoginRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/PasswordLoginRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/PasswordLoginRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Either the user profile (TOTP path, signed in) or a ceremony id with emailCodeSent=true."
          },
          "401": {
            "description": "Invalid credentials, or requiresTotp=true when a TOTP code is needed."
          }
        }
      }
    },
    "/api/v1/auth/password/login/complete": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Verify the emailed sign-in code and establish the session.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailCodeCompleteRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/EmailCodeCompleteRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/EmailCodeCompleteRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signed in. Returns the user profile."
          },
          "401": {
            "description": "Invalid, expired, or exhausted sign-in code."
          }
        }
      }
    },
    "/api/v1/auth/me": {
      "get": {
        "tags": [
          "Auth"
        ],
        "summary": "Get the signed-in user's profile.",
        "responses": {
          "200": {
            "description": "The user profile with registered passkeys."
          },
          "401": {
            "description": "No valid session."
          }
        }
      }
    },
    "/api/v1/auth/account": {
      "delete": {
        "tags": [
          "Auth"
        ],
        "summary": "Close the account permanently. Refused while accrued send21 fees are outstanding.",
        "description": "Deletes the user row; credentials, API keys, address book, webhooks, and drafts\n            cascade-delete by schema design (see GDPR.md §4 for what bookkeeping retains).",
        "responses": {
          "200": {
            "description": "OK"
          },
          "204": {
            "description": "Account closed and session ended."
          },
          "401": {
            "description": "No valid session."
          },
          "409": {
            "description": "Outstanding accrued fees must be settled first."
          }
        }
      }
    },
    "/api/v1/auth/totp/setup": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Generate a TOTP secret for an authenticator app.",
        "description": "Returns an otpauth:// URI to QR-encode. TOTP is not active until totp/enable confirms a valid code.",
        "responses": {
          "200": {
            "description": "Provisioning URI for the authenticator app."
          },
          "409": {
            "description": "TOTP already enabled."
          }
        }
      }
    },
    "/api/v1/auth/totp/enable": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Activate TOTP by confirming a code from the authenticator app.",
        "description": "Once enabled, every sign-in (passkey or password) requires a current TOTP code.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TotpCodeRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/TotpCodeRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/TotpCodeRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "204": {
            "description": "TOTP enabled."
          },
          "400": {
            "description": "Setup not run yet, or the code is invalid."
          }
        }
      }
    },
    "/api/v1/auth/totp/disable": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Disable TOTP. Requires a current valid code.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TotpCodeRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/TotpCodeRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/TotpCodeRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "204": {
            "description": "TOTP disabled and the secret erased."
          },
          "400": {
            "description": "TOTP not enabled, or the code is invalid."
          }
        }
      }
    },
    "/api/v1/checkout/{token}": {
      "get": {
        "tags": [
          "Checkout"
        ],
        "summary": "Get a payment request by share token: amount, offered currencies, and any active payment.",
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The request."
          },
          "404": {
            "description": "Unknown token."
          }
        }
      }
    },
    "/api/v1/checkout/{token}/select": {
      "post": {
        "tags": [
          "Checkout"
        ],
        "summary": "The payer picks a currency. Locks the conversion rate and returns wallet-ready\n            payment instructions (address, exact amount, QR-encodable URI). Reselect after the rate\n            lock expires — or to switch currency — and a fresh rate is locked.",
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CheckoutSelectRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CheckoutSelectRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/CheckoutSelectRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The request with active payment instructions."
          },
          "400": {
            "description": "Currency not offered by this request."
          },
          "404": {
            "description": "Unknown token."
          },
          "409": {
            "description": "Request no longer open."
          }
        }
      }
    },
    "/api/v1/checkout/{token}/status": {
      "get": {
        "tags": [
          "Checkout"
        ],
        "summary": "Lightweight status polling for the pay page.",
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Current status."
          },
          "404": {
            "description": "Unknown token."
          }
        }
      }
    },
    "/api/v1/drafts": {
      "post": {
        "tags": [
          "Drafts"
        ],
        "summary": "Create a payment draft.",
        "description": "A draft is a set of payment instructions — send21 never holds funds. Amount is either\n            exact base units or fiat converted at a rate locked at creation. Send an Idempotency-Key header\n            to make retries safe: a repeat returns the existing draft (200) instead of creating a duplicate (201).\n            After creation, fetch /tx-template and fund/sign it in your own wallet.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateDraftRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateDraftRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/CreateDraftRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Draft created.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/DraftResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DraftResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/DraftResponse"
                }
              }
            }
          },
          "200": {
            "description": "Idempotent replay; the existing draft is returned."
          },
          "400": {
            "description": "Validation failed (bad address, amount below dust, missing invoice, ...)."
          },
          "402": {
            "description": "Accrued send21 fees over the limit — settle them first (POST /api/v1/fees/accrued/settle)."
          }
        }
      },
      "get": {
        "tags": [
          "Drafts"
        ],
        "summary": "List the caller's drafts, newest first.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "description": "Filter by lifecycle status.",
            "schema": {
              "$ref": "#/components/schemas/DraftStatus"
            }
          },
          {
            "name": "network",
            "in": "query",
            "description": "Filter by network (Bitcoin, Solana, or Ethereum networks).",
            "schema": {
              "$ref": "#/components/schemas/PaymentNetwork"
            }
          },
          {
            "name": "method",
            "in": "query",
            "description": "Filter by payment method.",
            "schema": {
              "$ref": "#/components/schemas/PaymentMethod"
            }
          },
          {
            "name": "currency",
            "in": "query",
            "description": "Filter by settled asset (Btc, Usdc, Usdt, Eurc).",
            "schema": {
              "$ref": "#/components/schemas/PaymentCurrency"
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Search receiver address (exact substring), memo (case-insensitive), or paying txid (exact).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "Only drafts created at or after this time.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Only drafts created at or before this time.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "page",
            "in": "query",
            "description": "1-based page number.",
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 1
            }
          },
          {
            "name": "pageSize",
            "in": "query",
            "description": "Items per page, 1-100 (default 20).",
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paged result: { total, page, pageSize, items }."
          }
        }
      }
    },
    "/api/v1/drafts/batch": {
      "post": {
        "tags": [
          "Drafts"
        ],
        "summary": "Create up to 25 drafts in one call (designed for AI agents).",
        "description": "Items are processed in order and succeed or fail independently — check each result's error field.\n            Use per-item idempotency keys to make the whole batch safely retryable.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchCreateDraftsRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchCreateDraftsRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/BatchCreateDraftsRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Per-item results, index-aligned with the request.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/BatchDraftResult"
                  }
                }
              },
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/BatchDraftResult"
                  }
                }
              },
              "text/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/BatchDraftResult"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Batch empty or over 25 items."
          }
        }
      }
    },
    "/api/v1/drafts/{id}": {
      "get": {
        "tags": [
          "Drafts"
        ],
        "summary": "Get a draft by id.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The draft."
          },
          "404": {
            "description": "Not found or owned by another user."
          }
        }
      }
    },
    "/api/v1/drafts/{id}/status": {
      "get": {
        "tags": [
          "Drafts"
        ],
        "summary": "Get a lightweight status snapshot (cheap to poll; webhooks are pushed too).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Status, paying txid, and confirmation count."
          },
          "404": {
            "description": "Not found or owned by another user."
          }
        }
      }
    },
    "/api/v1/drafts/{id}/cancel": {
      "post": {
        "tags": [
          "Drafts"
        ],
        "summary": "Cancel a draft that is still awaiting payment.",
        "description": "Only unblocks bookkeeping — funds already broadcast are controlled solely by the sender's wallet.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The cancelled draft."
          },
          "404": {
            "description": "Not found or owned by another user."
          },
          "409": {
            "description": "Draft is no longer in AwaitingPayment."
          }
        }
      }
    },
    "/api/v1/drafts/{id}/requote": {
      "post": {
        "tags": [
          "Drafts"
        ],
        "summary": "Refresh the fiat rate lock on a fiat-priced draft (recalculates the base-unit amount).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The requoted draft with a fresh rate lock."
          },
          "404": {
            "description": "Not found or owned by another user."
          },
          "409": {
            "description": "Draft not requotable in its current state."
          }
        }
      }
    },
    "/api/v1/drafts/{id}/fees": {
      "get": {
        "tags": [
          "Drafts"
        ],
        "summary": "Full cost breakdown: receiver amount, platform fee, estimated mining fee, and fiat equivalents.",
        "description": "The receiver always gets the exact billed amount; the sender pays the platform fee (separate output,\n            in the sent asset) plus the network fee their own wallet chooses. Fiat figures are display-only.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The fee breakdown."
          },
          "404": {
            "description": "Not found or owned by another user."
          }
        }
      }
    },
    "/api/v1/drafts/{id}/pdf": {
      "get": {
        "tags": [
          "Drafts"
        ],
        "summary": "Download the draft as a branded PDF payment order.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "application/pdf attachment."
          },
          "404": {
            "description": "Not found or owned by another user."
          }
        }
      }
    },
    "/api/v1/drafts/{id}/tx-template": {
      "get": {
        "tags": [
          "Drafts"
        ],
        "summary": "Get the wallet-ready transaction template (outputs plus chain-specific artifacts:\n            bitcoin-cli example and pay-to-many CSV on Bitcoin, payment URIs/calldata on Solana/Ethereum).",
        "description": "Bitcoin: fund with walletcreatefundedpsbt or Electrum/Sparrow pay-to-many. Solana/Ethereum:\n            each transfer has a wallet-openable URI (and calldata on Ethereum). Review every output, then sign in\n            your own wallet. send21 never sees keys and cannot move funds.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The transaction template."
          },
          "404": {
            "description": "Not found or owned by another user."
          },
          "409": {
            "description": "Draft inactive or the rate lock expired (requote first)."
          }
        }
      }
    },
    "/api/v1/fees/schedule": {
      "get": {
        "tags": [
          "Fees"
        ],
        "summary": "Get the public fee schedule (no authentication) — verify pricing before signing up.",
        "description": "Tiers are fixed in code for auditability. The fee is priced in USD at draft creation and\n            paid in the sent asset as a separate transparent output; BTC fees converting below the dust limit are waived.",
        "responses": {
          "200": {
            "description": "The fee schedule.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/FeeScheduleResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeeScheduleResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeeScheduleResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/fees/accrued": {
      "get": {
        "tags": [
          "Fees"
        ],
        "summary": "Get your accrued send21 fee balance (from single-output payments) and whether it blocks new payments.",
        "description": "Fees accrue when a single-output payment confirms. Creation of drafts and payment requests is\n            blocked once the balance reaches the USD cap or the oldest unsettled fee exceeds the age limit.",
        "responses": {
          "200": {
            "description": "The balance and enforcement state.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/AccruedFeesResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccruedFeesResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccruedFeesResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/fees/accrued/settle": {
      "post": {
        "tags": [
          "Fees"
        ],
        "summary": "Generate (or return) a settlement payment clearing your full accrued fee balance.",
        "description": "Returns a normal draft paying send21's fee address: scan its paymentUri or fetch its tx-template\n            and pay from any wallet. The balance clears — and creation unblocks — when the payment confirms.\n            Calling again with the same currency and a live rate lock returns the existing settlement.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SettleAccruedFeesRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/SettleAccruedFeesRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/SettleAccruedFeesRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Settlement draft created.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/DraftResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DraftResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/DraftResponse"
                }
              }
            }
          },
          "200": {
            "description": "An active settlement already exists and is returned."
          },
          "400": {
            "description": "Nothing to settle, or unsupported currency/network combination."
          }
        }
      }
    },
    "/api/v1/payment-requests": {
      "post": {
        "tags": [
          "PaymentRequests"
        ],
        "summary": "Create a payment request. The payer picks one of the offered currencies on the\n            public pay page; the rate locks then. The payment is single-output and QR-scannable —\n            the send21 fee is accrued to your account instead of riding as a transaction output.\n            Send an Idempotency-Key header to make retries safe: the same key returns the original request.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePaymentRequestRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePaymentRequestRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePaymentRequestRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The request, including the public pay path.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentRequestResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentRequestResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentRequestResponse"
                }
              }
            }
          },
          "200": {
            "description": "Idempotent replay — the previously created request."
          },
          "400": {
            "description": "Invalid parameters (unknown currency/network combination, bad address)."
          },
          "402": {
            "description": "Accrued send21 fees over the limit — settle them first (POST /api/v1/fees/accrued/settle)."
          }
        }
      },
      "get": {
        "tags": [
          "PaymentRequests"
        ],
        "summary": "List your payment requests, newest first (max 100).",
        "responses": {
          "200": {
            "description": "The requests.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/PaymentRequestResponse"
                  }
                }
              },
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/PaymentRequestResponse"
                  }
                }
              },
              "text/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/PaymentRequestResponse"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/payment-requests/{id}": {
      "get": {
        "tags": [
          "PaymentRequests"
        ],
        "summary": "Get one payment request.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The request."
          },
          "404": {
            "description": "Not found or owned by another user."
          }
        }
      }
    },
    "/api/v1/payment-requests/{id}/cancel": {
      "post": {
        "tags": [
          "PaymentRequests"
        ],
        "summary": "Cancel an open payment request (and any unpaid underlying draft).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The cancelled request."
          },
          "404": {
            "description": "Not found or owned by another user."
          },
          "409": {
            "description": "Not open (already completed, cancelled, or expired)."
          }
        }
      }
    },
    "/api/v1/rates": {
      "get": {
        "tags": [
          "Rates"
        ],
        "summary": "Get the current price for an asset in a fiat currency.",
        "parameters": [
          {
            "name": "ccy",
            "in": "query",
            "description": "ISO currency code, e.g. USD, EUR, SEK (default USD).",
            "schema": {
              "type": "string",
              "default": "USD"
            }
          },
          {
            "name": "asset",
            "in": "query",
            "description": "Asset to price: Btc (default), Usdc, or Usdt.",
            "schema": {
              "$ref": "#/components/schemas/PaymentCurrency"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The current quote (rate is fiat per asset unit)."
          },
          "400": {
            "description": "Invalid currency code."
          },
          "503": {
            "description": "Price oracle unavailable."
          }
        }
      }
    },
    "/api/v1/recurring-templates": {
      "get": {
        "tags": [
          "RecurringTemplates"
        ],
        "summary": "List recurring templates, ordered by name.",
        "responses": {
          "200": {
            "description": "All templates for the caller."
          }
        }
      },
      "post": {
        "tags": [
          "RecurringTemplates"
        ],
        "summary": "Create a recurring template that generates a fresh draft each interval.",
        "description": "Fiat-priced templates are re-quoted at each run. Missed occurrences (e.g. while paused) are skipped, not back-filled.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateRecurringTemplateRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateRecurringTemplateRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/CreateRecurringTemplateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK"
          },
          "201": {
            "description": "Template created."
          },
          "400": {
            "description": "Draft parameters invalid or firstRunAt in the past."
          }
        }
      }
    },
    "/api/v1/recurring-templates/{id}/pause": {
      "post": {
        "tags": [
          "RecurringTemplates"
        ],
        "summary": "Pause a template; no drafts are generated while paused.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The paused template."
          },
          "404": {
            "description": "Not found or owned by another user."
          }
        }
      }
    },
    "/api/v1/recurring-templates/{id}/resume": {
      "post": {
        "tags": [
          "RecurringTemplates"
        ],
        "summary": "Resume a paused template. Missed occurrences are skipped.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The resumed template with its next run time."
          },
          "404": {
            "description": "Not found or owned by another user."
          }
        }
      }
    },
    "/api/v1/recurring-templates/{id}": {
      "delete": {
        "tags": [
          "RecurringTemplates"
        ],
        "summary": "Delete a template. Already-generated drafts are unaffected.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "204": {
            "description": "Deleted."
          },
          "404": {
            "description": "Not found or owned by another user."
          }
        }
      }
    },
    "/api/v1/suggestions": {
      "post": {
        "tags": [
          "Suggestions"
        ],
        "summary": "Send a suggestion or feature request to the send21 team.",
        "description": "Delivered by email to the operator. Include enough detail to act on;\n            replies go to the account's email address if follow-up is needed.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SuggestionRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/SuggestionRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/SuggestionRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Suggestion sent."
          },
          "503": {
            "description": "Email delivery is unavailable; try again later."
          }
        }
      }
    },
    "/api/v1/webhooks": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "List registered webhook endpoints, newest first.",
        "responses": {
          "200": {
            "description": "All endpoints for the caller."
          }
        }
      },
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Register a webhook endpoint. The HMAC signing secret is returned once and never again.",
        "description": "Deliveries carry an X-Send21-Signature header: HMAC-SHA256 of the raw request body\n            using this secret. Verify it on every delivery. Private/internal URLs are rejected.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateWebhookRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateWebhookRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/CreateWebhookRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Endpoint id and signing secret, shown exactly once."
          },
          "400": {
            "description": "URL rejected (not HTTPS, private address, ...)."
          }
        }
      }
    },
    "/api/v1/webhooks/{id}/test": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Queue a signed test delivery to verify the endpoint end-to-end.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "202": {
            "description": "Test event queued for delivery."
          },
          "404": {
            "description": "Not found or owned by another user."
          }
        }
      }
    },
    "/api/v1/webhooks/{id}": {
      "delete": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Delete a webhook endpoint and stop deliveries.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "204": {
            "description": "Deleted."
          },
          "404": {
            "description": "Not found or owned by another user."
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "AccruedFeesResponse": {
        "type": "object",
        "properties": {
          "outstandingUsd": {
            "type": "number",
            "description": "Unsettled accrued fees in USD (only confirmed payments accrue).",
            "format": "double"
          },
          "draftCount": {
            "type": "integer",
            "description": "Number of confirmed payments the balance comes from.",
            "format": "int32"
          },
          "oldestAccruedAt": {
            "type": "string",
            "description": "When the oldest unsettled fee was accrued.",
            "format": "date-time",
            "nullable": true
          },
          "maxOutstandingUsd": {
            "type": "number",
            "description": "Creating drafts/requests is blocked at this balance.",
            "format": "double"
          },
          "maxAgeDays": {
            "type": "integer",
            "description": "Creating drafts/requests is blocked when the oldest unsettled fee is older than this.",
            "format": "int32"
          },
          "blocked": {
            "type": "boolean",
            "description": "True when creation is currently blocked; settle to unblock."
          },
          "activeSettlementDraftId": {
            "type": "string",
            "description": "Pending settlement draft, when one exists — pay it to clear the balance.",
            "format": "uuid",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "The caller's accrued send21 fees (from single-output payments) and enforcement state."
      },
      "AddressBookUpsertRequest": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string",
            "description": "Unique label per user, max 200 characters.",
            "nullable": true
          },
          "address": {
            "type": "string",
            "description": "Address (Bitcoin, Solana, or Ethereum); validated against the network. Changing it clears verification.",
            "nullable": true
          },
          "network": {
            "$ref": "#/components/schemas/PaymentNetwork"
          },
          "verified": {
            "type": "boolean",
            "description": "Set true after the user confirmed the address against its source (anti-tamper aid)."
          },
          "direction": {
            "$ref": "#/components/schemas/AddressDirection"
          }
        },
        "additionalProperties": false,
        "description": "Creates or updates a saved address."
      },
      "AddressDirection": {
        "enum": [
          "Sending",
          "Receiving"
        ],
        "type": "string",
        "description": "Which way money flows relative to the owner of an address-book entry."
      },
      "AssertionResponse": {
        "required": [
          "authenticatorData",
          "clientDataJSON",
          "signature"
        ],
        "type": "object",
        "properties": {
          "authenticatorData": {
            "type": "string",
            "format": "byte",
            "nullable": true
          },
          "signature": {
            "type": "string",
            "format": "byte",
            "nullable": true
          },
          "clientDataJSON": {
            "type": "string",
            "format": "byte",
            "nullable": true
          },
          "userHandle": {
            "type": "string",
            "format": "byte",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "AttestationResponse": {
        "required": [
          "attestationObject",
          "clientDataJSON",
          "transports"
        ],
        "type": "object",
        "properties": {
          "attestationObject": {
            "type": "string",
            "format": "byte",
            "nullable": true
          },
          "clientDataJSON": {
            "type": "string",
            "format": "byte",
            "nullable": true
          },
          "transports": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AuthenticatorTransport"
            }
          }
        },
        "additionalProperties": false
      },
      "AuthenticationExtensionsClientOutputs": {
        "type": "object",
        "properties": {
          "example.extension.bool": {
            "type": "boolean",
            "nullable": true
          },
          "appid": {
            "type": "boolean"
          },
          "exts": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true
          },
          "uvm": {
            "type": "array",
            "items": {
              "type": "array",
              "items": {
                "type": "integer",
                "format": "int64"
              }
            },
            "nullable": true
          },
          "credProps": {
            "$ref": "#/components/schemas/CredentialPropertiesOutput"
          },
          "prf": {
            "$ref": "#/components/schemas/AuthenticationExtensionsPRFOutputs"
          },
          "largeBlob": {
            "$ref": "#/components/schemas/AuthenticationExtensionsLargeBlobOutputs"
          },
          "credProtect": {
            "$ref": "#/components/schemas/CredentialProtectionPolicy"
          }
        },
        "additionalProperties": false
      },
      "AuthenticationExtensionsLargeBlobOutputs": {
        "type": "object",
        "properties": {
          "supported": {
            "type": "boolean"
          },
          "blob": {
            "type": "string",
            "format": "byte",
            "nullable": true
          },
          "written": {
            "type": "boolean"
          }
        },
        "additionalProperties": false
      },
      "AuthenticationExtensionsPRFOutputs": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "results": {
            "$ref": "#/components/schemas/AuthenticationExtensionsPRFValues"
          }
        },
        "additionalProperties": false
      },
      "AuthenticationExtensionsPRFValues": {
        "required": [
          "first"
        ],
        "type": "object",
        "properties": {
          "first": {
            "type": "string",
            "format": "byte",
            "nullable": true
          },
          "second": {
            "type": "string",
            "format": "byte",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "AuthenticatorAssertionRawResponse": {
        "required": [
          "clientExtensionResults",
          "id",
          "rawId",
          "type"
        ],
        "type": "object",
        "properties": {
          "id": {
            "minLength": 1,
            "type": "string"
          },
          "rawId": {
            "type": "string",
            "format": "byte"
          },
          "response": {
            "$ref": "#/components/schemas/AssertionResponse"
          },
          "type": {
            "$ref": "#/components/schemas/PublicKeyCredentialType"
          },
          "extensions": {
            "$ref": "#/components/schemas/AuthenticationExtensionsClientOutputs"
          },
          "clientExtensionResults": {
            "$ref": "#/components/schemas/AuthenticationExtensionsClientOutputs"
          }
        },
        "additionalProperties": false
      },
      "AuthenticatorAttestationRawResponse": {
        "required": [
          "clientExtensionResults",
          "id",
          "rawId",
          "response",
          "type"
        ],
        "type": "object",
        "properties": {
          "id": {
            "minLength": 1,
            "type": "string"
          },
          "rawId": {
            "type": "string",
            "format": "byte"
          },
          "type": {
            "$ref": "#/components/schemas/PublicKeyCredentialType"
          },
          "response": {
            "$ref": "#/components/schemas/AttestationResponse"
          },
          "extensions": {
            "$ref": "#/components/schemas/AuthenticationExtensionsClientOutputs"
          },
          "clientExtensionResults": {
            "$ref": "#/components/schemas/AuthenticationExtensionsClientOutputs"
          }
        },
        "additionalProperties": false
      },
      "AuthenticatorTransport": {
        "enum": [
          "Usb",
          "Nfc",
          "Ble",
          "SmartCard",
          "Hybrid",
          "Internal"
        ],
        "type": "string"
      },
      "BatchCreateDraftsRequest": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BatchDraftItem"
            },
            "description": "Drafts to create, processed in order.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Batch draft creation, max 25 items. Items succeed or fail independently."
      },
      "BatchDraftItem": {
        "type": "object",
        "properties": {
          "draft": {
            "$ref": "#/components/schemas/CreateDraftRequest"
          },
          "idempotencyKey": {
            "type": "string",
            "description": "Optional per-item idempotency key; repeats return the existing draft instead of duplicating.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One draft in a batch request."
      },
      "BatchDraftResult": {
        "type": "object",
        "properties": {
          "index": {
            "type": "integer",
            "description": "Zero-based index of the item in the request.",
            "format": "int32"
          },
          "draft": {
            "$ref": "#/components/schemas/DraftResponse"
          },
          "error": {
            "type": "string",
            "description": "Failure reason, on error.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Per-item batch outcome."
      },
      "CheckoutSelectRequest": {
        "type": "object",
        "properties": {
          "currency": {
            "$ref": "#/components/schemas/PaymentCurrency"
          },
          "network": {
            "$ref": "#/components/schemas/PaymentNetwork"
          }
        },
        "additionalProperties": false,
        "description": "The payer's currency choice on the public pay page."
      },
      "CreateApiKeyRequest": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Label identifying the consumer, max 200 characters.",
            "nullable": true
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Granted scopes. Valid: drafts:read, drafts:write, addressbook:read, addressbook:write, webhooks:manage. Grant the minimum needed.",
            "nullable": true
          },
          "expiresAt": {
            "type": "string",
            "description": "Optional expiry; recommended for agent keys.",
            "format": "date-time",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Creates a scoped API key for programmatic access (AI agents, scripts, CI)."
      },
      "CreateDraftRequest": {
        "type": "object",
        "properties": {
          "receiverAddress": {
            "type": "string",
            "description": "Receiver address (Bitcoin, Solana, or Ethereum). Validated against the selected network.",
            "nullable": true
          },
          "network": {
            "$ref": "#/components/schemas/PaymentNetwork"
          },
          "paymentMethod": {
            "$ref": "#/components/schemas/PaymentMethod"
          },
          "amountSats": {
            "type": "integer",
            "description": "Exact amount in the asset's base units: satoshis for BTC (min 546, the dust limit), micro-units for USDC/USDT. Mutually exclusive with fiat fields.",
            "format": "int64",
            "nullable": true
          },
          "fiatCurrency": {
            "type": "string",
            "description": "ISO currency code (USD, EUR, GBP, SEK, ...). Requires FiatAmount; mutually exclusive with AmountSats.",
            "nullable": true
          },
          "fiatAmount": {
            "type": "number",
            "description": "Fiat amount converted to base units at a rate locked at creation. Requires FiatCurrency.",
            "format": "double",
            "nullable": true
          },
          "senderAddress": {
            "type": "string",
            "description": "Optional sender address for record-keeping; never used to move funds.",
            "nullable": true
          },
          "bolt11": {
            "type": "string",
            "description": "Receiver-supplied Lightning invoice; required when PaymentMethod is Lightning.",
            "nullable": true
          },
          "memo": {
            "type": "string",
            "description": "Free-text note, max 500 characters. Shown on the draft and PDF.",
            "nullable": true
          },
          "expiryHours": {
            "type": "integer",
            "description": "Draft lifetime in hours, 1-720. Default is server-configured.",
            "format": "int32",
            "nullable": true
          },
          "requiredConfirmations": {
            "type": "integer",
            "description": "Confirmations before the draft is marked Confirmed, 1-12.",
            "format": "int32",
            "nullable": true
          },
          "currency": {
            "$ref": "#/components/schemas/PaymentCurrency"
          },
          "singleOutput": {
            "type": "boolean",
            "description": "True = single-output payment: the whole amount goes to the receiver in one QR-scannable\n            transfer (works with any wallet) and the send21 fee is accrued to your account instead of riding as an output.\n            Accrued fees must be settled once they reach the configured cap. On-chain drafts only.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Parameters for a new payment draft. Amount is either exact base units or a fiat amount converted at a locked rate."
      },
      "CreatePaymentRequestRequest": {
        "type": "object",
        "properties": {
          "fiatCurrency": {
            "type": "string",
            "description": "ISO currency the request is priced in (USD, EUR, SEK, ...).",
            "nullable": true
          },
          "fiatAmount": {
            "type": "number",
            "description": "Amount in the fiat currency.",
            "format": "double"
          },
          "options": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PaymentRequestOptionDto"
            },
            "description": "Currencies the payer may choose from, each with your receiving address.",
            "nullable": true
          },
          "orderId": {
            "type": "string",
            "description": "Your own reference (e.g. shop order number); echoed in webhooks.",
            "nullable": true
          },
          "memo": {
            "type": "string",
            "description": "Free-text note shown to the payer, max 500 characters.",
            "nullable": true
          },
          "expiryHours": {
            "type": "integer",
            "description": "Request lifetime in hours, 1-720. Default is server-configured.",
            "format": "int32",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Creates a receiver-initiated payment request: a retail checkout or a P2P \"request X\".\n            Priced in fiat; the payer picks one of the offered currencies on the public pay page and the\n            rate locks at that moment. The payment is single-output (QR-scannable); the send21 fee is\n            accrued to the creator instead of riding as an output."
      },
      "CreateRecurringTemplateRequest": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Template label, max 200 characters.",
            "nullable": true
          },
          "interval": {
            "$ref": "#/components/schemas/RecurrenceInterval"
          },
          "firstRunAt": {
            "type": "string",
            "description": "First generation time; defaults to now. Cannot be in the past.",
            "format": "date-time",
            "nullable": true
          },
          "draft": {
            "$ref": "#/components/schemas/CreateDraftRequest"
          }
        },
        "additionalProperties": false,
        "description": "Creates a schedule that generates a fresh draft each interval."
      },
      "CreateWebhookRequest": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "Public HTTPS URL. Private/internal addresses are rejected (SSRF defense).",
            "nullable": true
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Event types to deliver (e.g. draft.seen, draft.confirmed, draft.expired).",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Registers an HTTPS endpoint for event notifications."
      },
      "CredentialPropertiesOutput": {
        "type": "object",
        "properties": {
          "rk": {
            "type": "boolean"
          },
          "authenticatorDisplayName": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false
      },
      "CredentialProtectionPolicy": {
        "enum": [
          "UserVerificationOptional",
          "UserVerificationOptionalWithCredentialIdList",
          "UserVerificationRequired"
        ],
        "type": "string"
      },
      "DraftResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Draft id.",
            "format": "uuid"
          },
          "receiverAddress": {
            "type": "string",
            "description": "Receiver address on the draft's network.",
            "nullable": true
          },
          "senderAddress": {
            "type": "string",
            "description": "Optional sender address (informational).",
            "nullable": true
          },
          "network": {
            "$ref": "#/components/schemas/PaymentNetwork"
          },
          "amountSats": {
            "type": "integer",
            "description": "Amount the receiver gets, in the asset's base units — satoshis or token micro-units (exact; fees are separate).",
            "format": "int64"
          },
          "amountBtc": {
            "type": "string",
            "description": "Same amount formatted in whole asset units.",
            "nullable": true
          },
          "fiatCurrency": {
            "type": "string",
            "description": "Fiat currency the draft was priced in, if fiat-priced.",
            "nullable": true
          },
          "fiatAmount": {
            "type": "number",
            "description": "Fiat amount, if fiat-priced.",
            "format": "double",
            "nullable": true
          },
          "quote": {
            "$ref": "#/components/schemas/QuoteInfo"
          },
          "platformFeeSats": {
            "type": "integer",
            "description": "send21 fee in base units of the sent asset, paid by the sender as a separate output.",
            "format": "int64"
          },
          "platformFeeAddress": {
            "type": "string",
            "description": "Address the fee output pays to.",
            "nullable": true
          },
          "platformFee": {
            "$ref": "#/components/schemas/PlatformFeeInfo"
          },
          "paymentMethod": {
            "$ref": "#/components/schemas/PaymentMethod"
          },
          "bolt11": {
            "type": "string",
            "description": "Lightning invoice, for Lightning drafts.",
            "nullable": true
          },
          "memo": {
            "type": "string",
            "description": "Free-text note.",
            "nullable": true
          },
          "status": {
            "$ref": "#/components/schemas/DraftStatus"
          },
          "paidTxId": {
            "type": "string",
            "description": "Transaction id observed paying this draft, once seen.",
            "nullable": true
          },
          "requiredConfirmations": {
            "type": "integer",
            "description": "Confirmations required before Confirmed.",
            "format": "int32"
          },
          "expiresAt": {
            "type": "string",
            "description": "When the draft expires.",
            "format": "date-time"
          },
          "createdAt": {
            "type": "string",
            "description": "Creation time.",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "description": "Last status change.",
            "format": "date-time"
          },
          "sek": {
            "$ref": "#/components/schemas/SekValueInfo"
          },
          "feeOutputs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FeeOutputInfo"
            },
            "description": "Fee split: one output per configured recipient.",
            "nullable": true
          },
          "currency": {
            "$ref": "#/components/schemas/PaymentCurrency"
          },
          "tokenAddress": {
            "type": "string",
            "description": "Token mint (Solana) / contract (Ethereum) snapshot; null for native BTC.",
            "nullable": true
          },
          "feeAccrued": {
            "type": "boolean",
            "description": "True when the send21 fee is billed to the creator's account instead of riding as an output (single-output payment)."
          },
          "isFeeSettlement": {
            "type": "boolean",
            "description": "True when this draft pays the creator's accrued send21 fees."
          },
          "paymentUri": {
            "type": "string",
            "description": "QR-encodable payment URI (bitcoin: / solana: / ethereum:) — only for single-transfer drafts; null when the payment needs multiple outputs.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A payment draft: instructions the sender funds, reviews, and signs in their own wallet."
      },
      "DraftStatus": {
        "enum": [
          "AwaitingPayment",
          "Seen",
          "Confirmed",
          "Expired",
          "Cancelled"
        ],
        "type": "string",
        "description": "Draft lifecycle: AwaitingPayment → Seen → Confirmed; or Expired/Cancelled."
      },
      "EmailCodeCompleteRequest": {
        "type": "object",
        "properties": {
          "ceremonyId": {
            "type": "string",
            "description": "Ceremony id returned by the preceding step; valid for 5 minutes.",
            "format": "uuid"
          },
          "code": {
            "type": "string",
            "description": "6-digit code from the email. Max 5 attempts per ceremony.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Completes an email-code step (registration or sign-in)."
      },
      "FeeOutputInfo": {
        "type": "object",
        "properties": {
          "recipient": {
            "type": "string",
            "description": "Configured recipient name.",
            "nullable": true
          },
          "address": {
            "type": "string",
            "description": "Address this share pays to.",
            "nullable": true
          },
          "sats": {
            "type": "integer",
            "description": "Share amount in base units of the sent asset.",
            "format": "int64"
          },
          "sharePercent": {
            "type": "number",
            "description": "Configured share of the total fee.",
            "format": "double"
          }
        },
        "additionalProperties": false,
        "description": "One recipient's share of the platform fee (a separate transaction output)."
      },
      "FeeScheduleResponse": {
        "type": "object",
        "properties": {
          "tiers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FeeTierDto"
            },
            "description": "Tiered percentages by USD amount.",
            "nullable": true
          },
          "minFeeUsd": {
            "type": "number",
            "description": "Minimum fee in USD.",
            "format": "double"
          },
          "maxFeeUsd": {
            "type": "number",
            "description": "Maximum fee in USD.",
            "format": "double"
          },
          "dustLimitSats": {
            "type": "integer",
            "description": "BTC fees converting below this are waived (Bitcoin drafts).",
            "format": "int64"
          },
          "pricedIn": {
            "type": "string",
            "description": "How and when the fee is priced.",
            "nullable": true
          },
          "note": {
            "type": "string",
            "description": "Human/agent-readable fee policy.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "The public send21 fee schedule (fixed in code for auditability)."
      },
      "FeeTierDto": {
        "type": "object",
        "properties": {
          "upToUsd": {
            "type": "number",
            "description": "Upper bound of the tier in USD (inclusive); null = no upper bound.",
            "format": "double",
            "nullable": true
          },
          "percent": {
            "type": "number",
            "description": "Fee percentage for amounts in this tier.",
            "format": "double"
          }
        },
        "additionalProperties": false,
        "description": "One fee tier."
      },
      "LoginCompleteRequest": {
        "type": "object",
        "properties": {
          "ceremonyId": {
            "type": "string",
            "description": "Ceremony id returned by login/begin; valid for 5 minutes.",
            "format": "uuid"
          },
          "credential": {
            "$ref": "#/components/schemas/AuthenticatorAssertionRawResponse"
          },
          "totpCode": {
            "type": "string",
            "description": "6-digit authenticator-app code; required only when TOTP is enabled.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Completes passkey login with the authenticator's assertion."
      },
      "PasswordChangeRequest": {
        "type": "object",
        "properties": {
          "currentPassword": {
            "type": "string",
            "description": "The account's current password.",
            "nullable": true
          },
          "newPassword": {
            "type": "string",
            "description": "New password, 10-200 characters. Stored only as a PBKDF2-SHA256 hash.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Changes the signed-in account's password."
      },
      "PasswordForgotCompleteRequest": {
        "type": "object",
        "properties": {
          "ceremonyId": {
            "type": "string",
            "description": "Ceremony id returned by password/forgot; valid for 5 minutes.",
            "format": "uuid"
          },
          "code": {
            "type": "string",
            "description": "6-digit code from the email. Max 5 attempts per ceremony.",
            "nullable": true
          },
          "newPassword": {
            "type": "string",
            "description": "New password, 10-200 characters. Stored only as a PBKDF2-SHA256 hash.",
            "nullable": true
          },
          "totpCode": {
            "type": "string",
            "description": "6-digit authenticator-app code; required only when TOTP is enabled.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Completes a password reset with the emailed code and the new password."
      },
      "PasswordForgotRequest": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "description": "Account email address.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Starts a password reset. A 6-digit code is emailed if the account exists."
      },
      "PasswordLoginRequest": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "description": "Account email address.",
            "nullable": true
          },
          "password": {
            "type": "string",
            "description": "Account password.",
            "nullable": true
          },
          "totpCode": {
            "type": "string",
            "description": "6-digit authenticator-app code; required only when TOTP is enabled.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Password sign-in. A second factor is always required: TOTP inline if enabled, otherwise an emailed code."
      },
      "PasswordRegisterRequest": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "description": "Account email address; must not already be registered. Receives the verification code.",
            "nullable": true
          },
          "password": {
            "type": "string",
            "description": "Password, 10-200 characters. Stored only as a PBKDF2-SHA256 hash.",
            "nullable": true
          },
          "displayName": {
            "type": "string",
            "description": "Optional human-friendly name shown in the UI.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Starts email + password registration. A 6-digit verification code is emailed."
      },
      "PaymentCurrency": {
        "enum": [
          "Btc",
          "Usdc",
          "Usdt",
          "Eurc"
        ],
        "type": "string",
        "description": "Asset a draft is denominated and settled in. The platform fee is always paid\n            in the same asset as the payment."
      },
      "PaymentMethod": {
        "enum": [
          "MultiOutput",
          "Lightning"
        ],
        "type": "string",
        "description": "How the draft is paid. On-chain drafts are PSBT-workflow only (multi-output, fee included);\n            supported wallets are PSBT-enabled ones (Bitcoin Core, Sparrow, Electrum, ...)."
      },
      "PaymentNetwork": {
        "enum": [
          "Mainnet",
          "Testnet4",
          "Signet",
          "Regtest",
          "Solana",
          "SolanaDevnet",
          "Ethereum",
          "EthereumSepolia",
          "Base",
          "BaseSepolia"
        ],
        "type": "string",
        "description": "Network a draft settles on. Bitcoin networks use the PSBT workflow; Solana and\n            Ethereum networks carry token payments (wrapped BTC, USDC, USDT) signed in the sender's wallet."
      },
      "PaymentRequestFeeInfo": {
        "type": "object",
        "properties": {
          "percent": {
            "type": "number",
            "description": "Tiered fee percentage applied (USD-priced at draft creation).",
            "format": "double",
            "nullable": true
          },
          "feeUsd": {
            "type": "number",
            "description": "Fee in USD at the creation-time rate.",
            "format": "double",
            "nullable": true
          },
          "feeBaseUnits": {
            "type": "integer",
            "description": "Fee in the sent asset's base units (sats / micro-units).",
            "format": "int64"
          },
          "feeAmount": {
            "type": "string",
            "description": "Fee in whole units of the sent asset.",
            "nullable": true
          },
          "currency": {
            "type": "string",
            "description": "Asset the fee is denominated in (the sent asset).",
            "nullable": true
          },
          "waived": {
            "type": "boolean",
            "description": "True when the fee was waived (sub-dust)."
          },
          "settledAt": {
            "type": "string",
            "description": "When a settlement payment covering this fee confirmed; null while unsettled.",
            "format": "date-time",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "send21 fee accrued for a receiver-initiated payment. The payer's transaction is a\n            single clean transfer; this fee is billed to the request creator's account."
      },
      "PaymentRequestOptionDto": {
        "type": "object",
        "properties": {
          "currency": {
            "$ref": "#/components/schemas/PaymentCurrency"
          },
          "network": {
            "$ref": "#/components/schemas/PaymentNetwork"
          },
          "address": {
            "type": "string",
            "description": "The CREATOR'S receiving address on that network.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One currency the payer may choose, with the creator's receiving address for it."
      },
      "PaymentRequestResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Request id.",
            "format": "uuid"
          },
          "shareToken": {
            "type": "string",
            "description": "Unguessable token in the public pay link.",
            "nullable": true
          },
          "payPath": {
            "type": "string",
            "description": "Path of the public pay page (prefix with your send21 origin).",
            "nullable": true
          },
          "orderId": {
            "type": "string",
            "description": "Creator's reference, echoed in webhooks.",
            "nullable": true
          },
          "fiatCurrency": {
            "type": "string",
            "description": "Fiat currency of the request.",
            "nullable": true
          },
          "fiatAmount": {
            "type": "number",
            "description": "Fiat amount of the request.",
            "format": "double"
          },
          "memo": {
            "type": "string",
            "description": "Note shown to the payer.",
            "nullable": true
          },
          "status": {
            "$ref": "#/components/schemas/PaymentRequestStatus"
          },
          "expiresAt": {
            "type": "string",
            "description": "When the request stops accepting payment.",
            "format": "date-time"
          },
          "createdAt": {
            "type": "string",
            "description": "Creation time.",
            "format": "date-time"
          },
          "options": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PaymentRequestOptionDto"
            },
            "description": "Offered currencies.",
            "nullable": true
          },
          "activeDraftId": {
            "type": "string",
            "description": "Underlying draft once the payer picked a currency.",
            "format": "uuid",
            "nullable": true
          },
          "draftStatus": {
            "$ref": "#/components/schemas/DraftStatus"
          },
          "fee": {
            "$ref": "#/components/schemas/PaymentRequestFeeInfo"
          }
        },
        "additionalProperties": false,
        "description": "A payment request as seen by its creator."
      },
      "PaymentRequestStatus": {
        "enum": [
          "Open",
          "Completed",
          "Cancelled",
          "Expired"
        ],
        "type": "string",
        "description": "Lifecycle of a receiver-initiated payment request / retail checkout."
      },
      "PlatformFeeInfo": {
        "type": "object",
        "properties": {
          "percent": {
            "type": "number",
            "description": "Tier percentage applied (see GET /api/v1/fees/schedule).",
            "format": "double",
            "nullable": true
          },
          "usd": {
            "type": "number",
            "description": "Fee in USD at the locked rate.",
            "format": "double",
            "nullable": true
          },
          "sats": {
            "type": "integer",
            "description": "Fee in the asset's base units — the amount actually paid.",
            "format": "int64"
          },
          "waived": {
            "type": "boolean",
            "description": "True when a BTC fee converted to less than the dust limit and was waived."
          },
          "usdPerBtc": {
            "type": "number",
            "description": "USD rate per whole asset unit used to convert the fee.",
            "format": "double",
            "nullable": true
          },
          "rateSource": {
            "type": "string",
            "description": "Price oracle that supplied the fee rate.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "send21 platform fee, priced in USD at creation and paid in the sent asset as a separate output."
      },
      "PublicKeyCredentialType": {
        "enum": [
          "PublicKey",
          "Invalid"
        ],
        "type": "string"
      },
      "QuoteInfo": {
        "type": "object",
        "properties": {
          "source": {
            "type": "string",
            "description": "Price oracle that supplied the rate.",
            "nullable": true
          },
          "rate": {
            "type": "number",
            "description": "Fiat units per 1 whole unit of the sent asset.",
            "format": "double"
          },
          "lockedAt": {
            "type": "string",
            "description": "When the rate was locked.",
            "format": "date-time"
          },
          "expiresAt": {
            "type": "string",
            "description": "When the lock expires; requote after this.",
            "format": "date-time"
          }
        },
        "additionalProperties": false,
        "description": "Fiat rate lock captured at draft creation."
      },
      "RecurrenceInterval": {
        "enum": [
          "Daily",
          "Weekly",
          "Monthly"
        ],
        "type": "string",
        "description": "How often a recurring template generates a new draft."
      },
      "RegisterBeginRequest": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "description": "Account email address; must not already be registered.",
            "nullable": true
          },
          "displayName": {
            "type": "string",
            "description": "Optional human-friendly name shown in the UI.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Starts passkey (WebAuthn) registration."
      },
      "RegisterCompleteRequest": {
        "type": "object",
        "properties": {
          "ceremonyId": {
            "type": "string",
            "description": "Ceremony id returned by register/begin; valid for 5 minutes.",
            "format": "uuid"
          },
          "credential": {
            "$ref": "#/components/schemas/AuthenticatorAttestationRawResponse"
          },
          "credentialName": {
            "type": "string",
            "description": "Optional label for this passkey (e.g. \"Work laptop\").",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Completes passkey registration with the authenticator's attestation."
      },
      "SekValueInfo": {
        "type": "object",
        "properties": {
          "ratePerBtc": {
            "type": "number",
            "description": "SEK per whole unit of the sent asset at creation.",
            "format": "double"
          },
          "rateSource": {
            "type": "string",
            "description": "Price oracle that supplied the SEK rate.",
            "nullable": true
          },
          "amount": {
            "type": "number",
            "description": "Receiver amount in SEK.",
            "format": "double",
            "nullable": true
          },
          "platformFee": {
            "type": "number",
            "description": "send21 fee in SEK (VAT-inclusive when VAT applies).",
            "format": "double",
            "nullable": true
          },
          "vatRatePercent": {
            "type": "number",
            "description": "Swedish VAT (moms) rate included in the fee; null when no VAT is charged.",
            "format": "double",
            "nullable": true
          },
          "platformFeeVat": {
            "type": "number",
            "description": "VAT portion of the fee in SEK; null when no VAT is charged.",
            "format": "double",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "SEK valuation captured at draft creation (Swedish bookkeeping; display-only)."
      },
      "SettleAccruedFeesRequest": {
        "type": "object",
        "properties": {
          "currency": {
            "$ref": "#/components/schemas/PaymentCurrency"
          },
          "network": {
            "$ref": "#/components/schemas/PaymentNetwork"
          }
        },
        "additionalProperties": false,
        "description": "Creates (or returns) a settlement draft paying the accrued fee balance to Send21."
      },
      "SuggestionRequest": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "description": "Short summary, max 200 characters.",
            "nullable": true
          },
          "description": {
            "type": "string",
            "description": "Full description, max 5000 characters.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A suggestion or feature request for the send21 team."
      },
      "TotpCodeRequest": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "description": "Current TOTP code.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A 6-digit authenticator-app code."
      }
    },
    "securitySchemes": {
      "ApiKey": {
        "type": "http",
        "description": "AI-agent API key (Authorization: Bearer s21_...). Humans use passkey session cookies instead.",
        "scheme": "bearer",
        "bearerFormat": "s21_..."
      }
    }
  },
  "security": [
    {
      "ApiKey": [ ]
    }
  ],
  "tags": [
    {
      "name": "AddressBook",
      "description": "Saved receiver addresses with optional re-entry verification (anti-tamper aid)."
    },
    {
      "name": "ApiKeys",
      "description": "API keys for programmatic access. Creation and revocation require a human session\n            (cookie) — an API key can never mint or manage other keys."
    },
    {
      "name": "Auth",
      "description": "Registration and sign-in. Two paths: passkeys (WebAuthn, recommended) or email + password\n            with a mandatory second factor (authenticator app TOTP, or an emailed one-time code).\n            Both establish a cookie session used by the web app; AI agents use API keys instead."
    },
    {
      "name": "Checkout",
      "description": "Public checkout endpoints for payers. No authentication — reached only via the\n            unguessable share token from the request creator's pay link."
    },
    {
      "name": "Drafts",
      "description": "Payment drafts — the core send21 resource. A draft is a non-custodial payment instruction:\n            send21 prepares the outputs; the sender funds, reviews, and signs in their own wallet\n            (PSBT on Bitcoin, token transfers on Solana/Ethereum)."
    },
    {
      "name": "Fees",
      "description": "Public fee information and the caller's accrued-fee balance."
    },
    {
      "name": "PaymentRequests",
      "description": "Receiver-initiated payments: retail checkouts and peer-to-peer requests.\n            The creator is the receiver; the payer uses the public pay link (see /api/v1/checkout)."
    },
    {
      "name": "Rates",
      "description": "Live asset price quotes (BTC, USDC, USDT) from the configured oracle (the same feed used for draft rate locks)."
    },
    {
      "name": "RecurringTemplates",
      "description": "Schedules that auto-generate payment drafts (daily, weekly, or monthly)."
    },
    {
      "name": "Suggestions",
      "description": "User suggestions and feature requests, forwarded to the operator by email."
    },
    {
      "name": "Webhooks",
      "description": "HMAC-signed webhook endpoints for draft lifecycle events\n            (draft.created, draft.seen, draft.confirmed, draft.expired, draft.cancelled)."
    }
  ]
}