Polyfence is in Early Access — APIs and limits may still change. Your feedback shapes what we build.

Skip to content

Error catalogue

Every error the API can return.

The machine code to branch on, the status it arrives with, why it fired and what to do about it. Written for the moment you have one in your hand.

15 codes, and the set is closed — branch on all 15 and you have covered every error the API itself returns. What it cannot cover is a request that never reached us. Check the status before you read the body.

The envelope

One shape, every time.

Every failure returns the same four fields, in the same flat shape. There is no nested error object to unwrap. Some errors add fields alongside these four — never instead of them.

Every errorthe four fields, always
{
  "success": false,
  "code":    "not_found",
  "error":   "Not found",
  "message": "Zone not found"
}
Validationadds a field-level errors array
{
  "success": false,
  "code":    "validation_error",
  "error":   "Validation error",
  "message": "Zone data validation failed",
  "errors":  [
    { "field": "radius_meters", "rule": "min", "value": 0 }
  ]
}
Quotaadds tier context on the root
{
  "success":    false,
  "code":       "quota_exceeded",
  "error":      "Quota exceeded",
  "message":    "Zone limit reached",
  "current":    100,
  "limit":      100,
  "tier":       "free",
  "upgradeUrl": "mailto:hello@polyfence.io"
}

successAlways false on an error. The one field you can test without knowing anything else.

codeThe stable discriminator. Branch on this — it is drawn from a closed enum and does not change.

errorA short title for the class of failure. For display, not for logic.

messageThe specific detail. Free to change and free to localise, so never parse it.

+ extrasSome errors carry more on the root. A validation failure adds errors, one entry per field and rule; a quota failure adds current, limit, tier and upgradeUrl. The four above are always there — treat anything else as additive.

The 15 values are a closed enum. Rather than typing them out, take the typed ErrorCode from the OpenAPI spec → — it is the same source this page is generated from, and it is in every generated SDK from 2.0.0.

The closed set

15 codes, and no sixteenth.

The whole enum, on one screen. The last two are ours or our upstream’s rather than yours — a 502 means a source you pointed us at failed, not Polyfence. Two more are easy to confuse: quota_exceeded is a monthly allowance running out, rate_limited is a per-second cap. They do not retry the same way.

CodeStatusTitleFires in

Your request

unauthenticated401Unauthorized3 situations
invalid_key401Unauthorized1 situation
forbidden403Forbidden2 situations
feature_unavailable403Feature unavailable1 situation
quota_exceeded403Quota exceeded4 situations
rate_limited429Too many requests3 situations
validation_error400Validation error21 situations
invalid_json400Invalid JSON1 situation
invalid_id400Invalid identifier1 situation
unsupported_format422Unsupported format1 situation
not_found404Not found4 situations
conflict409Conflict3 situations
payload_too_large413Payload too large1 situation

Ours, or upstream of us

upstream_error502Upstream error1 situation
internal_error500Internal error1 situation

Outside the envelope. A 504 comes from the gateway, not from Polyfence — usually a connector sync that ran past its time budget. There is no code, and no JSON to parse. Treat a response you cannot parse as a retryable infrastructure failure, not a client error. To make one traceable, send your own x-request-id — any UUID v4 — and quote it to hello@polyfence.io.

Where they come from

One code, many situations.

A code names the class of failure, not what you did. These are the situations, grouped by what you were trying to do — 48 of them, every message quoted as the API actually returns it.

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.

  • unauthenticated401Authentication 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.
    What to do
    Pass your API key via x-api-key (case-insensitive). Generate one at /account?tab=keys.
  • invalid_key401Invalid API key.
    When
    The x-api-key value doesn't match any stored key (SHA-256 hash lookup miss), or is malformed (no prefix).
    What to do
    Verify the key is copied correctly — keys have a pf_/pt_ prefix. If unsure, regenerate at /account?tab=keys.
  • unauthenticated401Unauthorized
    When
    Generic auth failure on a single-mode (API-key-only or session-only) endpoint.
    What to do
    Same as above — check your auth method matches the endpoint's requirement.
  • forbidden403Insufficient 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).
    What to do
    Issue a new key with the required scope. Scopes are immutable on existing keys.
  • forbidden403Key not authorised for this device
    When
    Device-bound key attempted to access a device_id it isn't bound to.
    What to do
    Use the device-bound key only for its target device. Use a tenant-scope key for cross-device operations.
  • unauthenticated401Session required. Sign in on the dashboard to provision a device.
    When
    Provisioning endpoint hit with an API key instead of a dashboard session.
    What to do
    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_limited429Rate limit exceeded (… req/sec on … tier). Contact hello@polyfence.io to raise it.
    When
    You exceeded the per-second request cap from a single API key or session.
    What to do
    Read the Retry-After header and back off. Implement exponential backoff with jitter for retries.
  • rate_limited429Too many requests. Please try again shortly.
    When
    Per-endpoint stricter limit hit (e.g. checkout endpoint has a tighter cap to prevent abuse).
    What to do
    Wait per Retry-After and retry.
  • rate_limited429Too many checkout attempts. Please try again shortly.
    When
    Checkout endpoint specifically — capped tighter than the general rate limit.
    What to do
    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_exceeded403Zone limit reached
    When
    You attempted to create a zone when your account is at the tier's zone cap.
    What to do
    Delete unused zones, or contact hello@polyfence.io to raise the limit.
  • quota_exceeded403Zone limit would be exceeded …
    When
    Bulk import would push your zone count above the tier cap.
    What to do
    Reduce the import batch, delete unused zones, or contact hello@polyfence.io to raise the limit.
  • quota_exceeded403Monthly API call limit exceeded (…/…). Contact hello@polyfence.io to raise it.
    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.)
    What to do
    Wait until the next billing cycle, or contact hello@polyfence.io to raise the cap.
  • quota_exceeded403API key limit reached
    When
    You already have the maximum number of active API keys (10 by default).
    What to do
    Revoke an unused key at /account?tab=keys before creating a new one.
  • feature_unavailable403Bulk 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).
    What to do
    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_json400Request body is not valid JSON.
    When
    Request body isn't valid JSON.
    What to do
    Verify Content-Type: application/json and that the body is well-formed.
  • validation_error400Validation failed
    When
    Zod schema rejected one or more fields. Response includes an errors array with per-field detail.
    What to do
    Inspect the errors array; each entry names the field and the rule that failed.
  • validation_error400Zone 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]).
    What to do
    POST the payload to /api/zones/validate first to surface the exact violation, or inspect the errors array on the response.
  • validation_error400Circle zones require center coordinates and radius
    When
    POST /zones with type=circle but missing center_lat, center_lng, or radius_meters.
    What to do
    Include all three fields for circle zones.
  • validation_error400Polygon zones require polygon coordinates
    When
    POST /zones with type=polygon but missing or empty polygon coordinates.
    What to do
    Provide a polygon with at least 3 [lng, lat] pairs.
  • validation_error400Invalid dev_eui — must be 16 hexadecimal characters
    When
    Device provisioning payload had a malformed DevEUI.
    What to do
    DevEUI must be exactly 16 hex characters (case-insensitive).
  • invalid_id400Zone ID must be a valid UUID
    When
    zone_id parameter isn't a valid UUID.
    What to do
    Zone IDs are UUIDs (e.g. 123e4567-e89b-12d3-a456-426614174000).
  • validation_error400Invalid scopes provided
    When
    API key creation request included scopes not in the allowed set.
    What to do
    Use scopes from: zones:read, zones:write, zones:delete, zones:* (and admin:* for admin keys).
  • validation_error400Missing Idempotency-Key header (required for deduplication)
    When
    An idempotent endpoint (e.g. provisioning) was called without an Idempotency-Key header.
    What to do
    Generate a UUID per logical operation and pass it as Idempotency-Key — retries with the same key are deduplicated.
  • unsupported_format422No zones found in the data
    When
    POST /zones/import fetched the source successfully but found no parseable zones in it.
    What to do
    Check the source returns GeoJSON/KML with at least one feature. 422 means the body was understood but unprocessable.
  • validation_error400One or more zone_ids are not accessible to this account
    When
    Bulk operation referenced zone IDs that don't belong to your tenant.
    What to do
    Filter the list to zones your account owns. The error doesn't reveal which IDs failed (no enumeration).
  • validation_error400Too many zones in bulk import
    When
    Bulk import payload exceeds the 500-zone cap.
    What to do
    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_found404Zone not found
    When
    Zone ID doesn't exist, or exists under a different tenant (profile_user_id filter doesn't match).
    What to do
    Verify the zone belongs to your account. Listing GET /zones shows everything your key can access.
  • not_found404Device not found
    When
    Device ID doesn't exist or belongs to a different tenant.
    What to do
    Same — verify the device is provisioned to your account.
  • internal_error500Profile not found for authenticated user
    When
    PATCH /v1/account/retention matched no row in profiles for the authenticated user — the account exists in auth but its profile row does not.
    What to do
    Sign out and back in to trigger the profile-creation hook. If it persists, contact hello@polyfence.io — a 500 here means the two stores disagree, not that you asked for something missing.
  • not_found404Not found
    When
    Generic 404 for unmatched routes or missing related resources (e.g. an event whose parent zone was deleted).
    What to do
    Check the URL path and any referenced IDs.
  • not_found404Assignment not found
    When
    Device-to-zone assignment lookup missed.
    What to do
    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.

  • conflict409This workspace already has a zone named “…”. Pick a different name so they stay easy to tell apart.
    When
    POST /zones with a name already used in the same workspace (the unique constraint is per workspace).
    What to do
    Pick a different name. Zone names only need to be unique inside the workspace, and the clash may be with a zone a colleague created.
  • conflict409DevEUI collision — retry the provisioning request
    When
    The auto-generated DevEUI collided with an existing device (rare; ~2^-64 odds per attempt).
    What to do
    Retry with the same Idempotency-Key — provisioning will pick a fresh DevEUI.
  • conflict409Provisioning already in progress for this device — retry
    When
    A concurrent provisioning request for the same device is still completing.
    What to do
    Wait ~1s and retry with the same Idempotency-Key.

Connectors and imports

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_error400Only HTTP and HTTPS protocols are allowed
    When
    Connector URL uses a non-HTTP scheme (file://, ftp://, gopher://, etc.).
    What to do
    Use https:// in production. http:// is allowed but discouraged.
  • validation_error400Cannot connect to localhost
    When
    Resolved DNS pointed at 127.0.0.1, ::1, or localhost.
    What to do
    Use a publicly addressable host. Connectors run from Polyfence's infrastructure — your localhost isn't reachable.
  • validation_error400Cannot connect to private IP address (10.x.x.x)
    When
    Resolved DNS pointed at the 10.0.0.0/8 RFC1918 block.
    What to do
    Expose the service publicly (Cloudflare Tunnel, ngrok, or a public ingress) and use that hostname.
  • validation_error400Cannot connect to private IP address (172.16-31.x.x)
    When
    DNS resolved into 172.16.0.0/12.
    What to do
    Same — expose publicly.
  • validation_error400Cannot connect to private IP address (192.168.x.x)
    When
    DNS resolved into 192.168.0.0/16.
    What to do
    Same — expose publicly.
  • validation_error400Cannot connect to cloud metadata service
    When
    DNS resolved into 169.254.169.254 (AWS/GCP/Azure metadata IP).
    What to do
    These IPs are blocked unconditionally — they're an SSRF risk regardless of cloud provider.
  • validation_error400Invalid URL format
    When
    URL didn't parse as a valid HTTP(S) URL.
    What to do
    Use a standard URL form: https://host[:port]/path?query.
  • payload_too_large413Response too large. Maximum size is 10MB.
    When
    Connector source returned a response body larger than 10MB.
    What to do
    Paginate at the source or use a connector that streams.
  • upstream_error502Failed 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).
    What to do
    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_error400Invalid tier for checkout
    When
    POST /api/checkout named a tier other than pro.
    What to do
    Use pro as the tier. Other tiers aren't wired for self-service checkout yet.
  • validation_error400No product configured for this tier.
    When
    The requested tier has no Polar product id on the server, so there is nothing to check out or change to.
    What to do
    Contact hello@polyfence.io — this one is a configuration issue on our side, not a malformed request, despite the 400.
  • validation_error400You are already on this plan.
    When
    POST /api/subscription/change named the tier the account is already on.
    What to do
    No action needed — the API is refusing a redundant change.
  • validation_error400No subscription to reactivate.
    When
    POST /api/subscription/change with action: "uncancel" on an account with no Polar subscription on record.
    What to do
    Start a fresh checkout via POST /api/checkout if you want to subscribe.

Still stuck?

If the code and the message do not explain it, that is on us to fix, not you to work around.

Or email hello@polyfence.io with the code, the endpoint, and your x-request-id.

API error codes and messages — Polyfence