Errors
Response envelope
Every non-2xx response shares the same JSON shape. The envelope carries a stable machine-readable code, a human-readable message, and an optional details payload with field-level information.
json
{
"success": false,
"error": {
"code": "UNPROCESSABLE",
"message": "Equipment is already inducted at this site",
"details": { "code": "equipment_already_inducted" },
"requestId": "8c2f…-ab4d"
}
}requestId is the request identifier — include it when contacting support so the server-side log line can be found.
Localisation
Set the Accept-Language header (en-AU or en-US) to receive a translated message. Error code values are always in English and stable across locales.
Error-code catalogue
The set of codes below is generated from the server's ErrorCode enum — a drift-guard test fails CI if a code is added or removed without updating this page. The HTTP status, meaning, and recovery columns are editorial.
| Code | HTTP | Meaning | Action |
|---|---|---|---|
BAD_REQUEST | 400 | Malformed request — missing header, invalid body shape, etc. | Fix the request body/query/headers and retry. |
VALIDATION_ERROR | 400 | Zod validation of a request field failed. | Inspect the `details` array for per-field errors. |
UNAUTHORIZED | 401 | Missing or invalid authentication token / API key. | Check that `Authorization: Bearer <token>` is present and valid. |
TOKEN_EXPIRED | 401 | Firebase ID token has expired. | Refresh the token client-side and retry. |
TOKEN_INVALID | 401 | Token signature invalid or tenant mismatch. | Ensure the token was issued by the correct Firebase tenant. |
TOKEN_REVOKED | 401 | Token was revoked (e.g., user signed out of all devices). | Re-authenticate. |
AUTH_ERROR | 401 | Login-flow failure — Firebase token unverifiable or user not in an authenticatable state. | Surfaced by `/auth/login` when the supplied ID token is malformed or the user/org is suspended. |
MFA_REQUIRED | 401 | MFA enrollment required — grace period has expired and the user must enroll before continuing. | Surfaced by `/auth/restore`. Carries the grace deadline in `details`. |
SSO_REQUIRED | 401 | Your organization requires single sign-on for this email domain, so password sign-in was refused. | Sign in through your identity provider instead. An administrator enabled SSO enforcement for a domain your account belongs to. |
SESSION_IDLE_EXPIRED | 401 | Server-enforced idle timeout — the session exceeded the maximum inactivity window (AUTH-08). | Re-authenticate; the session was invalidated after a period of inactivity. |
PAYMENT_REQUIRED | 402 | Subscription is past due, suspended, or canceled. | Contact your org owner to update billing. |
SUBSCRIPTION_INACTIVE | 402 | No active subscription on file. | Activate or resume the subscription on the admin console's Billing page. |
FORBIDDEN | 403 | Authenticated but the caller lacks the required role/scope. | Check your API key scopes or your org role (VIEWER/EDITOR/MANAGER/OWNER). |
SESSION_MAX_AGE_EXCEEDED | 403 | The session has been open longer than your organisation's configured session window and must be re-established, regardless of how recently it was used. | Sign in again. This is not an MFA failure and not an idle timeout — it can fire on a session that stayed active the whole time, and it applies to callers who hold no second factor. |
EMAIL_NOT_VERIFIED | 403 | The signed-in user has not verified their email. | Complete email verification from the account settings page. |
INSUFFICIENT_ROLE | 403 | Your org role is below the minimum required for this endpoint. | Ask an OWNER/MANAGER to elevate your role. |
SYSTEM_ROLE_REQUIRED | 403 | Endpoint is restricted to PSG support staff (cross-tenant admin). | This endpoint is not part of the public API surface. |
KIOSK_NOT_ALLOWED | 403 | Kiosk operators cannot invoke this endpoint. | Use a user account with a console role instead. |
FEATURE_DISABLED | 403 | The required product feature is not enabled on this organization. | Contact your org owner to enable the feature, or see /api (Feature-gated endpoints). |
OPERATIONAL_TOGGLE | 400 | The targeted feature flag is an operational toggle (e.g. `feedback`) and cannot be overridden per-org. | Operational toggles are managed via deployment env vars, not the feature-overrides admin UI. |
GLOBAL_FEATURE_DISABLED | 400 | The targeted feature flag is disabled globally via env kill-switch; per-org overrides cannot enable it. | Re-enable the flag at the deployment level (FEATURE_*) before applying per-org overrides. |
NOT_FOUND | 404 | Resource does not exist, OR exists in a different tenant (cross-tenant access is masked as 404). | Verify the resource ID and the `X-Org-Id` header. |
RESOURCE_NOT_FOUND | 404 | Typed resource-not-found (same semantics as NOT_FOUND, with a resource-type hint in `details`). | Inspect `details.resourceType` to identify which lookup missed. |
CONFLICT | 409 | Concurrency conflict or generic uniqueness violation. | Re-fetch the resource and retry. |
DUPLICATE_ENTRY | 409 | Uniqueness constraint violated (e.g., duplicate tag code). | Choose a different value for the unique field. |
COMMAND_IN_FLIGHT | 409 | A conflicting command to the target device is already pending. | Wait for the in-flight command to complete or be ack'd, then retry. |
IDEMPOTENCY_CONFLICT | 409 | A request under this Idempotency-Key is still running, or it already succeeded and returned a one-time secret, which is never replayed. | Retry once the first request finishes. For a one-time secret, see Idempotency. |
CONCURRENT_MODIFICATION | 409 | Another writer modified the resource between your read and write. | Re-read the resource and retry with the latest `updatedAt` / `version`. |
TAG_DEACTIVATED | 410 | NFC tag has been decommissioned (`isActive=false`) and is intentionally no longer in service. Distinct from 404 so mobile clients can render a dedicated "Tag deactivated" UX instead of the generic "Unknown tag" branch. | Surface the deactivation state to the operator and treat the tag as retired; do not retry with the same code. |
UNPROCESSABLE | 422 | Business rule violation — validation passed but the action is not allowed in the current state. | Check the error `details` payload for the specific rule that failed. |
BUSINESS_RULE | 422 | Domain-level constraint hit (typed; `details.code` identifies the rule). | See `details.code` for the specific rule that fired. |
SUBSCRIPTION_LIMIT_EXCEEDED | 422 | Your plan caps this resource count and the cap is full. For machines, the count is the active machines the organization owns plus those lent to it, a lend counting from the moment it is booked. An import that would pass the cap is refused whole. `details` carries `limit`, `current` and `tier`, plus `adding` for an import. Reactivating a machine that is out on hire also checks the operating organization's cap; that refusal carries no `details` and names the operating organization's count and limit in the message, as "(5 of 5)". | Upgrade the plan, or set unused machines inactive or delete them. For a machine out on hire, the operating organization has to make room. |
EQUIPMENT_NOT_ACTIVE | 422 | The target equipment record is not in an active state (decommissioned, archived, etc.). | Reactivate or choose a different piece of equipment. |
INVALID_STATE_TRANSITION | 422 | The requested state change is not allowed from the current state. | Inspect `details` for the allowed transitions. |
TIER_CHANGE_REQUIRES_STRIPE | 422 | The organisation has a live Stripe subscription, so its plan cannot be changed directly. | Change the plan's Price in Stripe instead; the subscription webhook reconciles the plan. |
TIER_CHANGE_NOT_ALLOWED | 422 | The Hire plan is immutable — an organization cannot change to or from it. | No retry — this plan change is not supported on any path; contact sales. |
BILLING_CONSENT_REQUIRED | 422 | Commissioning this billable device grows the monthly bill, and the request did not acknowledge the charge. | Show the monthly delta from `details`, then re-send with `acknowledgeBilling: true`. |
CHECKOUT_REQUIRED | 422 | The organization has no Stripe subscription to add this billable device to. | Complete the device-only checkout first, then retry commissioning. |
ADDON_SOURCE_FORBIDDEN | 400 | Add-ons granted through the admin API must use source 'contract'. | 'stripe' is written only by the billing webhook — omit the field or send 'contract'. |
ADDON_REQUIRES_PAID_PLAN | 400 | Add-ons are available on paid plans only; the organisation is on Plant Operator. | Move the organisation to a paid plan (or start a trial) before granting an add-on. |
ADDON_NOT_APPLICABLE_TO_PLAN | 400 | The add-on does not apply to the organisation's plan (fleet operations is Hire-only). | Grant it only to a Hire organisation; the other plans already hold or cannot use what it grants. |
ADDON_REVOKE_SOURCE_FORBIDDEN | 422 | The add-on is backed by a live Stripe subscription item and cannot be revoked here. | Remove the subscription item in Stripe; the webhook revokes the grant when it disappears. |
APP_VERSION_TOO_OLD | 426 | The client app version is below the minimum supported. | Upgrade the mobile/web client. |
RATE_LIMITED | 429 | Tier rate limit exceeded. | Back off and retry respecting `Retry-After`. Upgrading tier raises the limit. |
INTERNAL_ERROR | 500 | Unexpected server-side error. | Retry with backoff; persistent failures should be reported to support@prestartgo.com. |
SERVICE_UNAVAILABLE | 503 | The API is temporarily unable to serve requests (maintenance / transient DB outage). | Retry with exponential backoff. |
DEPENDENCY_UNAVAILABLE | 503 | A downstream dependency (Stripe, Firebase, etc.) is unreachable. | Retry later; check the PSG status page for incident reports. |