Skip to main content

Rate Limits

API keys are subject to two kinds of limits, set per tier.

  1. Call rate and monthly call volume — requests per second (rps), burst, and a monthly API call cap. Exceeding them returns 429 Too Many Requests.
  2. 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 403

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

Changing plan? Keep your key.

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 changesWhen it takes effect
Scopes, camera limitwithin 5 minutes
rps / burstwithin 5 minutes
Monthly call limit, volume quotasimmediately, 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

TierMax camerasrpsburst
free112
starter113
standard1036
pro991025
enterprise9992550

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 plans500 + (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):

TierCalls per cameraCameras (max)Monthly API calls
free1300
starter3,00013,500
standard7,0001070,500
pro10,00099990,500
enterprise15,00099914,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); positions 1..limit are covered, the rest are over_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 always covered (cameraPosition: null). Every dashcam row carries this as entitlement; the device list and detail also carry cameraPosition, and an over-limit detail adds requiredCameraLimit — 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:
RouteFor an over-limit dashcam
GET /v1/events/{eventId}/video403 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-jobs403 camera_limit_exceeded
POST /v1/devices/{psn}/reboot403 camera_limit_exceeded
POST /v1/devices403 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 byDefault
GET /v1/devices, GET /v1/fleet/locations, …/feed, GET /v1/events, …/feed, GET /v1/safety-events/statsall
GET /v1/fleet/summary, GET /v1/reports/drivingcovered — 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:

BucketDescriptionAllowance / monthUnitAvailable from
transfer_bytesVideo delivered to you — event video URLs and SD-card recalls0.8 GB per $1 (Standard) · 1 GB per $1 (Pro) · 1.2 GB per $1 (Enterprise) of the monthly subscriptionbytesstandard (0 below)
export_jobGPS Export jobs created50 × camerascountpro (0 below)
export_concurrentExport jobs running at oncefree/starter 0 · standard/pro 1 · enterprise 2 (fixed)countstandard

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:

CallCountsWhen
GET /v1/events/{eventId}/videoa 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}/videothe recording's size — sizeBytes as the dashcam reports it, or a flat 8 MB (sub) / 150 MB (main) when it does notchecked 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 filesnothing

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

HeaderWhenValue
Retry-After429 rate_limited (per-second throttle)1
Retry-After429 monthly_limit_exceeded (monthly call cap)seconds until 00:00 UTC on the 1st of next month
Retry-After409 device_busy (dashcam transfer in progress)5
X-Request-Idevery responsecorrelation 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');
}
Decide by status code, not by 429 body parsing

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/usage into your monitoring to get alerted before the monthly cap runs out.