Skip to content
Metric VaultHelp Center
Open app

Error codes

Every error code and HTTP status the Metric Vault API returns, with the exact message, what caused it, and what to do about it.

Last updated 2026-08-06

Summary#

This is the complete error catalogue for the Metric Vault HTTP API. It covers the machine-readable code values, the HTTP statuses they arrive with, the exact message text, the cause, and the fix. Support engineers can match a customer's screenshot to a row here; developers can build retry logic from the "Retry" column without guessing.

Overview#

The two error shapes#

Most of the API returns a flat object:

json
{ "error": "Human-readable message" }

Gated failures add a machine-readable code and, where it helps, structured context:

json
{
  "error": "You've used all 500 premium reports on the PRO plan this month. Light tools (keyword research, SERP, content, technical audits) keep working. Quota resets on September 1, 2026 (in 26 days). Upgrade to continue using premium reports now.",
  "code": "quota_exceeded",
  "plan": "pro",
  "used": 500,
  "quota": 500,
  "cost": 6,
  "reset_at": "2026-09-01T00:00:00.000Z",
  "reset_date": "September 1, 2026",
  "days_until_reset": 26,
  "upgrade_url": "https://metricvaultai.com/index.html#pricing"
}

The blog sub-router uses a nested shape instead:

json
{ "error": { "code": "forbidden", "message": "Your blog role does not allow this action (postWrite)." } }
Tip

Tip: Branch on code when it is present and fall back to error when it is not. Never match on the message text: messages are localised in the interface and their wording can change, while codes are stable.

Reading a response#

  • error is always a string, except on the blog sub-router where it is an object.
  • code is present only on gated failures. Its absence is normal.
  • Some endpoints report failure with HTTP 200. Those are listed separately below; check ok before you check the status.

HTTP status codes#

StatusMeaning in this APIRetry?
200Success, or a soft failure carrying ok: false. Check the bodyNot applicable
201Created. Blog site and taxonomy creation onlyNo
301Permanent redirect. Legacy paths onlyFollow it
400The request was wrong: missing field, bad value, unsupported typeNo, fix the request
401Not authenticated: missing, invalid, expired or revoked credentialNo, fix the credential
403Authenticated but not allowed: wrong plan, wrong role, suspended account, wrong secretNo, change the entitlement
404The resource does not exist, or does not belong to the callerNo
405Method not allowed. Blog site console onlyNo
409Conflict: already invited, or a newsletter already sentNo, or resend with force
410Gone. A share link that has passed its expiryNo
413Payload too large. Blog media import onlyNo, send a smaller file
429Rate limited or out of creditsYes, after the stated window
500An unhandled error inside the handler, or an upstream failureOnce
502An upstream fetch failed. Responsive analyzer and growth actionsOnce
503A dependency is unavailable or unconfigured, and the gate failed closedYes, with backoff
504An upstream fetch timed out. Responsive analyzer preview onlyOnce
Note

Note: The blog sub-router deliberately rewrites 502 to 503 before responding, because the platform replaces a Worker 502 body with its own plain-text page and the JSON error would be lost.


Machine-readable error codes#

Authentication and entitlement#

codeHTTPMessageCauseFixRetry
auth_required401Please sign in to run this.A metered call arrived with no identifiable accountSign in, or send user_emailNo
auth_required401Please sign in to use recommendations.Same, from the AI recommendations gateSign inNo
account_suspended403This account is suspended. Please contact support.An administrator suspended the accountContact support@metricvaultai.comNo
upgrade_required403This feature requires the <Tier> plan. Your account is on <Plan>. Upgrade to unlock it.The account's plan is below the feature's minimumUpgrade. required_plan names the tierNo
upgrade_required403This tool needs a paid plan. Free includes the 10 technical SEO tools; upgrade to Pro to unlock the rest.A Free account ran a tool that costs creditsUpgrade to a paid planNo
upgrade_required403Team invites require the Pro plan or higher. Your account is on Starter.The plan has no team seatsUpgrade to Pro or aboveNo
upgrade_required403Team seat limit reached (<cap> members on the <Plan> plan). Upgrade for more seats.Every seat is taken. Pending invites occupy a seatRemove a member or upgradeNo
reco_upgrade_required403AI recommendations are available on the <Plan> plan and above. Your account is on <Plan>. Upgrade to turn any result into a step-by-step action plan.Get Recommendations requires Pro or above by defaultUpgradeNo
forbidden403Your blog role does not allow this action (<capability>).The blog role lacks that capabilityAsk the site owner to raise your roleNo
forbidden403Only the account owner can create blog sites. / Only the account owner can change blog settings.A member attempted an owner-only blog actionAsk the ownerNo
forbidden403{"ok":false,"error":"forbidden"}A diagnostic route was called without the correct ?key=Supply the internal secretNo
no_session401Sign in requiredThe blog API could not resolve an actorSend user_email, x-mv-user or ?user_email=No

Quotas and rate limits#

codeHTTPMessageCauseFixRetry
quota_exceeded429You've used all <N> premium reports on the <PLAN> plan this month. …The month's credit allowance is spentWait for reset_date, or upgradeAfter reset
quota_exceeded429Premium reports aren't included on the <PLAN> plan. Light tools (keyword research, SERP, content, technical audits) stay free. Upgrade to a paid plan to run premium reports.The plan has a zero credit allowanceUpgradeNo
reco_quota_exceeded429You've used all <N> AI recommendations on the <PLAN> plan this month. Your quota resets on <date> (<when>). Upgrade to get more right away.The monthly recommendations allowance is spentWait for the reset, or upgradeAfter reset
hourly_rate_limit429Hourly fair-use limit reached (100 light-tool calls/hour). This protects our infrastructure from abuse while keeping your monthly usage unlimited. Please wait ~<N> minutes for the next hourly reset.More than 100 light-tool calls in one clock hourWait reset_in_minutesYes

Quota responses also carry plan, used, quota, cost, reset_at, reset_date, days_until_reset and upgrade_url. Rate-limit responses carry limit, used and reset_in_minutes. See Rate limits and quotas.

Dependency and configuration failures#

Every one of these means the platform refused rather than guessed. They are transient in the first three cases and operational in the rest.

codeHTTPMessageCauseFixRetry
quota_check_failed503Usage check temporarily unavailable, please retry.The usage lookup failed. The gate fails closedRetry with backoffYes
plan_check_failed503Plan check temporarily unavailable, please retry.The plan lookup failed. The gate fails closedRetry with backoffYes
seat_check_failed503Could not verify your team seat limit, please retry.The seat count could not be readRetryYes
stripe_not_configured503Billing is not configured yet.The billing secret is not set in this environmentOperator actionNo
webhook_not_configured503Webhook is not configured (STRIPE_WEBHOOK_SECRET is not set).The webhook signing secret is missingOperator actionNo
portal_not_configured503The Stripe billing portal has no configuration, and it could not be created automatically: <detail>The billing portal has no configuration and auto-creation failedOperator actionNo
no_customer400No subscription found for this account yet.The address has never had a subscriptionSubscribe firstNo
not_configured503Originality checking is not configured (DataForSEO credentials are not set).The search-data credentials are missingOperator actionNo
not_configured503Fact-checking is not configured (the Perplexity key is not set).The research provider key is missingOperator actionNo
ai_unavailable503Fact-checking is temporarily unavailable (no AI provider responded).Claim extraction failed on every providerRetry laterYes
ai_not_configured503AI translation is not configured (ANTHROPIC_API_KEY is missing).The model key is missingOperator actionNo
email_not_configured503Email sending is not configured yet (RESEND_API_KEY is missing).The email provider key is missingOperator actionNo
media_not_configured503Image storage is not set up yet (the BLOG_MEDIA R2 bucket is not bound).The object store is not boundOperator actionNo
send_failed503The email provider did not accept the messages. Please try again.The email provider rejected the batchRetryYes

Input validation#

codeHTTPMessageCauseFix
too_short400Paste at least a couple of sentences to check.Fewer than 25 words submitted to originality or fact-checkSend more text
no_sentences400No checkable sentences were found in this text.No extractable sentences in the inputSend prose, not fragments
bad_request400site_id is requiredA blog write arrived without a siteSend site_id or the x-mv-site header
bad_request400user_email and member_email are requiredA blog role change was missing fieldsSend both
bad_request400Cannot set a member's role to 'owner'.Ownership is intrinsic and not assignableUse editor or author
bad_locale400Provide a language code like es, fr, de, or ja.The locale did not match a two-letter code with optional regionSend a valid code
same_locale400The post is already in that language.Translation target equals the sourcePick another locale
not_published400Publish the post before sending it to subscribers.A newsletter was requested for a draftPublish first
no_subscribers400No confirmed subscribers to send to yet.The site has no confirmed subscribersCollect subscribers first
no_recipients400None of the selected addresses are confirmed subscribers of this website.The supplied address list matched nobodyCheck the addresses
create_failed400Name required / Author name requiredA taxonomy item was created without a nameSend a name
bad_form400Expected multipart/form-dataA media upload was not multipartUse multipart
no_file400No file uploadedThe multipart body had no image or file partAttach the file
bad_type400Use PNG, JPG, WEBP, or GIFUnsupported image typeConvert the image
too_large400Max 5MBA media upload exceeded 5 MBCompress the image
too_large413Image is larger than 15MBA media import by URL exceeded 15,000,000 bytesUse a smaller source
blocked_host400That image host is not allowedThe import URL resolved to a blocked or private hostUse a public host
subscribe_failed400Please enter a valid email addressThe subscription address failed validationCorrect the address
bad_method405Method not allowedAn unsupported method on the blog site consoleUse GET, POST or PATCH

Not found and conflict#

codeHTTPMessageCauseFix
not_found404That person is not a member of your team.A blog role change targeted a non-memberInvite them first
not_found404No such postUnknown post idCheck the id
not_found404No route for <METHOD> /api/blog/<rest>Unknown blog sub-routeCheck the path
not_found404No public route <rest>Unknown public blog sub-routeCheck the path
site_not_found404No such siteThe site id or slug does not existCheck the identifier
post_not_found404No such postThe public post slug or id does not exist, or the post is not livePublish it, or check the slug
already_sent409This post was already emailed to subscribers on <YYYY-MM-DD>.The newsletter was already deliveredSend again with force if intended

Errors without a code#

The public API and key management#

HTTPMessageCauseFix
401Missing API key. Send header: Authorization: Bearer mv_live_...No Authorization header, or a different schemeSend the header
401Invalid or revoked API keyUnknown, truncated or revoked keyVerify the key, or create a new one
400Provide a JSON body: { "url": "example.com" }Neither url nor domain was presentSend one of them
401UnauthorizedThe session token on a /api/keys/* call was missing, expired, or belongs to an unconfirmed addressSign in again and take a fresh token
400Key limit reached (5 active keys). Revoke one first.Five active keys already existRevoke one
400id requiredRevoke was called without an idTake the id from the list response

Tools#

HTTPMessageCauseFix
400Missing type/api/tools was called with no typeSend a tool type
400Missing url/api/tools was called with no urlSend a URL
400Invalid tool type: <type>The type is not one of the 18 allowed valuesUse a listed type
400Invalid URLThe URL could not be parsedSend a valid absolute or bare URL
400Only http(s) URLs are allowedA non-HTTP scheme was suppliedUse http or https
400That host is not allowedThe host resolved to a private, loopback, link-local or reserved addressPoint at a public host
400Invalid analysis type: <type>An unknown type reached the AI tool endpointUse a supported type
503This tool is temporarily unavailable. Please try again shortly.An operator has switched that tool offWait, or contact support
500Failed to parse AI responseThe model returned output that could not be parsed. The first 500 characters are echoed as rawRun again

The AI provider's own failures are rewritten into five friendly messages before they reach you: a region message asking you to run again, a rate-limit message asking you to wait ten seconds, a quota-exhausted message, an authentication message pointing at support, and a generic AI request failed. Please try again.

Real-data tools with nothing to report#

Thirty tools are marked as real-data tools and never fabricate numbers. When the provider has no data they return HTTP 200 with:

json
{ "data": { "noDataAvailable": true, "message": "No measured data was found for \"<query>\". This is a real-data tool, so Metric Vault shows nothing here rather than fabricating a number. Check the spelling, or try a more established domain, keyword or topic." } }

This is a successful response, not an error. Handle noDataAvailable explicitly.

Billing#

HTTPMessageCauseFix
400Invalid plan or billing period selected.Checkout could not resolve a price from plan and billingSend a valid pair
400Invalid signatureThe Stripe webhook signature did not verify, or was more than 300 seconds oldCheck the signing secret and clock
400unreadable body / invalid JSONThe webhook body could not be read or parsedResend
500DB unavailableThe database binding was missing during a webhook. Returned so Stripe retriesOperator action

Team#

HTTPMessageCauseFix
400user_email and member_email requiredA required field was missingSend both
400You can't invite yourself.The invite address equals the inviterInvite someone else
400A member cannot be invited as 'owner'.Ownership is not assignableUse another role
409Already invited or this person is already a member.A membership row already existsNothing to do
404Invalid invite — token may have expired or email does not match.The token and address pair did not match a rowAsk for a fresh invite

Monitoring, alerts, schedules, sharing#

HTTPMessageCauseFix
400That URL host is not allowedThe monitored URL resolved to a private or reserved addressUse a public URL
400Webhook host is not allowedA webhook URL resolved to a private or reserved addressUse a public endpoint
404not foundThe row does not exist or belongs to another accountCheck the id
400user_email, domain, keyword requiredA rank alert was created without its required fieldsSend all three
400user_email, workflow_key, target requiredA schedule was created without its required fieldsSend all three
400user_email and title requiredAn editorial item was created without a titleSend a title
400user_email and html_content requiredA share link was created with no reportSend the rendered HTML
400Report too large — max 2MBThe report HTML exceeded 2,000,000 charactersShare a smaller report
400step_key requiredA workflow failure was reported without its stepSend step_key
500MONITOR_DB not bound. Wrangler may need a redeploy.The database binding is missing in this deploymentOperator action

Branding#

HTTPMessageCauseFix
400Logo too large — keep it under 500KBThe logo data URL exceeded 520,000 charactersUse a smaller image
400Colors must be hex format (e.g. #7c5cfc)A color was not a 3 to 8 digit hex valueSend hex

Translation#

HTTPMessageCauseFix
400Too many strings (max 100 per request)More than 100 strings in one callSplit the batch
400Payload too large (max 20000 chars per request)Combined text exceeded 20,000 charactersSplit the batch
400Unsupported or missing target languageThe target was absent or not one of the 20 supported languagesSend a supported code
400No text providedNeither text nor texts was presentSend text
503Translation not configured. Enable the [ai] Workers AI binding or set a DEEPL_API_KEY secret.No translation provider is availableOperator action

Translation otherwise fails open: if a provider errors, the source string is returned unchanged rather than the request failing.

Responsive analyzer#

HTTPMessageCauseFix
400Enter a valid website address, for example example.comThe address could not be parsedCorrect the address
502That site took too long to respond. Try again, or check the address.The fetch timed outRetry
502Could not load that site. It may be offline, blocking automated requests, or the address may be wrong.The fetch failedCheck the site
502That site returned HTTP <n>. Check the address and try again.The target returned an error statusCheck the address
502That page returned almost no HTML, so there is nothing to analyze. It may be a redirect or an app shell.The response body was effectively emptyPoint at a rendered page
502The AI response was cut off before it finished. Please run the analysis again.The model response was truncatedRun again
502Could not read the AI response. Please run the analysis again.The model response could not be parsedRun again
400Type a question first.The follow-up chat was emptySend a message

The preview proxy is the one part of the API that answers with a styled HTML card instead of JSON, using 400 Invalid address, 403 Not allowed, 504 Timed out, 502 Cannot be previewed and 415 Not a web page.

Article queue#

HTTPMessageCauseFix
503Job queue unavailable (D1 binding missing)The database binding is missingOperator action
500Failed to start article job: <detail>The job row could not be writtenRetry
400Missing job idStatus was polled with no idSend ?id=
404Job not found or expired. Please re-submit.Jobs are pruned after one hourStart a new job
400Missing jobId or bodyThe internal processing call was incompleteInternal only

Administration#

Every /api/admin/* route answers a failed authorisation with HTTP 403 and {"error":"Unauthorized"}, not 401. Beyond that:

HTTPMessageCause
401token required/api/admin/data received no token
403Only an owner can manage admins.Owner-only route called by an admin role
403Only an owner can manage customers.As above
403Only an owner can change config.As above
403Only an owner can run jobs.As above
403Only an owner can clear the cache.As above
403Only an owner can change cache settings.As above
403Only an owner can moderate posts.As above
403Only an owner can disconnect a channel.As above
400A valid email is required.A customer or admin action had no valid address
400You cannot suspend or demote your own account.Self-protection guard
400This is a protected owner account.The target is a seeded owner
400You cannot remove your own account.Self-protection guard
400Unknown action. / Unknown job. / Unknown plan.An unrecognized action, job or plan name
400days must be between 1 and 365Cache lifetime out of range
400invalid statusBlog moderation status was not draft, trash or published

Internal and diagnostic routes#

HTTPBodyCause
503{"error":"Internal jobs are not configured (MV_INTERNAL_SECRET is not set)."}The internal secret is not configured. The check fails closed
401{"error":"Unauthorized internal call"}The supplied secret did not match
403{"ok":false,"error":"forbidden"}A diagnostic route was called without a matching ?key=
400{"error":"Unknown job: <name>","valid":[…]}/api/cron/run?job= named a job that does not exist

See Internal endpoints and Diagnostic endpoints.


Failures that arrive with HTTP 200#

Three places report failure inside a successful response. Check the body.

EndpointShapeMeanings
/api/library/save-run{"ok":false,"reason":"<reason>"}missing_fields, no_db, unserialisable, data_too_large (payload over 1,600,000 characters), failed
/api/library/saved/html{"ok":false} or {"ok":false,"reason":"too_large"}The HTML exceeded 900,000 characters
/api/publish/push{"ok":false,…}WordPress publishes. The other five refuse with a reason: Medium closed its publishing API, and Ghost, Webflow and Shopify are not built yet. No connection returns No WordPress site is connected to this account yet.

/api/library/saved/check also always returns 200, with {"found":false} when nothing matches. That is not an error.


Fail-open and fail-closed behavior#

Knowing which way a gate falls tells you whether a 503 is safe to retry.

GateOn dependency failureResult
Plan checkClosed503 plan_check_failed. Nothing runs, nothing is charged
Credit quota checkClosed503 quota_check_failed. Nothing runs, nothing is charged
Recommendations quotaClosed503 quota_check_failed or 503 plan_check_failed
Team seat checkClosed503 seat_check_failed
Stripe webhook signatureClosed503 webhook_not_configured when the secret is unset
Internal secretClosed503 when unset, 401 when wrong
Hourly rate limiterOpenA limiter failure allows the call rather than blocking it
Translation providersOpenThe source string is returned untranslated
Notification preferencesOpenAll four preferences default to on
Activity loggingOpenLogging never fails a request

Retry guidance#

SituationAction
503 with plan_check_failed or quota_check_failedRetry with exponential backoff. Nothing was charged
429 with hourly_rate_limitSleep for reset_in_minutes, then retry
429 with quota_exceededDo not retry. Wait for reset_date or upgrade
500 from a model callRetry once. A second failure is worth reporting
502 or 504 from the responsive analyzerRetry once
401 or 403Never retry. Fix the credential or the entitlement
400Never retry. Fix the request

See also

Was this article helpful?