Axelo API
The backend service powering the Axelo transport platform.
Overview
The Axelo API provides endpoints for managing transport orders, generating PDF reports, configuring vehicle classes and goods types, sending push notifications, and providing calendar feeds.
All API requests should be made to:
https://api.axelo.app
Available Modules
| Module | Base Path | Endpoints | Description |
|---|---|---|---|
| Cancellation | /reports/cancellation | 1 + Firestore | Storno system with form types F1–F5, photo upload, PDF generation |
| Reports | /reports | 4 | Generate, download, list, and delete delivery PDF reports |
| Vehicle Classes | /vehicle-classes | 8 | CRUD, pricing calculation, and recommendations |
| Goods Types | /goods-types | 6 | CRUD for shipment categories |
| Notifications | /notifications | 9 | Push notifications, broadcasts, maintenance alerts |
| Calendar | /calendar | 1 | iCal feed for order schedules |
| Admin | /api/admin | 1 | Admin token verification |
Authentication
Admin-protected endpoints require a Firebase Auth ID token in the Authorization header. The user must have isAdmin: true set in their Firestore users document.
Authorization: Bearer <firebase-id-token>
Endpoints marked with Admin require this header. Public endpoints have no authentication requirement.
Getting a Token
import FirebaseAuth
let token = try await Auth.auth().currentUser?.getIDToken()
var request = URLRequest(url: url)
request.setValue("Bearer \(token ?? "")", forHTTPHeaderField: "Authorization")
import com.google.firebase.auth.FirebaseAuth
val user = FirebaseAuth.getInstance().currentUser
user?.getIdToken(true)?.addOnSuccessListener { result ->
val token = result.token
val request = Request.Builder()
.url(url)
.header("Authorization", "Bearer $token")
.build()
}
import { getAuth } from "firebase/auth";
const token = await getAuth().currentUser.getIdToken();
const res = await fetch(url, {
headers: { "Authorization": `Bearer ${token}` }
});
Error Handling
The API uses standard HTTP status codes. Error responses include a JSON body:
{
"error": "Missing required field: reason",
"details": "Additional context about the error"
}
| Code | Meaning |
|---|---|
200 | Success |
201 | Created |
400 | Bad Request — missing or invalid parameters |
401 | Unauthorized — missing or invalid token |
403 | Forbidden — not an admin |
404 | Not Found |
409 | Conflict — resource already exists |
429 | Too Many Requests — rate limited |
500 | Internal Server Error |
Rate Limiting
Rate limits are enforced per IP address using draft-7 standard headers.
| Scope | Limit | Window |
|---|---|---|
| Global (all endpoints) | 100 requests | 1 minute |
| Auth endpoints | 5 requests | 1 minute |
| Action endpoints | 10 requests | 1 minute |
Cancellation (Storno)
Complete order cancellation system with form types, photo evidence, and PDF generation
Overview
The Axelo cancellation system (Storno) handles order cancellations based on the current order status and the user's role (driver or client). Depending on timing and circumstances, the system routes the user to one of five form types, each with different requirements, reasons, and cost implications.
The form type is determined automatically based on order status, user role, and time until scheduled pickup. The client application should implement the same routing logic described in the Form Routing section.
Form Types
| Form | Name | Condition | Paid |
|---|---|---|---|
F1 | Late Cancellation | Less than 60 minutes before pickup | Yes |
F2 | Cancellation En Route | Driver is en route to pickup location | Yes |
F3 | Cancellation at Pickup | Driver is at the pickup location | Yes |
F4 | Wait Time Report | Wait time exceeding 30 minutes | No |
F5 | General Issue | Complaint or general issue (client-only edge case) | No |
Cancellation Actions
Not all cancellations require a form. The system defines four possible actions:
| Action | Description | Form Required |
|---|---|---|
directCancel | Free cancellation — executed immediately after confirmation dialog | No |
form(F1–F5) | A cancellation form must be filled out with reason, description, and optional photos | Yes |
supportOnly | Cancellation only possible through support (goods already in transit) | No |
notPossible | Cancellation is not possible in this state (delivered, already cancelled) | No |
Cost Implications
| Form | Driver | Client |
|---|---|---|
F1 | Negative review (waivable with valid reason + proof) | Flat fee of €20.00 (waivable with valid reason + proof) |
F2 | Negative review + possible suspension on repeat (waivable) | Partial compensation fee (minimum rate for vehicle class) |
F3–F5 | Fees may apply depending on circumstances | |
Form Routing
The cancellation form type is determined by the order status, user role, and time until scheduled pickup. The client app must implement this decision matrix to display the correct UI.
Driver Decision Matrix
| Order Status | Condition | Action |
|---|---|---|
pending / requestedDriver | — | notPossible |
accepted | > 60 min until pickup (non-ASAP) | directCancel |
accepted | ≤ 60 min until pickup OR ASAP order | form(F1) |
drivingToPickup | — | form(F2) |
arrivedAtPickup | — | form(F3) |
pickedUp / drivingToDelivery | — | supportOnly |
arrivedAtDelivery / delivered | — | notPossible |
cancelled / cancellationRequested | — | notPossible |
Client Decision Matrix
| Order Status | Condition | Action |
|---|---|---|
pending / requestedDriver | — | directCancel |
accepted | > 60 min until pickup (non-ASAP) | directCancel |
accepted | ≤ 60 min until pickup OR ASAP order | form(F1) |
drivingToPickup | — | form(F2) |
arrivedAtPickup | — | form(F3) |
pickedUp / drivingToDelivery | — | form(F5) |
arrivedAtDelivery / delivered | — | notPossible |
cancelled / cancellationRequested | — | notPossible |
Implementation Example
func determineAction(status: OrderStatus, minutesUntilPickup: Int?, isASAP: Bool, role: UserRole) -> StornoAction {
switch (role, status) {
case (.driver, .pending), (.driver, .requestedDriver):
return .notPossible
case (.client, .pending), (.client, .requestedDriver):
return .directCancel
case (_, .accepted):
if !isASAP, let minutes = minutesUntilPickup, minutes > 60 {
return .directCancel
}
return .form(.f1)
case (_, .drivingToPickup):
return .form(.f2)
case (_, .arrivedAtPickup):
return .form(.f3)
case (.driver, .pickedUp), (.driver, .drivingToDelivery):
return .supportOnly
case (.client, .pickedUp), (.client, .drivingToDelivery):
return .form(.f5)
default:
return .notPossible
}
}
fun determineAction(status: OrderStatus, minutesUntilPickup: Int?, isASAP: Boolean, role: UserRole): StornoAction {
if (status == OrderStatus.CANCELLED || status == OrderStatus.CANCELLATION_REQUESTED) {
return StornoAction.NotPossible
}
return when (role) {
UserRole.DRIVER -> when (status) {
OrderStatus.PENDING, OrderStatus.REQUESTED_DRIVER -> StornoAction.NotPossible
OrderStatus.ACCEPTED -> {
if (!isASAP && minutesUntilPickup != null && minutesUntilPickup > 60)
StornoAction.DirectCancel
else StornoAction.Form(FormType.F1)
}
OrderStatus.DRIVING_TO_PICKUP -> StornoAction.Form(FormType.F2)
OrderStatus.ARRIVED_AT_PICKUP -> StornoAction.Form(FormType.F3)
OrderStatus.PICKED_UP, OrderStatus.DRIVING_TO_DELIVERY -> StornoAction.SupportOnly
else -> StornoAction.NotPossible
}
UserRole.CLIENT -> when (status) {
OrderStatus.PENDING, OrderStatus.REQUESTED_DRIVER -> StornoAction.DirectCancel
OrderStatus.ACCEPTED -> {
if (!isASAP && minutesUntilPickup != null && minutesUntilPickup > 60)
StornoAction.DirectCancel
else StornoAction.Form(FormType.F1)
}
OrderStatus.DRIVING_TO_PICKUP -> StornoAction.Form(FormType.F2)
OrderStatus.ARRIVED_AT_PICKUP -> StornoAction.Form(FormType.F3)
OrderStatus.PICKED_UP, OrderStatus.DRIVING_TO_DELIVERY -> StornoAction.Form(FormType.F5)
else -> StornoAction.NotPossible
}
}
}
function determineAction(status, minutesUntilPickup, isASAP, role) {
if (["cancelled", "cancellationRequested"].includes(status)) return "notPossible";
if (role === "driver") {
switch (status) {
case "pending": case "requestedDriver": return "notPossible";
case "accepted":
return (!isASAP && minutesUntilPickup > 60) ? "directCancel" : "form:F1";
case "drivingToPickup": return "form:F2";
case "arrivedAtPickup": return "form:F3";
case "pickedUp": case "drivingToDelivery": return "supportOnly";
default: return "notPossible";
}
} else {
switch (status) {
case "pending": case "requestedDriver": return "directCancel";
case "accepted":
return (!isASAP && minutesUntilPickup > 60) ? "directCancel" : "form:F1";
case "drivingToPickup": return "form:F2";
case "arrivedAtPickup": return "form:F3";
case "pickedUp": case "drivingToDelivery": return "form:F5";
default: return "notPossible";
}
}
}
Cancellation Reasons
Each form type has a predefined set of selectable reasons. Some reasons require the user to provide photo evidence.
F1 Reasons (Driver & Client)
| ID | Label | Photo Required |
|---|---|---|
vehicle_breakdown | Vehicle Breakdown | Yes |
accident | Accident | Yes |
medical_emergency | Medical Emergency | No |
extreme_weather | Extreme Weather | No |
police_check | Police Check | No |
private_emergency | Private Emergency | No |
business_reason | Business Reason | No |
other | Other Reason | No |
F2 Driver Reasons
| ID | Label | Photo Required |
|---|---|---|
vehicle_breakdown | Vehicle Breakdown | Yes |
accident | Accident | Yes |
medical_emergency | Medical Emergency | No |
road_closure | Road Closure | No |
extreme_weather | Extreme Weather | No |
police_check | Police Check | No |
other | Other Reason | No |
F2 Client Reasons
| ID | Label | Photo Required |
|---|---|---|
no_longer_needed | Order No Longer Needed | No |
wrong_order | Wrong Order | No |
goods_unavailable | Goods Not Available | No |
other_carrier | Found Another Carrier | No |
other | Other Reason | No |
F3–F5 Generic Reasons
| ID | Label | Photo Required |
|---|---|---|
general_issue | General Issue | No |
other | Other Reason | No |
Validation Rules
| Rule | F1 | F2 | F3–F5 |
|---|---|---|---|
| Min. description length | 20 chars | 30 chars | 20 chars |
| Max photos | 3 | 5 | 3 |
| Photo required if reason demands it | Yes | Yes | No |
confirmedTruthfulness | Required | Required | Required |
confirmedConditions | Required | Required | Required |
Photo Upload
Evidence photos are uploaded to Firebase Storage before submitting the cancellation request. The resulting download URLs are then included in the API request body.
Storage Path
cancellation_photos/{orderId}/{index}.jpg
Where {orderId} is the Firestore order document ID and {index} is the 0-based photo index.
Upload Example
import FirebaseStorage
let ref = Storage.storage().reference()
.child("cancellation_photos/\(orderId)/\(index).jpg")
guard let data = image.jpegData(compressionQuality: 0.7) else { return }
let metadata = StorageMetadata()
metadata.contentType = "image/jpeg"
let _ = try await ref.putDataAsync(data, metadata: metadata)
let url = try await ref.downloadURL()
// Store url.absoluteString in photoURLs array
val ref = FirebaseStorage.getInstance().reference
.child("cancellation_photos/$orderId/$index.jpg")
val baos = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 70, baos)
val data = baos.toByteArray()
ref.putBytes(data)
.addOnSuccessListener {
ref.downloadUrl.addOnSuccessListener { uri ->
photoURLs.add(uri.toString())
}
}
import { getStorage, ref, uploadBytes, getDownloadURL } from "firebase/storage";
const storage = getStorage();
const storageRef = ref(storage, `cancellation_photos/${orderId}/${index}.jpg`);
const blob = await fetch(imageUri).then(r => r.blob());
await uploadBytes(storageRef, blob, { contentType: "image/jpeg" });
const url = await getDownloadURL(storageRef);
photoURLs.push(url);
Firebase Storage Security Rules
match /cancellation_photos/{orderId}/{fileName} {
allow read: if request.auth != null;
allow write: if request.auth != null
&& fileName.matches('[0-9]+\\.jpg')
&& request.resource.size < 10 * 1024 * 1024
&& request.resource.contentType.matches('image/.*');
}
/reports/cancellation
Generates a cancellation report PDF (F1–F5). The server downloads evidence photos from their Firebase Storage URLs, embeds them into the PDF, uploads the result to Storage, and returns the PDF bytes.
Request Body
Wrap all fields inside a request object:
| Field | Type | Required | Description |
|---|---|---|---|
request.orderId | String | Yes | Firestore order document ID |
request.formType | String | Yes | "F1", "F2", "F3", "F4", or "F5" |
request.reason | String | Yes | Cancellation reason label (from the reason tables) |
request.detailedDescription | String | Yes | Detailed description (min 20–30 chars depending on form) |
request.confirmedTruthfulness | Boolean | Yes | Must be true |
request.confirmedConditions | Boolean | Yes | Must be true |
request.orderNumber | String | No | Human-readable order number (e.g. "AXL-2026-001234") |
request.requestedBy | String | No | "driver" or "client" |
request.requestedAt | ISO 8601 | No | Timestamp of the request |
request.orderStatus | String | No | Order status at time of request |
request.pickupAddress | String | No | Pickup address |
request.deliveryAddress | String | No | Delivery address |
request.scheduledPickupTime | ISO 8601 | No | Scheduled pickup time |
request.minutesUntilPickup | Number | No | Minutes until scheduled pickup |
request.vehicleClass | String | No | Vehicle class ID (e.g. "pkwCaddy") |
request.price | Number | No | Order price in EUR |
request.distanceToPickupKm | Number | No | F2: driver's distance to pickup in km |
request.estimatedArrivalMinutes | Number | No | F2: estimated arrival time in minutes |
request.photoURLs | String[] | No | Firebase Storage URLs of evidence photos |
Response
| Header | Description |
|---|---|
Content-Type | application/pdf |
X-Download-URL | Firebase Storage download URL of the uploaded PDF |
X-Storage-Path | Storage path: cancellation_reports/{orderId}_{formType}.pdf |
Example
curl -X POST https://api.axelo.app/reports/cancellation \
-H "Content-Type: application/json" \
-d '{
"request": {
"orderId": "abc123",
"formType": "F1",
"reason": "Vehicle Breakdown",
"detailedDescription": "Engine failure on highway A1 near km 42",
"confirmedTruthfulness": true,
"confirmedConditions": true,
"orderNumber": "AXL-2026-001234",
"requestedBy": "driver",
"requestedAt": "2026-02-21T14:30:00.000Z",
"pickupAddress": "Musterstr. 1, 1010 Wien",
"deliveryAddress": "Beispielweg 5, 4020 Linz",
"vehicleClass": "pkwCaddy",
"price": 45.90,
"photoURLs": [
"https://firebasestorage.googleapis.com/..."
]
}
}' --output cancellation.pdf
let requestBody: [String: Any] = [
"request": [
"orderId": order.id,
"formType": formType.rawValue,
"reason": selectedReason.label,
"detailedDescription": description,
"confirmedTruthfulness": true,
"confirmedConditions": true,
"orderNumber": order.deliveryOrderNumber ?? "",
"requestedBy": userRole == .driver ? "driver" : "client",
"requestedAt": ISO8601DateFormatter().string(from: Date()),
"pickupAddress": order.pickupLocation.address,
"deliveryAddress": order.deliveryLocation.address,
"vehicleClass": order.vehicleClass.rawValue,
"price": order.price,
"photoURLs": uploadedPhotoURLs
]
]
var urlRequest = URLRequest(url: URL(string: "https://api.axelo.app/reports/cancellation")!)
urlRequest.httpMethod = "POST"
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
urlRequest.timeoutInterval = 60
urlRequest.httpBody = try JSONSerialization.data(withJSONObject: requestBody)
let (data, response) = try await URLSession.shared.data(for: urlRequest)
let storageURL = (response as? HTTPURLResponse)?.value(forHTTPHeaderField: "X-Download-URL")
val requestJson = JSONObject().apply {
put("request", JSONObject().apply {
put("orderId", order.id)
put("formType", "F1")
put("reason", "Vehicle Breakdown")
put("detailedDescription", "Engine failure on highway A1")
put("confirmedTruthfulness", true)
put("confirmedConditions", true)
put("orderNumber", order.orderNumber)
put("requestedBy", "driver")
put("pickupAddress", order.pickupAddress)
put("deliveryAddress", order.deliveryAddress)
put("vehicleClass", order.vehicleClass)
put("price", order.price)
put("photoURLs", JSONArray(uploadedPhotoURLs))
})
}
val body = requestJson.toString().toRequestBody("application/json".toMediaType())
val request = Request.Builder()
.url("https://api.axelo.app/reports/cancellation")
.post(body)
.build()
val client = OkHttpClient.Builder()
.callTimeout(60, TimeUnit.SECONDS)
.build()
val response = client.newCall(request).execute()
val pdfBytes = response.body?.bytes()
val storageURL = response.header("X-Download-URL")
const res = await fetch("https://api.axelo.app/reports/cancellation", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
request: {
orderId: "abc123",
formType: "F1",
reason: "Vehicle Breakdown",
detailedDescription: "Engine failure on highway A1 near km 42",
confirmedTruthfulness: true,
confirmedConditions: true,
orderNumber: "AXL-2026-001234",
requestedBy: "driver",
pickupAddress: "Musterstr. 1, 1010 Wien",
deliveryAddress: "Beispielweg 5, 4020 Linz",
vehicleClass: "pkwCaddy",
price: 45.90,
photoURLs: ["https://firebasestorage.googleapis.com/..."]
}
})
});
const pdfBlob = await res.blob();
const storageURL = res.headers.get("X-Download-URL");
Firestore Data Model
After the PDF is generated, the complete cancellation request is saved to the cancellationRequests Firestore collection. This document serves as the permanent record for support review.
Collection Path
cancellationRequests/{requestId}
Document Schema
| Field | Type | Description |
|---|---|---|
id | String | UUID — document ID |
requesterId | String | Firebase Auth UID of the requesting user |
orderId | String | Referenced order document ID |
orderNumber | String | Human-readable order number |
formType | String | "F1"–"F5" |
requestedBy | String | "driver" or "client" |
requestedAt | Timestamp | ISO 8601 date of the request |
orderStatus | String | Order status at time of cancellation |
pickupAddress | String | Pickup address |
deliveryAddress | String | Delivery address |
scheduledPickupTime | Timestamp | Scheduled pickup time |
minutesUntilPickup | Number? | Minutes until pickup at time of request |
vehicleClass | String? | Vehicle class ID |
price | Number | Order price in EUR |
distanceToPickupKm | Number? | F2 only: distance to pickup |
estimatedArrivalMinutes | Number? | F2 only: ETA |
reason | String | Selected cancellation reason label |
detailedDescription | String | User-written detailed description |
photoURLs | String[] | Firebase Storage URLs of evidence photos |
confirmedTruthfulness | Boolean | User confirmed truthfulness |
confirmedConditions | Boolean | User accepted cancellation conditions |
status | String | "pending", "approved", or "rejected" |
pdfURL | String? | Firebase Storage URL of the generated PDF |
costAmount | Number? | Cancellation cost (set by support) |
costDescription | String? | Cost justification (set by support) |
reviewedBy | String? | UID of the support agent who reviewed |
reviewedAt | Timestamp? | Review timestamp |
reviewNotes | String? | Internal review notes |
Security Rules
match /cancellationRequests/{requestId} {
allow read: if request.auth != null
&& resource.data.requesterId == request.auth.uid;
allow create: if request.auth != null
&& request.resource.data.requesterId == request.auth.uid
&& request.resource.data.keys().hasAll([
'orderId', 'formType', 'reason', 'detailedDescription',
'confirmedTruthfulness', 'confirmedConditions'
]);
allow update: if false; // Only via Admin SDK
allow delete: if false;
}
Integration Flow
The complete cancellation flow consists of four sequential steps. Implement them in order for a full integration.
Step 1: Determine Action
Use the Form Routing logic to determine which cancellation action applies. If the result is directCancel, skip to updating the order status. If it's form(F1–F5), proceed with the wizard flow.
Step 2: Upload Evidence Photos
If the user selected photos, upload them to Firebase Storage at cancellation_photos/{orderId}/{index}.jpg. Collect the resulting download URLs. See Photo Upload.
Step 3: Generate Cancellation PDF
Call POST /reports/cancellation with the full request object including photoURLs. The server downloads the photos, embeds them into the PDF, uploads it to cancellation_reports/{orderId}_{formType}.pdf, and returns the PDF bytes. Save the X-Download-URL header value as the permanent PDF URL.
Step 4: Save to Firestore & Update Order
Save the complete StornoRequest (including pdfURL from the previous step) to the cancellationRequests collection. Then update the order status to cancellationRequested.
The order status should be updated after showing a success screen to the user. If updated immediately, the parent view may re-render and dismiss the cancellation UI before the user sees the confirmation.
Sequence Diagram
User Client App Firebase Storage API Server Firestore
| | | | |
| tap cancel | | | |
|───────────────>| determineAction() | | |
| |────────┐ | | |
| |<───────┘ form(F2) | | |
| | | | |
| fill form | | | |
| select photos | | | |
| confirm | | | |
|───────────────>| | | |
| | upload photos | | |
| |───────────────────>| save to Storage | |
| |<───────────────────| return URLs | |
| | | | |
| | POST /reports/cancellation | |
| |───────────────────────────────────────>| |
| | | fetch photos | |
| | |<──────────────────| |
| | |──────────────────>| |
| | | | generate PDF |
| | | upload PDF | |
| | |<──────────────────| |
| |<─────────────────────────────── PDF bytes + X-Download-URL|
| | | | |
| | save StornoRequest | | |
| |──────────────────────────────────────────────────────────>|
| | | | |
| show success | | | |
|<───────────────| | | |
| | | | |
| tap "Done" | | | |
|───────────────>| update order status: cancellationRequested |
| |──────────────────────────────────────────────────────────>|
Reports
Generate, download, list, and delete delivery PDF reports
/reports/delivery
Generates a professional delivery report PDF. Supports single-delivery and multi-stop orders. Optionally uploads the PDF to Firebase Storage.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
order | Object | Yes | The full transport order object |
clientName | String | No | Name of the client |
driverName | String | No | Name of the driver |
userId | String | No | If provided, uploads PDF to reports/{userId}/{orderId}.pdf |
pickupImageBase64 | String | No | Base64-encoded pickup photo |
pickupImageURL | String | No | Storage URL (fallback if no base64) |
deliveryImageBase64 | String | No | Base64-encoded delivery photo |
deliveryImageURL | String | No | Storage URL (fallback) |
signatureImageBase64 | String | No | Base64-encoded recipient signature |
signatureImageURL | String | No | Storage URL (fallback) |
Response Headers
| Header | Description |
|---|---|
Content-Type | application/pdf |
X-Download-URL | Firebase Storage download URL (if userId provided) |
X-Storage-Path | Storage path of the uploaded PDF |
Example
curl -X POST https://api.axelo.app/reports/delivery \
-H "Content-Type: application/json" \
-d '{
"order": {
"id": "abc123",
"deliveryOrderNumber": "AXL-2026-001",
"status": "delivered",
"pickupLocation": { "address": "Musterstr. 1, 1010 Wien" },
"deliveryLocation": { "address": "Beispielweg 5, 4020 Linz" },
"price": 45.90
},
"clientName": "Max Mustermann",
"driverName": "Hans Fahrer",
"userId": "firebase-uid"
}' --output report.pdf
var request = URLRequest(url: URL(string: "https://api.axelo.app/reports/delivery")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: Any] = [
"order": orderDict,
"clientName": "Max Mustermann",
"driverName": "Hans Fahrer",
"userId": Auth.auth().currentUser?.uid ?? ""
]
request.httpBody = try JSONSerialization.data(withJSONObject: body)
let (data, response) = try await URLSession.shared.data(for: request)
// data contains the PDF bytes
val client = OkHttpClient()
val json = JSONObject().apply {
put("order", orderJson)
put("clientName", "Max Mustermann")
put("driverName", "Hans Fahrer")
put("userId", FirebaseAuth.getInstance().currentUser?.uid)
}
val body = json.toString().toRequestBody("application/json".toMediaType())
val request = Request.Builder()
.url("https://api.axelo.app/reports/delivery")
.post(body)
.build()
val response = client.newCall(request).execute()
val pdfBytes = response.body?.bytes()
const res = await fetch("https://api.axelo.app/reports/delivery", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
order: { id: "abc123", deliveryOrderNumber: "AXL-2026-001",
status: "delivered", price: 45.90,
pickupLocation: { address: "Musterstr. 1, 1010 Wien" },
deliveryLocation: { address: "Beispielweg 5, 4020 Linz" } },
clientName: "Max Mustermann",
driverName: "Hans Fahrer",
userId: "firebase-uid"
})
});
const pdfBlob = await res.blob();
/reports/download/:userId/:orderIdDownloads a previously generated report PDF from Firebase Storage. Returns application/pdf stream or 404.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
userId | String | Firebase UID |
orderId | String | Order document ID |
curl https://api.axelo.app/reports/download/USER_ID/ORDER_ID --output report.pdf/reports/list/:userIdLists all generated reports for a user.
{
"reports": [
{ "name": "abc123.pdf", "path": "reports/uid/abc123.pdf",
"created": "2026-02-21T14:30:00.000Z", "size": "125432" }
]
}/reports/delete/:userId/:orderIdDeletes a report PDF from Firebase Storage.
{ "success": true, "message": "Report deleted" }Vehicle Classes
CRUD, pricing calculation, and vehicle recommendations
/vehicle-classesReturns all vehicle classes sorted by sortOrder. Use ?includeInactive=true to include inactive classes.
[{
"id": "pkw_caddy",
"name": "PKW/Caddy",
"icon": "car.fill",
"sortOrder": 0,
"active": true,
"maxPayload": 500,
"maxVolume": 1,
"interiorLength": 100,
"interiorWidth": 100,
"interiorHeight": 70,
"palletCapacity": 0,
"hasLiftgate": false,
"pricing": {
"basePrice": 25.0, "hourlyRate": 30.0,
"pricePerKmOutside": 0.85, "pricePerKmVienna": 0.72,
"nightWeekendSurcharge": 0.50, "rushHourSurcharge": 0.10
},
"formatted": { "payload": "500 kg", "volume": "Bis 1 m³", "dimensions": "~100×100×70 cm" }
}]/vehicle-classes/:idReturns a single vehicle class by document ID (e.g. pkw_caddy, klein_lkw, klein_lkw_lbw).
/vehicle-classesAdminCreates a new vehicle class. Returns 201 on success, 409 if ID exists.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
id | String | Yes | Unique ID (e.g. "lkw_7t") |
name | String | Yes | Display name |
pricing | Object | Yes | Pricing config (see GET response) |
icon | String | No | SF Symbol (default: "truck.box.fill") |
sortOrder | Number | No | Sort position (default: 99) |
maxPayload | Number | No | Max payload in kg |
maxVolume | Number | No | Max volume in m³ |
palletCapacity | Number | No | Euro pallet count |
hasLiftgate | Boolean | No | Has liftgate |
/vehicle-classes/:idAdminPartial update. Only provided fields are changed. Accepts the same fields as the create endpoint.
/vehicle-classes/:id/toggleAdminToggles active status. No request body needed.
{ "id": "pkw_caddy", "active": false }/vehicle-classes/:idAdminPermanently deletes a vehicle class.
{ "deleted": true, "id": "pkw_caddy" }/vehicle-classes/calculate-priceCalculates delivery price based on vehicle class, distance, and surcharges.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
vehicleClassId | String | Yes | Vehicle class ID |
distance | Number | Yes | Distance in km |
isWithinVienna | Boolean | No | Use Vienna rate |
isNightOrWeekend | Boolean | No | Night/weekend surcharge |
isRushHour | Boolean | No | Rush hour surcharge |
{
"vehicleClass": "PKW/Caddy", "distance": 25, "price": 46.25,
"breakdown": {
"basePrice": 25.0, "kmRate": 0.85, "distanceCost": 21.25,
"surcharges": { "nightWeekend": 0, "rushHour": 0 }
}
}/vehicle-classes/recommendRecommends the smallest vehicle class that fits all criteria. At least one of weight (kg), volume (m³), or pallets is required.
{
"recommended": { "id": "klein_lkw", "name": "Klein-LKW" },
"details": [
{ "criteria": "weight", "vehicleClass": "klein_lkw", "name": "Klein-LKW" }
]
}Goods Types
CRUD for shipment and goods categories
/goods-typesReturns all goods types sorted by sortOrder. Use ?includeInactive=true to include inactive.
[{
"id": "europalette", "name": "Europalette", "dimensions": "120×80 cm",
"length": 120, "width": 80, "height": null,
"icon": "shippingbox.fill", "isCustom": false, "sortOrder": 0, "active": true
}]/goods-types/:idReturns a single goods type by ID.
/goods-typesAdminCreates a new goods type. Required: id, name. Returns 201 / 409.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
id | String | Yes | Unique identifier |
name | String | Yes | Display name |
dimensions | String | No | Human-readable dimensions |
length / width / height | Number | No | Dimensions in cm |
icon | String | No | SF Symbol |
isCustom | Boolean | No | Custom dimension input |
sortOrder | Number | No | Sort position |
/goods-types/:idAdminPartial update. Only provided fields are changed.
/goods-types/:id/toggleAdminToggles active status.
/goods-types/:idAdminPermanently deletes a goods type.
Notifications
Push notifications, broadcasts, and maintenance alerts
/notifications/sendAdminSends a push notification to a single user via FCM.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
userId | String | Yes | Target Firebase UID |
title | String | Yes | Notification title |
body | String | Yes | Body text |
data | Object | No | Custom payload |
{ "success": true, "messageId": "projects/.../messages/..." }/notifications/broadcastAdminSends a notification to a list of users. Requires userIds (array), title, body.
{ "success": true, "sent": 15, "total": 20 }/notifications/broadcast-driversAdminBroadcasts to all available drivers. Filters by region (matches city, postal code, or delivery areas). If orderId is provided, triggers the automatic driver notification flow instead.
{ "success": true, "sent": 8, "totalDrivers": 12, "region": "Wien" }/notifications/maintenanceAdminCreates a maintenance alert and pushes it to the target audience.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
title | String | Yes | Alert title |
message | String | Yes | Alert message |
type | String | No | "info" | "warning" | "critical" |
targetAudience | String | No | "all" | "drivers" | "clients" |
endsAt | ISO String | No | End time |
{ "success": true, "id": "firestore-doc-id", "sent": 42 }/notifications/maintenanceAdminLists maintenance alerts. Use ?all=true to include deactivated. Returns up to 50 sorted by creation date.
/notifications/maintenance/:idAdminDeactivates a maintenance alert (sets active: false).
/notifications/logsAdminReturns recent notification logs from the in-memory buffer. Use ?limit=100 to control count (default: 200).
/notifications/statsAdminReturns aggregated notification statistics.
/notifications/variantsAdminReturns all predefined notification template variants used by the system.
Calendar
iCal feed for order schedules
/calendar/:userId.icsReturns an iCal (.ics) feed of a user's orders for Apple Calendar, Google Calendar, or Outlook. Use ?filter=all for all orders (default: active only).
Subscribe via URL in your calendar app. The feed auto-refreshes every 15 minutes.
https://api.axelo.app/calendar/FIREBASE_UID.ics
Admin
Admin token verification
/api/admin/verifyAdminVerifies the provided Firebase Auth token belongs to an admin user. Used by the admin dashboard to validate sessions.
{ "ok": true, "email": "admin@axelo.app" }