API errors
Every error the Polyfence API can return: the machine code to branch on, the HTTP status, the human message, when it fires, and what to do. Use this when writing retry logic, error handling, or onboarding integrations.
HTTP status codes
Polyfence uses standard HTTP semantics. The status tells you the failure class; the code field tells you the specific, stable cause — branch on code, never on the human message.
| Status | Meaning |
|---|---|
| 200/201/204 | Success. 200 for GET, 201 for resource creation, 204 for successful deletion. |
| 400 | Bad request — validation_error / invalid_json / invalid_id. |
| 401 | Unauthorized — unauthenticated / invalid_key. |
| 403 | Forbidden — forbidden (scope/role), quota_exceeded (tier quota), or feature_unavailable. |
| 404 | Not found — resource missing OR not accessible to your tenant (intentionally indistinguishable). |
| 409 | Conflict — duplicate, stale state, or in-progress operation. |
| 413 | Payload too large — payload_too_large. |
| 422 | Unprocessable — unsupported_format (understood but not parseable, e.g. no zones in an import). |
| 429 | Rate limited — rate_limited, per-second cap. Includes a Retry-After header. (Tier quotas are 403 quota_exceeded, NOT 429.) |
| 500 | Internal server error — internal_error. Report to hello@polyfence.io with the request ID. |
| 502 | Upstream error — upstream_error, an external fetch/sync failed (e.g. connector source). |
| 504 | Gateway timeout — typically a connector sync exceeded its time budget. Infrastructure-level (no JSON envelope). |
Error envelope
Every error response is the same flat shape. Branch on the machine-readable code (a closed enum) — never parse the human message. error is a short title (the category); message is the specific, human-readable detail (free to change/localise).
{
"success": false,
"code": "not_found",
"error": "Not found",
"message": "Zone not found"
}
// Validation errors add an "errors" array with field-level detail:
{
"success": false,
"code": "validation_error",
"error": "Validation error",
"message": "Zone data validation failed",
"errors": [
{ "field": "radius_meters", "rule": "min", "value": 0 }
]
}
// Quota errors carry tier context on the root for the upgrade flow:
{
"success": false,
"code": "quota_exceeded",
"error": "Quota exceeded",
"message": "Zone limit reached",
"current": 100, "limit": 100, "tier": "free", "upgradeUrl": "/account?tab=plan"
}The full list of code values is a closed enum — see the typed ErrorCode in the OpenAPI spec and the generated SDKs (v2.0.0+).
Authentication
Most endpoints accept either a session cookie or an `x-api-key` header (dual auth). A few are session-only (e.g. device provisioning, account deletion) and a few are key-only — the row below names the case where it matters. Authentication is checked BEFORE body/parameter validation, so a malformed request with no/invalid key returns an auth error, not a validation error.
unauthenticatedAuthentication required. Provide either a valid session token or x-api-key header.- When
- Request hit a dual-auth endpoint with no session cookie AND no x-api-key header.
- Fix
- Pass your API key via x-api-key (case-insensitive). Generate one at /account?tab=keys.
invalid_keyInvalid API key.- When
- The x-api-key value doesn't match any stored key (SHA-256 hash lookup miss), or is malformed (no prefix).
- Fix
- Verify the key is copied correctly — keys have a `pf_`/`pt_` prefix. If unsure, regenerate at /account?tab=keys.
unauthenticatedUnauthorized- When
- Generic auth failure on a single-mode (API-key-only or session-only) endpoint.
- Fix
- Same as above — check your auth method matches the endpoint's requirement.
forbiddenInsufficient permissions. Required scopes: …- When
- Your key authenticated, but doesn't have the scope required for this endpoint (e.g. attempted DELETE with a `zones:read` key).
- Fix
- Issue a new key with the required scope. Scopes are immutable on existing keys.
forbiddenKey not authorised for this device- When
- Device-bound key attempted to access a device_id it isn't bound to.
- Fix
- Use the device-bound key only for its target device. Use a tenant-scope key for cross-device operations.
unauthenticatedSession required. Sign in on the dashboard to provision a device.- When
- Provisioning endpoint hit with an API key instead of a dashboard session.
- Fix
- Provisioning is dashboard-only — sign in at polyfence.io/auth/login first.
Rate limits
Polyfence enforces 50 requests per second per identifier during Early Access. `rate_limited` responses are 429 and include a Retry-After header. NOTE: monthly tier quotas are a SEPARATE failure class — they return 403 `quota_exceeded` (see Tier limits), not 429. A 429 always means “slow down,” never “out of quota.”
rate_limitedRate limit exceeded (… req/sec on … tier). Upgrade your plan for higher limits.- When
- You exceeded the per-second request cap from a single API key or session.
- Fix
- Read the Retry-After header and back off. Implement exponential backoff with jitter for retries.
rate_limitedToo many requests. Please try again shortly.- When
- Per-endpoint stricter limit hit (e.g. checkout endpoint has a tighter cap to prevent abuse).
- Fix
- Wait per Retry-After and retry.
rate_limitedToo many checkout attempts. Please try again shortly.- When
- Checkout endpoint specifically — capped tighter than the general rate limit.
- Fix
- Wait at least 60s before retrying. If checkout repeatedly fails, contact support.
Tier limits
Polyfence's tier system enforces quotas on zones, API calls, and active API keys. Hits return 403 `quota_exceeded` (distinct from 429 `rate_limited`, so clients can tell “out of quota” from “slow down”). During Early Access most quotas are unlimited; the pricing flip moves these to Free/Pro tier values.
quota_exceededZone limit reached- When
- You attempted to create a zone when your account is at the tier's zone cap.
- Fix
- Delete unused zones, or contact hello@polyfence.io to upgrade.
quota_exceededZone limit would be exceeded …- When
- Bulk import would push your zone count above the tier cap.
- Fix
- Reduce the import batch, delete unused zones, or upgrade.
quota_exceededMonthly API call limit exceeded (…/…). Upgrade your plan for more API calls.- When
- Your monthly API call quota is exhausted for the billing period. (Was 429 before 2026-06-12 — now 403 to distinguish from per-second rate limiting.)
- Fix
- Wait until the next billing cycle, or upgrade for a higher cap.
quota_exceededAPI key limit reached- When
- You already have the maximum number of active API keys (10 by default).
- Fix
- Revoke an unused key at /account?tab=keys before creating a new one.
feature_unavailableBulk import is not available during Early Access at your current usage level. Contact hello@polyfence.io if you need this feature.- When
- A feature gated by your tier/Early-Access flag was used (e.g. POST /zones/bulk-import without the bulkImportUI feature).
- Fix
- The feature isn't enabled for your tier yet — contact hello@polyfence.io. Distinct from quota_exceeded (you're not out of quota; the capability is gated off).
Validation
Request bodies are validated with Zod schemas. Validation failures return 400 `validation_error` (or a more specific code like `invalid_json` / `invalid_id`) with a message naming the offending field or rule.
invalid_jsonRequest body is not valid JSON.- When
- Request body isn't valid JSON.
- Fix
- Verify Content-Type: application/json and that the body is well-formed.
validation_errorValidation failed- When
- Zod schema rejected one or more fields. Response includes an `errors` array with per-field detail.
- Fix
- Inspect the `errors` array; each entry names the field and the rule that failed.
validation_errorZone data validation failed- When
- Zone payload failed deep validation (e.g. polygon self-intersects, radius out of range, coordinate out of [-90, 90] or [-180, 180]).
- Fix
- POST the payload to /api/zones/validate first to surface the exact violation, or inspect the `errors` array on the response.
validation_errorCircle zones require center coordinates and radius- When
- POST /zones with type=circle but missing center_lat, center_lng, or radius_meters.
- Fix
- Include all three fields for circle zones.
validation_errorPolygon zones require polygon coordinates- When
- POST /zones with type=polygon but missing or empty polygon coordinates.
- Fix
- Provide a polygon with at least 3 [lng, lat] pairs.
validation_errorInvalid dev_eui — must be 16 hexadecimal characters- When
- Device provisioning payload had a malformed DevEUI.
- Fix
- DevEUI must be exactly 16 hex characters (case-insensitive).
invalid_idZone ID must be a valid UUID- When
- zone_id parameter isn't a valid UUID.
- Fix
- Zone IDs are UUIDs (e.g. `123e4567-e89b-12d3-a456-426614174000`).
validation_errorInvalid scopes provided- When
- API key creation request included scopes not in the allowed set.
- Fix
- Use scopes from: zones:read, zones:write, zones:delete, zones:* (and admin:* for admin keys).
validation_errorMissing Idempotency-Key header (required for deduplication)- When
- An idempotent endpoint (e.g. provisioning) was called without an Idempotency-Key header.
- Fix
- Generate a UUID per logical operation and pass it as Idempotency-Key — retries with the same key are deduplicated.
unsupported_formatNo zones found in the data- When
- POST /zones/import fetched the source successfully but found no parseable zones in it.
- Fix
- Check the source returns GeoJSON/KML with at least one feature. 422 means the body was understood but unprocessable.
validation_errorOne or more zone_ids are not accessible to this account- When
- Bulk operation referenced zone IDs that don't belong to your tenant.
- Fix
- Filter the list to zones your account owns. The error doesn't reveal which IDs failed (no enumeration).
validation_errorToo many zones in bulk import- When
- Bulk import payload exceeds the 500-zone cap.
- Fix
- Split into multiple batches of ≤500 zones each.
Not found
Polyfence returns 404 `not_found` both for genuinely missing resources and for cross-tenant access attempts. The two are intentionally indistinguishable to avoid resource enumeration.
not_foundZone not found- When
- Zone ID doesn't exist, or exists under a different tenant (`profile_user_id` filter doesn't match).
- Fix
- Verify the zone belongs to your account. Listing GET /zones shows everything your key can access.
not_foundDevice not found- When
- Device ID doesn't exist or belongs to a different tenant.
- Fix
- Same — verify the device is provisioned to your account.
not_foundProfile not found for authenticated user- When
- The authenticated user has no row in the profiles table (rare — usually a sync issue with Supabase trigger).
- Fix
- Sign out and sign back in to trigger the profile-creation hook. If it persists, contact support.
not_foundNot found- When
- Generic 404 for unmatched routes or missing related resources (e.g. an event whose parent zone was deleted).
- Fix
- Check the URL path and any referenced IDs.
not_foundAssignment not found- When
- Device-to-zone assignment lookup missed.
- Fix
- Verify the device is currently assigned to the zone via GET /v1/devices/{deviceId}.
Conflict / idempotency
Mostly relevant to device provisioning and bulk operations. 409 `conflict` responses signal a state collision rather than a malformed request.
conflictYou already have a zone named “…”. Pick a different name to keep your zones distinguishable.- When
- POST /zones with a name that's already used in your account (the unique constraint is per-tenant).
- Fix
- Pick a unique name. Zone names don't need to be globally unique — only within your account.
conflictDevEUI collision — retry the provisioning request- When
- The auto-generated DevEUI collided with an existing device (rare; ~2^-64 odds per attempt).
- Fix
- Retry with the same Idempotency-Key — provisioning will pick a fresh DevEUI.
conflictProvisioning already in progress for this device — retry- When
- A concurrent provisioning request for the same device is still completing.
- Fix
- Wait ~1s and retry with the same Idempotency-Key.
Connector / SSRF
Data connector endpoints reject URLs targeting private networks, localhost, link-local, and cloud metadata services. Validation runs both at config-save time AND at fetch time (DNS rebinding protection). These reject with 400 `validation_error`; an oversized fetched body returns 413 `payload_too_large`.
validation_errorOnly HTTP and HTTPS protocols are allowed- When
- Connector URL uses a non-HTTP scheme (file://, ftp://, gopher://, etc.).
- Fix
- Use https:// in production. http:// is allowed but discouraged.
validation_errorCannot connect to localhost- When
- Resolved DNS pointed at 127.0.0.1, ::1, or localhost.
- Fix
- Use a publicly addressable host. Connectors run from Polyfence's infrastructure — your localhost isn't reachable.
validation_errorCannot connect to private IP address (10.x.x.x)- When
- Resolved DNS pointed at the 10.0.0.0/8 RFC1918 block.
- Fix
- Expose the service publicly (Cloudflare Tunnel, ngrok, or a public ingress) and use that hostname.
validation_errorCannot connect to private IP address (172.16-31.x.x)- When
- DNS resolved into 172.16.0.0/12.
- Fix
- Same — expose publicly.
validation_errorCannot connect to private IP address (192.168.x.x)- When
- DNS resolved into 192.168.0.0/16.
- Fix
- Same — expose publicly.
validation_errorCannot connect to link-local address- When
- DNS resolved into 169.254.0.0/16 (link-local) or fe80::/10 (IPv6 link-local).
- Fix
- Use a routable address.
validation_errorCannot connect to cloud metadata service- When
- DNS resolved into 169.254.169.254 (AWS/GCP/Azure metadata IP).
- Fix
- These IPs are blocked unconditionally — they're an SSRF risk regardless of cloud provider.
validation_errorInvalid URL format- When
- URL didn't parse as a valid HTTP(S) URL.
- Fix
- Use a standard URL form: `https://host[:port]/path?query`.
payload_too_largeResponse too large. Maximum size is 10MB.- When
- Connector source returned a response body larger than 10MB.
- Fix
- Paginate at the source or use a connector that streams.
upstream_errorFailed to fetch URL: … (upstream status)- When
- The connector/import source returned a non-2xx response or was unreachable (e.g. POST /zones/import against a URL that 5xx'd or timed out).
- Fix
- Verify the source URL is reachable and returns 2xx. 502 means the failure is upstream of Polyfence, not in your request.
Subscription
Billing-related endpoints (currently routed via Polar). Most users won't hit these during Early Access.
validation_errorInvalid tier for checkout- When
- Checkout request named a tier that doesn't have a Polar product configured.
- Fix
- Use `pro` as the tier. Other tiers aren't wired for self-service checkout yet.
internal_errorNo product configured for this tier.- When
- POLAR_PRO_PRODUCT_ID env is unset on the server.
- Fix
- Contact hello@polyfence.io — this is a configuration issue on our side, not yours.
conflictYou are already on this plan.- When
- Checkout attempted for the tier the account is currently on.
- Fix
- No action needed — the API is preventing a redundant subscription.
not_foundNo subscription to reactivate.- When
- Reactivation endpoint hit but the account has no canceled subscription on record.
- Fix
- Start a fresh checkout via POST /api/checkout if you want to subscribe.
Looking for the rest of the docs? Back to developer documentation.