{
  "openapi": "3.0.3",
  "info": {
    "title": "Data Provider Detector Ingestion API",
    "description": "The *Data Provider Detector Ingestion API* provides an endpoint to submit raw detector measurements\nfor a product instance. Submitted measurements are validated, archived, and forwarded for processing.",
    "version": "1.1.0",
    "contact": {
      "name": "PTV Group"
    }
  },
  "servers": [
    {
      "url": "https://api.ptvgroup.tech/dataprovider/ingestion/v1"
    }
  ],
  "tags": [
    {
      "name": "Ingestion"
    }
  ],
  "paths": {
    "/detectors/data/ingest": {
      "post": {
        "tags": [
          "Ingestion"
        ],
        "operationId": "ingestMeasurements",
        "summary": "Ingest raw detector measurements",
        "description": "Submit a batch of raw detector measurements for a product instance.\n\nEach measurement item is validated independently. The response indicates per-item acceptance\nor rejection. If all items are accepted the response status is 202; if some are rejected\nthe status is 207; if all are rejected the status is 400.",
        "parameters": [
          {
            "name": "X-Request-Id",
            "in": "header",
            "required": false,
            "schema": {
              "format": "uuid",
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/IngestionBatchRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "All measurements accepted for processing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestionResponse202"
                }
              }
            }
          },
          "207": {
            "description": "Some measurements were accepted and some were rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestionResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request is invalid or all submitted measurements were rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IngestionResponse400"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credentials.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse401"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient access rights.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse403"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse429"
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "Service unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse503"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "IngestionBatchRequest": {
        "required": [
          "measurements"
        ],
        "type": "object",
        "properties": {
          "measurements": {
            "maxItems": 50000,
            "minItems": 1,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MeasurementItem"
            }
          }
        },
        "example": {
          "measurements": [
            {
              "detectorCode": "DET-1024",
              "measureTypeCode": "FLOW",
              "timestampFrom": "2025-06-01T08:00:00Z",
              "timestampTo": "2025-06-01T08:05:00Z",
              "value": 42,
              "detectedVehicleTypeCode": "CAR",
              "quality": 0.95
            },
            {
              "detectorCode": "DET-1024",
              "measureTypeCode": "SPEED",
              "timestampFrom": "2025-06-01T08:00:00Z",
              "timestampTo": "2025-06-01T08:05:00Z",
              "value": 53.4,
              "quality": 0.9
            }
          ]
        }
      },
      "MeasurementItem": {
        "required": [
          "detectorCode",
          "measureTypeCode",
          "timestampFrom",
          "timestampTo",
          "value"
        ],
        "type": "object",
        "properties": {
          "detectorCode": {
            "description": "Human-readable detector code. Must contain only alphanumeric characters, hyphens, and underscores.",
            "maxLength": 255,
            "minLength": 1,
            "pattern": "^[a-zA-Z0-9_-]+$",
            "type": "string",
            "example": "DET-1024"
          },
          "measureTypeCode": {
            "description": "Measure type code. Must contain only alphanumeric characters, hyphens, and underscores.\n\nBuilt-in system measure types: `FLOW`, `SPEED`, `TRAVELTIME`, `DENSITY`, `COUNT`, `OCCUPANCY`, `HEADWAY`, `QUEUE_LENGTH`. Tenant-specific measure types may also be configured.",
            "maxLength": 255,
            "minLength": 1,
            "pattern": "^[a-zA-Z0-9_-]+$",
            "type": "string",
            "example": "FLOW"
          },
          "timestampFrom": {
            "format": "date-time",
            "description": "Inclusive start of the measurement time range in RFC 3339 date-time format (UTC).",
            "type": "string",
            "example": "2025-06-01T08:00:00Z"
          },
          "timestampTo": {
            "format": "date-time",
            "description": "Exclusive end of the measurement time range in RFC 3339 date-time format (UTC). It must be later than timestampFrom.",
            "type": "string",
            "example": "2025-06-01T08:05:00Z"
          },
          "value": {
            "format": "double",
            "type": "number",
            "example": 42
          },
          "detectedVehicleTypeCode": {
            "description": "Optional detected vehicle type code. Must contain only alphanumeric characters, hyphens, and underscores when provided.\n\nBuilt-in system detected vehicle types: `PEDESTRIAN`, `TWO_WHEELS`, `E_SCOOTER`, `SMALL_CAR`, `CAR`, `VAN`, `TRUCK`, `BUS`, `OTHER`, `NC`. Tenant-specific vehicle types may also be configured.",
            "pattern": "^[a-zA-Z0-9_-]+$",
            "type": "string",
            "example": "CAR",
            "nullable": true
          },
          "quality": {
            "format": "double",
            "maximum": 1,
            "minimum": 0,
            "type": "number",
            "example": 0.95,
            "nullable": true
          },
          "linkId": {
            "type": "string",
            "nullable": true
          },
          "fromNode": {
            "type": "string",
            "nullable": true
          },
          "turn": {
            "type": "string",
            "nullable": true
          },
          "offset": {
            "format": "double",
            "type": "number",
            "nullable": true
          }
        }
      },
      "IngestionResponse": {
        "required": [
          "accepted",
          "rejected",
          "rejections"
        ],
        "type": "object",
        "properties": {
          "accepted": {
            "format": "int32",
            "description": "Number of measurements accepted for processing.",
            "minimum": 0,
            "type": "integer",
            "example": 2
          },
          "rejected": {
            "format": "int32",
            "description": "Number of measurements rejected. Details in rejections array.",
            "minimum": 0,
            "type": "integer",
            "example": 1
          },
          "rejections": {
            "description": "Per-item rejection details. Items not listed here were accepted.",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/IngestionItemResult"
            }
          }
        },
        "example": {
          "accepted": 2,
          "rejected": 1,
          "rejections": [
            {
              "index": 2,
              "error": {
                "errorCode": "GENERAL_INVALID_VALUE",
                "description": "Measure type and vehicle type are not allowed for the detector.",
                "parameter": "$.measurements[2].measureTypeCode",
                "details": {
                  "value": "SPEED:BUS"
                }
              }
            }
          ]
        }
      },
      "IngestionResponse202": {
        "required": [
          "accepted",
          "rejected",
          "rejections"
        ],
        "type": "object",
        "properties": {
          "accepted": {
            "format": "int32",
            "minimum": 0,
            "type": "integer"
          },
          "rejected": {
            "format": "int32",
            "minimum": 0,
            "type": "integer"
          },
          "rejections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/IngestionItemResult"
            }
          }
        },
        "example": {
          "accepted": 2,
          "rejected": 0,
          "rejections": []
        }
      },
      "IngestionResponse400": {
        "required": [
          "accepted",
          "rejected",
          "rejections"
        ],
        "type": "object",
        "properties": {
          "accepted": {
            "format": "int32",
            "minimum": 0,
            "type": "integer"
          },
          "rejected": {
            "format": "int32",
            "minimum": 0,
            "type": "integer"
          },
          "rejections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/IngestionItemResult"
            }
          }
        },
        "example": {
          "accepted": 0,
          "rejected": 2,
          "rejections": [
            {
              "index": 0,
              "error": {
                "errorCode": "GENERAL_INVALID_VALUE",
                "description": "Detector does not exist for the product instance.",
                "parameter": "$.measurements[0].detectorCode",
                "details": {
                  "value": "DET-9999"
                }
              }
            },
            {
              "index": 1,
              "error": {
                "errorCode": "INVALID_TIME_RANGE",
                "description": "timestampFrom must be earlier than timestampTo.",
                "parameter": "$.measurements[1].timestampFrom",
                "details": {
                  "value": "2025-06-01T08:05:00Z"
                }
              }
            }
          ]
        }
      },
      "IngestionItemResult": {
        "description": "A rejected measurement item with its zero-based index and error details.",
        "required": [
          "index",
          "error"
        ],
        "type": "object",
        "properties": {
          "index": {
            "format": "int32",
            "description": "Zero-based position in the submitted measurements array.",
            "minimum": 0,
            "type": "integer",
            "example": 2
          },
          "error": {
            "$ref": "#/components/schemas/CausingError"
          }
        }
      },
      "ErrorResponse": {
        "required": [
          "description",
          "errorCode",
          "traceId"
        ],
        "type": "object",
        "properties": {
          "description": {
            "description": "A human readable message that describes the error.",
            "type": "string"
          },
          "errorCode": {
            "description": "A constant string that can be used to identify this error class programmatically. An errorCode can have **details** to provide information on additional properties which are described with the code they apply to. They are of type string unless otherwise specified.\n\nNote that additional errorCodes as well as the **details** of existing errorCodes may be added at any time. Furthermore, the **description** may change at any time.\n\n**HTTP status code: 400**\n* `GENERAL_VALIDATION_ERROR` - The validation of the request failed. Details can be found in **causes**.\n* `GENERAL_PARSING_ERROR` - The JSON syntax is invalid.\n\n**HTTP status code: 401**\n* `GENERAL_UNAUTHENTICATED` - Invalid or missing authentication credentials.\n  * `message` - An additional error message.\n\n**HTTP status code: 403**\n* `GENERAL_FORBIDDEN` - Insufficient access rights.\n* `GENERAL_QUOTA_EXCEEDED` - The transaction limit is exceeded.\n  * `message` - An additional error message.\n\n**HTTP status code: 404**\n* `GENERAL_RESOURCE_NOT_FOUND` - A requested resource does not exist.\n  * `message` - An additional error message.\n\n**HTTP status code: 429**\n* `GENERAL_RATE_LIMIT_EXCEEDED` - The rate limit is exceeded.\n\n**HTTP status code: 500**\n* `GENERAL_INTERNAL_SERVER_ERROR` - The request could not be processed due to an internal error.\n  * `message` - An additional error message.\n  * `hint` - A hint on how to solve the problem.\n\n**HTTP status code: 503**\n* `GENERAL_SERVICE_UNAVAILABLE` - The service is temporarily unavailable.",
            "type": "string"
          },
          "traceId": {
            "description": "A unique identifier of the corresponding trace forest. It can be used to trace errors by the support.",
            "type": "string",
            "example": "4d1f6c2e-8b1a-4e21-9c3e-2f6a1b7d9e10"
          },
          "errorId": {
            "description": "A unique identifier specific to this error instance. It can be used to trace errors by the support.",
            "type": "string",
            "example": "a1b2c3d4-5678-40ab-89ef-1234567890ab"
          },
          "causes": {
            "description": "A list of affected parameters and/or properties that caused this error.",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CausingError"
            }
          },
          "details": {
            "description": "Additional properties specific to this error class.",
            "type": "object",
            "additionalProperties": true
          }
        },
        "example": {
          "errorCode": "GENERAL_INTERNAL_SERVER_ERROR",
          "description": "The request could not be processed due to an internal error.",
          "traceId": "9d4a0b3c-5e6f-4c7a-9b8d-0f1a2b3c4d5e",
          "errorId": "d4e5f6a7-8901-42de-9cfa-4567890123de",
          "details": {
            "message": "An unexpected error occurred while archiving the batch.",
            "hint": "Retry the request; if the problem persists, contact support with the traceId."
          }
        }
      },
      "ErrorResponse401": {
        "required": [
          "description",
          "errorCode",
          "traceId"
        ],
        "type": "object",
        "properties": {
          "description": {
            "type": "string"
          },
          "errorCode": {
            "type": "string"
          },
          "traceId": {
            "type": "string"
          },
          "errorId": {
            "type": "string"
          },
          "causes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CausingError"
            }
          },
          "details": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "example": {
          "errorCode": "GENERAL_UNAUTHENTICATED",
          "description": "Authentication credentials are missing or invalid.",
          "traceId": "4d1f6c2e-8b1a-4e21-9c3e-2f6a1b7d9e10",
          "errorId": "a1b2c3d4-5678-40ab-89ef-1234567890ab",
          "details": {
            "message": "The provided bearer token is invalid or expired."
          }
        }
      },
      "ErrorResponse403": {
        "required": [
          "description",
          "errorCode",
          "traceId"
        ],
        "type": "object",
        "properties": {
          "description": {
            "type": "string"
          },
          "errorCode": {
            "type": "string"
          },
          "traceId": {
            "type": "string"
          },
          "errorId": {
            "type": "string"
          },
          "causes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CausingError"
            }
          },
          "details": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "example": {
          "errorCode": "GENERAL_FORBIDDEN",
          "description": "The product instance does not have sufficient access rights to ingest detector measurements.",
          "traceId": "6b2e8f1a-3c4d-4a5e-9f6b-7d8e9f0a1b2c",
          "errorId": "b2c3d4e5-6789-40bc-9adf-2345678901bc"
        }
      },
      "ErrorResponse429": {
        "required": [
          "description",
          "errorCode",
          "traceId"
        ],
        "type": "object",
        "properties": {
          "description": {
            "type": "string"
          },
          "errorCode": {
            "type": "string"
          },
          "traceId": {
            "type": "string"
          },
          "errorId": {
            "type": "string"
          },
          "causes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CausingError"
            }
          },
          "details": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "example": {
          "errorCode": "GENERAL_RATE_LIMIT_EXCEEDED",
          "description": "The rate limit for this product instance has been exceeded.",
          "traceId": "8c3f9a2b-4d5e-4b6f-8a7c-9e0f1a2b3c4d",
          "errorId": "c3d4e5f6-7890-41cd-8bef-3456789012cd"
        }
      },
      "ErrorResponse503": {
        "required": [
          "description",
          "errorCode",
          "traceId"
        ],
        "type": "object",
        "properties": {
          "description": {
            "type": "string"
          },
          "errorCode": {
            "type": "string"
          },
          "traceId": {
            "type": "string"
          },
          "errorId": {
            "type": "string"
          },
          "causes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CausingError"
            }
          },
          "details": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "example": {
          "errorCode": "GENERAL_SERVICE_UNAVAILABLE",
          "description": "The service is temporarily unavailable. Please retry later.",
          "traceId": "0e5b1c4d-6f7a-4d8b-8c9e-1a2b3c4d5e6f",
          "errorId": "e5f6a7b8-9012-43ef-8daf-5678901234ef"
        }
      },
      "CausingError": {
        "required": [
          "description",
          "errorCode"
        ],
        "type": "object",
        "properties": {
          "description": {
            "description": "A human readable message that describes the error.",
            "type": "string",
            "example": "Measure type and vehicle type are not allowed for the detector."
          },
          "errorCode": {
            "description": "A constant string that can be used to identify this error class programmatically. An errorCode can have **details** to provide information on additional properties which are described with the code they apply to. They are of type string unless otherwise specified.\n\nNote that additional errorCodes as well as the **details** of existing errorCodes may be added at any time. Furthermore, the **description** may change at any time.\n\n**Error codes for** `GENERAL_VALIDATION_ERROR`\n* `GENERAL_INVALID_VALUE` - A parameter is set to an invalid value.\n  * `value` - The invalid value.\n* `GENERAL_UNRECOGNIZED_PARAMETER` - A parameter is unknown.\n* `GENERAL_DUPLICATE_PARAMETER` - A parameter is duplicated.\n* `GENERAL_MINIMUM_VALUE_VIOLATED` - The minimum value restriction is violated.\n  * `minimumValue` - The minimum value (integer or double).\n* `GENERAL_MAXIMUM_VALUE_VIOLATED` - The maximum value restriction is violated.\n  * `maximumValue` - The maximum value (integer or double).",
            "type": "string",
            "example": "GENERAL_INVALID_VALUE"
          },
          "parameter": {
            "description": "The name of the affected query or path parameter or a JSONPath to the affected property of the request.",
            "type": "string",
            "example": "$.measurements[2].measureTypeCode"
          },
          "details": {
            "description": "Additional properties specific to this error class.",
            "type": "object",
            "additionalProperties": true,
            "example": {
              "value": "SPEED:BUS"
            }
          }
        }
      }
    },
    "securitySchemes": {
      "apiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "apiKey"
      }
    }
  },
  "security": [
    {
      "apiKeyAuth": []
    }
  ]
}