API Logs
Every request made with your organization's API keys is recorded — successful
or not — as one row. Use it to debug an integration, confirm a call actually
reached us, see which key produced which traffic, and understand a 4xx
before contacting Fleeta support.
curl "https://openapi.fleeta.io/v1/api-logs?status=5xx" \
-H "Authorization: Bearer flt_live_..."
The endpoint is GET /v1/api-logs and requires the usage:read scope — the
same scope as GET /v1/usage, available on every paid tier.
What is recorded
One row per request, holding only request metadata:
| Field | Description |
|---|---|
id | Request identifier. Same value as the X-Request-Id response header and the requestId in an error body — quote it when you contact Fleeta support |
at | When the request was handled (RFC 3339, UTC) |
method | HTTP method |
route | The route template (/v1/devices/{psn}), not the resolved path |
status | HTTP status code |
statusClass | 2xx / 4xx / 5xx … derived from status |
errorCode | The code from the problem+json body on failures; null on success |
latencyMs | Server-side handler time in milliseconds (excludes network time) |
keyId | The API key that made the call |
ip | Client IP address |
userAgent | Client user agent (truncated to 256 characters) |
What is not recorded
Request and response bodies are never stored. Fleet data flowing through
this API — GPS traces, event video metadata, driver-identifiable records — is
personal data, and keeping copies of it in a log is a liability rather than a
feature. So a log row tells you that a call happened and how it ended, never
what it carried. errorCode is the one field that carries the failure reason,
which is why it exists.
Query strings and path parameters are not stored either — route is the
template, so a PSN or a job ID never lands in the log.
Requests that never reach the log
Some rejections are stopped before the API runs, so they cannot be attributed to an organization and are not listed:
401 unauthorized— a missing, malformed, or revoked key is rejected at the authentication layer. There is no organization to file the row under.429 rate_limited— requests-per-second throttling is applied at the edge, before your request is dispatched.429 quota_exceeded— the edge usage-plan safety net, likewise applied before dispatch (see Rate Limits).404 route_not_found— a path or method that does not exist is answered at the edge (as problem+json with arequestId) and never routes to the API, so a typo in the URL leaves no trace here.
A 429 monthly_limit_exceeded is recorded, because the monthly call
counter is enforced inside the API. If you are debugging silence rather than
errors, keep this asymmetry in mind: an empty log can mean "the key was
rejected", not only "nothing was sent".
Retention
| Data | Retained |
|---|---|
Individual request rows (GET /v1/api-logs) | 30 days |
Daily rollups (GET /v1/usage) | 13 months |
Rows expire automatically once they are 30 days old, so the queried window may not exceed 30 days. Daily totals live for 13 months so you can compare against the same month last year and settle billing questions after the raw rows are gone.
Querying
| Parameter | Description |
|---|---|
from / to | Time range (ISO 8601). Defaults to the last 24 hours; the window may not exceed 30 days |
status | An exact status code (200) or a class (2xx, 4xx, 5xx) |
method | GET, POST, PUT, PATCH, DELETE |
route | Route template prefix — /v1/devices matches /v1/devices, /v1/devices/{psn}, /v1/devices/{psn}/gps … |
keyId | Restrict to a single API key |
limit | Page size (default 50, max 200) |
after | Cursor from the previous response |
Rows come back newest first, with cursor pagination — the log is a time series that keeps growing, so page numbers would silently duplicate or skip rows.
# All 5xx responses from one key over the last 7 days
curl -G "https://openapi.fleeta.io/v1/api-logs" \
-H "Authorization: Bearer flt_live_..." \
--data-urlencode "from=2026-07-30T00:00:00Z" \
--data-urlencode "status=5xx" \
--data-urlencode "keyId=key_9f2c"
{
"data": [
{
"id": "8916e1c1-2f4a-4d1e-9a7b-0b2c3d4e5f60",
"at": "2026-08-06T04:12:07Z",
"method": "GET",
"route": "/v1/devices/{psn}",
"status": 200,
"statusClass": "2xx",
"errorCode": null,
"latencyMs": 74,
"keyId": "key_9f2c",
"ip": "203.0.113.24",
"userAgent": "fleeta-openapi-node/1.2.0"
},
{
"id": "3f0b7a52-91cd-4a10-8e77-1d5b2c9f4a08",
"at": "2026-08-06T04:09:55Z",
"method": "GET",
"route": "/v1/events/{eventId}/video",
"status": 403,
"statusClass": "4xx",
"errorCode": "quota_exceeded",
"latencyMs": 41,
"keyId": "key_9f2c",
"ip": "203.0.113.24",
"userAgent": "fleeta-openapi-node/1.2.0"
}
],
"pagination": {
"nextCursor": "eyJkIjoiMjAyNi0wOC0wNiIsInNrIjoiMTc4NTk5ODg5ODAxMiJ9",
"hasMore": true
}
}
Walking the full range is the usual cursor loop:
async function* apiLogs(params) {
let after;
do {
const qs = new URLSearchParams({ ...params, limit: '200', ...(after ? { after } : {}) });
const res = await fetch(`https://openapi.fleeta.io/v1/api-logs?${qs}`, {
headers: { Authorization: `Bearer ${process.env.FLEETA_API_KEY}` },
});
const { data, pagination } = await res.json();
yield* data;
after = pagination.hasMore ? pagination.nextCursor : undefined;
} while (after);
}
// Which endpoints failed in the last 24 hours?
const failures = {};
for await (const row of apiLogs({ status: '5xx' })) {
failures[row.route] = (failures[row.route] || 0) + 1;
}
Trends: daily volume and success rate
GET /v1/usage carries the rolled-up view of the same data alongside your
quota status — the last 30 UTC calendar days of call volume and the split by
status class.
{
"data": {
"period": "2026-08",
"tier": "standard",
"apiCalls": { "used": 27400, "limit": 70500 },
"rate": { "limitRps": 3, "burst": 6 },
"volumeQuotas": [{ "bucket": "transfer_bytes", "limit": 60129542144, "used": 5550677, "period": "2026-08", "unit": "bytes" }],
"callHistory": {
"windowDays": 30,
"from": "2026-07-08",
"to": "2026-08-06",
"aggregatedDays": 2,
"avgLatencyMs": 128,
"days": [
{
"date": "2026-08-05",
"calls": 412,
"avgLatencyMs": 131,
"statusClasses": [
{ "statusClass": "2xx", "count": 402 },
{ "statusClass": "4xx", "count": 9 },
{ "statusClass": "5xx", "count": 1 }
]
},
{
"date": "2026-08-06",
"calls": 418,
"avgLatencyMs": 125,
"statusClasses": [
{ "statusClass": "2xx", "count": 410 },
{ "statusClass": "4xx", "count": 8 }
]
}
]
},
"statusBreakdown": {
"windowDays": 30,
"aggregatedDays": 2,
"total": 830,
"classes": [
{ "statusClass": "2xx", "count": 812, "ratio": 0.9783 },
{ "statusClass": "4xx", "count": 17, "ratio": 0.0205 },
{ "statusClass": "5xx", "count": 1, "ratio": 0.0012 }
]
}
}
}
callHistory.days contains only days that have a rollup. A day that is
absent means no rollup exists for it — which includes today until its rollup is
produced — and that is not the same statement as "you made zero calls". Days
are never padded with zeroes, so do not read absence as traffic data.
aggregatedDays tells you how many days the figures actually cover, and
statusBreakdown.ratio is computed over exactly that set. Both callHistory
and statusBreakdown are null when the rollup is unavailable.
ratio is null rather than 0 when the total is zero — a share of nothing
is undefined, not zero. avgLatencyMs is null when latency was not rolled up
for every day in the window, rather than an average of the days that happened
to have it.
API logs vs. audit logs
They are stored separately and answer different questions. Reach for the one that matches your question:
API logs (/v1/api-logs) | Audit logs (/v1/audit-logs) | |
|---|---|---|
| Question | What calls were made? | What changed in the account? |
| Subject | Requests to this API | Account activity — by people and by API keys |
| Reader | Developers integrating the API | Administrators, compliance |
| Typical use | Debugging a 403, tracing a request ID, capacity planning | Who deleted that geofence, who changed device settings |
| Scope | usage:read (paid tiers) | audit:read (enterprise) |
| Retention | 30 days (13 months rolled up) | Per the platform audit policy |
An API log row says "DELETE /v1/geofences/{geofenceId} returned 204 in 96 ms".
The corresponding audit log entry says which geofence was removed and by whom.
Neither replaces the other.
API-key actions in the audit log
Changes made through this API are recorded in the same audit trail as the actions people take in the Fleeta webviewer and apps — administrators see both on one timeline in the webviewer's Audit Log screen. In this endpoint's responses you can tell the two apart:
| Person (webviewer / app) | API key (this API) | |
|---|---|---|
actorType | null — that pipeline records no actor kind, and we do not guess one | api_key (or system for an action the platform took on its own, such as suspending a webhook after repeated delivery failures) |
actorId | The account email | The API key ID (key_…) — the same keyId you see in API logs |
ip / userAgent | The person's browser or app | The client that called the API |
Only actions that change or command something are recorded — creating,
updating or deleting a geofence or webhook, renaming or pre-registering a
device, rebooting, starting or cancelling a GPS export or a recording recall,
deleting an SD-card recording, issuing an event video URL, and the SD-card
listing and metadata commands that wake the dashcam. Plain reads
(GET /v1/devices, GET /v1/events, …) are not audit events; they are in the
API log.
Each entry's eventType follows the category.action form of the rest of the
audit log — for example geofence.create, device.device_reboot,
export.job_create, video.recall_create, video.url.issue,
auth.webhook_create, auth.webhook_rotate_secret — and target carries the
resource the action touched (psn, geofenceId, webhookId, jobId,
eventId, filename). Webhook subscription changes are filed under the
auth category (they belong to your API key), so category=auth lists them
alongside sign-in and account-security events. Filter by key with
actor=key_…, or search with q. Sandbox (flt_test_*) activity is not
written to the audit trail.
# Everything one API key changed in the last 24 hours
curl -G "https://openapi.fleeta.io/v1/audit-logs" \
-H "Authorization: Bearer flt_live_..." \
--data-urlencode "actor=key_9f2c" \
--data-urlencode "from=2026-08-05T04:00:00Z"
The entry lands in the audit log within seconds of the response and is written on a best-effort basis: if the audit pipeline is unreachable the API call still succeeds, and the entry is logged on the server for later review rather than being retried.
Related links
- Rate Limits — quotas,
429handling, and current usage - Authentication — keys, scopes, and
keyId - Pagination — the cursor loop in detail
- Errors — the
errorCodevalues you will see in log rows - Tenant Isolation — why you only ever see your own traffic