Rate Limits
API keys are subject to two kinds of limits, set per tier.
- Call rate and monthly call volume — requests per second (rps), burst,
and a monthly API call cap. Exceeding them returns
429 Too Many Requests. - Volume quotas — monthly caps on expensive resources such as video URL
issuance and GPS Export jobs. Exceeding them returns
403+quota_exceeded.
quota_exceeded is not always a 403The API edge keeps a usage-plan safety net in front of the monthly call cap,
and when that net fires it emits the same quota_exceeded code with status
429. So a 429 is retryable only after you have checked the code:
rate_limited and monthly_limit_exceeded clear on their own, while
quota_exceeded does not clear until the billing period rolls over. Branch on
code, never on the status alone — see the
branching example.
There are five tiers — free / starter / standard / pro / enterprise. The tier is derived from your Fleeta subscription plan — you do not pick it yourself, and the API is included in that subscription at no extra cost. Two things set your limits: the plan level and the number of cameras on it (each plan has a maximum camera count). Prices of the plans themselves live on fleeta.io/pricing; this page only describes what each plan allows the API to do.
Your key is an identity, not a plan. When your subscription changes — a tier up or down, cameras added or removed — the key you already have picks up the new limits. You never reissue a key or redeploy to change plan.
| What changes | When it takes effect |
|---|---|
| Scopes, camera limit | within 5 minutes |
| rps / burst | within 5 minutes |
| Monthly call limit, volume quotas | immediately, on the next call |
The five-minute window is our authorization cache; nothing is required from you to trigger it.
Downgrades apply the same day, not at the end of the month. If you drop to
a smaller plan after already spending more calls than the new plan allows, the
remaining calls for that month return
429 monthly_limit_exceeded until the
counter resets. The monthly counter itself is not reset by a plan change.
Limits by tier
| Tier | Max cameras | rps | burst |
|---|---|---|---|
| free | 1 | 1 | 2 |
| starter | 1 | 1 | 3 |
| standard | 10 | 3 | 6 |
| pro | 99 | 10 | 25 |
| enterprise | 999 | 25 | 50 |
Monthly API call limit
The monthly API call cap scales with the plan and the number of cameras on it, not with a fixed per-tier number:
- free — fixed 300 calls / month
- paid plans — 500 + (calls per camera × cameras), where each camera adds 3,000 (starter) / 7,000 (standard) / 10,000 (pro) / 15,000 (enterprise) calls a month. For example, a standard plan with 10 cameras gets 500 + 7,000 × 10 = 70,500 calls / month.
Representative values (at the plan's maximum camera count):
| Tier | Calls per camera | Cameras (max) | Monthly API calls |
|---|---|---|---|
| free | — | 1 | 300 |
| starter | 3,000 | 1 | 3,500 |
| standard | 7,000 | 10 | 70,500 |
| pro | 10,000 | 99 | 990,500 |
| enterprise | 15,000 | 999 | 14,985,500 |
When you hit the monthly limit
Calls beyond the monthly cap are blocked — there is no pay-as-you-go
overage and unused calls do not roll over to the next month. Once the cap
is reached, every further request returns 429 with a problem+json body
whose code is monthly_limit_exceeded, plus a Retry-After header
indicating the seconds remaining until the next billing month (the 1st of
the next month, UTC), when the counter resets:
{
"type": "https://developers.fleeta.io/errors/monthly_limit_exceeded",
"title": "Too Many Requests",
"status": 429,
"code": "monthly_limit_exceeded",
"detail": "Monthly API call limit reached (limit=70500). Upgrade your plan or wait until the next billing month.",
"requestId": "b3c7e210-58d4-4f9a-8c61-2a7d9e4b0f15",
"limit": 70500,
"used": 70500,
"resetsAt": "2026-09-01T00:00:00Z"
}
If you need more calls before the reset, move to a higher subscription plan (or a larger camera count) — the limit scales with the plan and its cameras.
Changing tier
There is no API for changing a key's tier, and the tier is not a parameter you send at issuance — it is resolved from your subscription plan, and re-read from it on later requests. You do not issue a new key when the plan changes: scopes and the camera limit follow within about five minutes (our authorization cache), the request-rate limit moves with the plan in the same window, and the monthly call limit is recalculated on the very next call — in both directions, upgrades and downgrades alike. The table under Changing plan? Keep your key above lists each limit's timing.
A subscription plan with no Open API mapping cannot issue a key — the
issuance request is rejected with a 403.
Camera seats (over-limit dashcams)
A subscription covers a fixed number of cameras — the camera count chosen in
the Fleeta web viewer, reported as cameraLimit on GET /v1/usage. Neither
the API nor the mobile app registers a cloud dashcam past that count, so an
organization normally never holds more dashcams than seats. It does happen
when the subscription is downgraded or lapses while the dashcams stay
registered. Then:
- Seats are assigned automatically, newest first. Cloud dashcams are ranked
by registration date (
registeredAt, most recent first); positions1..limitarecovered, the rest areover_limit. There is no way to pick which dashcam keeps a seat — the rule is the one the web viewer and the mobile app already apply. Dashcams from the Wi-Fi-only inventory hold no seat and are alwayscovered(cameraPosition: null). Every dashcam row carries this asentitlement; the device list and detail also carrycameraPosition, and an over-limit detail addsrequiredCameraLimit— the count that would cover it. - An over-limit dashcam is readable, not hidden. It stays in
GET /v1/devices(the web viewer shows it greyed out), and everything already stored about it keeps working: location and the location feed, trips and tracks, events and the event feed, thumbnails, statistics, geofences and geofence alerts, GPS export, the settings snapshot, battery and firmware, the per-device driving report, webhook deliveries. - What is locked is everything that pulls new media off the dashcam or moves it:
| Route | For an over-limit dashcam |
|---|---|
GET /v1/events/{eventId}/video | 403 camera_limit_exceeded — judged before the URL is issued, so no transfer_bytes allowance is spent |
GET /v1/devices/{psn}/sd-files, …/sd-files/{filename}/metadata, …/sd-files/{filename}/video, DELETE …/sd-files/{filename} | 403 camera_limit_exceeded |
POST /v1/media/recall-jobs | 403 camera_limit_exceeded |
POST /v1/devices/{psn}/reboot | 403 camera_limit_exceeded |
POST /v1/devices | 403 camera_limit_exceeded when registered + pending + requested dashcams would exceed the count — all-or-nothing, nothing is registered, and remainingSeats says how many still fit |
The seat check runs after 404 device_not_found, 422 cloud_only and
422 unsupported_device: a dashcam that could never do the operation answers
those first, and a 403 here always means "buy a seat and it works".
The cameras block and the entitlement filter
Every organization-wide response carries a cameras block beside data,
not inside it — GET /v1/devices, GET /v1/fleet/locations and its feed,
GET /v1/fleet/summary, GET /v1/reports/driving,
GET /v1/safety-events/stats and GET /v1/usage:
{
"data": [ "…" ],
"pagination": { "page": 1, "perPage": 20, "total": 13, "totalPages": 1 },
"cameras": { "limit": 10, "registered": 13, "overLimit": 3 }
}
registered counts the organization's cloud dashcams and overLimit how many
of them sit beyond limit. limit is null when the subscription could not
be resolved for that request — nothing is locked then, and overLimit is
0. GET /v1/usage adds overLimitPsns, the serial numbers beyond the limit
in position order, so a monitor can name exactly which dashcams will answer
403 without making a failing call.
?entitlement=covered|over_limit|all narrows the dashcams a list covers:
| Accepted by | Default |
|---|---|
GET /v1/devices, GET /v1/fleet/locations, …/feed, GET /v1/events, …/feed, GET /v1/safety-events/stats | all |
GET /v1/fleet/summary, GET /v1/reports/driving | covered — the web viewer leaves over-limit dashcams out of its report numbers and online count, and these two endpoints match those numbers by default. Pass all for the whole fleet; an organization with over-limit dashcams sees smaller totals until it does |
The cameras block always describes the whole organization regardless of the
filter, and GET /v1/reports/driving/{psn} answers 200 for an over-limit
dashcam (with its entitlement) — the default only trims the list.
The 403 body
{
"type": "https://developers.fleeta.io/errors/camera_limit_exceeded",
"title": "Forbidden",
"status": 403,
"code": "camera_limit_exceeded",
"detail": "Device '7XBPK0BE00000012' is dashcam 12 of 13 registered, but the subscription covers 10. Raise the camera count to 12 or more in the Fleeta web viewer (Account › Subscription) — this key picks it up within 5 minutes, no reissue. Its location, trips, events, thumbnails, geofences and GPS export stay available; video, SD-card access, recall and remote commands need a covered camera.",
"requestId": "1c7209ba-f033-475e-9289-e20452412b18",
"psn": "7XBPK0BE00000012",
"cameraPosition": 12,
"cameraLimit": 10,
"registeredCameras": 13,
"requiredCameraLimit": 12
}
cameraLimit, registeredCameras and requiredCameraLimit are always
present; single-device routes add psn and cameraPosition, and
POST /v1/devices adds psns and remainingSeats instead. There is no
Retry-After — the answer does not change until the subscription does, so
treat it as a hard stop, not a backoff target. Every such refusal is listed in
API Logs with status 403 and this code.
Unlocking
Raise the camera count in the Fleeta web viewer — Account › Subscription —
to at least requiredCameraLimit, or remove dashcams you no longer run. The
key you already have picks it up on its next authorization, within 5
minutes (the authorization cache in the table at the top of this page),
with no reissue and nothing to change in your code. Sandbox test keys
never lock a seat: every demo dashcam is covered and cameras.limit equals
registered, so 403 camera_limit_exceeded can only be rehearsed with a live
key on an organization that actually holds over-limit dashcams.
Volume quotas
Separately from call volume, two resources have monthly allowances of their
own, because their cost is not per call: video delivered to you and GPS
export jobs. Like the call limit, they grow with what the plan is paid for —
a plan that gates a bucket to 0 gives 0:
| Bucket | Description | Allowance / month | Unit | Available from |
|---|---|---|---|---|
transfer_bytes | Video delivered to you — event video URLs and SD-card recalls | 0.8 GB per $1 (Standard) · 1 GB per $1 (Pro) · 1.2 GB per $1 (Enterprise) of the monthly subscription | bytes | standard (0 below) |
export_job | GPS Export jobs created | 50 × cameras | count | pro (0 below) |
export_concurrent | Export jobs running at once | free/starter 0 · standard/pro 1 · enterprise 2 (fixed) | count | standard |
Per camera that is 5.6 GB a month on Standard ($7), 10 GB on Pro ($10) and
18 GB on Enterprise ($15): a Standard plan with 10 cameras has 56 GB, a Pro
plan with 99 cameras 990 GB, an Enterprise plan with 100 cameras 1,800 GB.
GET /v1/usage reports the exact figure in bytes, where 1 GB = 2³⁰ bytes,
with unit: "bytes"; the count buckets carry unit: "count".
What counts, and when:
| Call | Counts | When |
|---|---|---|
GET /v1/events/{eventId}/video | a flat 6 MB per issued URL (the measured average event clip) | when the URL is issued — reissuing the same clip counts again, so reuse a URL for its 5-minute life |
POST /v1/media/recall-jobs · GET /v1/devices/{psn}/sd-files/{filename}/video | the recording's size — sizeBytes as the dashcam reports it, or a flat 8 MB (sub) / 150 MB (main) when it does not | checked before the dashcam is contacted (against the size of your last recall of that file, else the flat estimate) and recorded once the transfer has started; a link served from the media server's existing copy counts too — the file reaches you either way |
| Thumbnails, event and SD-card listings, recording metadata, recall-job status, GPS export files | nothing | — |
A sub recording is 5–10 MB; a full-quality main recording is often 300 MB
or more. Prefer quality=sub unless you need full resolution — the same
allowance buys roughly 700 sub recordings or 35 minutes of main footage
per camera on Standard, about twice that on Pro and three times on Enterprise.
Unlike a rate limit, a volume quota overrun is not resolved by retrying, so
it is returned as a distinct 403. limit and used are in the bucket's
unit — bytes here — and detail states the limit in GB:
{
"type": "https://developers.fleeta.io/errors/quota_exceeded",
"title": "Forbidden",
"status": 403,
"code": "quota_exceeded",
"detail": "Monthly quota for 'transfer_bytes' exceeded (limit=56.0 GB).",
"requestId": "7e5d1a94-30bf-4c28-9d63-1f8a2b6c4e07",
"bucket": "transfer_bytes",
"limit": 60129542144,
"used": 60125000000
}
Additional transfer packs for months that need more are planned for a later release; until then, the way to get more is a higher plan or more cameras.
Quota enforcement is fail-closed: if the quota store cannot be reached to determine your remaining allowance, the request is rejected rather than allowed through unmetered.
Handling 429 — Retry-After and exponential backoff
Response headers
| Header | When | Value |
|---|---|---|
Retry-After | 429 rate_limited (per-second throttle) | 1 |
Retry-After | 429 monthly_limit_exceeded (monthly call cap) | seconds until 00:00 UTC on the 1st of next month |
Retry-After | 409 device_busy (dashcam transfer in progress) | 5 |
X-Request-Id | every response | correlation ID — quote it when contacting support |
Both headers are declared in the OpenAPI spec, so generated clients see them as typed
response headers. Browser clients can read them too — they are listed in Access-Control-Expose-Headers.
Exceeding the rps or monthly call volume returns 429 Too Many Requests.
If the Retry-After header is present, wait for that duration before
retrying; otherwise apply exponential backoff.
async function requestWithBackoff(url, options, maxRetries = 5) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const res = await fetch(url, options);
if (res.status !== 429) return res;
const retryAfter = Number(res.headers.get('Retry-After'));
const waitMs = Number.isFinite(retryAfter) && retryAfter > 0
? retryAfter * 1000 // server directive takes priority
: Math.min(2 ** attempt * 1000, 30_000); // exponential backoff: 1s→2s→4s… capped at 30s
await new Promise((r) => setTimeout(r, waitMs + Math.random() * 250)); // jitter
}
throw new Error('rate limit: retry limit exceeded');
}
The 429 body is problem+json with a code of rate_limited for rps
overruns and monthly_limit_exceeded for monthly call volume overruns.
For the retry decision, however, keying off the HTTP status code 429
is simpler and safer than parsing code. Note that retrying is only
useful for rate_limited — a monthly_limit_exceeded response carries a
Retry-After pointing at the next billing month, so treat it as a hard
stop rather than a backoff target.
The official SDK waits for Retry-After and automatically retries once on
429 — see SDKs.
Checking current usage
GET /v1/usage (scope usage:read) returns this month's call volume, rate
limit, and volume quota status.
{
"data": {
"period": "2026-08",
"tier": "standard",
"cameraLimit": 10,
"apiCalls": {
"used": 27400,
"limit": 70500,
"limitBasis": "500 + 1000 × (pricePerCam × cameraLimit)",
"measured": true,
"source": "self"
},
"rate": { "limitRps": 3, "burst": 6 },
"volumeQuotas": [
{ "bucket": "transfer_bytes", "limit": 60129542144, "used": 5550677, "period": "2026-08", "unit": "bytes" },
{ "bucket": "export_job", "limit": 0, "used": 0, "period": "2026-08", "unit": "count" },
{ "bucket": "export_concurrent", "limit": 1, "used": 0, "period": "2026-08", "unit": "count" }
]
},
"cameras": { "limit": 10, "registered": 13, "overLimit": 3, "overLimitPsns": ["7XBPK0BE00000011", "7XBPK0BE00000012", "7XBPK0BE00000013"] }
}
cameras, beside data, is the camera-seat position — overLimitPsns names
the dashcams currently beyond the subscription's camera count, which answer
403 camera_limit_exceeded for video, SD-card access, recall and reboot; see
Camera seats above.
volumeQuotas always carries all three buckets, including the ones your tier
does not offer (limit: 0). apiCalls.limitBasis spells out the formula
behind limit, and measured / source state that the number is a real
self-metered counter rather than a gateway estimate.
apiCalls.used is the real self-metered call volume for the current
month — the API increments its own per-organization monthly counter on
every authenticated request, and it is the exact counter the
monthly_limit_exceeded enforcement reads — and volumeQuotas[].used
reflects real resource counters.
The same response also carries callHistory (daily call volume over the
last 30 days) and statusBreakdown (the 2xx/4xx/5xx split), so you
can see whether you are trending toward the cap before you hit it. To find
out which calls consumed the quota, list the individual requests with
API Logs.
Best practices
- Don't poll aggressively. For event delivery, a Webhooks push subscription is faster and cheaper; for location tracking, feed cursor polling at intervals of 30 seconds or more (see Pagination) saves quota while staying fresh.
- Burst is a momentary allowance only. Design sustained throughput around the rps figure.
- For batch jobs, cap concurrent requests below the rps limit and always implement 429 backoff.
- Wire
GET /v1/usageinto your monitoring to get alerted before the monthly cap runs out.
Related links
- Vehicles & Dashcams — the device record, camera seats and what
PUTcan change - Webhooks — event delivery without polling
- Pagination — feed cursor tail polling
- Authentication — scopes by tier
- API Logs — the individual requests behind these counters
- Errors ›
camera_limit_exceeded— the 403 an over-limit dashcam answers - Errors — distinguishing
quota_exceededfrom 429