Gå til innholdet
ErmisAI

API error and status reference

The unified API error shape and what each status means across the route surface.

Every ErmisAI API route handler answers failures with the same JSON envelope. This page documents that envelope, the status vocabulary the codebase actually uses, and the domain error codes you will see in practice — with the verbatim English message each one carries.

The error envelope

All typed errors are produced by two helpers in src/lib/utils/error-format.ts.

createRouteErrorResponse (src/lib/utils/error-format.ts:330-345) writes:

{
  "queuePosition": 3,
  "queuedCount": 12,
  "deliveredToday": 20,
  "code": "story_queued",
  "error": "Story is currently queued.",
  "message": "Story is currently queued."
}
  • error and message always carry the same string.
  • code is present only when the thrower supplied one.
  • Any payload keys are spread at the top level of the object, not nested. error and message are written after the spread and always win; a payload code survives only when the thrower supplied no code of its own.

createInternalRouteErrorResponse (error-format.ts:313-328) is the catch-all for unexpected throws. It logs to stderr, forwards to Sentry, and returns { error, message } with no code and status 500 unless overridden.

RouteError

Domain modules throw RouteError (error-format.ts:38-54), which carries status, code, and payload. Route handlers convert it with a single guard:

import { createRouteErrorResponseFromThrown } from '@/lib/utils/error-format'

catch (err) {
  return (
    createRouteErrorResponseFromThrown(err) ??
    createInternalRouteErrorResponse({
      context: 'api:stories::storyId:draft:patch',
      error: err,
      message: 'Failed to save the story draft',
    })
  )
}

createRouteErrorResponseFromThrown returns null for anything that is not a RouteError, so the handler decides the fallback. Not every route uses this path — several hand-write Response.json(...) with an inline status.

Not every error response is uniform. Bare Response.json({ error: 'Forbidden' }, { status: 403 }) is common for auth denials and carries no code. POST /api/stories/[storyId]/completion returns { code, message } with no error key on 401 and 400 (src/app/api/stories/[storyId]/completion/route.ts:183-189, 243-249). Treat error and message as interchangeable and both as optional when writing a client.

Reading errors from a client

Client components never call fetch directly; they go through the typed helpers in src/lib/api/*, which share one wrapper (src/lib/api/client.ts:76-103). It throws ApiError with:

Egenskap

Type

A response that parses as JSON but fails its zod contract throws ApiError with status 500 and code invalid_response_payload — a client-side synthetic error, never something the server sent (client.ts:113-121).

X-Ermis-Trace and X-Ermis-Source are set by the client on outbound requests and echoed back by middleware on every response (src/proxy.ts:180-193), so the trace id in an ApiError matches the [request-origin] line in the server log. These headers are only populated inside the tenant workspace — AppTelemetryProvider is mounted in src/app/(tenant)/app/layout.tsx only, so admin and marketing requests carry no trace id.

What each status means here

StatusMeaning in this codebase
400Payload failed zod validation, or a URL/endpoint failed format validation
401No Clerk session, missing API key, or a bad webhook signature
402Capacity or quota exhausted — the delivery cap and the AI envelope both use this
403Role gate, plan gate, or BotID rejection
404Not found, or a flag-gated feature that is switched off
409State conflict: optimistic-concurrency loss, wrong editorial state, seat limit, plan already active
410A one-time WordPress connect session was missing or expired
422Content filtered, context limit exceeded, or an unsupported alert delivery mode
429Edge rate limit, AI usage window, concurrent-stream limit, or platform spend cap
502The AI provider returned something unusable
503A dependency is unavailable (Postgres, Polar, the AI runtime) or a kill switch is on
504The AI provider timed out

402 is not a payment prompt. It is the status for "you are over an allowance": story_queued when the daily delivery cap is reached, and four ai_* codes when the monthly AI envelope is reached.

Authentication and authorisation

There is no global middleware auth gate for API routes — every handler authenticates itself, and tests/api-route-auth-guard.vitest.ts fails CI if a new route.ts carries no recognised gate. See Route surfaces, proxy and auth gates.

The two denials are literal and uncoded:

StatusBody
401{"error":"Unauthorized"}
403{"error":"Forbidden"}

Both appear verbatim in the admin session helpers (for example src/app/api/admin/flags/route.ts:13-36), the tenant routes, and src/lib/api/integrations-access.ts:35-57. Because they carry no code, a client cannot distinguish "wrong role" from "wrong plan" from the code alone — only the message differs, and only for plan gates.

Bot-gated routes answer {"error":"Bot traffic blocked","code":"botid_rejected"} with status 403 (src/lib/api/bot-guard.ts:32). Only three routes verify server-side: POST /api/integrations/webhook/test, POST /api/newsroom/profile/custom-sources, and POST /api/newsroom/profile/custom-sources/validate.

Cron routes reject with {"error":"Unauthorized"} and status 401 when the Authorization: Bearer ${CRON_SECRET} header does not match under a constant-time comparison. isAuthorizedCronRequest returns false when CRON_SECRET is unset, so a missing variable cannot open the endpoint (src/lib/api/cron-auth.ts:46-65).

Rate limiting

buildRateLimitErrorResponse (src/lib/api/rate-limit.ts:218-241) returns status 429 with:

{
  "error": "Too many requests from this network. Please retry shortly.",
  "code": "rate_limited",
  "scope": "ip",
  "retryAfterSeconds": 42
}

The per-user message is "You have sent too many AI requests. Please wait and retry.". Headers on every rate-limit response: Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset.

Limits in force (rate-limit.ts:42-77), IP first then user:

KeyPer IPPer userApplied at
ai:chat20 / 60 s30 / 60 sPOST /api/stories/[storyId]/chat
ai:completion30 / 60 s60 / 60 sPOST /api/stories/[storyId]/completion
ai:draft-compose10 / 60 s15 / 60 sPOST /api/stories/[storyId]/draft/compose
integration:m2m60 / 60 s120 / 60 sCMS/WordPress export, connect exchange, disconnect
integration:webhook-test10 / 60 s15 / 60 sPOST /api/integrations/webhook/test
integration:source-validate20 / 60 s30 / 60 scustom-sources create and validate
team:invite10 / 60 s20 / 60 sPOST /api/team/invite
account:delete5 / 3600 s3 / 3600 sDELETE /api/account
email:unsubscribe10 / 60 s/unsubscribe

The keys ai:compose and ai:global are declared in the config maps but have no call site.

Edge rate limiting fails open. When the Upstash environment variables are absent getLimiter returns null, and when Redis is configured but unreachable the limiter catch block returns an allowed decision (rate-limit.ts:203-215). This is deliberate — burst limiting is abuse prevention, not billing — but it means a missing Redis silently removes every limit in the table above. Spend stays bounded by the per-tenant guardrail, which does not fail open on standing checks.

Story access and the feed

CodeHTTPMessage
story_queued402Story is currently queued.
story_access_unavailable503Story access is temporarily unavailable.
stories_feed_unavailable503Stories feed unavailable

story_queued is returned when the story is over the tenant's daily delivery cap. The payload carries queuePosition, queuedCount, and deliveredToday (src/lib/stories/story-access-response.ts:7-18). The cap resets at 00:00 UTC. See Plan and quota reference.

The same story_queued code ships two different messages. createStoryAccessErrorResponse says Story is currently queued.; the chat route's own copy says Story is currently unavailable. (src/app/api/stories/[storyId]/chat/route.ts:236-244). Branch on the code, never on the message.

story_access_unavailable is only the fallback. When the underlying failure is a TenantEntitlementUnavailableError, its code is passed through instead — in practice cost_envelope_unavailable, because the delivery cap resolution needs the billing entitlement (src/lib/platform/local-platform-data.ts:2044-2065).

stories_feed_unavailable carries an extra details string with the underlying error message and is returned when the feed build throws or exceeds the 95-second route budget (src/app/api/stories/route.ts:23, 104-117).

Draft lifecycle and editorial review

CodeHTTPMessage
story_draft_owner_conflict403Members can only edit drafts they own. (and per-route variants)
story_draft_initial_compose_required409Generate the first article draft before submitting for review.
story_draft_published_locked409Published drafts cannot be moved back to review.
story_draft_locked409Draft cannot be updated in its current state
story_draft_compose_state409Only draft-state stories or placeholder drafts can be generated from compose.
story_draft_compose_state_conflict409Only draft-state stories or placeholder drafts can be generated from compose.
editorial_draft_version_conflict409This story was updated by someone else. Refresh and try again.
invalid_chat_history409Stored chat history was reset due to schema mismatch. Please retry your message.

story_draft_owner_conflict is the member-ownership gate — a member may only mutate a draft they own or an unclaimed one. The message is per route: Members can only edit drafts they own. (draft/route.ts:75), Members can only submit drafts they own. (draft/submit-review/route.ts:89), Members can only compose drafts they own. (draft/compose/route.ts:106), Members can only use story chat for drafts they own. (chat/route.ts:308-312), Members can only generate completions for drafts they own. (completion/route.ts:230-235), Members can only add notes to drafts they own. (notes/route.ts:109-111).

story_draft_initial_compose_required is also returned by the chat route with a different message — Generate the first article draft before using story chat. (chat/route.ts:316-323). This is the hard block that makes compose the only way to produce a story's first article: refinement chat on a placeholder draft is a 409, not an empty response.

The same failure has two codes on the compose route. The pre-flight check returns story_draft_compose_state; the runtime re-check under the write throws StoryDraftComposeStateError, which carries story_draft_compose_state_conflict (src/app/api/stories/[storyId]/draft/compose/route.ts:115-119, 222-230; src/lib/platform/errors.ts:12-17). Both are 409 with an identical message. Handle both.

editorial_draft_version_conflict comes from the optimistic-concurrency guard on editorial draft upserts (src/lib/contracts/editorial-draft-conflict.ts:16-26). The constant EDITORIAL_DRAFT_VERSION_CONFLICT_CODE is exported for client comparison and is used by both StoryWorkspace and EditorialReviewWorkspace, which replace the server message with a localised toast: Someone else updated this draft. The latest version was loaded - review and retry. (messages/en.json, app.editorial.toast.versionConflict).

The editorial review routes (/api/editorial/review/...) otherwise return only uncoded Unauthorized / Forbidden / Invalid request payload / Review item not found.

AI capacity and guardrails

These are thrown by the admission guardrail before any model call (src/lib/ai/usage-errors.ts). Messages are verbatim.

CodeHTTPMessage
ai_paused503AI features are temporarily paused. Please try again later.
ai_platform_spend_cap_reached429The platform-wide {daily|monthly} AI spend limit has been reached. Please try again later.
ai_subscription_billing_blocked402AI usage is blocked because the subscription billing state is not active.
ai_envelope_not_configured402AI capacity has not been configured for this workspace yet. Contact support to activate your contracted monthly envelope.
ai_monthly_hard_limit_reached402AI monthly hard limit reached for the current billing period.
ai_overage_disabled402AI usage is above the included allowance and overage is disabled.
ai_usage_limit_reached429AI usage limit reached for the current window.
ai_user_window_limit_reached429AI usage limit reached for the current user window.
ai_concurrent_stream_limit_reached429Too many concurrent AI streams are active for this tenant.

AiUserWindowLimitReachedError also has a spend variant passed at the call site: AI usage spend limit reached for the current user window.

The table is in evaluation order. ai_paused and ai_platform_spend_cap_reached are kill switches checked first — they apply even when aiGuardrailsMode is disabled and even to platform-scoped pipeline runs. Everything below them is per tenant. Full ordering and the reservation mechanics are in AI usage accounting, guardrails and metering.

ai_overage_disabled is the common one: overage is off by default on every plan, so the monthly envelope doubles as the hard limit and reaching 100 % blocks rather than bills. Only an operator can enable overage. See Billing and AI capacity.

None of these nine codes is localised. getLocalizedComposeFailureMessage (src/components/features/story/StoryWorkspace.tsx:142-183) maps six AI provider codes to translated strings and has no case for any billing or guardrail code, so they fall through to getApiErrorMessage and a Greek, Polish, or Swedish newsroom sees the raw English server string in a toast.

AI provider and runtime errors

classifyAiRuntimeError (src/lib/ai/error-classification.ts) turns AI SDK, gateway, and provider failures into one code set. createAiOperationRouteError wraps the result into a RouteError whose message is "<Operation label> ..." — for example Story completion generation timed out while waiting for the AI provider. Please retry. The payload carries aiErrorType, aiErrorSource, aiAudience, providerStatusCode, retryable, and upgradeRequired.

CodeHTTPCause
ai_runtime_not_configured503Provider credentials or settings could not be loaded
ai_gateway_activation_required503Gateway account not activated (no card on file)
ai_provider_auth_failed503Provider rejected the credentials
ai_provider_forbidden503Provider denied the request
ai_provider_deployment_missing503Configured deployment not found
ai_provider_model_missing503Configured model not found
ai_provider_quota_exceeded503Provider-side quota or credit exhausted
ai_provider_unavailable503Provider returned 5xx or 424
ai_network_error503The provider could not be reached
ai_unsupported_functionality503Model does not support the required output format
ai_request_timed_out504Provider deadline exceeded
ai_provider_rate_limited429Provider-side 429
ai_provider_content_filtered422Safety system blocked prompt or response
ai_context_limit_exceeded422Story context exceeded the model window
ai_request_invalid502Provider rejected the request format
ai_invalid_provider_response502Response body was unparseable
ai_structured_output_invalid502Structured output failed schema validation
ai_output_truncated502Response was cut off
ai_no_output_generated502No usable output
ai_unknown_error502Nothing matched

Only six of these get a translated message in the story workspace — ai_provider_rate_limited, ai_provider_quota_exceeded, ai_provider_content_filtered, ai_context_limit_exceeded, ai_request_timed_out, and the pair ai_network_error / ai_provider_unavailable (StoryWorkspace.tsx:148-179). The rest render the server's English message.

Provider details are in AI runtime: providers, stages and prompts.

Billing and checkout

CodeHTTPMessage
polar_checkout_not_configured503Polar checkout is not configured at runtime. Missing: <env list>.
polar_portal_not_configured503Portal equivalent of the above
polar_customer_lookup_failed503Polar customer lookup failed. Confirm POLAR_ACCESS_TOKEN is available in the deployed runtime and valid for the target Polar environment.
polar_checkout_session_failed503Polar checkout session creation failed before a checkout URL was returned. ...
polar_portal_session_failed503Portal equivalent of the above
billing_plan_contact_only409This plan is handled through direct contact. Use the contact form to request access.

The 503 responses carry provider: "polar" and, where relevant, server and missingEnv in the payload (src/app/api/billing/checkout/route.ts:56-69).

Two billing conflicts carry no code:

  • {"error":"Plan is already active"} — 409, when the requested plan matches the active subscription, it has a polarSubscriptionId, standing grants entitlements, and the seat count is unchanged (checkout/route.ts:135-145). A past_due or canceled subscription on the same plan is a win-back and passes.
  • {"error":"Seat limit reached for the <plan> plan. Remove an active or invited member before sending another invitation."} — 409, from assertSeatAvailabilityForInvite (src/lib/auth/clerk-organization-management.ts:295-298).

Plan gates return 403 with a message and no code:

MessageGate
API keys require Pro or EnterpriseauthorizeApiKeysAccess
Webhook delivery requires Plus or aboveauthorizeWebhookDeliveryAccess
WordPress integration requires Pro or EnterpriseauthorizeWordPressConnectionsAccess
Team management requires a Business or Enterprise plan.POST /api/team/invite
CMS story export requires Pro or Enterprisepublic CMS/WordPress export

All of these resolve the plan through resolveEffectivePlanIdForStanding, so a past_due or canceled subscription is evaluated as Free.

The settings page uses plan-only helpers while the API routes use standing-aware ones. A past_due Pro tenant still sees the Team and Integrations tabs and then gets 403s from every route behind them (src/app/(tenant)/app/settings/page.tsx:199, 372-380).

Monitoring and alerts

CodeHTTPMessage
monitoring_rules_limit_reached403Monitoring rules are not available on the current plan. (limit ≤ 0) or Monitoring rule limit reached for the current plan (<n>).
monitoring_rule_limit_unavailable503Monitoring rule limits are unavailable right now.
cost_envelope_unavailable503Monthly AI capacity is unavailable right now.
alert_delivery_mode_unavailable422Email digest delivery is not available in the current runtime.

monitoring_rules_limit_reached carries limit in the payload and is thrown inside the row-locked state mutator, so two concurrent creates at limit − 1 cannot both succeed (src/lib/platform/errors.ts:19-33, src/lib/platform/alerts/index.ts:329-350).

The two 503s come from TenantEntitlementUnavailableError and carry entitlement in the payload. When the failure is schema drift the message is rewritten to ... is unavailable because the database schema is behind the current app migrations. (src/lib/platform/shared/entitlements.ts:25-37, error-format.ts:254-277).

alert_delivery_mode_unavailable is hard to reach through the API: the route schema is z.enum(['instant']) (src/app/api/alerts/rules/route.ts:24), so any other value fails validation first with an uncoded 400 Invalid alert rule payload. The 422 fires when the domain assertion sees a non-instant mode — for example a legacy stored rule. The combined-mode variant reads Combined instant + digest delivery is not available until email delivery is configured.

Newsroom profile and sources

CodeHTTPMessage
invalid_newsroom_profile400Validation-specific
invalid_custom_source_feed400Source validation failed: <detail> or Source URL did not resolve to a valid feed
newsroom_custom_source_conflict409Conflict-specific (duplicate feed)
newsroom_custom_source_limit_reached409Source limit reached for <tier> tier
newsroom_custom_source_not_found404Custom source not found
newsroom_profile_concurrent_update409Newsroom profile for <tenantScopeId> was modified concurrently
newsroom_profile_storage_unavailable503Newsroom profile storage is temporarily unavailable. Changes were not saved.
newsroom_profile_unavailable503Newsroom profile is unavailable right now.
newsroom_source_catalog_unavailable503Newsroom source catalog is unavailable right now.

invalid_custom_source_feed carries details.triedUrls and details.discoveredFeedUrls, which is what the UI needs to explain a failed feed probe (src/app/api/newsroom/profile/custom-sources/route.ts:86-100).

newsroom_profile_concurrent_update is only surfaced after three optimistic write retries are exhausted (src/lib/platform/newsroom-preferences.ts:82-105).

The three 503s are the fail-closed production path: when isLocalStateFallbackAllowed() is false — which is the case whenever VERCEL_ENV is set — a repository error throws instead of silently falling back to platform_state. The last two also swap in the schema-drift hint when the underlying Postgres error is a missing relation, type, column, constraint, or function.

Integrations and the public export API

The public integration endpoints have a narrower envelope than the rest of the app: they return { error } only, with no message and no code (src/lib/platform/integrations/public-cms-story-export.ts:95-102).

StatusMessage
401Tenant API key is required
401Invalid tenant API key
403CMS story export requires Pro or Enterprise
500Failed to load stories for export

The published OpenAPI document declares PublicIntegrationErrorResponse as { error (required), message, code, additionalProperties: true } (src/lib/openapi/public-integration-api.ts:178-196) and its 500 example for the disconnect endpoint shows code: "wordpress_disconnect_failed". The runtime 500 comes from createInternalRouteErrorResponse, which emits { error, message } and no code. Do not key integration logic on a code from that surface.

Tenant-side integration errors:

CodeHTTPSource
invalid_webhook_endpoint400WebhookEndpointValidationError, src/lib/platform/errors.ts:51-56
tenant_scope_required400Webhook config write or API-key create with no resolvable tenant scope

Endpoint validation messages are literal, for example Webhook endpoint must use HTTPS in production. and Webhook endpoint must resolve to a public network address. Saving a webhook config does not run the DNS check — only dispatch does — so a config can save cleanly and every delivery still fail. The delivery-log status codes (400, 422, 502, 504) are a separate vocabulary recorded in the log, not HTTP statuses returned to you; see Outbound webhooks.

WordPress connect

CodeHTTPMessage
invalid_wordpress_connect_request400Invalid WordPress connect request., Return URL must use the same origin as the WordPress site URL., Return URL must point to a WordPress admin endpoint.
invalid_wordpress_connect_url400<field> must be a valid absolute URL. / must use http or https. / must not include credentials. / must use HTTPS in production.
invalid_wordpress_connect_exchange400Invalid WordPress connect exchange payload.
wordpress_connect_session_missing410WordPress connect session is missing or expired.
wordpress_integration_unavailable403WordPress integration requires Pro or Enterprise
wordpress_disconnect_missing_key401Tenant API key is required

The 410 is the one to handle in a connector client: connect codes are single-use with a 15-minute TTL, so a retried exchange always gets it.

POST /api/integrations/wordpress/connect/authorize is different from every other route on this page. On success and on failure it answers with a 303 redirect back to the plugin's returnUrl rather than a JSON body (src/app/api/integrations/wordpress/connect/authorize/route.ts:110-133). Failures append:

  • ermisai_status=error
  • ermisai_code= one of unauthorized, forbidden, wordpress_integration_unavailable, invalid_wordpress_connect_request, wordpress_connect_authorize_failed
  • ermisai_message= a rewritten English string (Authentication required., You do not have permission to authorize WordPress., WordPress integration requires the Pro or Enterprise plan., Invalid WordPress connection request., or Failed to authorize WordPress connection.)
  • state= the original state value

A JSON error body is returned only when the request could not be parsed far enough to know the returnUrl. Cancellation from the approval page uses ermisai_status=cancelled. Full handshake in WordPress connector handshake.

Feature-gated 404s and stream 204s

Flag-gated routes return 404, not 403, when their flag is off:

{ "error": "Feature disabled", "code": "feature_disabled" }

Applied to /api/stories/queue and /api/stories/queue/[storyId] (flag editorPersonalQueue), /api/stories/[storyId]/notes (storyNotes), and /api/stories/[storyId]/review-nudge (reviewNudges). Flags are per platform with optional per-tenant overrides; see Feature flags and runtime controls.

Two SSE endpoints answer 204 with no body instead of erroring:

  • GET /api/notifications/events when the notificationsRestPolling flag is on (src/app/api/notifications/events/route.ts:50-55).
  • GET /api/feed/events when there is no tenant scope (src/app/api/feed/events/route.ts:35-37).

EventSource does not reconnect after a 204, which is the point — the client falls back to polling cleanly. Treat 204 on those routes as "streaming is off", not as a failure. See Realtime streams and polling fallback.

Inbound webhooks and health

The three signed receivers — /api/webhooks/clerk, /api/webhooks/resend, /api/billing/webhooks/polar — share a status vocabulary:

StatusCondition
503The signing secret env var is not configured
400Empty or unparseable body
401{"error":"Invalid webhook signature"}
200Processed, ignored, or permanently failed
500Only for retryable database errors, so the provider retries

The Polar receiver acks 200 for every non-retryable processing failure by design, including unmapped products and a missing customer externalId. A tenant can be stuck on the wrong plan while the provider's delivery log stays green. Details in Inbound webhooks: Clerk, Polar and Resend.

GET /api/health is the readiness probe and the one route whose status is a signal rather than an error. It returns 200 with status: "ok" or 503 with status: "unavailable", driven solely by the Postgres check — which includes an explicit platform_state probe. If that table is missing the detail reads platform_state unreachable (run db:migrate before serving traffic): <error> (src/app/api/health/route.ts:52-76, 134-163). Redis degradation is reported in checks.redis but never fails readiness.

Operations

CodeHTTPMessage
erasure_grace_window_active409Erasure grace window has not elapsed (scheduled for <ISO timestamp>). Pass force=true to override.
invalid_app_origin503APP_URL or NEXT_PUBLIC_APP_URL must be a valid absolute URL. and three sibling messages
missing_app_origin503APP_URL or NEXT_PUBLIC_APP_URL must be configured for external redirects.
tenant_summaries_unavailable503Tenant summaries are unavailable right now.

erasure_grace_window_active is returned by POST /api/admin/erasure when an operator confirms a purge before the 14-day window elapses. There is no second confirmation prompt — force: true in the body is the override. See GDPR erasure runbook.

The two app-origin errors come from src/lib/auth/clerk-session.ts:118-188. They fire on any route that builds an external redirect when APP_URL / NEXT_PUBLIC_APP_URL is unset, non-absolute, carries credentials, or is non-HTTPS in production. See Environment variable reference.

The admin AI config route (PUT /api/admin/ai/config) validates each stage model and returns 400 with invalid_stage_model_id, unsupported_stage_model_provider, provider_mode_stage_model_mismatch, or unknown_stage_model_id (src/app/api/admin/ai/config/route.ts:143-196).

Naming inconsistencies to expect

These are real and unlikely to change without a coordinated refactor. Code defensively.

  • Most codes are snake_case. The completion and chat routes use colon-namespaced codes: unauthorized:completion, bad_request:completion, bad_request:chat_history.
  • Auth denials and plan gates carry no code at all. Only the message distinguishes them.
  • story_draft_compose_state and story_draft_compose_state_conflict are the same condition.
  • story_queued has two messages depending on which route emitted it.
  • The public export surface omits message and code entirely.
  • The OpenAPI document's error examples include codes the runtime does not emit.

På denne siden