{
  "openapi": "3.1.1",
  "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
  "info": {
    "title": "SubraLabs Calibration Telemetry API",
    "version": "1.0.0",
    "description": "Public API for anonymous, explicit-opt-in telemetry from Sense-Calibrator. This is not a general-purpose SubraLabs agent API. A telemetry failure must never block controller calibration.",
    "contact": {
      "name": "SubraLabs",
      "url": "https://subralabs.com/contact.html",
      "email": "martino@subralabs.com"
    }
  },
  "servers": [
    {
      "url": "https://subralabs.com",
      "description": "Production"
    }
  ],
  "security": [],
  "tags": [
    {
      "name": "Calibration telemetry",
      "description": "Anonymous technical calibration results submitted only after explicit user opt-in."
    }
  ],
  "paths": {
    "/api/calib/health": {
      "get": {
        "operationId": "getCalibrationTelemetryHealth",
        "summary": "Check telemetry service availability",
        "description": "Returns a minimal availability signal. Do not poll aggressively and do not treat a successful health check as a guarantee that a later submission will be accepted.",
        "tags": ["Calibration telemetry"],
        "responses": {
          "200": {
            "description": "The telemetry service is available.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/calib/v1/sessions": {
      "post": {
        "operationId": "submitCalibrationSession",
        "summary": "Submit an opted-in calibration result",
        "description": "Stores one anonymous technical result from Sense-Calibrator. Call only after the person running that calibration gives explicit opt-in. Do not add account, contact, IP, User-Agent, or other request metadata. A failed request must not interrupt calibration.",
        "tags": ["Calibration telemetry"],
        "requestBody": {
          "required": true,
          "description": "The measured controller state and calibration result. The JSON body must be no larger than 8 KB and must not contain unknown properties.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CalibrationSession"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "The calibration result was accepted and stored.",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "400": {
            "description": "The JSON body is malformed or does not match the calibration schema.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "The JSON body exceeds 8 KB.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "415": {
            "description": "The request body does not use a supported JSON media type or encoding.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "The per-IP submission limit has been reached.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 60
                }
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "507": {
            "description": "The telemetry store has reached its configured capacity. Do not retry automatically.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "headers": {
      "RateLimitPolicy": {
        "description": "Quota policy in the current IETF HTTPAPI draft format: 10 submissions in 60 seconds.",
        "schema": {
          "type": "string",
          "example": "\"calibration-submissions\";q=10;w=60"
        }
      },
      "RateLimit": {
        "description": "Current remaining quota and effective window in the current IETF HTTPAPI draft format.",
        "schema": {
          "type": "string",
          "example": "\"calibration-submissions\";r=9;t=60"
        }
      }
    },
    "responses": {
      "InternalError": {
        "description": "The service could not process the request. Do not retry immediately.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      }
    },
    "schemas": {
      "HealthResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": ["status"],
        "properties": {
          "status": {
            "type": "string",
            "const": "ok",
            "description": "Current service availability."
          }
        }
      },
      "CalibrationPair": {
        "type": "object",
        "additionalProperties": false,
        "required": ["off", "noise"],
        "properties": {
          "off": {
            "$ref": "#/components/schemas/MeasurementPair"
          },
          "noise": {
            "$ref": "#/components/schemas/MeasurementPair"
          }
        }
      },
      "MeasurementPair": {
        "type": "array",
        "description": "Two finite axis measurements.",
        "minItems": 2,
        "maxItems": 2,
        "items": {
          "type": "number",
          "minimum": 0,
          "maximum": 200
        }
      },
      "CalibrationSession": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "t",
          "board",
          "fw",
          "before",
          "after",
          "passes",
          "unstableEvents",
          "gate",
          "gateOff"
        ],
        "properties": {
          "t": {
            "type": "string",
            "format": "date-time",
            "description": "Timestamp recorded by the calibrator."
          },
          "board": {
            "type": ["string", "null"],
            "maxLength": 20,
            "description": "Controller board identifier when available."
          },
          "fw": {
            "type": ["integer", "null"],
            "description": "Controller firmware number when available."
          },
          "before": {
            "description": "Measurements before calibration, or null when unavailable.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/CalibrationPair"
              },
              {
                "type": "null"
              }
            ]
          },
          "after": {
            "description": "Measurements after calibration, or null when unavailable.",
            "anyOf": [
              {
                "$ref": "#/components/schemas/CalibrationPair"
              },
              {
                "type": "null"
              }
            ]
          },
          "passes": {
            "type": "array",
            "maxItems": 8,
            "items": {
              "type": "number"
            },
            "description": "Up to eight finite pass measurements."
          },
          "unstableEvents": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100,
            "description": "Number of detected instability events."
          },
          "gate": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Selected calibration gate."
          },
          "gateOff": {
            "type": "boolean",
            "description": "Whether the calibration gate was disabled."
          }
        },
        "examples": [
          {
            "t": "2026-09-09T10:00:00.000Z",
            "board": "DUALSHOCK4",
            "fw": 312,
            "before": {
              "off": [10, 12],
              "noise": [5, 6]
            },
            "after": {
              "off": [2, 1],
              "noise": [3, 4]
            },
            "passes": [0.12, 0.08],
            "unstableEvents": 2,
            "gate": 0.95,
            "gateOff": false
          }
        ]
      },
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem details with stable agent-oriented extensions.",
        "required": ["type", "title", "status", "detail", "code", "resolution", "error"],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri",
            "description": "Documentation URI for the problem type."
          },
          "title": {
            "type": "string",
            "description": "Stable, human-readable problem summary."
          },
          "status": {
            "type": "integer",
            "minimum": 400,
            "maximum": 599,
            "description": "HTTP status code repeated in the body."
          },
          "detail": {
            "type": "string",
            "description": "Explanation of this occurrence."
          },
          "code": {
            "type": "string",
            "enum": [
              "invalid_request",
              "payload_too_large",
              "unsupported_media_type",
              "rate_limit_exceeded",
              "storage_capacity_reached",
              "endpoint_not_found",
              "internal_error"
            ],
            "description": "Stable machine-readable error code."
          },
          "resolution": {
            "type": "string",
            "description": "Safe next action for a client or agent."
          },
          "error": {
            "type": "string",
            "deprecated": true,
            "description": "Compatibility alias that mirrors detail."
          }
        }
      }
    }
  }
}
