openapi: 3.1.0
info:
  title: Sigil API
  version: 3.0.0
  summary: Public HTTP API for the Sigil self-custodial wallet platform.
  description: |
    Three lanes by audience and auth model:

    - **Wallet** (`/v1/wallets/...`): publishable key + end-user JWT. Used
      by the Sigil iframe through `@sigilkeys/sdk`. You can also call these
      directly if you build your own client.
    - **Server-to-server** (`/v1/s2s/...`): secret key (`sk_live_…`).
      For your backend. Keys support IP allowlisting and scopes (`full` or `read`).
    - **TEE signing** (`/v1/wallets/me/sign`, `/v1/s2s/sign`): available only
      for organizations using TEE signing mode. Signing happens server-side
      inside a hardware-attested Trusted Execution Environment.

    Production base URL: `https://api.sigilkeys.com`.

    Every JSON response uses one envelope: `{ "data": ..., "error": null }`
    or `{ "data": null, "error": { "code": "...", "message": "..." } }`.
    Error messages are always user-safe and never leak internal details.

    **Security details:** see [Security model](https://developers.sigilkeys.com/wallets/security/)
    and [TEE security model](https://developers.sigilkeys.com/wallets/tee/).
  contact:
    name: Sigil developers
    url: https://developers.sigilkeys.com
servers:
  - url: https://api.sigilkeys.com
    description: Production

tags:
  - name: Health
  - name: Wallet auth
    description: Email OTP for the Sigil-hosted end-user auth mode.
  - name: Wallets
    description: End-user wallet lifecycle (multi-network).
  - name: Recovery
    description: Recover a wallet on a new device.
  - name: EVM transactions
    description: Raw EIP-1559 transactions from the user's EOA.
  - name: TEE signing
    description: |
      Server-side signing for organizations in TEE mode. Available via the
      end-user lane (interactive) and the S2S lane (agent/backend).
  - name: S2S Wallets
    description: Back-office wallet listing and management for your backend.
  - name: Smart accounts
    description: |
      ERC-4337 LightAccount v2 records sitting on top of an EOA wallet.
      Opt-in per chain. Sigil sponsors gas via Pimlico, capped at
      €10 / wallet / calendar month (EUR-equivalent across chains).
  - name: UserOps
    description: |
      ERC-4337 build → sign → submit pipeline for the gas-sponsored
      lane. Only routes that have a smart-account row for the requested
      chain are accepted; otherwise the SDK falls back to the EOA
      `/v1/wallets/evm/*` path.
  - name: Agents (A2A)
    description: |
      Public Agent-to-Agent surface for inter-agent delegation. Implements a
      pragmatic subset of the [A2A protocol](https://google.github.io/A2A/):
      each Sigil agent exposes a Discovery card and a JSON-RPC endpoint that
      accepts `tasks/send` / `tasks/get` from authenticated callers (other
      Sigil agents, external A2A clients).
  - name: Agent webhooks
    description: |
      Inbound HTTP webhooks that trigger an agent task. The customer
      configures the URL + an HMAC secret in the portal; their upstream
      system POSTs payloads here.

security: []  # per-operation

paths:

  /v1/health:
    get:
      tags: [Health]
      summary: Liveness check
      responses:
        '200':
          description: Always 200 if the API is up.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Envelope'
              example:
                data: { status: "ok" }
                error: null

  # ---------------- Wallet auth ----------------

  /v1/wallets/auth/email-otp/start:
    post:
      tags: [Wallet auth]
      summary: Start the Sigil-hosted email-OTP login
      description: |
        Publishable key only — the user doesn't have a JWT yet. Returns
        204 regardless of whether the email is registered (no enumeration leak).

        Rate limit: max 5 OTP codes per (org, email) in a rolling window.
      security:
        - PublishableKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email }
      responses:
        '204': { description: OTP queued for delivery. }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/wallets/auth/email-otp/verify:
    post:
      tags: [Wallet auth]
      summary: Verify an OTP and receive an end-user JWT
      description: |
        Max 5 verification attempts per OTP code. After 5 failures the
        code is invalidated and the user must request a new one.

        The returned JWT has a **1-hour TTL** with no refresh token.
        When the token expires, the user re-authenticates via OTP.
        Tokens can be revoked server-side via `POST /v1/wallets/me/logout`.
      security:
        - PublishableKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, code]
              properties:
                email: { type: string, format: email }
                code:  { type: string, example: "449922" }
      responses:
        '200':
          description: JWT minted (1h TTL).
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          token: { type: string, description: "HS256 JWT, 1h TTL" }
                          user_identity_id: { type: string, format: uuid }
                          email: { type: string, format: email }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429': { $ref: '#/components/responses/RateLimited' }

  # ---------------- Wallets ----------------

  /v1/wallets/identify:
    post:
      tags: [Wallets]
      summary: Auth canary — confirms the publishable key + JWT line up
      description: |
        Also returns the organization's `supported_networks` and `signing_mode`
        so the client knows which key management model applies.
      security: [{ PublishableKey: [], EndUserJWT: [] }]
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Identity' }
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/wallets/me:
    get:
      tags: [Wallets]
      summary: Get the primary active wallet for the authenticated end user
      description: |
        Returns the most recently created active wallet (defaults to secp256k1).
        Use `GET /v1/wallets/me/all` to retrieve wallets for all curves.
      security: [{ PublishableKey: [], EndUserJWT: [] }]
      responses:
        '200':
          description: Wallet record.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Wallet' }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/wallets/me/all:
    get:
      tags: [Wallets]
      summary: List all active wallets for the authenticated end user
      description: |
        Returns one wallet per curve (secp256k1, ed25519, etc.) depending
        on which networks the organization has enabled.
      security: [{ PublishableKey: [], EndUserJWT: [] }]
      responses:
        '200':
          description: Array of active wallets across all curves.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Wallet' }

  /v1/wallets:
    post:
      tags: [Wallets]
      summary: Create a wallet
      description: |
        Creates one wallet per curve. Idempotent on
        `(organization_id, user_identity_id, curve)`. A duplicate
        returns `409 wallet_exists` with the existing wallet.

        **Client mode:** the request body must include key shares generated
        in the browser iframe. The `provider_share`, `recovery_share`, and
        their indices are required.

        **TEE mode:** key generation happens server-side inside the TEE.
        The share fields should be omitted — the server generates the key,
        splits it, and encrypts the shares automatically. Only `curve` and
        optionally `account_type` are needed.
      security: [{ PublishableKey: [], EndUserJWT: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                address:
                  type: string
                  description: Required for client mode. Omit for TEE mode (derived server-side).
                public_key:
                  type: string
                  description: 0x-prefixed hex. Required for client mode. Omit for TEE mode.
                chain_type: { type: string }
                curve:
                  type: string
                  enum: [secp256k1, ed25519]
                  default: secp256k1
                  description: |
                    Cryptographic curve. secp256k1 for EVM/Bitcoin/Tron/Cosmos,
                    ed25519 for Solana/Near/Aptos/Stellar. Defaults to secp256k1.
                provider_share:
                  type: string
                  description: base64. Required for client mode. Omit for TEE mode.
                provider_share_index:
                  type: integer
                  minimum: 1
                  description: Required for client mode. Omit for TEE mode.
                recovery_share:
                  type: string
                  description: base64. Required for client mode. Omit for TEE mode.
                recovery_share_index:
                  type: integer
                  minimum: 1
                  description: Required for client mode. Omit for TEE mode.
                account_type:
                  type: string
                  enum: [eoa]
                  default: eoa
      responses:
        '201':
          description: Wallet created.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Wallet' }
        '409': { $ref: '#/components/responses/Conflict' }

  /v1/wallets/me/shares/provider:
    post:
      tags: [Wallets]
      summary: Return the provider share so the iframe can sign
      description: |
        **Client mode only.** Returns the decrypted provider share for the
        specified curve so the iframe can reconstruct the key and sign locally.

        Returns `400 signing_mode_mismatch` if the organization uses TEE mode.

        Rate limited: max 3 burst, ~1 request per 5 seconds per user.
      security: [{ PublishableKey: [], EndUserJWT: [] }]
      parameters:
        - name: curve
          in: query
          schema: { type: string, enum: [secp256k1, ed25519], default: secp256k1 }
          description: Which curve's provider share to return.
      responses:
        '200':
          description: Cleartext provider share.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          wallet_id: { type: string, format: uuid }
                          provider_share: { type: string, description: base64 }
                          share_index: { type: integer }
        '400':
          description: Not available in TEE mode.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/wallets/me/logout:
    post:
      tags: [Wallets]
      summary: Revoke the current JWT
      description: |
        Adds the current token to the server-side revocation list.
        The token is immediately invalid for all subsequent requests,
        even before its natural 1h expiry.
      security: [{ PublishableKey: [], EndUserJWT: [] }]
      responses:
        '204': { description: Token revoked. }

  # ---------------- TEE signing (interactive) ----------------

  /v1/wallets/me/sign:
    post:
      tags: [TEE signing]
      summary: Sign a message or transaction inside the TEE
      description: |
        **TEE mode only.** The authenticated end user requests a signature
        for the specified payload. The TEE reconstructs the private key
        from its encrypted shares, signs, and wipes all key material.

        Returns `400 signing_mode_mismatch` if the organization uses client mode.

        Rate limited: max 3 burst, ~1 request per 5 seconds per user.
      security: [{ PublishableKey: [], EndUserJWT: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SignRequest'
      responses:
        '200':
          description: Signature produced inside the TEE.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/SignResponse' }
        '400':
          description: Not available in client mode or invalid payload.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        '429': { $ref: '#/components/responses/RateLimited' }

  # ---------------- Recovery ----------------

  /v1/wallets/me/recovery/initiate:
    post:
      tags: [Recovery]
      summary: Email an OTP to start a recovery process
      description: |
        **Client mode only.** Starts a recovery process with a 15-minute TTL.
        The OTP is sent to the user's registered email address.

        TEE-mode organizations do not use recovery (no device share exists).
      security: [{ PublishableKey: [], EndUserJWT: [] }]
      responses:
        '200':
          description: Recovery process id (pass to verify).
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          recovery_process_id: { type: string }

  /v1/wallets/me/recovery/{id}/verify:
    post:
      tags: [Recovery]
      summary: Verify the OTP and return both shares
      description: |
        Max 5 verification attempts per recovery process. After 5 failures,
        the process is locked and the user must start a new one.
        The recovery process expires after 15 minutes.
      security: [{ PublishableKey: [], EndUserJWT: [] }]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code: { type: string }
      responses:
        '200':
          description: Both shares returned (iframe rebuilds the key in memory).
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          wallet_id: { type: string, format: uuid }
                          provider_share: { type: string, description: base64 }
                          recovery_share: { type: string, description: base64 }
                          polynomial_version: { type: integer }
        '410':
          description: Recovery process expired.
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/wallets/me/recovery/{id}/complete:
    post:
      tags: [Recovery]
      summary: Persist new shares with a fresh polynomial
      description: |
        Rotates all key material to a new polynomial version, invalidating
        old shares. A notification email is sent to the user confirming
        the recovery completion with timestamp.
      security: [{ PublishableKey: [], EndUserJWT: [] }]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [new_provider_share, new_provider_share_index, new_recovery_share, new_recovery_share_index]
              properties:
                new_provider_share: { type: string, description: base64 }
                new_provider_share_index: { type: integer }
                new_recovery_share: { type: string, description: base64 }
                new_recovery_share_index: { type: integer }
      responses:
        '204': { description: Recovery completed. Notification email sent. }

  # ---------------- EVM transactions ----------------

  /v1/wallets/evm/prepare-tx:
    post:
      tags: [EVM transactions]
      summary: Prepare an unsigned EIP-1559 transaction
      description: |
        Resolves nonce, gas parameters, and chainId for the user's EOA
        on the specified chain. The iframe signs the prepared transaction
        locally and broadcasts via `POST /v1/wallets/evm/broadcast-tx`.
      security: [{ PublishableKey: [], EndUserJWT: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [chain, to]
              properties:
                chain: { type: string, description: "e.g. ethereum, base, arbitrum" }
                to: { type: string, description: 0x-prefixed contract address }
                data: { type: string, description: 0x-prefixed calldata }
                value: { type: string, description: decimal wei }
                gas_limit: { type: string, description: decimal gas units (override) }
      responses:
        '200':
          description: Unsigned transaction parameters.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          chain: { type: string }
                          chain_id: { type: string, description: 0x hex }
                          nonce: { type: string, description: 0x hex }
                          from: { type: string }
                          to: { type: string }
                          data: { type: string }
                          value: { type: string, description: 0x hex }
                          gas_limit: { type: string, description: 0x hex }
                          max_fee_per_gas: { type: string, description: 0x hex }
                          max_priority_fee_per_gas: { type: string, description: 0x hex }

  /v1/wallets/evm/broadcast-tx:
    post:
      tags: [EVM transactions]
      summary: Broadcast a signed EIP-1559 transaction
      security: [{ PublishableKey: [], EndUserJWT: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [chain, raw_tx]
              properties:
                chain: { type: string }
                raw_tx: { type: string, description: 0x-prefixed RLP-encoded signed tx }
      responses:
        '200':
          description: Transaction submitted.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          chain: { type: string }
                          chain_id: { type: string }
                          tx_hash: { type: string }

  # ---------------- Smart accounts (ERC-4337) ----------------

  /v1/wallets/me/smart-accounts:
    get:
      tags: [Smart accounts]
      summary: List smart accounts for the authenticated end user
      description: |
        Returns every ERC-4337 smart account provisioned on top of the
        user's secp256k1 (EVM) wallet, across chains. The address is the
        same across EVM chains for the same (wallet, factory) — but each
        row tracks per-chain deployed state and sponsor policy.
      security: [{ PublishableKey: [], EndUserJWT: [] }]
      responses:
        '200':
          description: List of smart accounts (one per (wallet, chain, factory)).
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          items:
                            type: array
                            items: { $ref: '#/components/schemas/SmartAccount' }
    post:
      tags: [Smart accounts]
      summary: Provision a smart account on a given EVM chain
      description: |
        Opt-in step the user (or your iframe) triggers per chain. The
        endpoint derives the counterfactual LightAccount v2 address by
        eth_call against the factory's `getAddress(owner, salt=0)` view
        function and records one (wallet, chain, factory) row.

        Idempotent — calling it twice for the same (wallet, chain) returns
        the existing row.

        Refuses non-secp256k1 wallets with `invalid_curve` (smart accounts
        are EVM-only).
      security: [{ PublishableKey: [], EndUserJWT: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [chain]
              properties:
                chain: { type: string, description: "EVM chain handle, e.g. base, arbitrum, ethereum" }
      responses:
        '201':
          description: Smart account provisioned.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/SmartAccount' }
        '400':
          description: Wallet's curve is not secp256k1, or chain handle unknown.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }

  # ---------------- ERC-4337 UserOps ----------------

  /v1/userop/build:
    post:
      tags: [UserOps]
      summary: Build and sponsor a UserOp
      description: |
        Resolves the wallet's smart account for the target chain, encodes
        the calls into LightAccount v2's `execute` (single call) or
        `executeBatch` (multiple) calldata, asks the bundler (Pimlico) to
        sponsor and estimate gas, then persists the half-built op so the
        signing client can later splice its signature and submit.

        Returns `409 smart_account_not_provisioned` if the wallet has no
        SA row for the requested chain — call `POST
        /v1/wallets/me/smart-accounts` first.

        Returns `402 gas_cap_exceeded` if the wallet has burned its
        monthly sponsorship budget (€10 / wallet / calendar month).
      security: [{ PublishableKey: [], EndUserJWT: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [chain, calls]
              properties:
                chain: { type: string, description: "EVM chain handle" }
                calls:
                  type: array
                  description: One or more inner calls the smart account will execute.
                  items:
                    type: object
                    required: [to]
                    properties:
                      to: { type: string }
                      data: { type: string, description: "0x-prefixed calldata; default 0x" }
                      value: { type: string, description: "decimal wei; default 0" }
                preview: { $ref: '#/components/schemas/SignPreview' }
      responses:
        '200':
          description: UserOp ready to sign.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          user_op_hash: { type: string, description: "0x-prefixed 32-byte hash to sign with the owner EOA" }
                          preview: { $ref: '#/components/schemas/SignPreview' }
                          expires_at: { type: string, format: date-time }
        '402':
          description: Monthly sponsorship cap reached.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        '409':
          description: No smart account provisioned for this wallet on the requested chain.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }

  /v1/userop/submit:
    post:
      tags: [UserOps]
      summary: Splice the owner signature, submit to the bundler, await receipt
      description: |
        The signing client (iframe or backend) signs the `user_op_hash`
        returned by `/v1/userop/build` with the owner EOA's key
        (EIP-191-wrapped). This endpoint splices that signature onto the
        stored UserOp, sends it via the bundler, then polls up to 30 s
        for the on-chain receipt.

        Returns `status: "submitted"` if the bundler accepted but the op
        didn't land in time — clients poll `GET /v1/userop/{hash}` until
        `confirmed` or `failed`.
      security: [{ PublishableKey: [], EndUserJWT: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [user_op_hash, signature]
              properties:
                user_op_hash: { type: string }
                signature: { type: string, description: "0x-prefixed 65-byte r||s||v ECDSA signature over the EIP-191-wrapped hash" }
      responses:
        '200':
          description: Submitted (and possibly already confirmed).
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/UserOpStatus' }

  /v1/userop/{hash}:
    get:
      tags: [UserOps]
      summary: Poll the status of a previously submitted UserOp
      security: [{ PublishableKey: [], EndUserJWT: [] }]
      parameters:
        - name: hash
          in: path
          required: true
          schema: { type: string, description: "user_op_hash returned by /v1/userop/build" }
      responses:
        '200':
          description: Current status.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/UserOpStatus' }
        '404': { $ref: '#/components/responses/NotFound' }

  # ---------------- Server-to-server ----------------

  /v1/s2s/wallets:
    get:
      tags: [S2S Wallets]
      summary: List wallets in the org
      description: |
        Requires a secret key (`sk_live_…`). Keys with `scope: read` have
        access. Keys with IP allowlisting are restricted to listed CIDRs.
        Rate limit: 30 req/s sustained, 60 burst per key.
      security: [{ SecretKey: [] }]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: Paged list.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          items:
                            type: array
                            items: { $ref: '#/components/schemas/WalletWithIdentity' }
                          total: { type: integer }
                          limit: { type: integer }
                          offset: { type: integer }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/s2s/wallets/{walletID}:
    get:
      tags: [S2S Wallets]
      summary: Read one wallet
      security: [{ SecretKey: [] }]
      parameters:
        - name: walletID
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Wallet.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/WalletWithIdentity' }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/s2s/wallets/{walletID}/archive:
    post:
      tags: [S2S Wallets]
      summary: Archive a compromised wallet
      description: |
        Moves the wallet to `archived` status. Archived wallets cannot
        sign — the iframe rejects operations on them (client mode) or the
        TEE refuses to reconstruct the key (TEE mode). This is the kill
        switch for compromised wallets. Requires `scope: full`.
      security: [{ SecretKey: [] }]
      parameters:
        - name: walletID
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '204': { description: Wallet archived. }
        '403':
          description: API key has read-only scope.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/s2s/sign:
    post:
      tags: [TEE signing]
      summary: Sign a payload server-to-server via the TEE
      description: |
        **TEE mode only.** Your backend sends a signing request using a
        secret key. The TEE reconstructs the private key from encrypted
        shares, signs the payload, and wipes all key material.

        This endpoint enables agent-driven and automated signing workflows
        without any browser interaction. Requires `scope: full`.

        Returns `400 signing_mode_mismatch` if the organization uses client mode.

        Rate limit: 30 req/s sustained, 60 burst per key.
      security: [{ SecretKey: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/SignRequest'
                - type: object
                  required: [wallet_id]
                  properties:
                    wallet_id:
                      type: string
                      format: uuid
                      description: The wallet to sign with.
      responses:
        '200':
          description: Signature produced inside the TEE.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/SignResponse' }
        '400':
          description: Not available in client mode or invalid payload.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        '403':
          description: API key has read-only scope.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /v1/s2s/wallets/{walletID}/smart-accounts:
    get:
      tags: [Smart accounts]
      summary: List smart accounts for a wallet
      description: |
        Owner-facing read of the same data the end-user lane returns.
        Useful for back-office dashboards.
      security: [{ SecretKey: [] }]
      parameters:
        - name: walletID
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: List of smart accounts on top of this wallet.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          items:
                            type: array
                            items: { $ref: '#/components/schemas/SmartAccount' }
        '404': { $ref: '#/components/responses/NotFound' }
    post:
      tags: [Smart accounts]
      summary: Provision a smart account on behalf of an end user
      description: |
        Symmetric to `POST /v1/wallets/me/smart-accounts` but driven by
        the owner platform — useful when you want to seed smart accounts
        at onboarding without prompting the user. Requires `scope: full`.
      security: [{ SecretKey: [] }]
      parameters:
        - name: walletID
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [chain]
              properties:
                chain: { type: string }
      responses:
        '201':
          description: "Smart account provisioned (created_by = owner)."
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/SmartAccount' }
        '403':
          description: API key has read-only scope.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorEnvelope' }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/agents/{agentID}/.well-known/agent-card.json:
    get:
      tags: [Agents (A2A)]
      summary: Discovery card for an agent
      description: |
        Returns the agent's public Discovery card per the A2A protocol:
        capabilities, supported tasks, JSON-RPC endpoint, contact info.
        Always public — no auth required — so external A2A clients can
        introspect before sending tasks.
      security: []
      parameters:
        - in: path
          name: agentID
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: A2A Discovery card (JSON).
          content:
            application/json:
              schema:
                type: object
                description: A2A `AgentCard` document. Includes `id`, `name`, `description`, `capabilities`, and the JSON-RPC `endpoint_url`.
        '404':
          description: Agent does not exist or is archived.

  /v1/agents/{agentID}/a2a:
    post:
      tags: [Agents (A2A)]
      summary: Send an Agent-to-Agent JSON-RPC request
      description: |
        JSON-RPC 2.0 endpoint. Supported methods:

        - `tasks/send` — submit a new task (objective + optional metadata).
        - `tasks/get` — poll a previously submitted task by id.

        Authentication is a bearer token in the `Authorization` header. For
        Sigil-to-Sigil delegation the token is an `ac_live_…` JWT minted by
        the calling agent's runner; for external A2A clients you provide a
        Sigil-issued API token bound to the target agent (`as_live_…`).
      security:
        - SigilJWT: []
      parameters:
        - in: path
          name: agentID
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [jsonrpc, method, id]
              properties:
                jsonrpc: { type: string, enum: ['2.0'] }
                method:  { type: string, enum: [tasks/send, tasks/get] }
                id:      { description: 'JSON-RPC request id (string or number).' }
                params:
                  type: object
                  description: |
                    For `tasks/send`: `{ id, message: { role, parts: [{type: "text", text}] } }`.
                    For `tasks/get`:  `{ id }`.
      responses:
        '200':
          description: JSON-RPC 2.0 response envelope. `result` carries an A2A `Task` with `status.state` ∈ submitted | working | input-required | completed | canceled | failed and an optional `artifacts[]` array.
          content:
            application/json:
              schema:
                type: object
                required: [jsonrpc, id]
                properties:
                  jsonrpc: { type: string, enum: ['2.0'] }
                  id:      { description: 'Mirror of the request id.' }
                  result:  { type: object, description: 'A2A Task object.' }
                  error:
                    type: object
                    properties:
                      code:    { type: integer }
                      message: { type: string }
        '401':
          description: Missing or invalid bearer token.
        '404':
          description: Agent does not exist or is archived.

  /v1/webhooks/{taskID}:
    post:
      tags: [Agent webhooks]
      summary: Trigger an agent task via inbound webhook
      description: |
        Operator-configured webhook receiver. The customer's upstream
        system POSTs a JSON payload here; Sigil HMAC-verifies it against
        the task's stored secret and starts a run with the payload as the
        objective context.

        Request must include the `X-Sigil-Signature` header containing
        `sha256=<hex-hmac>` over the raw body. Signature verification is
        constant-time.

        The `taskID` is the Sigil agent task id shown in the portal under
        the agent's Triggers tab. Rotate the secret with the in-portal
        button if it leaks.
      security: []
      parameters:
        - in: path
          name: taskID
          required: true
          schema: { type: string, format: uuid }
        - in: header
          name: X-Sigil-Signature
          required: true
          schema: { type: string }
          description: HMAC-SHA256 of the raw body, formatted `sha256=<hex>`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Free-form JSON the operator's upstream sends; Sigil passes it through to the agent's first turn as context.
      responses:
        '202':
          description: Webhook accepted; a run has been scheduled.
        '401':
          description: Signature missing or invalid.
        '404':
          description: Task not found, archived, or has webhooks disabled.

components:

  securitySchemes:
    PublishableKey:
      type: apiKey
      in: header
      name: X-Sigil-Publishable-Key
      description: |
        Publishable key (`pk_live_…`). Identifies your org from the iframe.
        Safe to ship in the browser bundle. CORS origin is validated against
        the organization's allowed-origins list configured in the portal.
    SecretKey:
      type: http
      scheme: bearer
      bearerFormat: sk_live_…
      description: |
        Secret key for server-to-server endpoints. Treat like a password.
        Supports optional IP allowlisting (CIDRs) and scopes (`full` or `read`).
        Rate limited to 30 req/s sustained, 60 burst.
    EndUserJWT:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        End-user JWT. TTL: 1 hour, no refresh tokens. In Sigil-hosted mode
        it's minted by `POST /v1/wallets/auth/email-otp/verify`. In OIDC mode
        it's the access token from your IdP, validated against your JWKS.
        Tokens can be revoked server-side via `POST /v1/wallets/me/logout`.
    SigilJWT:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        Sigil-issued bearer token for the public A2A surface. Two flavours:

        - `ac_live_…` ("agent caller"): minted by a Sigil agent's runner
          when it invokes `call_agent` against another Sigil agent. Bound
          to (caller agent id → target agent id) and short-TTL.
        - `as_live_…` ("agent session"): minted by an org operator from
          the portal for external A2A clients. Long-lived, revocable,
          scoped to a single agent.

        Use the `/v1/agents/{agentID}/.well-known/agent-card.json` endpoint
        to discover the JSON-RPC URL before authenticating.

  parameters:
    Limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
    Offset:
      name: offset
      in: query
      schema: { type: integer, minimum: 0, default: 0 }

  responses:
    Unauthorized:
      description: Auth missing, invalid, or revoked.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    Conflict:
      description: Conflict (e.g. wallet already exists for this curve).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    RateLimited:
      description: Too many requests. Back off and retry.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }

  schemas:

    Envelope:
      type: object
      required: [data, error]
      properties:
        data: {}
        error:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/AppError'

    ErrorEnvelope:
      type: object
      required: [data, error]
      properties:
        data:
          type: 'null'
        error: { $ref: '#/components/schemas/AppError' }

    AppError:
      type: object
      required: [code, message]
      description: |
        Error codes are the API contract; messages are human-readable
        and safe to display to end users. Messages never leak internal
        details such as stack traces, SQL, or file paths.
      properties:
        code: { type: string, example: wallet_not_found }
        message: { type: string }

    Identity:
      type: object
      properties:
        organization_id: { type: string, format: uuid }
        user_identity_id: { type: string, format: uuid }
        auth_provider: { type: string, enum: [sigil, oidc] }
        provider_user_id: { type: string }
        email: { type: string, format: email }
        signing_mode:
          type: string
          enum: [client, tee]
          description: |
            The organization's signing mode. Determines where key generation
            and signing happen. `client` = browser iframe (2-of-3 Shamir).
            `tee` = server-side Confidential Space TEE (2-of-2).
            Set at organization creation and immutable.
        supported_networks:
          type: array
          items: { type: string }
          description: |
            Networks enabled for this organization (e.g. ethereum, solana, bitcoin).
            The iframe uses this to determine which curves and keypairs to generate.

    Wallet:
      type: object
      properties:
        id: { type: string, format: uuid }
        organization_id: { type: string, format: uuid }
        user_identity_id: { type: string, format: uuid }
        address:
          type: string
          description: |
            Default address for this curve's primary network. Format depends
            on the curve: 0x hex for secp256k1/Ethereum, base58 for ed25519/Solana,
            bech32 for Bitcoin, etc.
        public_key: { type: string, description: 0x-prefixed hex }
        chain_type: { type: string, description: Legacy field. Use `curve` instead. }
        curve:
          type: string
          enum: [secp256k1, ed25519]
          description: |
            Cryptographic curve used by this wallet.
            secp256k1: Ethereum, Bitcoin, Tron, Cosmos.
            ed25519: Solana, Near, Aptos, Stellar.
        networks:
          type: array
          items: { type: string }
          description: |
            Networks this wallet serves, derived from the organization's
            supported_networks filtered by the wallet's curve.
        status: { type: string, enum: [active, archived, deprecated] }
        signing_mode:
          type: string
          enum: [client, tee]
          description: |
            Per-wallet signing mode. `client` = browser SSS reconstruction;
            `tee` = server-side reconstruction inside the attested TEE.
            Set at creation, immutable. Mirrors the Identity-level
            signing_mode for legacy clients; new clients should rely on
            this per-wallet field.
        created_at: { type: string, format: date-time }
        account_type:
          type: string
          enum: [eoa]
          default: eoa
          description: |
            Always `eoa`. ERC-4337 smart accounts sit on top of an EOA in
            a separate record set; see `/v1/wallets/me/smart-accounts`.

    WalletWithIdentity:
      allOf:
        - $ref: '#/components/schemas/Wallet'
        - type: object
          properties:
            email: { type: string, format: email }
            provider_user_id: { type: string }
            auth_provider: { type: string, enum: [sigil, oidc] }
            user_last_seen_at: { type: string, format: date-time }

    SignRequest:
      type: object
      required: [payload, encoding]
      description: |
        A signing request. The TEE reconstructs the key, signs the payload,
        and wipes all material. The `curve` field selects which wallet to use
        when multiple curves are available.
      properties:
        payload:
          type: string
          description: |
            The data to sign. Encoding is specified by the `encoding` field.
            For EVM transactions, this is the unsigned transaction hash (32 bytes).
            For arbitrary messages, this is the message bytes.
        encoding:
          type: string
          enum: [hex, base64]
          description: Encoding of the payload field.
        curve:
          type: string
          enum: [secp256k1, ed25519]
          default: secp256k1
          description: Which curve's wallet to sign with.
        chain:
          type: string
          description: |
            Optional chain hint (e.g. ethereum, solana). Used for
            chain-specific signature formatting when applicable.

    SignResponse:
      type: object
      properties:
        wallet_id: { type: string, format: uuid }
        signature:
          type: string
          description: |
            The produced signature in hex encoding. For secp256k1 this is
            a 65-byte ECDSA signature (r + s + v). For ed25519 this is a
            64-byte EdDSA signature.
        public_key: { type: string, description: 0x-prefixed hex }

    SmartAccount:
      type: object
      description: |
        An ERC-4337 LightAccount v2 record sitting on top of a Sigil EOA
        wallet, scoped to one EVM chain. The address is counterfactual
        (CREATE2 derived) until the first sponsored UserOp lands; from
        that point on `deployed` flips to true.
      properties:
        id: { type: string, format: uuid }
        wallet_id: { type: string, format: uuid, description: The owner EOA wallet. }
        owner_address:
          type: string
          description: The owning EOA's address (the signer for UserOps).
        chain:
          type: string
          description: "EVM chain handle, e.g. base, arbitrum, ethereum."
        factory:
          type: string
          enum: [lightaccount_v2]
          description: |
            The factory used to derive the smart-account address. Today
            only LightAccount v2 is supported.
        address:
          type: string
          description: |
            The counterfactual smart-account address. Same value across
            EVM chains for the same (wallet, factory) tuple.
        deployed:
          type: boolean
          description: |
            False until the first UserOp on this chain succeeds (the
            bundler bundles the deployment into that op). True afterwards.
        sponsor_policy:
          type: string
          description: |
            Optional Pimlico sponsorship policy id bound to this record.
            Empty when the default policy applies.
        created_by:
          type: string
          enum: [user, owner]
          description: |
            `user` — provisioned by the end user via the wallet lane.
            `owner` — provisioned by the integrator via the S2S lane.
        created_at: { type: string, format: date-time }

    SignPreview:
      type: object
      description: |
        Human-readable summary the wallet iframe renders on the confirm
        screen. The integration passes one in if it has product context;
        otherwise Sigil auto-generates a generic preview.
      properties:
        title: { type: string }
        description: { type: string }
        fields:
          type: array
          items:
            type: object
            required: [label, value]
            properties:
              label: { type: string }
              value: { type: string }

    UserOpStatus:
      type: object
      description: |
        Status of a previously built UserOp. Lifecycle: `pending` →
        `submitted` (bundler accepted, not yet on-chain) → `confirmed`
        or `failed`.
      properties:
        user_op_hash: { type: string }
        tx_hash: { type: string, description: "0x-prefixed L1/L2 tx hash, populated once the op lands." }
        success: { type: boolean, description: "True when status is confirmed and the on-chain execution succeeded." }
        block_number: { type: string, description: "0x-prefixed block number, populated once confirmed." }
        status:
          type: string
          enum: [pending, submitted, confirmed, failed]
