Error codes and known API pitfalls
This page documents real integration issues that have occurred in production or testing.
Schema
| Error Code | Cause | Solution / Bug |
|---|---|---|
404 Website not found | Some website sub-endpoints previously accepted only the legacy numeric website ID, even though the docs showed websitePublicId. | Fixed. GET /api/websites/:websitePublicId/check-history and GET /api/websites/:websitePublicId/uptime-stats now accept public IDs and still keep legacy-ID compatibility. |
403 Forbidden on GET /api/organizations/:organizationPublicId | The route previously interpreted the path parameter directly as a number. A UUID therefore failed during permission checks. | Fixed. Organization detail and billing routes now resolve publicId correctly. |
400 Bad Request with ZodError on customer-ips or customer-domains | Query parameters like organizationId=, page=, or perPage= were sent as empty strings. Zod coerced them to 0, which then violated min constraints. | Fixed for organizationId, page, and perPage. Optional still means: omit unused parameters instead of sending empty strings. |
Monitor ownership error codes
Since the managed vs. self-service model, monitor write endpoints of all types (website, DNS, ICMP, SMTP, SSH, FTP, IMAP/POP, domain, DNSBL) can return these 403 codes in data.code:
| Code | Meaning |
|---|---|
managed_by_organization | A customer-scoped caller tried to edit/delete a managed monitor without the canEditManaged exception. Open a change request instead. |
selfServiceNotAllowed | A customer-scoped caller tried to create a monitor but the customer's resolved allowSelfService is false. |
selfServiceQuotaReached | Creating (or flipping to) a self_service monitor would exceed the customer's maxSelfServiceUrls: the quota counts all monitor types together. |
managementTypeOrgOnly | A non-org-admin tried to change a monitor's managementType. Class flips are organization-admin-only, and since 04.09.2026 that is enforced against customer-scoped API tokens too: such a token is synthesized with an admin role but restricted scope, and the flip now requires unrestricted scope. |
readonlyMayNotDelete | A caller with the readonly role tried to delete a monitor. Deleting is closed to that role on every ownership class, self_service included, and an API token issued by a read-only user inherits the restriction. Editing a self_service monitor stays permitted. |
tooManyOpenRequests | (429) The customer already has 10 open change requests. |
Monitor quota error codes
An organization has a monitor quota. Creating a monitor while that quota is exhausted either fails, or, when the organization has automatic quota upgrading switched on, moves it to the next quota tier - and that tier has a higher monthly price, with the difference billed for the rest of the current period.
| Code | Meaning |
|---|---|
monitorQuotaReached | (403) The organization is at its monitor quota and does NOT upgrade automatically. Free up a monitor or raise the quota. |
monitorQuotaNoHigherTier | (403) The organization is at its quota, upgrades automatically, and is already on the highest predefined tier. Contact support. |
paidQuotaUpgradeNotAuthorized | (403) The call would have triggered a PAID upgrade and did not authorize one. This can only happen on an agent (MCP) connection: a person in the dashboard sees the tier and its price, a model does not, so a billing effect starts opted out. The message names the tier and the monthly price the call would have bought. Send allowPaidQuotaUpgrade: true once a person has agreed to the higher bill, or free up a monitor first. Every other kind of caller, including an API token, is unaffected and never sees this code. |
Customer package error codes
POST /api/customers and PATCH /api/customers/:customerPublicId resolve the package a customer is on. Both reject a package the organization does not have:
| Code | Meaning |
|---|---|
invalidPackageType | (400) The packageId does not belong to this organization, or the legacy packageType matched neither a configured package key nor a package display name of this organization. |
PATCH previously stored an unknown packageType verbatim and left packageId empty. Such a customer was attached to no package config at all, which left its data retention unresolvable. Send packageId, or a packageType that exists on the organization.
Website check-interval error codes
checkInterval is capped by the monitor's type, because the two meanings differ: for actively scheduled monitors it is the polling rate and stays at 1-1440 minutes (24 hours), while for heartbeat monitors it is the expected ping interval and accepts 1-43200 minutes (30 days), a monthly cron job is a legitimate monitor.
| Code | Meaning |
|---|---|
checkIntervalTooLow | (400) POST /api/websites: checkInterval is below the minimum the customer's package allows. The statusMessage names the minimum. |
checkIntervalTooHigh | (400) PATCH /api/websites/:id: checkInterval exceeds the ceiling for the monitor's type. The type is taken from monitoringType when the request sends it and from the stored monitor otherwise, so raising the interval past 1440 on a non-heartbeat monitor is rejected even when the request omits monitoringType. |
invalidCheckInterval | (400) PATCH /api/websites/:id (partial update without customerId, name and url): checkInterval is not an integer within 1 and the type's ceiling. The package minimum applies here too and answers checkIntervalTooLow. |
invalidTimeout | (400) PATCH /api/websites/:id (partial update): timeoutSeconds is not an integer between 1 and 60. |
invalidCheckConfig | (400) PATCH /api/websites/:id (partial update): a check field is outside its range, e.g. sslNoticeDays above 365 or minPageSize below 0. The six check fields, checkDomainExpiryEnabled, the SSL and domain-expiry thresholds and the page-size bounds are applied by a partial update; the package clamps them the same way it does on a full update. |
invalidUrl | (400) PATCH /api/websites/:id (partial update): url points at a private, loopback, link-local or otherwise non-public address. The same check that POST /api/websites applies. heartbeatToken is ignored on this path; the token is only ever generated server-side. |
POST /api/websites reports the same ceiling as a validation issue on the checkInterval path rather than a data.code, because the type is known from the request body alone.
Website connect target (Ersatzziel) error codes
connectHost, connectPort and connectTlsInsecure connect the check to a different host than
the one in url, while url, the Host header and TLS SNI stay unchanged - the semantics of
curl --resolve. Both POST /api/websites and PATCH /api/websites/:id (partial update
included) validate them the same way.
| Code | Meaning |
|---|---|
connectHostInvalid | (400) connectHost failed to parse: it carries a protocol or a path, contains whitespace, has an unbracketed IPv6 address, or its port is not a number in 1-65535. |
connectHostNotPublic | (400) connectHost resolves to a private, loopback, link-local, or otherwise blocked address - the same SSRF guard applied to url. |
connectTlsInsecureWithoutHost | (400) connectTlsInsecure was sent as true while the resulting connectHost is empty. On PATCH, this also fires when a request clears connectHost (null) but still sends connectTlsInsecure: true. |
Not available for playwright scenarios or heartbeat monitors: neither runs the HTTP/SSL check
pipeline connectHost plugs into, so the fields are accepted on those monitors but have no
effect.
Check mode error codes (SSH/SMTP/FTP/IMAP-POP monitors)
The credentialed monitor kinds (SSH, SMTP, FTP, IMAP/POP) support a checkMode field (protocol | tcp, default protocol). In tcp mode the monitor performs a bare TCP port-reachability check and no credentials are required or stored. Create and update endpoints for these four kinds can return these 400 codes in data.code:
| Code | Meaning |
|---|---|
portRequiredForTcp | IMAP/POP create or update: checkMode is (or resolves to) tcp, but no port is set on the request or already stored on the monitor. Only IMAP/POP requires an explicit port for tcp mode. SSH, SMTP, and FTP fall back to their protocol default port. |
invalidCheckMode | Update (PATCH) endpoints only: checkMode was provided but is neither protocol nor tcp. |
Incident error codes
Only manual (status-page) incidents can be deleted. DELETE /api/incidents/:id returns this data.code:
| Code | Meaning |
|---|---|
notManualIncident | (403) The incident is monitor-generated. Only manual incidents created by an admin can be deleted; automatic incidents are worker-owned and are never deletable via the API. |
Maintenance window error codes
POST /api/maintenance-windows/preview-occurrences (docs) can return this data.code:
| Code | Meaning |
|---|---|
invalidWindow | (400) endTime is not after startTime. |
Creating, updating and deleting a window (POST /api/maintenance-windows, PATCH /api/maintenance-windows/{id}, DELETE /api/maintenance-windows/{id}) can return these:
| Code | Meaning |
|---|---|
endTimeBeforeStartTime | (400, create and update) endTime is not after startTime. The same condition the preview endpoint answers with invalidWindow; the two codes differ, so branch on both. |
missingTargetId | (400, create) The request names a target kind but no id for it. A window needs at least one monitor, customer, customer IP or customer domain to cover. |
maintenanceWindowNotFound | (404, update and delete) No window with that id is visible to this caller. Out-of-scope and non-existent are deliberately the same answer. |
customerNotFound, customerIpNotFound, customerDomainNotFound | (404, create) The named target exists in no organization this caller can see. |
maintenanceWindowTargetMissing | (500, update and delete) The stored window has lost the row it covered, which is a data inconsistency rather than a request error. Report it instead of retrying. |
A repeated create call is safe: a window whose whole request body matches one that already exists is not created a second time, the existing window is returned and its subscribers are not announced to twice.
Organization Reports error codes
Endpoints under /api/organization/reports and /api/organization/report-runs can return these data.code values:
| Code | Meaning |
|---|---|
invalidReportSchedule | A weekly report is missing weekday, or a monthly report is missing dayOfMonth (1-28). |
invalidReportScope | Report scope ids are not owned by the organization, or threshold mode has no threshold set. |
reportRunNotFound | The requested report run does not exist for this organization. |
reportRunNoPdf | The report run has no archived PDF (it was an email-only report or was skipped). |
reportPdfNotFound | (404) The run record has a PDF path, but the object could not be read from object storage. |
Data export error codes
Endpoints under /api/organization/data-export (see Request Data Export) can return these data.code values:
| Code | Meaning |
|---|---|
invalidExportRecipient | (400) recipientEmails contains an address that is not a member of this organization. The rejected addresses are listed in data.unknown. |
exportAlreadyRunning | (409) An export of this organization is already queued or generating. The running export is in data.export. |
exportCooldown | (429) An export was requested within the cooldown window (data.cooldownHours, default 6). |
exportNotReady | (409) Download was requested while the export is queued, generating or failed. Current state in data.status. |
exportExpired | (410) The 7-day download window has closed and the file has been deleted. |
missingExportId | (400) The route was called without an export id. |
Agent-auth error codes
Agent access tokens (wsma_, see Agent authentication) are hard-gated to a GET/HEAD-only allowlist by default. A token minted with a write scope (api.write.monitors, api.write.incidents) is gated per method-and-path instead, against a fixed positive list covering only those two domains today -- everything else, including billing, credentials and any path on the shared write denylist, stays unreachable regardless of scope. Either kind of token still cannot see or reach anything outside its own read allowlist.
| Code | Meaning |
|---|---|
agentTokenReadOnly | (403) Returned for any write attempt while the platform-wide write kill-switch is off, regardless of the token's scope -- and, for reads too, when the path is outside the token's own read allowlist. |
agentTokenScopeForbidden | (403) The kill-switch is on, but this specific method and path are not in the token's write allowlist -- including a token that holds no write scope at all. Distinct from agentTokenReadOnly on purpose: this token may write, just not this -- a different write, or a different domain, can still succeed. |
API token error codes
A customer-scoped API token (wsm_ with a customerId) is confined to its customer. Organization-wide paths have no customer dimension, so the token cannot reach them at all: everything under /api/organizations/**, /api/organization/** (except GET /api/organization), /api/users/**, /api/admin/**, /api/platform-admin/**, /api/escalation-config/**, /api/custom-pricing, /api/custom-fields/**, /api/im/**, /api/notifications/send-alert, writes to /api/package-configs/**, creating or deleting customers, and /api/customers/bulk. No API token, customer-scoped or not, can create further tokens.
| Code | Meaning |
|---|---|
customerScopedTokenForbidden | (403) A customer-bound caller reached an organization-wide path or resource. Since 04.09.2026 this is raised not only for customer-scoped API tokens but for every customer-bound session as well: an editor with customer assignments and a platform admin in customer context creating a customer, or changing or deleting an organization-wide maintenance window (customerId: null). Use an organization-wide token, or an unrestricted user session, for organization settings, billing, users, reports and organization-wide windows. |
apiTokenCannotMintTokens | (403) POST /api/customer/tokens was called with an API token. Tokens are minted by a signed-in user, never by another token. |
apiTokenPathAmbiguous | (403) The request path contains an encoded slash, dot segment or double slash. The token guards refuse to normalize such paths. |
csrfOriginRejected | (403) A cookie-authenticated write came from a cross-site page: Sec-Fetch-Site: cross-site, or an Origin that matches neither the request host nor a trusted origin. API tokens are never affected; browser integrations must call from the app's own origin. |
Claim flow error codes
The service_auth registration and claim ceremony (see Claim flow) can return these error / data.code values, in addition to the base agent-auth codes above:
| Code | Meaning |
|---|---|
service_auth_not_enabled | (400) POST /agent/identity: the service_auth identity type is not enabled on this deployment. |
invalid_claim_token | (400 at POST /agent/identity/claim and POST /api/agent-claim/confirm, 404 at GET /api/agent-claim/context) The claim token, claim-attempt token, or user_code is unknown/expired/wrong-type, the signed-in user is not the bound email, or the confirming user's access cannot be resolved to a single organization/customer scope. |
claimed_or_in_flight | (409) POST /agent/identity/claim: the registration is already claimed. |
claim_expired | (400) POST /agent/identity/claim: the outer 24h claim window (from registration creation) has elapsed. |
authorization_pending | (400) POST /oauth2/token with the claim grant: the user has not confirmed yet; keep polling. |
slow_down | (400) POST /oauth2/token with the claim grant: polled faster than the returned interval (5s); back off. |
expired_token | (400) POST /oauth2/token with the claim grant: the claim token/registration expired, was revoked, or was already redeemed. |
ID-JAG error codes
identity_assertion registrations (see Identity assertion (ID-JAG)) can additionally return:
| Code | Meaning |
|---|---|
issuer_not_enabled | (400) POST /agent/identity: Uptimeify trusts no identity provider on this deployment, so identity_assertion is off. |
login_required | (401) POST /agent/identity: the ID-JAG's auth_time is older than max_age (3600 s, returned alongside); sign in again at the identity provider. |
interaction_required | (401) POST /agent/identity: the verified email belongs to an Uptimeify account and the agent is not approved yet; send the user to claim.verification_uri_complete and poll with claim_token. |
invalid_grant | (400) POST /oauth2/token with the jwt-bearer grant for an approved ID-JAG agent: the requested scope exceeds the approval (scope_exceeds_grant), the user's customer binding changed (binding_changed), the approving user no longer exists (no_human), or nothing is left after dropping write access for a global supporter (nothing_left). The reason is named in error_description. |
invalid_request (err) | (400) POST /agent/event/notify: wrong Content-Type, empty body, or the Security Event Token was not accepted. Which check failed is deliberately not said. |
Connected apps (OAuth connections) error codes
GET /api/oauth/connections and DELETE /api/oauth/connections/:clientId (see Connected apps) can return these data.code values, in addition to the standard 401 unauthorized:
| Code | Meaning |
|---|---|
missingClientId | (400) DELETE only: the :clientId path parameter is missing. |
unknownOauthConnection | (404) DELETE only: the caller has no OAuth connection for that client_id (already revoked, expired, or never connected). |
OAuth consent error codes
These codes come from the consent screen every OAuth/MCP authorization now passes through
(POST, on /api/oauth/consent). Like the step-up and account-deletion routes, it is
session-only and not part of the public API surface, and the method and the path are written
apart here for the same reason: a METHOD /api/... route-line in these pages is what publishes
an endpoint to /openapi.json. They are listed because the codes are what the screen and any
support enquiry go by. The scope catalogue itself is on Connected apps.
The checks run in the order below, so the first one that fails is the code you get. The last row is the exception: it comes from Better Auth, after every check on this list has passed.
| Code | Status | Meaning |
|---|---|---|
unauthorized | 401 | No active session. The endpoint resolves the session itself rather than sitting behind the shared middleware, because it writes to the record an authorization code will be minted from. |
invalidConsentDecision | 400 | accept is absent or is not a real boolean. It is deliberately not defaulted to false: refusing deletes the pending authorization, and a malformed request must not do that on the user's behalf. |
missingConsentCode | 400 | consent_code is absent or is not a string. |
unknownConsentCode | 400 | consent_code is unknown or has expired. Consent codes are short-lived; start the authorization again from the client. |
consentCodeNotOwned | 403 | The pending authorization belongs to a different account than the current session, or to no account at all (verification.userId is null). Checked before both branches, accept and refuse alike, because refusing is the destructive one. |
consentAlreadySettled | 400 | This authorization has already been consented to. Its scope is settled at that point, and narrowing it afterwards would leave the issued token disagreeing with the record the user approved. Checked before both branches, accept and refuse alike: Better Auth checks the same condition ahead of its own accept branch, so a refusal that skipped it came back as consentForwardRejected instead, which the consent screen cannot act on. |
emptyConsentSelection | 400 | Accept only: read areas were on offer and none of them was selected. Refuse the authorization instead of accepting an empty one. A client that requested no MCP scope at all sees no checkboxes and never hits this. |
consentScopeEscalation | 400 | Accept only: the selection names a scope the client did not request, or one that is not a granular read scope at all. A selection can only ever narrow what was asked for. |
consentForwardRejected | 4xx | Better Auth itself rejected the delegated call, and the status is the one it returned. It carries a reason alongside the code. Since the checks above run before both branches, this is only reachable through a genuine race: something settled the same consent code between our read and our forward. Like the codes above it, retrying does not help; start the authorization again from the client. |
Status page subscriber error codes
List, delete, and export endpoints for status page subscribers can return these data.code values, in addition to the standard 401 unauthorized and the two-tier statusPageNotFound behavior described on each page (a malformed or unknown :id is a code-less 400/404 from public-ID resolution; only an :id that resolves but fails the organization/customer-scope check carries data.code: statusPageNotFound):
| Code | Meaning |
|---|---|
invalidSubscriberId | (400, delete only) :subscriberId does not parse as a positive integer. |
subscriberNotFound | (404, delete only) :subscriberId does not exist, or belongs to a different status page than :id. |
Public IDs in DNS and DNSBL monitor routes
The following path endpoints support public IDs in docs and client integrations:
GET /api/customer-domains/:customerDomainPublicIdPATCH /api/customer-domains/:customerDomainPublicIdDELETE /api/customer-domains/:customerDomainPublicIdGET /api/customer-ips/:customerIpPublicIdPATCH /api/customer-ips/:customerIpPublicIdDELETE /api/customer-ips/:customerIpPublicId
Legacy numeric IDs are still accepted for backward compatibility, but public IDs are the recommended integration format.
Incident Management (IM) API error codes
The public Incident Management API (POST /api/im/events, GET/POST /api/im/incidents/**, GET /api/im/teams, GET/POST /api/im/schedules/**, GET /api/im/on-call) requires an organization-wide API token and can return these data.code values:
| Code | Meaning |
|---|---|
imAccessDenied | (403) The caller is not allowed into Incident Management: no session/token, a customer-scoped API token, a customer (readonly) or globalsupporter login, or an org login without an IM-eligible role (admin, editor, responder). |
imNotEnabled | (403) Incident Management has not been activated for this organization. |
payloadTooLarge | (413, Events Ingest only) The request body exceeds 256 KB. |
invalidJson | (422, Events Ingest only) The request body is not valid JSON. |
payloadTooDeep | (422, Events Ingest only) The parsed JSON payload nests more than 20 levels deep. |
imIncidentNotFound | (404) The incident does not exist, or belongs to another organization. |
imIncidentInvalidStatusTransition | (422, Acknowledge / Update Incident Status) The requested status is not a valid target for this endpoint (resolved and merged are not, use Resolve Incident instead), the incident's current status cannot transition through this endpoint, or status equals the incident's current status. |
imIncidentAlreadyClosed | (422, Resolve Incident) The incident is already resolved or merged. |
imIncidentStatusConflict | (409, status/resolve endpoints) The incident's status changed concurrently between the read and the write. Safe to retry. |
imTeamWriteDenied | (403, Create Incident, Schedules, Schedule Overrides) The caller's role is not admin and they are not a team admin (im_team_member.imRole = 'admin') of the target team. |
imTeamMembershipRequired | (403, Resolve Incident, merge, escalate-now, snooze, status change, add-responder, bulk actions) The caller is an IM user but not a member of the incident's team (incident.teamId), and neither an organization admin nor a platform admin. Acknowledging stays open to every IM user. Bulk endpoints refuse the whole request and list the affected ids in deniedIncidentIds. |
imTeamAdminRequired | (403, add-responder with notify: true) Paging another responder requires a team admin (im_team_member.imRole = 'admin') of the incident's team or an organization admin; a plain team member may add a responder without notifying. |
imUserNotEligible | (400, team members, team invite of an existing account, schedule overrides) The target user is inactive or has no IM-eligible role (admin, editor, responder). |
imSnoozeRangeInvalid | (400, snooze) snoozedUntil is not in the future or more than 7 days ahead. |
imTeamNotFound | (404, Schedules create) teamId does not exist, or belongs to another organization. |
imScheduleNotFound | (404, Schedule Overrides) :id does not exist, or belongs to another organization. |
invalidTeamId | (422, Create Incident) teamId does not belong to your organization. |
invalidCustomerId | (422, Create Incident) customerId is given but does not belong to your organization. |
imRoutingInvalidPolicyId | (422, Create Incident) escalationPolicyId is given but does not belong to your organization. |
userNotFound | (404, Schedule Overrides add) userId does not belong to your organization. |
GET /api/im/incidents, Create Incident, the status/resolve endpoints, Schedules create, and Schedule Overrides add can also return the generic 400 invalidRequestBody for malformed input (e.g. an unknown status filter value, a non-integer :id, a missing/malformed rotation layer). POST /api/im/events can additionally return 429 Too Many Requests (600 requests/minute per organization, with a retryAfter field but no data.code), or 503 with data.code: unavailable if the event could not be queued, safe to retry.
User management error codes
PATCH /api/users/:id (docs) and DELETE /api/users/:id (docs) protect the organization against locking itself out:
| Code | Meaning |
|---|---|
userSelfChangeForbidden | (400, update) You tried to change your own role or set your own account to isActive: false. Ask another organization admin. |
lastAdminProtected | (400, update and delete) The target is the last active organization admin; it cannot be demoted, deactivated or deleted. Promote another user first. |
globalUserProtected | (403, update and delete) The target carries a platform flag (isGlobalAdmin or isGlobalSupporter) and may only be changed by a platform admin. |
Tag error codes
POST /api/tags records the human who created a tag in created_by, a column that is NOT NULL and carries no foreign key. A call arriving over an agent session (an MCP connection, create_tag) is therefore refused rather than given a substitute value, because writing anything else there would break the "creator may edit" branch of the permission check silently and permanently:
| Code | Meaning |
|---|---|
authorizingUserUnresolved | (403, create) The human who authorized this agent session can no longer be resolved, so there is no user id to record as the tag's creator. The tag is not created. A tag always records the authorizing human, never the agent. Re-authorize the connection, or create the tag with a session that belongs to a person. |
The rest of the tag surface (POST /api/tags, PATCH /api/tags/{id}, DELETE /api/tags/{id}, and attaching or detaching one with POST /api/monitor-tags and DELETE /api/monitor-tags) can return these:
| Code | Meaning |
|---|---|
invalidTagColor | (400, create and update) The colour is not one of the palette values. The palette is a fixed set, not a free hex field. |
tagCustomerRequired | (400, create) A customer-scoped caller with more than one customer in scope did not say which of them the tag belongs to. A caller with exactly one customer in scope gets that one by default. |
invalidCustomer | (400, create and update) The named customer belongs to another organization. |
tagNotFound | (404) No tag with that id is visible to this caller. |
invalidMonitorType | (400, attach and detach) monitorType is not one of the supported monitor families. |
monitorNotFound | (404, attach and detach) No monitor of that type and id is visible to this caller. |
tagMonitorOrgMismatch | (400, attach) The tag and the monitor belong to different organizations. |
tagLimitReached | (422, attach) The monitor already carries the maximum of five tags. |
Attaching a tag is idempotent: a tag already on the monitor is not attached a second time and the call still succeeds.
Change request error codes
POST /api/monitors/:type/:id/change-requests and the legacy website-only route under /api/websites/:id/change-requests record the requesting human in requested_by, a NOT NULL column with a foreign key to user. A call over an agent session is refused rather than given a substitute value:
| Code | Meaning |
|---|---|
authorizingUserUnresolved | (403, create) The human who authorized this agent session can no longer be resolved. No request is created. Resolving a request (PATCH /api/change-requests/:id) is not affected: resolved_by is nullable and simply stays empty. |
Notification channel error codes
| Code | Meaning |
|---|---|
configRequiredForTypeChange | (400, Update Notification Channel) type changed but the request carries no config. A type change rebuilds the configuration from the request; the previous type's fields are never carried over, so send the full config for the new type. |
notificationChannelRecipientExists | (409, Create Notification Channel) an ACTIVE channel of the same type in the same scope already delivers to the same recipient; data.channelId names it. Repeating a create call whose response never arrived lands here instead of creating a second channel that would deliver every alert twice. Inactive channels never block, and the name is not part of the check. |
invalidRequestBody | (400, Create and Update Notification Channel) a target URL in config is unparseable, uses a scheme other than http/https, or points at a private, loopback or link-local address, at localhost, at a reserved private-use name (.local, .internal, .home.arpa) or at a single-label host. Every top-level config string starting with http:// or https:// is treated as a target URL; message-content fields (bodyTemplate, webhookBodyTemplate, headers) are exempt. No DNS is resolved, the delivery-time guard covers a name that only later resolves privately. |
sourceChannelIdRequired | (400, create) The request asks for a website-specific override but names no sourceChannelId. An override always derives from an existing organization-level channel. |
invalidSourceChannelId | (400, create) sourceChannelId is not a usable id for a website override. |
sourceChannelNotFound | (404, create) The named source channel is visible to no organization this caller can see. |
websiteChannelOverrideExists | (409, create) This website already has an override for that source channel. Update the existing override instead of creating a second one. |
channelIdRequired, invalidChannelId | (400, update and delete) The path carries no channel id, or one that is not a usable id. |
channelNotFound | (404, update and delete) No channel with that id is visible to this caller. |
A Matrix channel is a special case worth knowing about: its homeserverUrl is checked for shape when the channel is saved and resolved again at delivery time. A homeserver that only resolves to a private or reserved address is refused at delivery, and the refusal is recorded against that alert rather than raised as an error on this endpoint.
Custom domain error codes
The verify and activate endpoints of white-label domains and status page domains claim a hostname for your organization. Since 04.09.2026 a hostname is unique only among claimed entries (verified/active); a pending entry never blocks anyone and expires after 7 days.
| Code | Meaning |
|---|---|
domainAlreadyClaimed | (409) Another organization has already verified or activated this hostname. The response never names that organization. |
hostnameReserved | (400, add domain) The hostname is one of the platform's own hosts, a subdomain of them, or anything under uptimeify.io. |
404 for out-of-scope resources
Since 04.09.2026 every resource endpoint answers 404 with its usual not-found code (websiteNotFound, monitorNotFound, customerNotFound, customerDomainNotFound, customerIpNotFound, statusPageNotFound) both when the resource does not exist and when it exists but is not within your organization or customer scope. A 403 is reserved for role-based denials (wrong role, customer-scoped token on an organization path). Tests that asserted 403 for "belongs to another customer" need updating.
Rate limit and size error codes
| Code | Meaning |
|---|---|
imIngestRateLimited | (429, POST /api/im/ingest/:token) More than 300 requests per minute for one alert source. data.retryAfter carries the seconds to wait. |
imIngestByteBudgetExceeded | (429, POST /api/im/ingest/:token) The organization sent more than 50 MiB of ingest bodies within one hour. data.retryAfter carries the seconds to wait. |
imOutboundRetryRateLimited | (429, POST /api/im/outbound/:id/retry) More than 10 manual retries per minute for one integration. |
invalidDateRange | (400, every check-history, alert-history and incident-history endpoint) from or to is not a parseable date, or from is after to. Previously such values were silently ignored. |
noLocationsConfigured | (400, POST /api/websites/:id/trigger-check) The website has no monitoring location it could run on. |
invalidScreenshotId | (400, screenshot download) The screenshot id is not a UUID. |
invalidHostname | (400, TCP/SSH/FTP monitor create and update) The hostname is not a public target. This used to surface as a 500. |
POST /api/organizations/:id/vat-id/validate is limited to 10 checks per hour and organization (429 with data.retryAfter). POST /api/im/sources/:id/test-alert now applies the same body limits as the ingest URL (payloadTooLarge, invalidJson, payloadTooDeep).
Webhook and SMTP target error codes
| Code | Meaning |
|---|---|
webhookHostHeaderForbidden | (400, escalation config and IM channel config) A custom Host header is not accepted for webhooks; the header is always set from the URL. |
smtpHostNotPublic | (400, PATCH /api/organization/smtp) The SMTP host resolves to a private, loopback, link-local, CGNAT or otherwise non-public address. The same check runs again at send time; a host that stops being public falls back to the platform mail provider. |
Organization suspended or deleted
A gate ahead of the account-state gate below locks every /api/* call, a user session, a wsm_ API token, or a wsma_ agent access token alike, to the caller's organization status. Platform admins and supporters are exempt, the same way they are everywhere else.
A suspended organization gets 423 Organization suspended with no data.code, except for a narrow allowlist that stays reachable so it can pay its way back: reading the organization, updating its quota tier, the data export endpoints above, the tenant lookup, and the Mollie mandate return/reconcile and billing-discount routes. Every other route is blocked.
A deleted organization does not get that allowlist, and it also loses two routes the suspended case keeps: signing out (POST to /api/auth/sign-out) and both session bootstrap calls (GET on /api/auth/get-session and /api/_internal/auth/get-session) answer 423 Organization deleted with data.code organizationSuspended as well, deliberately, because signing out deletes the session row from Redis and that row is evidence for the deletion audit. Before 15.09.2026 a deleted organization's session, API token and agent token reached every route unchanged, the same gap this closes. "Deleted" here means status = 'deleted' OR the permanent deletion_completed_at marker is set. What decides this is whether the deletion has actually completed, not what status the organization currently displays. Two more surfaces follow the same rule: /mcp refuses a deleted organization's API tokens and OAuth connections with 423 before it builds a server, and no password reset e-mail is sent, the endpoint answers exactly as it does for an unknown address. A cancelled organization inside its grace period passes this gate unaffected only as long as its deletion has not actually completed. This gate takes effect within a few seconds rather than instantly, because the account state behind it is cached briefly.
Account state on the auth endpoints
Better Auth's own routes (/api/auth/**, e.g. change-password, update-user, two-factor/*, mcp/token refresh) refuse a deactivated account or a suspended organization the same way the API does. Sign-in, sign-out, password reset, e-mail verification and get-session stay reachable. A deleted organization is narrower still: only sign-in, sign-up, password reset and e-mail verification stay reachable, sign-out and get-session answer 423 as described above, and a fresh sign-in fails anyway once credentials are checked, because session creation itself refuses.
| Code | Meaning |
|---|---|
accountDeactivated | (403) The signed-in user is deactivated. |
organizationSuspended | (423) The user's organization is suspended or deleted. A cancelled organization inside its grace period is not affected, unless its deletion has already completed. |
MFA error codes
These error codes are specific to two-step verification (MFA) and can be returned by otherwise unrelated endpoints once the underlying gate applies:
| Code | Status | Meaning |
|---|---|---|
mfa_enrollment_required | 403 | Returned by any gated /api/* call when the caller (a platform admin/supporter when MFA_ENFORCE_PLATFORM_ADMINS is on, or a member of an organization whose matching per-role switch requireMfaAdmins, requireMfaEditors or requireMfaReadonly is on) has not enabled two-step verification yet. Set up two-step verification, then retry the request. |
mfa_step_up_required | 403 | Returned only by API token mutations (POST, PATCH and DELETE on /api/organization/tokens, and POST and DELETE on /api/customer/tokens; customer-scoped tokens cannot be updated) and only when the caller has two-step verification enabled: the caller has not verified a code in the last 5 minutes. Re-verify a code, then retry within the 5-minute window. Callers without MFA enabled never see this code; creating and revoking tokens works unchanged for them. |
mfa_step_up_locked | 429 | Returned by the internal, session-only step-up endpoint (POST, /api/account/mfa/step-up, not part of the public API surface) after 5 wrong codes from the same account. Locked for 15 minutes (data.retryAfterSeconds); wait and re-verify with your authenticator once it elapses. |
nonUserSessionForbidden | 403 | Returned by POST /api/users/:id/mfa/reset for any API token or agent access token caller, and by PATCH /api/organization(s) when the request body includes requireMfaAdmins, requireMfaEditors or requireMfaReadonly and the caller is an API token or agent access token. Both MFA-administration actions require a real, logged-in user session, an organization-scoped API token with admin-level access is not enough. |
These codes are data.code values on the JSON error body, alongside the listed HTTP status.
Account deletion error codes
These codes come from the in-app account deletion (GET, POST and DELETE, on
/api/account/deletion-request). Like the step-up endpoint above, those routes are
session-only and not part of the public API surface, and the method and the path are written
apart here for the same reason: a METHOD /api/... route-line in these pages is what publishes
an endpoint to /openapi.json, they are listed here because the codes
are what the app and any support enquiry go by.
A deletion is requested, not performed on the spot: the request sets a deadline 30 days out and can be withdrawn until then. The organization's administrators are notified when it is requested and when it is withdrawn, because they are the people who can backfill an on-call rotation the departure would leave open.
| Code | Status | Meaning |
|---|---|---|
accountDeletionSoleOwner | 409 | The caller is the last active administrator (role = 'admin') of their organization. Deleting that user would leave an organization with no way in, with monitors and billing still running, so this case leads to the organization cancellation instead. data.organizationId names the organization. Appoint a second administrator and the same request succeeds. |
accountDeletionNotRequested | 404 | The DELETE on /api/account/deletion-request found no standing request to withdraw. Either it was never made, or it has already been carried out. |
accountDeletionUserNotFound | 404 | The account does not exist in the caller's organization. |
accountDeletionNotRecorded | 409 | The request could not be recorded because it was withdrawn concurrently in another session while it was being written. Safe to retry. |
VAT ID validation error codes
POST /api/organizations/:organizationPublicId/vat-id/validate (see Validate VAT ID) can return these data.code values, in addition to the standard 401 unauthorized, 403 forbidden and 404 organizationNotFound:
| Code | Status | Meaning |
|---|---|---|
vatIdMissing | 422 | The organization has no vatId stored. Set one first with Update Organization. |
vatIdChangedDuringValidation | 409 | vatId or countryCode was edited concurrently while the EU VIES lookup was in flight. The in-flight verdict is discarded rather than written to data nobody actually checked. Safe to retry. |
Update Billing Details: required body
Endpoint:
PATCH /api/organizations/:organizationPublicId/billing
Currently supported request body:
{
"billingEmail": "billing@deinkunde.com"
}This endpoint currently accepts only billingEmail.