Documentation
API v1.0

Axelo API

The backend service powering the Axelo transport platform.

REST JSON HTTPS Node.js

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.

Base URL

All API requests should be made to:

https://api.axelo.app

Available Modules

ModuleBase PathEndpointsDescription
Cancellation/reports/cancellation1 + FirestoreStorno system with form types F1–F5, photo upload, PDF generation
Reports/reports4Generate, download, list, and delete delivery PDF reports
Vehicle Classes/vehicle-classes8CRUD, pricing calculation, and recommendations
Goods Types/goods-types6CRUD for shipment categories
Notifications/notifications9Push notifications, broadcasts, maintenance alerts
Calendar/calendar1iCal feed for order schedules
Admin/api/admin1Admin 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 Header
Authorization: Bearer <firebase-id-token>
Note

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 Response Format
{
  "error": "Missing required field: reason",
  "details": "Additional context about the error"
}
CodeMeaning
200Success
201Created
400Bad Request — missing or invalid parameters
401Unauthorized — missing or invalid token
403Forbidden — not an admin
404Not Found
409Conflict — resource already exists
429Too Many Requests — rate limited
500Internal Server Error

Rate Limiting

Rate limits are enforced per IP address using draft-7 standard headers.

ScopeLimitWindow
Global (all endpoints)100 requests1 minute
Auth endpoints5 requests1 minute
Action endpoints10 requests1 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.

Important

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

FormNameConditionPaid
F1Late CancellationLess than 60 minutes before pickupYes
F2Cancellation En RouteDriver is en route to pickup locationYes
F3Cancellation at PickupDriver is at the pickup locationYes
F4Wait Time ReportWait time exceeding 30 minutesNo
F5General IssueComplaint or general issue (client-only edge case)No

Cancellation Actions

Not all cancellations require a form. The system defines four possible actions:

ActionDescriptionForm Required
directCancelFree cancellation — executed immediately after confirmation dialogNo
form(F1–F5)A cancellation form must be filled out with reason, description, and optional photosYes
supportOnlyCancellation only possible through support (goods already in transit)No
notPossibleCancellation is not possible in this state (delivered, already cancelled)No

Cost Implications

FormDriverClient
F1Negative review (waivable with valid reason + proof)Flat fee of €20.00 (waivable with valid reason + proof)
F2Negative review + possible suspension on repeat (waivable)Partial compensation fee (minimum rate for vehicle class)
F3–F5Fees 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 StatusConditionAction
pending / requestedDrivernotPossible
accepted> 60 min until pickup (non-ASAP)directCancel
accepted≤ 60 min until pickup OR ASAP orderform(F1)
drivingToPickupform(F2)
arrivedAtPickupform(F3)
pickedUp / drivingToDeliverysupportOnly
arrivedAtDelivery / deliverednotPossible
cancelled / cancellationRequestednotPossible

Client Decision Matrix

Order StatusConditionAction
pending / requestedDriverdirectCancel
accepted> 60 min until pickup (non-ASAP)directCancel
accepted≤ 60 min until pickup OR ASAP orderform(F1)
drivingToPickupform(F2)
arrivedAtPickupform(F3)
pickedUp / drivingToDeliveryform(F5)
arrivedAtDelivery / deliverednotPossible
cancelled / cancellationRequestednotPossible

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)

IDLabelPhoto Required
vehicle_breakdownVehicle BreakdownYes
accidentAccidentYes
medical_emergencyMedical EmergencyNo
extreme_weatherExtreme WeatherNo
police_checkPolice CheckNo
private_emergencyPrivate EmergencyNo
business_reasonBusiness ReasonNo
otherOther ReasonNo

F2 Driver Reasons

IDLabelPhoto Required
vehicle_breakdownVehicle BreakdownYes
accidentAccidentYes
medical_emergencyMedical EmergencyNo
road_closureRoad ClosureNo
extreme_weatherExtreme WeatherNo
police_checkPolice CheckNo
otherOther ReasonNo

F2 Client Reasons

IDLabelPhoto Required
no_longer_neededOrder No Longer NeededNo
wrong_orderWrong OrderNo
goods_unavailableGoods Not AvailableNo
other_carrierFound Another CarrierNo
otherOther ReasonNo

F3–F5 Generic Reasons

IDLabelPhoto Required
general_issueGeneral IssueNo
otherOther ReasonNo

Validation Rules

RuleF1F2F3–F5
Min. description length20 chars30 chars20 chars
Max photos353
Photo required if reason demands itYesYesNo
confirmedTruthfulnessRequiredRequiredRequired
confirmedConditionsRequiredRequiredRequired

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

Storage Path Pattern
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

Storage 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/.*');
}
POST /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:

FieldTypeRequiredDescription
request.orderIdStringYesFirestore order document ID
request.formTypeStringYes"F1", "F2", "F3", "F4", or "F5"
request.reasonStringYesCancellation reason label (from the reason tables)
request.detailedDescriptionStringYesDetailed description (min 20–30 chars depending on form)
request.confirmedTruthfulnessBooleanYesMust be true
request.confirmedConditionsBooleanYesMust be true
request.orderNumberStringNoHuman-readable order number (e.g. "AXL-2026-001234")
request.requestedByStringNo"driver" or "client"
request.requestedAtISO 8601NoTimestamp of the request
request.orderStatusStringNoOrder status at time of request
request.pickupAddressStringNoPickup address
request.deliveryAddressStringNoDelivery address
request.scheduledPickupTimeISO 8601NoScheduled pickup time
request.minutesUntilPickupNumberNoMinutes until scheduled pickup
request.vehicleClassStringNoVehicle class ID (e.g. "pkwCaddy")
request.priceNumberNoOrder price in EUR
request.distanceToPickupKmNumberNoF2: driver's distance to pickup in km
request.estimatedArrivalMinutesNumberNoF2: estimated arrival time in minutes
request.photoURLsString[]NoFirebase Storage URLs of evidence photos

Response

HeaderDescription
Content-Typeapplication/pdf
X-Download-URLFirebase Storage download URL of the uploaded PDF
X-Storage-PathStorage 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

Firestore Path
cancellationRequests/{requestId}

Document Schema

FieldTypeDescription
idStringUUID — document ID
requesterIdStringFirebase Auth UID of the requesting user
orderIdStringReferenced order document ID
orderNumberStringHuman-readable order number
formTypeString"F1""F5"
requestedByString"driver" or "client"
requestedAtTimestampISO 8601 date of the request
orderStatusStringOrder status at time of cancellation
pickupAddressStringPickup address
deliveryAddressStringDelivery address
scheduledPickupTimeTimestampScheduled pickup time
minutesUntilPickupNumber?Minutes until pickup at time of request
vehicleClassString?Vehicle class ID
priceNumberOrder price in EUR
distanceToPickupKmNumber?F2 only: distance to pickup
estimatedArrivalMinutesNumber?F2 only: ETA
reasonStringSelected cancellation reason label
detailedDescriptionStringUser-written detailed description
photoURLsString[]Firebase Storage URLs of evidence photos
confirmedTruthfulnessBooleanUser confirmed truthfulness
confirmedConditionsBooleanUser accepted cancellation conditions
statusString"pending", "approved", or "rejected"
pdfURLString?Firebase Storage URL of the generated PDF
costAmountNumber?Cancellation cost (set by support)
costDescriptionString?Cost justification (set by support)
reviewedByString?UID of the support agent who reviewed
reviewedAtTimestamp?Review timestamp
reviewNotesString?Internal review notes

Security Rules

Firestore 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.

Order Status Update

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

Flow Overview
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

POST /reports/delivery

Generates a professional delivery report PDF. Supports single-delivery and multi-stop orders. Optionally uploads the PDF to Firebase Storage.

Request Body

FieldTypeRequiredDescription
orderObjectYesThe full transport order object
clientNameStringNoName of the client
driverNameStringNoName of the driver
userIdStringNoIf provided, uploads PDF to reports/{userId}/{orderId}.pdf
pickupImageBase64StringNoBase64-encoded pickup photo
pickupImageURLStringNoStorage URL (fallback if no base64)
deliveryImageBase64StringNoBase64-encoded delivery photo
deliveryImageURLStringNoStorage URL (fallback)
signatureImageBase64StringNoBase64-encoded recipient signature
signatureImageURLStringNoStorage URL (fallback)

Response Headers

HeaderDescription
Content-Typeapplication/pdf
X-Download-URLFirebase Storage download URL (if userId provided)
X-Storage-PathStorage 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();
GET/reports/download/:userId/:orderId

Downloads a previously generated report PDF from Firebase Storage. Returns application/pdf stream or 404.

Path Parameters

ParameterTypeDescription
userIdStringFirebase UID
orderIdStringOrder document ID
cURL
curl https://api.axelo.app/reports/download/USER_ID/ORDER_ID --output report.pdf
GET/reports/list/:userId

Lists all generated reports for a user.

Response
{
  "reports": [
    { "name": "abc123.pdf", "path": "reports/uid/abc123.pdf",
      "created": "2026-02-21T14:30:00.000Z", "size": "125432" }
  ]
}
DELETE/reports/delete/:userId/:orderId

Deletes a report PDF from Firebase Storage.

Response
{ "success": true, "message": "Report deleted" }

Vehicle Classes

CRUD, pricing calculation, and vehicle recommendations

GET/vehicle-classes

Returns all vehicle classes sorted by sortOrder. Use ?includeInactive=true to include inactive classes.

Response
[{
  "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" }
}]
GET/vehicle-classes/:id

Returns a single vehicle class by document ID (e.g. pkw_caddy, klein_lkw, klein_lkw_lbw).

POST/vehicle-classesAdmin

Creates a new vehicle class. Returns 201 on success, 409 if ID exists.

Request Body

FieldTypeRequiredDescription
idStringYesUnique ID (e.g. "lkw_7t")
nameStringYesDisplay name
pricingObjectYesPricing config (see GET response)
iconStringNoSF Symbol (default: "truck.box.fill")
sortOrderNumberNoSort position (default: 99)
maxPayloadNumberNoMax payload in kg
maxVolumeNumberNoMax volume in m³
palletCapacityNumberNoEuro pallet count
hasLiftgateBooleanNoHas liftgate
PUT/vehicle-classes/:idAdmin

Partial update. Only provided fields are changed. Accepts the same fields as the create endpoint.

PATCH/vehicle-classes/:id/toggleAdmin

Toggles active status. No request body needed.

Response
{ "id": "pkw_caddy", "active": false }
DELETE/vehicle-classes/:idAdmin

Permanently deletes a vehicle class.

Response
{ "deleted": true, "id": "pkw_caddy" }
POST/vehicle-classes/calculate-price

Calculates delivery price based on vehicle class, distance, and surcharges.

Request Body

FieldTypeRequiredDescription
vehicleClassIdStringYesVehicle class ID
distanceNumberYesDistance in km
isWithinViennaBooleanNoUse Vienna rate
isNightOrWeekendBooleanNoNight/weekend surcharge
isRushHourBooleanNoRush hour surcharge
Response
{
  "vehicleClass": "PKW/Caddy", "distance": 25, "price": 46.25,
  "breakdown": {
    "basePrice": 25.0, "kmRate": 0.85, "distanceCost": 21.25,
    "surcharges": { "nightWeekend": 0, "rushHour": 0 }
  }
}
POST/vehicle-classes/recommend

Recommends the smallest vehicle class that fits all criteria. At least one of weight (kg), volume (m³), or pallets is required.

Response
{
  "recommended": { "id": "klein_lkw", "name": "Klein-LKW" },
  "details": [
    { "criteria": "weight", "vehicleClass": "klein_lkw", "name": "Klein-LKW" }
  ]
}

Goods Types

CRUD for shipment and goods categories

GET/goods-types

Returns all goods types sorted by sortOrder. Use ?includeInactive=true to include inactive.

Response
[{
  "id": "europalette", "name": "Europalette", "dimensions": "120×80 cm",
  "length": 120, "width": 80, "height": null,
  "icon": "shippingbox.fill", "isCustom": false, "sortOrder": 0, "active": true
}]
GET/goods-types/:id

Returns a single goods type by ID.

POST/goods-typesAdmin

Creates a new goods type. Required: id, name. Returns 201 / 409.

Request Body

FieldTypeRequiredDescription
idStringYesUnique identifier
nameStringYesDisplay name
dimensionsStringNoHuman-readable dimensions
length / width / heightNumberNoDimensions in cm
iconStringNoSF Symbol
isCustomBooleanNoCustom dimension input
sortOrderNumberNoSort position
PUT/goods-types/:idAdmin

Partial update. Only provided fields are changed.

PATCH/goods-types/:id/toggleAdmin

Toggles active status.

DELETE/goods-types/:idAdmin

Permanently deletes a goods type.

Notifications

Push notifications, broadcasts, and maintenance alerts

POST/notifications/sendAdmin

Sends a push notification to a single user via FCM.

Request Body

FieldTypeRequiredDescription
userIdStringYesTarget Firebase UID
titleStringYesNotification title
bodyStringYesBody text
dataObjectNoCustom payload
Response
{ "success": true, "messageId": "projects/.../messages/..." }
POST/notifications/broadcastAdmin

Sends a notification to a list of users. Requires userIds (array), title, body.

Response
{ "success": true, "sent": 15, "total": 20 }
POST/notifications/broadcast-driversAdmin

Broadcasts 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.

Response
{ "success": true, "sent": 8, "totalDrivers": 12, "region": "Wien" }
POST/notifications/maintenanceAdmin

Creates a maintenance alert and pushes it to the target audience.

Request Body

FieldTypeRequiredDescription
titleStringYesAlert title
messageStringYesAlert message
typeStringNo"info" | "warning" | "critical"
targetAudienceStringNo"all" | "drivers" | "clients"
endsAtISO StringNoEnd time
Response
{ "success": true, "id": "firestore-doc-id", "sent": 42 }
GET/notifications/maintenanceAdmin

Lists maintenance alerts. Use ?all=true to include deactivated. Returns up to 50 sorted by creation date.

DELETE/notifications/maintenance/:idAdmin

Deactivates a maintenance alert (sets active: false).

GET/notifications/logsAdmin

Returns recent notification logs from the in-memory buffer. Use ?limit=100 to control count (default: 200).

GET/notifications/statsAdmin

Returns aggregated notification statistics.

GET/notifications/variantsAdmin

Returns all predefined notification template variants used by the system.

Calendar

iCal feed for order schedules

GET/calendar/:userId.ics

Returns 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).

Calendar Subscription

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

GET/api/admin/verifyAdmin

Verifies the provided Firebase Auth token belongs to an admin user. Used by the admin dashboard to validate sessions.

Response
{ "ok": true, "email": "admin@axelo.app" }