{
  "openapi": "3.0.3",
  "info": {
    "title": "Kenshiki Pulse Bonded Sessions API",
    "version": "0.1.0",
    "description": "Pulse bonds a high-risk browser workflow to the applicant's carried phone. The browser is a presentation surface, not the root of trust. The server must verify a bonded session before accepting a protected action.\n"
  },
  "servers": [
    {
      "url": "https://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev/api/v1",
      "description": "Kenshiki Pulse Worker production API"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Sessions"
    },
    {
      "name": "Attestations"
    }
  ],
  "paths": {
    "/sessions": {
      "post": {
        "tags": ["Sessions"],
        "summary": "Create a pending bonded session",
        "description": "Creates a short-lived pending session. Production integrations should call this from the customer's backend with a server credential, store completion_secret server-side, and return only the public session object to the browser. The hosted embed renders the QR or mobile handoff from that public session, opens the WebSocket, and waits for the carried phone to bond the session.\n",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateSessionRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Pending session created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BondedSession"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/sessions/{session_id}": {
      "get": {
        "tags": ["Sessions"],
        "summary": "Retrieve a bonded session",
        "description": "Server-side integrations must call this endpoint before accepting a gated submit. Trust only a fresh bonded session scoped to the expected workflow and action.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/SessionId"
          }
        ],
        "responses": {
          "200": {
            "description": "Session state and bounded verification summary",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BondedSession"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "tags": ["Sessions"],
        "summary": "Kill a session",
        "parameters": [
          {
            "$ref": "#/components/parameters/SessionId"
          }
        ],
        "responses": {
          "204": {
            "description": "Session killed"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/sessions/{session_id}/form-completed": {
      "post": {
        "tags": ["Sessions"],
        "summary": "Complete the action and close the bond (bonded → completed)",
        "description": "Call once the protected action has durably succeeded. Moves the session from bonded to completed and flips the carried phone's Pulse drawer to success. A bond is for one action; if you never complete it, it simply expires on its TTL. Requires the per-session completion secret returned by POST /sessions (X-Pulse-Completion-Secret) — a party holding only the session_id cannot complete. In production, the customer's backend creates the session and keeps this secret server-side.\n",
        "parameters": [
          {
            "$ref": "#/components/parameters/SessionId"
          },
          {
            "name": "X-Pulse-Completion-Secret",
            "in": "header",
            "required": true,
            "description": "The completion_secret from the session create response.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Action completed; session moved to completed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "example": true
                    },
                    "session_id": {
                      "type": "string",
                      "example": "sess_9f2c"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/attestations": {
      "post": {
        "tags": ["Attestations"],
        "summary": "Submit a bonded pair assertion",
        "description": "Called by the Pulse mobile app after it evaluates continuity of life and signs the session assertion. Customer websites should not call this from the browser.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BondedPairAssertion"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Session bonded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BondedSession"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "server beta credential"
      }
    },
    "parameters": {
      "SessionId": {
        "name": "session_id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "example": "sess_9f2c"
        }
      }
    },
    "responses": {
      "Error": {
        "description": "Typed error response",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      }
    },
    "schemas": {
      "CreateSessionRequest": {
        "type": "object",
        "properties": {
          "workflow": {
            "type": "string",
            "example": "credit_application"
          },
          "action": {
            "type": "string",
            "example": "submit_application"
          }
        }
      },
      "BondedSession": {
        "type": "object",
        "required": [
          "id",
          "state",
          "nonce",
          "api_base_url",
          "qr_payload",
          "universal_link",
          "challenge_expires_at",
          "expires_at",
          "verification"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "sess_9f2c"
          },
          "state": {
            "type": "string",
            "enum": ["pending", "bonded", "step_up_required", "completed", "killed", "expired"]
          },
          "nonce": {
            "type": "string",
            "example": "nonce_abc"
          },
          "qr_payload": {
            "type": "string",
            "example": "pulse://bond?session_id=sess_9f2c&nonce=nonce_abc"
          },
          "qr_svg_url": {
            "type": "string",
            "example": "/v1/sessions/sess_9f2c/qr.svg?nonce=nonce_abc"
          },
          "api_base_url": {
            "type": "string",
            "description": "Worker origin that minted this session. The phone app and browser polling must use this same origin.",
            "example": "https://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev"
          },
          "universal_link": {
            "type": "string",
            "example": "https://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev/bond?session_id=sess_9f2c&nonce=nonce_abc"
          },
          "websocket_url": {
            "type": "string",
            "example": "wss://kenshiki-pulse-worker-production.pulsekenshikilabscom.workers.dev/sessions/sess_9f2c"
          },
          "completion_secret": {
            "type": "string",
            "nullable": true,
            "description": "Per-session capability to complete the action (close the bond), returned only on create. In a server-authoritative flow keep it server-side; never expose it to untrusted parties or the browser.\n",
            "example": "cs_secret_abc"
          },
          "challenge_expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "workflow": {
            "type": "string",
            "nullable": true
          },
          "action": {
            "type": "string",
            "nullable": true
          },
          "verification": {
            "nullable": true,
            "allOf": [
              {
                "$ref": "#/components/schemas/PulseVerification"
              }
            ]
          }
        }
      },
      "PulseVerification": {
        "type": "object",
        "description": "Bounded proof summary. It is not raw sensor history, location history, or identity-provider internals.\n",
        "properties": {
          "device_possession": {
            "type": "boolean"
          },
          "local_auth": {
            "type": "string",
            "enum": ["verified", "failed"]
          },
          "carrier": {
            "type": "object",
            "properties": {
              "number_verified": {
                "type": "boolean"
              },
              "sim_swap_recent": {
                "type": "boolean"
              },
              "device_status": {
                "type": "string",
                "enum": ["active", "inactive", "unknown"]
              }
            }
          },
          "sensor_continuity": {
            "type": "object",
            "properties": {
              "motion_present": {
                "type": "boolean"
              },
              "continuity_score": {
                "type": "number",
                "minimum": 0,
                "maximum": 1
              }
            }
          },
          "passport": {
            "type": "object",
            "description": "Optional workflow-scoped passport evidence. Passport is not globally required for every Pulse user.",
            "properties": {
              "tier": {
                "type": "string",
                "enum": ["none", "document", "chip"]
              }
            }
          }
        }
      },
      "BondedPairAssertion": {
        "type": "object",
        "required": [
          "session_id",
          "nonce",
          "device_id",
          "subject_id",
          "issued_at",
          "app_attest",
          "verified_evidence"
        ],
        "properties": {
          "session_id": {
            "type": "string"
          },
          "nonce": {
            "type": "string"
          },
          "device_id": {
            "type": "string"
          },
          "subject_id": {
            "type": "string"
          },
          "issued_at": {
            "type": "string",
            "format": "date-time"
          },
          "app_attest": {
            "type": "object",
            "required": ["key_id", "client_data_hash", "authenticator_data", "signature"],
            "properties": {
              "key_id": {
                "type": "string"
              },
              "client_data_hash": {
                "type": "string",
                "description": "Base64url SHA-256 hash over the Pulse bond challenge payload."
              },
              "authenticator_data": {
                "type": "string",
                "description": "Base64url App Attest authenticator data."
              },
              "signature": {
                "type": "string",
                "description": "Base64url ECDSA P-256 signature from the registered App Attest key."
              }
            }
          },
          "verified_evidence": {
            "type": "object",
            "required": [
              "issued_at",
              "expires_at",
              "local_auth",
              "carrier",
              "sensor_continuity",
              "signature"
            ],
            "description": "Server-signed evidence summary. Client self-asserted evidence is not accepted in production.",
            "properties": {
              "issued_at": {
                "type": "string",
                "format": "date-time"
              },
              "expires_at": {
                "type": "string",
                "format": "date-time"
              },
              "local_auth": {
                "type": "object"
              },
              "carrier": {
                "type": "object"
              },
              "sensor_continuity": {
                "type": "object"
              },
              "signature": {
                "type": "string",
                "description": "HMAC over the canonical evidence payload."
              }
            }
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "example": "invalid_request"
              },
              "message": {
                "type": "string"
              },
              "requestId": {
                "type": "string"
              }
            }
          }
        }
      }
    }
  }
}
