Provider API Usage Quickstart
This guide is for provider teams who need to call the API with the minimum required setup.
It covers batch submission and async job tracking.
:::tip Get your token first Before sending any request, obtain a bearer token:
- Token Acquisition Guide — generic setup for any environment
- INT Test Credentials — copy-paste ready for the INT environment
Use it in every request: Authorization: Bearer <access_token>
:::
1. Endpoints
| Purpose | Method | Path |
|---|---|---|
| Submit batch | POST | /api/provider/v1/controls |
| Check job status | GET | /api/provider/v1/jobs/{job_id} |
| Check per-item results | GET | /api/provider/v1/jobs/{job_id}/items |
Base URLs
| Environment | URL |
|---|---|
| Local | http://localhost:3002 |
| INT | https://kontrolleplus-dev-data.orangeglacier-1536cc06.switzerlandnorth.azurecontainerapps.io |
2. Request Model
POST /api/provider/v1/controls expects:
{
"schema_version": "1.0",
"device_id": "AndId_c428366d643e9ce2",
"controls": [ /* array, 1–500 items */ ]
}
Required fields at request root
| Field | Description | Format |
|---|---|---|
schema_version | Contract version | Must be exactly "1.0" |
device_id | Unique identifier of the provider device/app instance that sent the batch | String, max 100 chars |
controls | Batch of controls to ingest | Non-empty array, max 500 |
Required fields per controls[] item
| Field | Description | Format |
|---|---|---|
started_at | Control run start | ISO 8601 with timezone |
partial_close | Interim close timestamp | ISO 8601 with timezone; must be after started_at |
ended_at | End-of-inspection timestamp | Optional ISO 8601; if set, must be ≥ partial_close |
control_grade_pct | Inspection coverage level | Integer: 25, 50, 75, or 100 |
executing_tu_code | Organization executing the control | Non-empty numeric string (e.g. 801) |
inspector | Inspector identifier | Non-empty string |
journey | Journey context block | Object — see below |
paper_tickets | Aggregated paper-ticket counts | Non-empty array — see below |
Required journey fields
| Field | Description | Format |
|---|---|---|
journey.carrier_tu_code | Carrier/company responsible for transport execution | Non-empty numeric string (e.g. 801) |
journey.from_stop_name | Human-readable boarding stop name | Non-empty string |
journey.to_stop_name | Human-readable destination stop name | Non-empty string |
journey.line.slnid | Swiss Line ID | ch:1:slnid:XXXXXX |
journey.line.start | Direction start of the line | Non-empty string |
journey.line.end | Direction end of the line | Non-empty string |
journey.from_stop_sloid | Boarding stop identifier | ch:1:sloid:XXXXXX or legacy numeric ID |
journey.to_stop_sloid | Destination stop identifier | ch:1:sloid:XXXXXX or legacy numeric ID |
journey.planned_departure | Planned departure for timetable matching | ISO 8601 with timezone |
Required paper_tickets[] fields
| Field | Description | Format |
|---|---|---|
category | Ticket category | EINZELBILLETT, VERBUNDABO, INCOMING, or SONSTIGE |
count | Count for this category at this timestamp | Integer 0–999 |
timestamp | When the count was recorded | ISO 8601 with timezone |
3. Submit Example
curl -X POST "https://kontrolleplus-dev-data.orangeglacier-1536cc06.switzerlandnorth.azurecontainerapps.io/api/provider/v1/controls" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-d '{
"schema_version": "1.0",
"device_id": "AndId_c428366d643e9ce2",
"controls": [
{
"started_at": "2026-03-09T08:00:00Z",
"partial_close": "2026-03-09T08:10:00Z",
"ended_at": "2026-03-09T08:20:00Z",
"control_grade_pct": 75,
"executing_tu_code": "801",
"inspector": "UE67277",
"journey": {
"carrier_tu_code": "801",
"sjyid": "ch:1:sjyid:100058:12952-002",
"line": {
"slnid": "ch:1:slnid:103456",
"start": "Bernau (Schw), Poche",
"end": "Neuenburg (Baden), Bahnhof"
},
"from_stop_sloid": "ch:1:sloid:8578391",
"to_stop_sloid": "ch:1:sloid:8578391",
"from_stop_name": "Bernau (Schw), Poche",
"to_stop_name": "Neuenburg (Baden), Bahnhof",
"planned_departure": "2026-03-09T08:06:00Z"
},
"paper_tickets": [
{
"category": "EINZELBILLETT",
"count": 2,
"timestamp": "2026-03-09T08:09:00Z"
}
]
}
]
}'
Submit Response (202 Accepted)
{
"job_id": "job-20260309-082001-8f9ab1",
"status": "QUEUED",
"message": "Batch accepted for asynchronous processing",
"accepted_count": 1,
"created_at": "2026-03-09T08:20:01Z",
"status_url": "/api/provider/v1/jobs/job-20260309-082001-8f9ab1"
}
4. Check Job Status
curl -X GET "https://kontrolleplus-dev-data.orangeglacier-1536cc06.switzerlandnorth.azurecontainerapps.io/api/provider/v1/jobs/$JOB_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN"
{
"job_id": "job-20260309-082001-8f9ab1",
"status": "RUNNING",
"total_items": 100,
"processed_items": 45,
"succeeded_items": 38,
"duplicate_items": 2,
"failed_items": 5,
"created_at": "2026-03-09T08:20:01Z",
"started_at": "2026-03-09T08:20:03Z",
"completed_at": null
}
Possible job statuses: QUEUED · RUNNING · COMPLETED · COMPLETED_WITH_ERRORS · FAILED · EXPIRED
5. Check Per-Item Results
# All items
curl -X GET ".../api/provider/v1/jobs/$JOB_ID/items" \
-H "Authorization: Bearer $ACCESS_TOKEN"
# Only failed items
curl -X GET ".../api/provider/v1/jobs/$JOB_ID/items?status=VALIDATION_FAILED" \
-H "Authorization: Bearer $ACCESS_TOKEN"
{
"job_id": "job-20260309-082001-8f9ab1",
"items": [
{
"item_id": "item-001",
"control_id": "insp-ue67277-20260309T080000Z",
"status": "PROCESSED",
"retryable": false,
"message": "Control processed successfully"
},
{
"item_id": "item-002",
"control_id": "insp-ue67277-20260309T081000Z",
"status": "VALIDATION_FAILED",
"retryable": false,
"message": "Validation failed for control_grade_pct",
"error_code": "INVALID_VALUE"
}
]
}
Possible item statuses: PROCESSED · DUPLICATE · VALIDATION_FAILED · TRANSIENT_ERROR · PERMANENT_ERROR
6. HTTP Status Reference
POST /api/provider/v1/controls
| Code | Meaning |
|---|---|
202 Accepted | Batch accepted and queued |
400 Bad Request | Malformed payload |
401 Unauthorized | Token missing, invalid, or expired |
403 Forbidden | Role or allow-list restriction |
422 Unprocessable Entity | Semantic validation failed |
429 Too Many Requests | Rate limit exceeded |
500 Internal Server Error | Unexpected error |
GET /jobs/{job_id} and GET /jobs/{job_id}/items
| Code | Meaning |
|---|---|
200 OK | Success |
401 Unauthorized | Token issue |
403 Forbidden | Role restriction |
404 Not Found | Unknown job_id |