Fleetiva API
With the Fleetiva API your system creates transport inquiries directly with a transport service that works with Fleetiva, for example from a booking form on a website or from the software of an institution. The transport service reviews every inquiry in its inbox. Nothing is booked automatically.
This page is for developers connecting a system. All calls with fields, data formats and a test console are in the reference; the field descriptions there are in English, as in the OpenAPI file.
Contents
Introduction
What the API does
The Fleetiva API connects your system with transport services that work with Fleetiva: transport for people with special needs, school transport, medical and senior transport. A connected system creates inquiries there, reads their processing status and reads master data such as customers.
Today the API can create an inquiry (POST /inquiries), read its status (GET /inquiries/{id}) and read and sync customers (GET /customers). More areas such as passengers, trips and tours will be added without changing the existing calls (see Versions).
Terms
- Transport service: the company that runs the trips and uses Fleetiva. The inquiries belong to it.
- Access: the credentials of a connected system, created by the transport service, with a key and scopes.
- Inquiry: a request for one or more trips, with the requesting person, passengers and consent.
- Customer: an institution, authority, cost bearer or private person the transport service works for.
- Passenger: a person to be transported. Each passenger becomes one item of the inquiry.
- Inbox: the list in Fleetiva where the transport service sees and processes new inquiries.
- Acceptance: the transport service takes an inquiry over into its planning.
Quickstart
1. Get access
The transport service creates the key in Fleetiva under Settings → Integrations → API access, with the scope “Create inquiries”. It starts with flt_ and is shown only once. Have it handed over securely, not by email or chat, and store it on your server only, never in a web page or app.
2. Base URL
All calls go to this address, always with https://. Use it exactly as shown: a client that follows a redirect to another host drops the Authorization header, and the call fails with 401.
https://www.fleetiva.de/api/v1
3. Send an inquiry
Send the inquiry as JSON, with your key and an Idempotency-Key that you choose for this inquiry, for example your booking number. To try it out, also send the header Fleetiva-Dry-Run: true: Fleetiva then checks everything and creates nothing (see Dry run).
curl -X POST 'https://www.fleetiva.de/api/v1/inquiries' \
-H "Authorization: Bearer $FLEETIVA_API_KEY" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: booking-2026-11-0042' \
--data-binary @- <<'JSON'
{
"requester": {
"role": "institution",
"first_name": "Petra",
"last_name": "Schulz",
"email": "p.schulz@example.org",
"phone": "+49 30 1234567",
"organization": "Tagesförderstätte Sonnenhof",
"postal_address": {
"street": "Lindenallee",
"house_number": "12",
"zip": "10115",
"city": "Berlin"
}
},
"passengers": [
{
"first_name": "Jonas",
"last_name": "Weber",
"birth_year": 2012,
"pickup": {
"street": "Gartenweg",
"house_number": "4",
"zip": "10117",
"city": "Berlin"
},
"dropoff": {
"street": "Lindenallee",
"house_number": "12",
"zip": "10115",
"city": "Berlin"
},
"trip_kind": "recurring",
"requested_date": "2026-12-01",
"requested_time_window": "arrival by 8:00",
"return_trip": true,
"return_time_window": "15:30",
"needs": {
"wheelchair": true
}
}
],
"message": "Monday to Friday, school days only.",
"consent": {
"given": true,
"text_version": "2026-11"
}
}
JSON
The same from JavaScript or TypeScript on the server, for example in a Next.js route:
const response = await fetch('https://www.fleetiva.de/api/v1/inquiries', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.FLEETIVA_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': booking.id, // your own ID, the same on every retry
},
body: JSON.stringify(inquiry),
});
if (response.status === 201 || response.status === 200) {
const { id } = await response.json(); // store it with the booking
} else if (response.status === 429 || response.status === 503) {
// 429: wait Retry-After seconds, 503: wait a moment — then send the same request again
} else {
const problem = await response.json(); // problem.code, problem.issues, problem.request_id
}
4. Handle the response
201 means the inquiry was created. Store the id so you can query the status. 200 with duplicate: true means this inquiry has already arrived; no second one is created. On 429 wait the seconds from Retry-After, on 503 a moment, then send the same request with the same Idempotency-Key again.
Location: /api/v1/inquiries/0b7c6d1e-2f3a-4b5c-8d9e-0f1a2b3c4d5e
{
"id": "0b7c6d1e-2f3a-4b5c-8d9e-0f1a2b3c4d5e",
"duplicate": false
}
5. Query the status
Use the id to read the processing status. You only see inquiries created through your own access.
curl 'https://www.fleetiva.de/api/v1/inquiries/0b7c6d1e-2f3a-4b5c-8d9e-0f1a2b3c4d5e' \
-H "Authorization: Bearer $FLEETIVA_API_KEY"
{
"id": "0b7c6d1e-2f3a-4b5c-8d9e-0f1a2b3c4d5e",
"status": "in_progress",
"received_at": "2026-11-02T09:15:00+00:00"
}
6. Test
There is no sandbox. Check your calls first with Fleetiva-Dry-Run: true, most easily in the test console. For real test inquiries the transport service creates a separate test access (see Test access).
Access and testing
Key
The transport service creates the access in Fleetiva: Settings → Integrations → API access. It needs the Professional or Enterprise plan. The key starts with flt_ and is shown exactly once; Fleetiva does not store it in plain text.
The key is a password for the transport service. Have it handed over securely, not by email or chat, and keep it on your server only, never in the source of a web page or app. When the transport service renews the key, the old one stops working immediately; a blocked access gets 401.
Scopes
An access may only do what its scopes allow. If the scope for a call is missing, the API answers 403 with the code forbidden.
inquiries.create(in Fleetiva “Create inquiries”): create inquiries and read the status of inquiries created through this access.master_data.read(in Fleetiva “Read master data”): read and sync customers and passengers.accounting.read(in Fleetiva “Read accounting”): together withinclude=billingthe billing data of a customer or passenger, for example bank account, VAT ID or billing address.health_data(in Fleetiva “Health data”): together withinclude=healththe health data of a passenger. Every access is logged for the operator.
Test access
There is no sandbox; testing happens against the live system. For development the transport service creates a separate access, for example “Booking form test”. Real test inquiries appear in its inbox under this name; it rejects them and deletes the test access once your connection works. For live operation it creates a separate access.
Dry run
With the header Fleetiva-Dry-Run: true a creating call checks everything the real call checks: key, scope, whether API access is active at the transport service, the limits, all fields, addresses and the consent. Nothing is created, stored or notified. On success the API answers 204 without a body, otherwise with the same error as the real call.
Not checked: whether the Idempotency-Key was already used for another inquiry. A dry run counts as a reading call. Without the header or with false the call takes effect; any other value is rejected with 400.
curl -X POST 'https://www.fleetiva.de/api/v1/inquiries' \
-H "Authorization: Bearer $FLEETIVA_API_KEY" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: booking-2026-11-0042' \
-H 'Fleetiva-Dry-Run: true' \
--data-binary @- <<'JSON'
{
"requester": {
"role": "institution",
"first_name": "Petra",
"last_name": "Schulz",
"email": "p.schulz@example.org",
"phone": "+49 30 1234567",
"organization": "Tagesförderstätte Sonnenhof",
"postal_address": {
"street": "Lindenallee",
"house_number": "12",
"zip": "10115",
"city": "Berlin"
}
},
"passengers": [
{
"first_name": "Jonas",
"last_name": "Weber",
"birth_year": 2012,
"pickup": {
"street": "Gartenweg",
"house_number": "4",
"zip": "10117",
"city": "Berlin"
},
"dropoff": {
"street": "Lindenallee",
"house_number": "12",
"zip": "10115",
"city": "Berlin"
},
"trip_kind": "recurring",
"requested_date": "2026-12-01",
"requested_time_window": "arrival by 8:00",
"return_trip": true,
"return_time_window": "15:30",
"needs": {
"wheelchair": true
}
}
],
"message": "Monday to Friday, school days only.",
"consent": {
"given": true,
"text_version": "2026-11"
}
}
JSON
Test console
In the reference every call has a “Try it” panel. Enter your key at the top of the reference, adjust parameters and body and send the call. The console shows status, headers and response and the matching curl command.
The key stays in this browser tab only; it is not stored and is sent only to /api/v1 on this address. “Dry run” is preselected. In live operation the key still belongs on your server only.
Concepts
From inquiry to trip
An inquiry is a request, not a booking. It lands in the inbox of the transport service, and everyone with the permission is notified. The transport service reviews the inquiry and takes it over into its planning or rejects it.
Everything else, such as questions, confirmation and trip times, the transport service settles with the requesting person. The API reports the status of the inquiry.
Status
The status of an inquiry is always one of these values:
| Status | Description |
|---|---|
new | received, not processed yet |
in_progress | the transport service is working on it |
accepted | taken over into planning |
rejected | rejected |
Idempotency
Networks lose responses. So that a retry does not create a second inquiry, every creating call carries the header Idempotency-Key: 1 to 120 visible ASCII characters, a separate value for each inquiry, for example your booking number or a UUID.
If the same key arrives again with the same content, the API answers 200 with duplicate: true and the existing id. With different content it rejects the call with 422 (idempotency_key_reused). The content is the requesting person, the passengers and the message, not the consent. A key applies per access and stays bound to its inquiry.
Consent and health data
The needs (needs) are health data under Art. 9 GDPR. Obtain the person’s consent before sending and include with every inquiry the version of the consent text they saw (consent.text_version). Fleetiva stores this version as proof. Without consent the API does not accept an inquiry.
consent.given_at is optional; without it the time of receipt applies. If the time is more than 5 minutes in the future, the inquiry is rejected (in_future).
Lists and sync
A list answers with data and next_cursor. For the next page send next_cursor unchanged as cursor, with the same filters. null means there are no more records. A page has 50 entries, up to 200 with limit.
Records come ordered by their last change, oldest first. If a record changes while you page, it comes again further back. So store by id: create it when it is new, otherwise overwrite it.
To keep a copy current:
- Remember the start time of each sync. Next time, ask from that time minus a few minutes, because a change carries the time it started.
- Page through
GET /customers?updated_since=…and store every record byid. - Page through
GET /customers/deleted?since=…with the same time and delete these records on your side.
The same applies to passengers with /passengers and /passengers/deleted.
Deletions stay available for 90 days. If since is further back, the API answers 400 with the reason sync_window_exceeded. Then reload all records.
Field groups
Sensitive parts of a record are field groups. They come only when the call asks for them with include and the access has the matching scope. Without the request the key of the group is missing from the response; with it the key is always there and empty fields are null.
billing(scopeaccounting.read): billing data of a customer (tax number, VAT ID, Leitweg-ID, payment method, bank account) or of a passenger (name and address for invoices, VAT rate).health(scopehealth_data, API keys only): health data of a passenger – medical notes, allergies, emergency contact, notes on the needs and the flags autism, visual and cognitive impairment. Every passenger delivered with this group is logged for the operator (access, time,request_id). If Fleetiva cannot log the access, the API answers503instead of data.
If the scope for a requested group is missing, the API answers 403 (forbidden) without reading any data.
Technical rules
Base URL
All paths on this page are relative to https://www.fleetiva.de/api/v1. Use the address exactly as shown: a client that follows a redirect to another host drops the Authorization header, and the call fails with 401.
HTTPS only. A call over http:// is redirected to https://, but by then the key and the body have already been sent unencrypted. So always enter the base URL with https://. If it happened anyway, have the transport service renew the key. The HTTP/1.1 … lines in the examples are only the status line of the response; the connection is always HTTPS.
Authentication
Every call carries the header Authorization: Bearer <key>. If it is missing or the key is wrong, blocked or renewed, the API answers 401 with the header WWW-Authenticate.
Body and encoding
Creating calls send a JSON object with Content-Type: application/json in UTF-8, at most 64 KB. Another content type gives 415, a body that is too large 413, invalid JSON 400 with invalid_json. Unknown fields are rejected (unknown_field), so a typo in a field name shows up at once.
Reading calls take their parameters from the address, for example ?limit=100&include=billing. Unknown parameters are rejected here as well (unknown_field). A parameter may only be repeated for lists such as include.
Empty values
In every optional field, null, an empty string and only spaces mean “not given”. You do not need to remove empty fields.
In responses every field is always present, an empty field is null. The exception are field groups, which come only on request.
Addresses
An address may be left out entirely. If any part is given, it needs at least street and city; otherwise the API rejects it with the reason address_incomplete at the missing field. House number and postal code are optional.
Limits
Per access, 60 creating and 600 reading calls per hour are possible, per transport service across all accesses 300 and 3000. Above that the API answers 429 and gives the waiting time in seconds in the Retry-After header. A limit per IP address applies as well.
Versions
/api/v1 only grows: optional fields, new calls and new reasons in issues may be added, existing ones keep their meaning. Ignore unknown fields in responses. An incompatible change gets a new version with its own path. What changed is listed under Changes.
Request ID
Every response carries the header X-Request-Id, error responses also the field request_id. Quote it when you contact support; it lets support find the call. Responses are never cached (Cache-Control: no-store).
Error handling
Structure of an error response
Errors come as application/problem+json (RFC 9457). Branch on the code field, not on the text. For invalid input, issues names path and reason, never the submitted value. Quote the request_id when you contact support. The fields are listed in the data format Error response.
Error codes
| Code | HTTP | Meaning |
|---|---|---|
unauthorized | 401 | Key missing, wrong, unknown or blocked. |
forbidden | 403 | The access lacks the scope for this call or for a requested field group. |
access_inactive | 403 | API access is not active at the transport service, for example after a plan change. |
not_found | 404 | Unknown, deleted or, for inquiries, created through another access. |
invalid_input | 400 | Invalid JSON, a field breaks the rules or the Idempotency-Key is missing; details in issues. |
idempotency_key_reused | 422 | The same Idempotency-Key was already used with different content. |
payload_too_large | 413 | The body is larger than allowed. |
unsupported_media_type | 415 | The Content-Type is not application/json. |
rate_limited | 429 | Too many calls; try again after Retry-After seconds. |
unavailable | 503 | Temporarily unavailable; try again after a moment. |
Reasons in issues
With invalid_input, issues names for every problem the path of the field and a fixed reason in message. For lengths, counts and numbers the broken limit is in limit. The list only grows; treat an unknown reason like invalid_value.
| Reason | Meaning |
|---|---|
required | Required field missing or empty. |
too_short | Text shorter than limit characters. |
too_long | Text longer than limit characters. |
too_few | Fewer than limit entries. |
too_many | More than limit entries. |
too_small | Number below limit. |
too_large | Number above limit. |
invalid_type | Wrong type, for example text instead of a number. |
invalid_value | Value not allowed, for example not one of the possible values. |
invalid_format | Wrong format, for example of an email address, a timestamp or a UUID. |
unknown_field | Unknown field, for example a typo in the name. |
invalid_json | The body is not valid JSON (empty path). |
invalid_characters | Characters not allowed, for example in the Idempotency-Key. |
contact_required | Email or phone of the requesting person is missing. |
address_incomplete | Half-filled address: street or city is missing. |
invalid_date | Not a real calendar date in the format YYYY-MM-DD. |
in_future | The time lies in the future. |
return_time_requires_return_trip | Return time given, but return_trip: false. |
invalid_cursor | The cursor was not taken from an API response or is damaged. Without cursor the list starts from the beginning. |
sync_window_exceeded | since is more than limit days back; older deletions are no longer known. Reload all records. |
Example of an error response
Content-Type: application/problem+json
{
"type": "tag:fleetiva.de,2026:api/problems/invalid_input",
"title": "Invalid request",
"status": 400,
"code": "invalid_input",
"request_id": "5f0d3c2a-7b1e-4c9d-a6f8-2e4b1c7d9a03",
"issues": [
{
"path": "passengers.0.pickup.city",
"message": "address_incomplete"
}
]
}
Support
Who helps with what
- Access, keys and scopes: the transport service you create inquiries for. Only it can create, block and renew accesses.
- Questions about an inquiry: also the transport service; it processes the inquiry in its inbox.
- Technical questions about the API: Fleetiva at support@fleetiva.de. Quote the
request_id, the time and the call. Never send the key.
FAQ
Why do I get 401 although the key is right?
Usually the client follows a redirect to another host and drops the Authorization header. Use the base URL exactly as shown. Otherwise the key was renewed or the access blocked; ask the transport service.
What happens on a call over http://?
The server redirects it to https:// (308), but by then the key and the body have already been sent unencrypted. Correct the base URL to https:// and have the transport service renew the key (see Base URL).
Why do I get 200 instead of 201?
The inquiry with this Idempotency-Key and this content already exists. The response names its id with duplicate: true; no second inquiry is created.
Why do I get 422 when I retry?
The same Idempotency-Key arrived with different content. A retry needs the same content, a new inquiry a new key (see Idempotency).
How do I test without a sandbox?
With a dry run: the header Fleetiva-Dry-Run: true checks everything and creates nothing. For real test inquiries there is a separate test access.
Why does the status call return 404?
You only see inquiries created through your own access. Inquiries of other accesses, unknown and deleted ones are all “not found”.
What do I do on 429 or 503?
On 429 wait the seconds from Retry-After, on 503 a moment. Then send the same request with the same Idempotency-Key; no second inquiry is created.
May I use the key in the browser?
No. The key belongs on your server only; a form in the browser sends its data to your server, which calls the API. The test console on this page is meant for trying things out only.
Changes
What changed in /api/v1, newest first. Existing fields and calls keep their meaning.
- · Read and sync passengers (
GET /passengers,GET /passengers/{id},GET /passengers/deleted), scopehealth_datawith the field grouphealth(every access is logged) and the field groupbillingon the passenger. - · Read and sync customers (
GET /customers,GET /customers/{id},GET /customers/deleted), scopesmaster_data.readandaccounting.read, field groupbillingand the reasonsinvalid_cursorandsync_window_exceeded. - · First version: create inquiries (
POST /inquiries) including the postal address of the requesting person, query the status (GET /inquiries/{id}), dry run withFleetiva-Dry-Run: trueand fixed reasons inissues.