{
  "components": {
    "schemas": {
      "ApiKeyCreate": {
        "properties": {
          "bike_ids": {
            "items": {
              "format": "uuid",
              "type": "string"
            },
            "title": "Bike Ids",
            "type": "array"
          },
          "bike_scope": {
            "default": "listed",
            "title": "Bike Scope",
            "type": "string"
          },
          "environment": {
            "default": "live",
            "title": "Environment",
            "type": "string"
          },
          "expires_in_days": {
            "anyOf": [
              {
                "maximum": 3650.0,
                "minimum": 1.0,
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Expires In Days"
          },
          "label": {
            "maxLength": 80,
            "minLength": 1,
            "title": "Label",
            "type": "string"
          },
          "scopes": {
            "items": {
              "type": "string"
            },
            "title": "Scopes",
            "type": "array"
          }
        },
        "required": [
          "label"
        ],
        "title": "ApiKeyCreate",
        "type": "object"
      },
      "ApiKeyCreated": {
        "description": "The ONLY response in the entire API that ever carries the secret.\n\nA distinct model, not an optional field on `ApiKeyOut`, so that a future handler cannot\naccidentally return the plaintext from a list or a read: to do so it would have to name\nthis class, which does not appear in any other route's `response_model`.",
        "properties": {
          "bike_ids": {
            "items": {
              "type": "string"
            },
            "title": "Bike Ids",
            "type": "array"
          },
          "bike_scope": {
            "title": "Bike Scope",
            "type": "string"
          },
          "created_at": {
            "format": "date-time",
            "title": "Created At",
            "type": "string"
          },
          "daily_sample_quota": {
            "title": "Daily Sample Quota",
            "type": "integer"
          },
          "display": {
            "title": "Display",
            "type": "string"
          },
          "environment": {
            "title": "Environment",
            "type": "string"
          },
          "expires_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Expires At"
          },
          "id": {
            "title": "Id",
            "type": "string"
          },
          "is_alive": {
            "title": "Is Alive",
            "type": "boolean"
          },
          "key": {
            "title": "Key",
            "type": "string"
          },
          "key_id": {
            "title": "Key Id",
            "type": "string"
          },
          "label": {
            "title": "Label",
            "type": "string"
          },
          "last_used_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Used At"
          },
          "rate_per_minute": {
            "title": "Rate Per Minute",
            "type": "integer"
          },
          "revoked_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Revoked At"
          },
          "revoked_reason": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Revoked Reason"
          },
          "scopes": {
            "items": {
              "type": "string"
            },
            "title": "Scopes",
            "type": "array"
          },
          "warning": {
            "default": "This is the only time this key will be shown. Store it now; we keep only a one-way hash and cannot recover it.",
            "title": "Warning",
            "type": "string"
          }
        },
        "required": [
          "id",
          "key_id",
          "label",
          "environment",
          "display",
          "scopes",
          "bike_scope",
          "bike_ids",
          "rate_per_minute",
          "daily_sample_quota",
          "created_at",
          "expires_at",
          "last_used_at",
          "revoked_at",
          "revoked_reason",
          "is_alive",
          "key"
        ],
        "title": "ApiKeyCreated",
        "type": "object"
      },
      "ApiKeyOut": {
        "description": "The READ shape. Structurally incapable of carrying a secret — see `ApiKeyCreated`.",
        "properties": {
          "bike_ids": {
            "items": {
              "type": "string"
            },
            "title": "Bike Ids",
            "type": "array"
          },
          "bike_scope": {
            "title": "Bike Scope",
            "type": "string"
          },
          "created_at": {
            "format": "date-time",
            "title": "Created At",
            "type": "string"
          },
          "daily_sample_quota": {
            "title": "Daily Sample Quota",
            "type": "integer"
          },
          "display": {
            "title": "Display",
            "type": "string"
          },
          "environment": {
            "title": "Environment",
            "type": "string"
          },
          "expires_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Expires At"
          },
          "id": {
            "title": "Id",
            "type": "string"
          },
          "is_alive": {
            "title": "Is Alive",
            "type": "boolean"
          },
          "key_id": {
            "title": "Key Id",
            "type": "string"
          },
          "label": {
            "title": "Label",
            "type": "string"
          },
          "last_used_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Used At"
          },
          "rate_per_minute": {
            "title": "Rate Per Minute",
            "type": "integer"
          },
          "revoked_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Revoked At"
          },
          "revoked_reason": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Revoked Reason"
          },
          "scopes": {
            "items": {
              "type": "string"
            },
            "title": "Scopes",
            "type": "array"
          }
        },
        "required": [
          "id",
          "key_id",
          "label",
          "environment",
          "display",
          "scopes",
          "bike_scope",
          "bike_ids",
          "rate_per_minute",
          "daily_sample_quota",
          "created_at",
          "expires_at",
          "last_used_at",
          "revoked_at",
          "revoked_reason",
          "is_alive"
        ],
        "title": "ApiKeyOut",
        "type": "object"
      },
      "ApiKeyPatch": {
        "description": "Relabel, or NARROW. Every field here may only ever reduce what the key can do.",
        "properties": {
          "bike_ids": {
            "anyOf": [
              {
                "items": {
                  "format": "uuid",
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Bike Ids"
          },
          "bike_scope": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Bike Scope"
          },
          "label": {
            "anyOf": [
              {
                "maxLength": 80,
                "minLength": 1,
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Label"
          },
          "scopes": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Scopes"
          }
        },
        "title": "ApiKeyPatch",
        "type": "object"
      },
      "BatchOut": {
        "properties": {
          "accepted_count": {
            "title": "Accepted Count",
            "type": "integer"
          },
          "clock_skew_seconds": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Clock Skew Seconds"
          },
          "decompressed_bytes": {
            "title": "Decompressed Bytes",
            "type": "integer"
          },
          "duplicate_count": {
            "title": "Duplicate Count",
            "type": "integer"
          },
          "id": {
            "title": "Id",
            "type": "string"
          },
          "received_at": {
            "format": "date-time",
            "title": "Received At",
            "type": "string"
          },
          "rejected_count": {
            "title": "Rejected Count",
            "type": "integer"
          },
          "rejection_reasons": {
            "anyOf": [
              {
                "additionalProperties": {
                  "type": "integer"
                },
                "type": "object"
              },
              {
                "type": "null"
              }
            ],
            "title": "Rejection Reasons"
          },
          "response_status": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Response Status"
          },
          "sample_count": {
            "title": "Sample Count",
            "type": "integer"
          },
          "sent_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Sent At"
          }
        },
        "required": [
          "id",
          "received_at",
          "sent_at",
          "clock_skew_seconds",
          "sample_count",
          "accepted_count",
          "duplicate_count",
          "rejected_count",
          "decompressed_bytes",
          "response_status"
        ],
        "title": "BatchOut",
        "type": "object"
      },
      "Body_ingestLog": {
        "properties": {
          "device_uid": {
            "title": "Device Uid",
            "type": "string"
          },
          "file": {
            "format": "binary",
            "title": "File",
            "type": "string"
          }
        },
        "required": [
          "file",
          "device_uid"
        ],
        "title": "Body_ingestLog",
        "type": "object"
      },
      "DeviceOut": {
        "properties": {
          "bike_id": {
            "title": "Bike Id",
            "type": "string"
          },
          "created_at": {
            "format": "date-time",
            "title": "Created At",
            "type": "string"
          },
          "current_sign_flip": {
            "title": "Current Sign Flip",
            "type": "boolean"
          },
          "device_uid": {
            "title": "Device Uid",
            "type": "string"
          },
          "firmware": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Firmware"
          },
          "id": {
            "title": "Id",
            "type": "string"
          },
          "is_blocked": {
            "title": "Is Blocked",
            "type": "boolean"
          },
          "label": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Label"
          },
          "last_seen_at": {
            "anyOf": [
              {
                "format": "date-time",
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Seen At"
          },
          "last_skew_seconds": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Last Skew Seconds"
          },
          "model": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Model"
          },
          "retain_raw_payloads": {
            "title": "Retain Raw Payloads",
            "type": "boolean"
          },
          "sample_count": {
            "title": "Sample Count",
            "type": "integer"
          },
          "samples_last_24h": {
            "default": 0,
            "title": "Samples Last 24H",
            "type": "integer"
          },
          "vendor": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Vendor"
          },
          "vin": {
            "title": "Vin",
            "type": "string"
          }
        },
        "required": [
          "id",
          "bike_id",
          "vin",
          "device_uid",
          "vendor",
          "model",
          "firmware",
          "label",
          "is_blocked",
          "retain_raw_payloads",
          "current_sign_flip",
          "created_at",
          "last_seen_at",
          "last_skew_seconds",
          "sample_count"
        ],
        "title": "DeviceOut",
        "type": "object"
      },
      "DevicePair": {
        "properties": {
          "bike_id": {
            "format": "uuid",
            "title": "Bike Id",
            "type": "string"
          },
          "device_uid": {
            "maxLength": 64,
            "minLength": 1,
            "title": "Device Uid",
            "type": "string"
          },
          "firmware": {
            "anyOf": [
              {
                "maxLength": 40,
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Firmware"
          },
          "label": {
            "anyOf": [
              {
                "maxLength": 80,
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Label"
          },
          "model": {
            "anyOf": [
              {
                "maxLength": 60,
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Model"
          },
          "vendor": {
            "anyOf": [
              {
                "maxLength": 60,
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Vendor"
          }
        },
        "required": [
          "bike_id",
          "device_uid"
        ],
        "title": "DevicePair",
        "type": "object"
      },
      "DevicePatch": {
        "properties": {
          "current_sign_flip": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Current Sign Flip"
          },
          "firmware": {
            "anyOf": [
              {
                "maxLength": 40,
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Firmware"
          },
          "is_blocked": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Is Blocked"
          },
          "label": {
            "anyOf": [
              {
                "maxLength": 80,
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Label"
          },
          "retain_raw_payloads": {
            "anyOf": [
              {
                "type": "boolean"
              },
              {
                "type": "null"
              }
            ],
            "title": "Retain Raw Payloads"
          }
        },
        "title": "DevicePatch",
        "type": "object"
      },
      "HTTPValidationError": {
        "properties": {
          "detail": {
            "items": {
              "$ref": "#/components/schemas/ValidationError"
            },
            "title": "Detail",
            "type": "array"
          }
        },
        "title": "HTTPValidationError",
        "type": "object"
      },
      "LatestOut": {
        "properties": {
          "age_seconds": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Age Seconds"
          },
          "device_id": {
            "title": "Device Id",
            "type": "string"
          },
          "sample": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/SampleOut"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "sample",
          "age_seconds",
          "device_id"
        ],
        "title": "LatestOut",
        "type": "object"
      },
      "LimitsOut": {
        "description": "The credential's remaining allowance.\n\nEvery figure is nullable, and `metered` says which case you are in: a session JWT is not\nmetered on this surface at all, and reporting it as `0 of 0` reads as exhausted — the\nopposite of the truth, and the kind of answer a client backs off on for ever.",
        "properties": {
          "daily_sample_quota": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Daily Sample Quota"
          },
          "daily_samples_remaining": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Daily Samples Remaining"
          },
          "daily_samples_used": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Daily Samples Used"
          },
          "key_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Key Id"
          },
          "metered": {
            "default": true,
            "title": "Metered",
            "type": "boolean"
          },
          "quota_reset_seconds": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Quota Reset Seconds"
          },
          "rate_per_minute": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Rate Per Minute"
          },
          "rate_remaining": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Rate Remaining"
          },
          "rate_reset_seconds": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Rate Reset Seconds"
          }
        },
        "required": [
          "key_id",
          "rate_per_minute",
          "rate_remaining",
          "rate_reset_seconds",
          "daily_sample_quota",
          "daily_samples_used",
          "daily_samples_remaining",
          "quota_reset_seconds"
        ],
        "title": "LimitsOut",
        "type": "object"
      },
      "Problem": {
        "description": "RFC 9457 problem document. Branch on `type`.",
        "properties": {
          "detail": {
            "description": "Prose. Do not branch on it.",
            "type": "string"
          },
          "retryable": {
            "description": "Whether repeating the identical request could later succeed. Not part of RFC 9457; part of this contract.",
            "type": "boolean"
          },
          "status": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "type": {
            "description": "Stable identifier; resolves to the docs section.",
            "format": "uri",
            "type": "string"
          }
        },
        "required": [
          "type",
          "title",
          "status",
          "detail",
          "retryable"
        ],
        "type": "object"
      },
      "SampleOut": {
        "description": "One stored reading. `sample_time` is the device's own clock and is returned so an\nintegrator can diff what they sent against what landed; `observed_at` is the corrected\ntime everything else reads.",
        "properties": {
          "clock_skew_seconds": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Clock Skew Seconds"
          },
          "observed_at": {
            "format": "date-time",
            "title": "Observed At",
            "type": "string"
          },
          "sample_time": {
            "format": "date-time",
            "title": "Sample Time",
            "type": "string"
          },
          "values": {
            "title": "Values",
            "type": "object"
          }
        },
        "required": [
          "sample_time",
          "observed_at",
          "values"
        ],
        "title": "SampleOut",
        "type": "object"
      },
      "ScopeOut": {
        "properties": {
          "description": {
            "title": "Description",
            "type": "string"
          },
          "name": {
            "title": "Name",
            "type": "string"
          }
        },
        "required": [
          "name",
          "description"
        ],
        "title": "ScopeOut",
        "type": "object"
      },
      "ValidationError": {
        "properties": {
          "loc": {
            "items": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "integer"
                }
              ]
            },
            "title": "Location",
            "type": "array"
          },
          "msg": {
            "title": "Message",
            "type": "string"
          },
          "type": {
            "title": "Error Type",
            "type": "string"
          }
        },
        "required": [
          "loc",
          "msg",
          "type"
        ],
        "title": "ValidationError",
        "type": "object"
      },
      "WhoamiOut": {
        "description": "Everything a device needs before its first push, in one call.",
        "properties": {
          "authenticated": {
            "title": "Authenticated",
            "type": "boolean"
          },
          "bike_scope": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Bike Scope"
          },
          "devices": {
            "items": {
              "type": "object"
            },
            "title": "Devices",
            "type": "array"
          },
          "key_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Key Id"
          },
          "limits": {
            "additionalProperties": {
              "type": "integer"
            },
            "title": "Limits",
            "type": "object"
          },
          "notice": {
            "default": "Telemetry is stored against the device and is NOT yet counted in rides, charges, totals, battery health or share links.",
            "title": "Notice",
            "type": "string"
          },
          "schema_version": {
            "title": "Schema Version",
            "type": "integer"
          },
          "scopes": {
            "items": {
              "type": "string"
            },
            "title": "Scopes",
            "type": "array"
          },
          "server_time": {
            "format": "date-time",
            "title": "Server Time",
            "type": "string"
          },
          "telemetry_is_promoted": {
            "default": false,
            "title": "Telemetry Is Promoted",
            "type": "boolean"
          },
          "via": {
            "title": "Via",
            "type": "string"
          }
        },
        "required": [
          "authenticated",
          "via",
          "key_id",
          "scopes",
          "bike_scope",
          "devices",
          "limits",
          "schema_version",
          "server_time"
        ],
        "title": "WhoamiOut",
        "type": "object"
      }
    },
    "securitySchemes": {
      "ApiKey": {
        "bearerFormat": "zlk_live_<key_id>_<secret><crc>",
        "description": "A key minted at /dashboard/devices. Shown once. Scoped at issue and narrowable afterwards, never wideable.",
        "scheme": "bearer",
        "type": "http"
      },
      "HTTPBearer": {
        "scheme": "bearer",
        "type": "http"
      },
      "SessionJWT": {
        "bearerFormat": "JWT",
        "description": "An ordinary signed-in session. Accepted on the ingest routes so a first-party app with a logged-in rider need not mint a machine credential; REQUIRED on the key-management routes, because a key may never mint, widen or revoke another key.",
        "scheme": "bearer",
        "type": "http"
      }
    }
  },
  "info": {
    "description": "The Zero Logs Reader public API: push telemetry and log files from a third-party device\ninto a rider's own account.\n\nIt exists for two clients. A Zero-specific OBD dongle relaying through a phone, and a\nmobile app that reads the motorcycle over Bluetooth. Both authenticate the same way and\nboth use the same endpoints.\n\n**Authentication.** A key, minted by the rider in their dashboard, presented as\n`Authorization: Bearer zlk_live_…`. There is no query parameter, no cookie and no\n`X-API-Key` header. The key is shown once, at creation; we store a one-way hash and cannot\nrecover it.\n\n**Which motorcycle.** A device is PAIRED to a motorcycle once, by its owner, in the\ndashboard. From then on the device names itself with its own `device_uid` and nothing else.\nA VIN never selects a motorcycle — it is stamped on the frame and printed in every log file,\nso it identifies a vehicle and authorises nothing. Where you send one it is a cross-check,\nand a disagreement is reported rather than resolved.\n\n**Errors** are RFC 9457 `application/problem+json`. Branch on `type`, which is a stable URI\nthat resolves to the section of this documentation explaining it; `detail` is prose and will\nbe reworded. Every problem also carries `retryable`, which tells a device whether repeating\nthe identical request could ever succeed.\n\n**Delivery.** Send batches, retry freely, and let the server deduplicate: a sample is\nidentified by `(device, timestamp)` permanently, so a retransmission after a week in a\ngarage with no signal writes nothing twice. Duplicates are reported as SUCCESS. Use\n`Idempotency-Key` for retry safety at the request level — it replays the original response\nverbatim for 48 hours — but do not rely on it for deduplication, because a device offline\nfor longer retries outside any such window.\n\n**What telemetry is not, yet.** Pushed samples are stored against the device and read back\nthrough this API and the rider's device page. They are NOT yet counted in rides, charges,\ndistance totals, battery health or public share links. That is stated in every response\n(`\"promoted\": false`) so no integrator has to discover it.\n",
    "title": "Zero Logs Reader — Public API",
    "version": "v1"
  },
  "openapi": "3.1.0",
  "paths": {
    "/api/v1/devices": {
      "get": {
        "description": "The devices this caller may write to.\n\nThe first call any firmware should make. It exists so a device can tell a bad\n`device_uid` from a missing scope from a blocked device WITHOUT opening a support\nticket — three failures that otherwise all look like \"my push does not work\".",
        "operationId": "listDevices",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/DeviceOut"
                  },
                  "title": "Response Listdevices",
                  "type": "array"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Two credentials presented, or an unsupported credential header."
          },
          "401": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "No credential, or one that is not accepted."
          },
          "403": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Scope missing, motorcycle out of scope, device blocked, or the demo account."
          },
          "404": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "No such device or motorcycle on this account."
          },
          "429": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Rate limit or daily sample quota."
          }
        },
        "summary": "List the devices and motorcycles this credential may write to",
        "tags": [
          "Public API",
          "Public API"
        ]
      },
      "post": {
        "description": "Pair a device to a motorcycle the caller owns.\n\nThe ONLY place the device-to-bike binding is created, and the reason the ingest path\nnever needs to trust anything a device says about which motorcycle it is on. Re-pairing\nthe same `device_uid` UPDATES the row rather than forking the rider's history — a\nfirmware that re-registers on boot is a firmware people write.",
        "operationId": "pairDevice",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DevicePair"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeviceOut"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Two credentials presented, or an unsupported credential header."
          },
          "401": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "No credential, or one that is not accepted."
          },
          "403": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Scope missing, motorcycle out of scope, device blocked, or the demo account."
          },
          "404": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "No such device or motorcycle on this account."
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            },
            "description": "Validation Error"
          },
          "429": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Rate limit or daily sample quota."
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "summary": "Pair a device to a motorcycle this account owns",
        "tags": [
          "Public API",
          "Public API"
        ]
      }
    },
    "/api/v1/devices/{device_id}": {
      "delete": {
        "description": "Unpair, and delete everything this device ever sent.\n\nDestructive and deliberately total. It is the answer to \"a dongle pushed rubbish for a\nmonth\", and it is possible in two statements only because `device_samples` leads its\nprimary key with `device_id` and carries no foreign keys — so no other rider's data can\nbe swept up and no cascade walks millions of rows.",
        "operationId": "deleteDevice",
        "parameters": [
          {
            "in": "path",
            "name": "device_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Device Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Two credentials presented, or an unsupported credential header."
          },
          "401": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "No credential, or one that is not accepted."
          },
          "403": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Scope missing, motorcycle out of scope, device blocked, or the demo account."
          },
          "404": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "No such device or motorcycle on this account."
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            },
            "description": "Validation Error"
          },
          "429": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Rate limit or daily sample quota."
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "summary": "Unpair a device and delete everything it ever sent",
        "tags": [
          "Public API",
          "Public API"
        ]
      },
      "patch": {
        "description": "Label it, block it, stop keeping its raw frames, or flip its current polarity.",
        "operationId": "updateDevice",
        "parameters": [
          {
            "in": "path",
            "name": "device_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Device Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DevicePatch"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeviceOut"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Two credentials presented, or an unsupported credential header."
          },
          "401": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "No credential, or one that is not accepted."
          },
          "403": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Scope missing, motorcycle out of scope, device blocked, or the demo account."
          },
          "404": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "No such device or motorcycle on this account."
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            },
            "description": "Validation Error"
          },
          "429": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Rate limit or daily sample quota."
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "summary": "Relabel, block, or change a device's current-sign convention",
        "tags": [
          "Public API",
          "Public API"
        ]
      }
    },
    "/api/v1/devices/{device_id}/batches": {
      "get": {
        "description": "What this device sent, when, how much landed, and why anything was refused.\n\nThe answer to \"my push returned 207 and I see nothing\" — without it, a partial failure\nis only visible in the response the device already threw away.",
        "operationId": "listDeviceBatches",
        "parameters": [
          {
            "in": "path",
            "name": "device_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Device Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 50,
              "maximum": 200,
              "minimum": 1,
              "title": "Limit",
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/BatchOut"
                  },
                  "title": "Response Listdevicebatches",
                  "type": "array"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Two credentials presented, or an unsupported credential header."
          },
          "401": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "No credential, or one that is not accepted."
          },
          "403": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Scope missing, motorcycle out of scope, device blocked, or the demo account."
          },
          "404": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "No such device or motorcycle on this account."
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            },
            "description": "Validation Error"
          },
          "429": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Rate limit or daily sample quota."
          }
        },
        "summary": "The audit log: what was sent, what landed, and what was refused",
        "tags": [
          "Public API",
          "Public API"
        ]
      }
    },
    "/api/v1/devices/{device_id}/latest": {
      "get": {
        "description": "The newest reading and how old it is — the \"SoC 63% · 12 minutes ago\" strip, and a\ndevice's own confirmation that its last push actually landed.",
        "operationId": "getDeviceLatest",
        "parameters": [
          {
            "in": "path",
            "name": "device_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Device Id",
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LatestOut"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Two credentials presented, or an unsupported credential header."
          },
          "401": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "No credential, or one that is not accepted."
          },
          "403": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Scope missing, motorcycle out of scope, device blocked, or the demo account."
          },
          "404": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "No such device or motorcycle on this account."
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            },
            "description": "Validation Error"
          },
          "429": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Rate limit or daily sample quota."
          }
        },
        "summary": "The newest reading and how old it is",
        "tags": [
          "Public API",
          "Public API"
        ]
      }
    },
    "/api/v1/devices/{device_id}/samples": {
      "get": {
        "description": "The samples this device sent, newest first, within a window.\n\nOrdered and filtered on `observed_at` — the corrected time — because a device with a\nskewed clock would otherwise be invisible in its own chart.",
        "operationId": "listDeviceSamples",
        "parameters": [
          {
            "in": "path",
            "name": "device_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Device Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "hours",
            "required": false,
            "schema": {
              "default": 24,
              "maximum": 744,
              "minimum": 1,
              "title": "Hours",
              "type": "integer"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 1000,
              "maximum": 5000,
              "minimum": 1,
              "title": "Limit",
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/SampleOut"
                  },
                  "title": "Response Listdevicesamples",
                  "type": "array"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Two credentials presented, or an unsupported credential header."
          },
          "401": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "No credential, or one that is not accepted."
          },
          "403": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Scope missing, motorcycle out of scope, device blocked, or the demo account."
          },
          "404": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "No such device or motorcycle on this account."
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            },
            "description": "Validation Error"
          },
          "429": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Rate limit or daily sample quota."
          }
        },
        "summary": "Read back the samples a device sent, newest first",
        "tags": [
          "Public API",
          "Public API"
        ]
      }
    },
    "/api/v1/devices/{device_id}/samples.csv": {
      "get": {
        "description": "Data portability on day one, and how an integrator verifies what actually landed.\n\nThe header is the channel vocabulary in wire order, so a firmware author can diff the\nCSV against the JSON they sent, field for field, with no mapping table in between.",
        "operationId": "exportDeviceSamplesCsv",
        "parameters": [
          {
            "in": "path",
            "name": "device_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Device Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "hours",
            "required": false,
            "schema": {
              "default": 168,
              "maximum": 2208,
              "minimum": 1,
              "title": "Hours",
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "The samples, oldest first, with the channel vocabulary as the header row."
          },
          "400": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Two credentials presented, or an unsupported credential header."
          },
          "401": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "No credential, or one that is not accepted."
          },
          "403": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Scope missing, motorcycle out of scope, device blocked, or the demo account."
          },
          "404": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "No such device or motorcycle on this account."
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            },
            "description": "Validation Error"
          },
          "429": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Rate limit or daily sample quota."
          }
        },
        "summary": "Export a device's samples as CSV, in the wire channel order",
        "tags": [
          "Public API",
          "Public API"
        ]
      }
    },
    "/api/v1/ingest/limits": {
      "get": {
        "description": "Current allowance, WITHOUT spending any of it.\n\n`spend=False` is the whole point: a device that has to burn a request to find out\nwhether it has requests left cannot back off before a flush, only after.",
        "operationId": "ingestLimits",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LimitsOut"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Two credentials presented, or an unsupported credential header."
          },
          "401": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "No credential, or one that is not accepted."
          },
          "403": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Scope missing, motorcycle out of scope, device blocked, or the demo account."
          },
          "404": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "No such device or motorcycle on this account."
          },
          "429": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Rate limit or daily sample quota."
          }
        },
        "summary": "Read the remaining allowance without spending any",
        "tags": [
          "Public API",
          "Public API"
        ]
      }
    },
    "/api/v1/ingest/logs": {
      "post": {
        "description": "Upload a real log file on behalf of a paired device.\n\nA `.bin` ring-buffer dump or a serial/OBD console capture. The console path is the one\nthat matters most for an OBD product: it is by a wide margin the richest thing a dongle\non the diagnostic connector can obtain, carrying cell voltages, temperatures and the\nbike's own event history that no live feed provides.\n\nThe DISPATCH IS ON CONTENT, never on an extension or a caller-supplied `type`, which is\nhow every other ingest shape in this codebase decides. The file then goes through the\nEXISTING pipeline unchanged — the same size cap, the same per-user content dedup, the\nsame quota, the same identity resolution, the same Celery parse — so this endpoint adds\na caller, not a second pipeline. The report belongs to the KEY'S OWNER, which is exactly\nwhat `POST /logs/upload` already does for a signed-in rider, so no authority is created\nthat did not already exist.",
        "operationId": "ingestLog",
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/Body_ingestLog"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Two credentials presented, or an unsupported credential header."
          },
          "401": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "No credential, or one that is not accepted."
          },
          "403": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Scope missing, motorcycle out of scope, device blocked, or the demo account."
          },
          "404": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "No such device or motorcycle on this account."
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            },
            "description": "Validation Error"
          },
          "429": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Rate limit or daily sample quota."
          }
        },
        "summary": "Upload a .bin or serial console capture for a paired device",
        "tags": [
          "Public API",
          "Public API"
        ]
      }
    },
    "/api/v1/ingest/telemetry": {
      "post": {
        "description": "Push a batch of samples from one paired device.\n\nThe whole body is read here rather than declared as a pydantic model, for two reasons\nthat are not style: the body may be gzipped and has to be decompressed against a running\nbyte budget before anything parses it, and the per-sample outcome has to be an indexed\nRESULT rather than a 422 that names a field path — a firmware author cannot act on\n`body.samples.317.pack_voltage_v`.",
        "operationId": "ingestTelemetry",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "device_uid": {
                    "description": "The device's own id, as paired.",
                    "maxLength": 64,
                    "type": "string"
                  },
                  "samples": {
                    "items": {
                      "additionalProperties": true,
                      "description": "Flat and sparse: one key per channel, omit what you did not read, never send null. Unknown keys are kept in `extra`.",
                      "properties": {
                        "raw": {
                          "description": "The device's own frames, kept verbatim.",
                          "type": "object"
                        },
                        "t": {
                          "description": "ISO 8601 instant with an offset, or epoch seconds. A date alone is not an instant.",
                          "type": "string"
                        }
                      },
                      "required": [
                        "t"
                      ],
                      "type": "object"
                    },
                    "maxItems": 500,
                    "type": "array"
                  },
                  "sent_at": {
                    "description": "The DEVICE's clock at transmit. Carries the same offset as the samples, so the subtraction cancels it — you do not have to be right about the time, only consistent within one request.",
                    "format": "date-time",
                    "type": "string"
                  },
                  "v": {
                    "description": "Payload schema version. Independent of /v1/.",
                    "enum": [
                      1
                    ],
                    "type": "integer"
                  },
                  "vin": {
                    "description": "Optional CROSS-CHECK. Never selects a motorcycle; a mismatch is 409.",
                    "maxLength": 17,
                    "type": "string"
                  }
                },
                "required": [
                  "v",
                  "device_uid",
                  "samples"
                ],
                "type": "object"
              }
            }
          },
          "required": true
        },
        "responses": {
          "207": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "accepted": {
                      "type": "integer"
                    },
                    "batch_id": {
                      "format": "uuid",
                      "type": "string"
                    },
                    "clock_skew_seconds": {
                      "description": "Positive = your clock reads later than ours.",
                      "type": [
                        "integer",
                        "null"
                      ]
                    },
                    "duplicates": {
                      "type": "integer"
                    },
                    "promoted": {
                      "description": "Always false today: telemetry is stored and is not yet counted in rides, charges, totals, battery health or share links.",
                      "type": "boolean"
                    },
                    "received_at": {
                      "format": "date-time",
                      "type": "string"
                    },
                    "rejected": {
                      "type": "integer"
                    },
                    "results": {
                      "items": {
                        "properties": {
                          "index": {
                            "type": "integer"
                          },
                          "problem": {
                            "$ref": "#/components/schemas/Problem"
                          },
                          "retryable": {
                            "type": "boolean"
                          },
                          "status": {
                            "enum": [
                              "accepted",
                              "duplicate",
                              "rejected"
                            ],
                            "type": "string"
                          },
                          "t": {
                            "description": "Your own timestamp, echoed. Truncate your queue by IDENTITY, never by count.",
                            "type": "string"
                          },
                          "warnings": {
                            "items": {
                              "type": "string"
                            },
                            "type": "array"
                          }
                        },
                        "required": [
                          "index",
                          "status",
                          "retryable"
                        ],
                        "type": "object"
                      },
                      "type": "array"
                    }
                  },
                  "required": [
                    "batch_id",
                    "accepted",
                    "duplicates",
                    "rejected",
                    "results",
                    "promoted"
                  ],
                  "type": "object"
                }
              }
            },
            "description": "Every sample judged. ALWAYS 207 once the envelope parses — a duplicate is success, and a client has one code path."
          },
          "400": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Malformed envelope, unknown schema version, or two credentials."
          },
          "401": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "No credential, or one that is not accepted."
          },
          "403": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Scope missing, motorcycle out of scope, device blocked, or demo."
          },
          "404": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "No such paired device."
          },
          "409": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "VIN cross-check failed, or this Idempotency-Key is in flight."
          },
          "413": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Over a published cap."
          },
          "415": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Unsupported Content-Encoding."
          },
          "422": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Idempotency-Key reused with a different payload."
          },
          "429": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Rate limit or daily sample quota."
          }
        },
        "summary": "Push a batch of telemetry samples from one paired device",
        "tags": [
          "Public API",
          "Public API"
        ]
      }
    },
    "/api/v1/ingest/whoami": {
      "get": {
        "description": "Confirm the credential, discover its authority, and read the server clock.\n\nDeliberately requires no scope. A credential that cannot find out what it is cannot be\ndebugged, and a device that cannot read `server_time` before its first push has no way\nto avoid the one unrecoverable clock error.",
        "operationId": "ingestWhoami",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WhoamiOut"
                }
              }
            },
            "description": "Successful Response"
          },
          "400": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Two credentials presented, or an unsupported credential header."
          },
          "401": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "No credential, or one that is not accepted."
          },
          "403": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Scope missing, motorcycle out of scope, device blocked, or the demo account."
          },
          "404": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "No such device or motorcycle on this account."
          },
          "429": {
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            },
            "description": "Rate limit or daily sample quota."
          }
        },
        "summary": "Confirm the credential and read the server clock",
        "tags": [
          "Public API",
          "Public API"
        ]
      }
    },
    "/api/v1/users/me/api-keys": {
      "get": {
        "description": "The rider's own credentials. Revoked ones are listed by default, because \"when did\nthis stop working and why\" is the first question after a leak.",
        "operationId": "listApiKeys",
        "parameters": [
          {
            "in": "query",
            "name": "include_revoked",
            "required": false,
            "schema": {
              "default": true,
              "title": "Include Revoked",
              "type": "boolean"
            }
          },
          {
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 100,
              "maximum": 200,
              "minimum": 1,
              "title": "Limit",
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/ApiKeyOut"
                  },
                  "title": "Response Listapikeys",
                  "type": "array"
                }
              }
            },
            "description": "Successful Response"
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            },
            "description": "Validation Error"
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "summary": "List this rider's machine credentials",
        "tags": [
          "API keys",
          "API keys"
        ]
      },
      "post": {
        "description": "Mint a key. The plaintext is in this response and nowhere else, ever.",
        "operationId": "createApiKey",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ApiKeyCreate"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyCreated"
                }
              }
            },
            "description": "Successful Response"
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            },
            "description": "Validation Error"
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "summary": "Mint a key — the only response that ever carries the secret",
        "tags": [
          "API keys",
          "API keys"
        ]
      }
    },
    "/api/v1/users/me/api-keys/scopes": {
      "get": {
        "description": "The closed scope registry, with its published wording.\n\nServed rather than hard-coded in the UI so the create dialog and the developers page\ncannot describe a scope differently from the server that enforces it.",
        "operationId": "listApiKeyScopes",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/ScopeOut"
                  },
                  "title": "Response Listapikeyscopes",
                  "type": "array"
                }
              }
            },
            "description": "Successful Response"
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "summary": "The closed scope registry, with its published wording",
        "tags": [
          "API keys",
          "API keys"
        ]
      }
    },
    "/api/v1/users/me/api-keys/{key_id}": {
      "delete": {
        "description": "Revoke. SOFT — the row stays, because a deleted row cannot answer \"when did this stop\nworking and why\", which is the first question after a leak.",
        "operationId": "revokeApiKey",
        "parameters": [
          {
            "in": "path",
            "name": "key_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Key Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "reason",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "maxLength": 40,
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "title": "Reason"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {}
              }
            },
            "description": "Successful Response"
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            },
            "description": "Validation Error"
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "summary": "Revoke a key. Soft: the row survives so a leak can be traced",
        "tags": [
          "API keys",
          "API keys"
        ]
      },
      "patch": {
        "description": "Relabel, or narrow. A widening request is REFUSED rather than silently clipped.\n\nOne-way, like a published share link: something is already using this credential, and\nquietly granting it more authority than it had when it was handed out is the failure\nthis rule exists to make impossible. Widening means minting a new key, which is visible.",
        "operationId": "updateApiKey",
        "parameters": [
          {
            "in": "path",
            "name": "key_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Key Id",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ApiKeyPatch"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyOut"
                }
              }
            },
            "description": "Successful Response"
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            },
            "description": "Validation Error"
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "summary": "Relabel a key, or narrow its scopes — never widen them",
        "tags": [
          "API keys",
          "API keys"
        ]
      }
    },
    "/api/v1/users/me/api-keys/{key_id}/rotate": {
      "post": {
        "description": "Mint a successor WITHOUT immediately killing the incumbent.\n\nRotation that revokes on the spot is rotation nobody does: a dongle in a garage cannot\nbe updated at the moment you click the button. The outgoing key gets a scheduled\n`expires_at` instead, `last_used_at` tells the owner when it has actually drained, and\n`rotated_to_id` records what replaced it.",
        "operationId": "rotateApiKey",
        "parameters": [
          {
            "in": "path",
            "name": "key_id",
            "required": true,
            "schema": {
              "format": "uuid",
              "title": "Key Id",
              "type": "string"
            }
          },
          {
            "in": "query",
            "name": "overlap_hours",
            "required": false,
            "schema": {
              "default": 24,
              "maximum": 720,
              "minimum": 0,
              "title": "Overlap Hours",
              "type": "integer"
            }
          }
        ],
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyCreated"
                }
              }
            },
            "description": "Successful Response"
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            },
            "description": "Validation Error"
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "summary": "Mint a successor without killing the incumbent immediately",
        "tags": [
          "API keys",
          "API keys"
        ]
      }
    }
  },
  "security": [
    {
      "ApiKey": []
    },
    {
      "SessionJWT": []
    }
  ],
  "servers": [
    {
      "description": "Production",
      "url": "https://api.zerologs.bike"
    }
  ],
  "x-zerologs": {
    "channels": [
      {
        "max": 400.0,
        "min": 0.0,
        "name": "speed_kmh",
        "type": "number"
      },
      {
        "max": 100.0,
        "min": 0.0,
        "name": "throttle_pct",
        "type": "number"
      },
      {
        "max": 20000.0,
        "min": -20000.0,
        "name": "motor_rpm",
        "type": "number"
      },
      {
        "max": 1000.0,
        "min": -1000.0,
        "name": "torque_nm",
        "type": "number"
      },
      {
        "max": 500.0,
        "min": 0.0,
        "name": "pack_voltage_v",
        "type": "number"
      },
      {
        "max": 1500.0,
        "min": -1500.0,
        "name": "pack_current_a",
        "type": "number"
      },
      {
        "max": 500.0,
        "min": -500.0,
        "name": "power_kw",
        "type": "number"
      },
      {
        "max": 100.0,
        "min": 0.0,
        "name": "soc_pct",
        "type": "number"
      },
      {
        "max": 300.0,
        "min": -60.0,
        "name": "motor_temp_c",
        "type": "number"
      },
      {
        "max": 150.0,
        "min": -60.0,
        "name": "battery_temp_c",
        "type": "number"
      },
      {
        "max": 300.0,
        "min": -60.0,
        "name": "controller_temp_c",
        "type": "number"
      },
      {
        "max": 80.0,
        "min": -80.0,
        "name": "ambient_temp_c",
        "type": "number"
      },
      {
        "max": 2000000.0,
        "min": 0.0,
        "name": "odometer_km",
        "type": "number"
      },
      {
        "max": 500.0,
        "min": 0.0,
        "name": "discharge_limit_kw",
        "type": "number"
      },
      {
        "max": 500.0,
        "min": 0.0,
        "name": "charge_limit_kw",
        "type": "number"
      },
      {
        "max": 6.0,
        "min": 0.0,
        "name": "cell_voltage_min_v",
        "type": "number"
      },
      {
        "max": 6.0,
        "min": 0.0,
        "name": "cell_voltage_max_v",
        "type": "number"
      },
      {
        "max": 6.0,
        "min": 0.0,
        "name": "cell_voltage_delta_v",
        "type": "number"
      },
      {
        "max": 6.0,
        "min": 0.0,
        "name": "cell_voltage_ocv_min_v",
        "type": "number"
      },
      {
        "max": 100000.0,
        "min": 0.0,
        "name": "isolation_kohm",
        "type": "number"
      },
      {
        "max": 20000.0,
        "min": 0.0,
        "name": "charge_cycles",
        "type": "number"
      },
      {
        "max": 2000.0,
        "min": 0.0,
        "name": "range_km",
        "type": "number"
      },
      {
        "max": 5000.0,
        "min": 0.0,
        "name": "wh_per_km",
        "type": "number"
      },
      {
        "max_length": 24,
        "name": "bike_state",
        "type": "string"
      },
      {
        "name": "is_charging",
        "type": "boolean"
      },
      {
        "name": "is_under_load",
        "type": "boolean"
      },
      {
        "max_length": 40,
        "name": "fault_code",
        "type": "string"
      },
      {
        "max": 90.0,
        "min": -90.0,
        "name": "lat",
        "type": "number"
      },
      {
        "max": 180.0,
        "min": -180.0,
        "name": "lon",
        "type": "number"
      },
      {
        "max": 9000.0,
        "min": -500.0,
        "name": "altitude_m",
        "type": "number"
      },
      {
        "max": 360.0,
        "min": 0.0,
        "name": "heading_deg",
        "type": "number"
      },
      {
        "max": 10000.0,
        "min": 0.0,
        "name": "gps_accuracy_m",
        "type": "number"
      }
    ],
    "limits": {
      "idempotency_hours": 48,
      "max_bytes_per_sample": 8192,
      "max_decompressed_bytes": 4194304,
      "max_request_bytes": 2097152,
      "max_samples_per_batch": 500,
      "observed_future_seconds": 300,
      "raw_clock_future_days": 30
    },
    "problems": [
      {
        "explain": "Send your key as `Authorization: Bearer zlk_live_…`. There is no query parameter and no cookie.",
        "retryable": false,
        "slug": "missing-credential",
        "status": 401,
        "title": "No credential presented",
        "type": "https://zerologs.bike/developers#missing-credential"
      },
      {
        "explain": "The key is unknown, malformed, revoked or expired. All four answer identically on purpose, so this response cannot be used to find out which keys exist.",
        "retryable": false,
        "slug": "invalid-credential",
        "status": 401,
        "title": "Credential not accepted",
        "type": "https://zerologs.bike/developers#invalid-credential"
      },
      {
        "explain": "The request carried both `Authorization` and `X-API-Key`. Send exactly one; `X-API-Key` is not supported.",
        "retryable": false,
        "slug": "conflicting-credentials",
        "status": 400,
        "title": "Two credentials presented",
        "type": "https://zerologs.bike/developers#conflicting-credentials"
      },
      {
        "explain": "`X-API-Key` is not supported. The key is a bearer token: send `Authorization: Bearer zlk_live_…`. It is refused loudly rather than ignored, because a client that believes it authenticated and is treated as anonymous gets a 401 it cannot explain. Note that on the two POST routes the header-only preflight answers first and returns `missing-credential` instead — also a refusal, and also accurate: no bearer credential was presented.",
        "retryable": false,
        "slug": "unsupported-credential-header",
        "status": 400,
        "title": "Credential sent in an unsupported header",
        "type": "https://zerologs.bike/developers#unsupported-credential-header"
      },
      {
        "explain": "The credential is valid but was not issued with the scope this operation needs. Scopes are fixed at issue — mint a new key rather than widening this one.",
        "retryable": false,
        "slug": "insufficient-scope",
        "status": 403,
        "title": "Key lacks the required scope",
        "type": "https://zerologs.bike/developers#insufficient-scope"
      },
      {
        "explain": "The key is limited to named motorcycles and this is not one of them.",
        "retryable": false,
        "slug": "bike-not-in-scope",
        "status": 403,
        "title": "Key may not act on this motorcycle",
        "type": "https://zerologs.bike/developers#bike-not-in-scope"
      },
      {
        "explain": "The public demo account cannot write. Create a free account.",
        "retryable": false,
        "slug": "demo-account",
        "status": 403,
        "title": "The demo account is read-only",
        "type": "https://zerologs.bike/developers#demo-account"
      },
      {
        "explain": "`device_uid` does not name a device paired to this account. Pair it once in the dashboard; pairing is the only place a device is bound to a motorcycle, and it needs a signed-in rider.",
        "retryable": false,
        "slug": "unknown-device",
        "status": 404,
        "title": "No such paired device",
        "type": "https://zerologs.bike/developers#unknown-device"
      },
      {
        "explain": "The owner has switched this device off. Nothing it sends is stored.",
        "retryable": false,
        "slug": "device-blocked",
        "status": 403,
        "title": "Device is blocked by its owner",
        "type": "https://zerologs.bike/developers#device-blocked"
      },
      {
        "explain": "The id does not name a device or a motorcycle this account owns. It is 404 and never 403 on purpose: \"this exists and is not yours\" is an existence oracle, so the two answers are deliberately the same one.",
        "retryable": false,
        "slug": "not-found",
        "status": 404,
        "title": "No such device or motorcycle on this account",
        "type": "https://zerologs.bike/developers#not-found"
      },
      {
        "explain": "The envelope volunteered a `vin`, or the uploaded log file contained one, that is not the VIN of the motorcycle this device is paired to. The VIN is a cross-check; it never selects a motorcycle and it can never claim one. Pair a device for that motorcycle and upload it against that device instead.",
        "retryable": false,
        "slug": "vin-mismatch",
        "status": 409,
        "title": "VIN does not match the paired motorcycle",
        "type": "https://zerologs.bike/developers#vin-mismatch"
      },
      {
        "explain": "The body must be a JSON object with `v`, `device_uid` and `samples`. A bare array is not accepted — it could never carry a schema version.",
        "retryable": false,
        "slug": "malformed-envelope",
        "status": 400,
        "title": "Request body is not a valid envelope",
        "type": "https://zerologs.bike/developers#malformed-envelope"
      },
      {
        "explain": "`v` names a payload version this server does not implement.",
        "retryable": false,
        "slug": "unsupported-schema-version",
        "status": 400,
        "title": "Unknown envelope version",
        "type": "https://zerologs.bike/developers#unsupported-schema-version"
      },
      {
        "explain": "Only `gzip` is accepted. Send the body uncompressed or gzipped.",
        "retryable": false,
        "slug": "unsupported-encoding",
        "status": 415,
        "title": "Unsupported Content-Encoding",
        "type": "https://zerologs.bike/developers#unsupported-encoding"
      },
      {
        "explain": "One of the three caps was crossed: samples per batch, decompressed bytes, or bytes in a single sample. `GET /api/v1/ingest/whoami` publishes all three; split the batch and resend.",
        "retryable": false,
        "slug": "batch-too-large",
        "status": 413,
        "title": "Batch exceeds a published limit",
        "type": "https://zerologs.bike/developers#batch-too-large"
      },
      {
        "explain": "The body did not parse, or the gzip stream was truncated.",
        "retryable": false,
        "slug": "malformed-body",
        "status": 400,
        "title": "Body is not valid JSON",
        "type": "https://zerologs.bike/developers#malformed-body"
      },
      {
        "explain": "That key was already used for a different payload. A key identifies ONE request; use a new one for new samples.",
        "retryable": false,
        "slug": "idempotency-key-reused",
        "status": 422,
        "title": "Idempotency-Key reused with a different body",
        "type": "https://zerologs.bike/developers#idempotency-key-reused"
      },
      {
        "explain": "The original is still being processed. Retry shortly — you will get its answer, not a second execution.",
        "retryable": true,
        "slug": "idempotency-in-flight",
        "status": 409,
        "title": "A request with this Idempotency-Key is running",
        "type": "https://zerologs.bike/developers#idempotency-in-flight"
      },
      {
        "explain": "The per-minute burst allowance for this key is spent. Honour `Retry-After`, and back off exponentially with jitter.",
        "retryable": true,
        "slug": "rate-limited",
        "status": 429,
        "title": "Too many requests for this key",
        "type": "https://zerologs.bike/developers#rate-limited"
      },
      {
        "explain": "This key has written its daily sample allowance. It resets at the next UTC midnight; `Retry-After` gives the seconds.",
        "retryable": true,
        "slug": "quota-exceeded",
        "status": 429,
        "title": "Daily sample quota exhausted",
        "type": "https://zerologs.bike/developers#quota-exceeded"
      },
      {
        "explain": "`t` is outside the accepted window. A device with no real-time clock should read `server_time` from `GET /api/v1/ingest/whoami` before its first push — a clock stuck at the epoch cannot be corrected, because the timestamp is also the deduplication key.",
        "retryable": false,
        "slug": "sample-time-out-of-range",
        "status": 400,
        "title": "Sample timestamp is implausible",
        "type": "https://zerologs.bike/developers#sample-time-out-of-range"
      },
      {
        "explain": "A sample must be a JSON object carrying `t`. Unknown channels are kept, not refused — but a sample with no usable timestamp cannot be stored.",
        "retryable": false,
        "slug": "sample-malformed",
        "status": 400,
        "title": "Sample is not a valid object",
        "type": "https://zerologs.bike/developers#sample-malformed"
      }
    ],
    "sample_schema_version": 1,
    "scopes": [
      {
        "description": "Push telemetry samples for a paired device.",
        "name": "telemetry:write"
      },
      {
        "description": "Upload a log file (.bin or serial console capture) for a paired device.",
        "name": "logs:write"
      },
      {
        "description": "Read back the samples a paired device has sent.",
        "name": "telemetry:read"
      },
      {
        "description": "List the devices and motorcycles this key may write to.",
        "name": "bikes:read"
      }
    ]
  }
}
