Skip to content
Metric VaultHelp Center
Open app

Internal endpoints

The operator-only routes protected by MV_INTERNAL_SECRET, what each one does, and why they fail closed with 503 when the secret is not set.

Last updated 2026-08-06

Summary#

A small set of routes are not for customers at all. They drive background work, run reconciliation, and complete operator sign-ins. They are protected by a single shared secret, MV_INTERNAL_SECRET, and the check fails closed: if the secret is not configured in the environment, every one of them refuses every call with HTTP 503. This page lists each route, its contract, and the operational rules around the secret.

Warning

Warning: These endpoints bypass the ordinary per-account gates. Anyone holding the secret can trigger platform-wide work. Treat it as a production credential of the highest sensitivity.

Overview#

Why they exist#

The site is deployed as a Cloudflare Pages project. Pages never invokes a Worker's scheduled() handler, because cron triggers are a Workers-only feature. The Worker does define scheduled(), and it would run four jobs if it were ever called, but on Pages it is not.

Background work therefore has to be pushed rather than pulled. An external scheduler calls a secured HTTP endpoint, and the Worker runs whatever is due. That endpoint is POST /api/cron/run.

The same secret then serves two further needs: an internal HTTP handoff that gives a long article job its own execution budget, and a small number of operator actions that must never be reachable from a customer session.

The two gate shapes#

There are two implementations of the check, and they answer differently. Knowing which is which saves a confusing debugging session.

GateAcceptsMissing secretWrong secret
Standard internal gateHeader x-mv-internal-secret, or ?key= on a GET503 {"error":"Internal jobs are not configured (MV_INTERNAL_SECRET is not set)."}401 {"error":"Unauthorized internal call"}
Diagnostic gate?key= only403 {"ok":false,"error":"forbidden"} with Cache-Control: no-store403 {"ok":false,"error":"forbidden"}

The standard gate protects /api/cron/run and /api/billing/sync. The diagnostic gate protects /api/dbgdfs, everything under /api/diag/, and the operator blog sign-in. The diagnostic gate is deliberately uniform so it leaks nothing: an unset secret and a wrong key look identical from outside.

The secret must be set in three places#

MV_INTERNAL_SECRET must hold the same value in all three:

LocationHow to set it
The Pages project environmentSettings → Environment variables
The standalone cron Workercd cron-worker && wrangler secret put MV_INTERNAL_SECRET
The repository secrets, for the backup schedulerRepository settings → Secrets → MV_INTERNAL_SECRET

If AI article generation suddenly returns 503, or background jobs stop running, a missing or mismatched value here is by far the most likely cause.


POST /api/cron/run#

PropertyValue
MethodsPOST and GET
AuthenticationHeader x-mv-internal-secret, or ?key=
Query parameter?job=<name> runs one job. Omit it to run all
ResponseImmediate, while the jobs continue in the background
IdempotentYes. Jobs self-throttle in the database

The job registry#

JobWhat it doesIts own cadence
monitorRuns due Competitor Monitor checks and dispatches alertsEach monitored URL's interval, 1 to 48 hours
schedulesAdvances due scheduled reports, up to 20 per runEach schedule's next run time
rank_alertsRe-checks enabled rank alerts, up to 30 per runEvery 24 hours per alert
tier2Measures AI visibility for tracked brands, up to 5 per runAbout every 22 hours
crawlAdvances any site crawl still running, one bounded slice per passEvery pass
sweep_orphansClears rows left behind by a deleted website: rank alerts and scheduled reports that would still spend and still sendEach run
purge_resultsDeletes saved tool results past the 90-day retention windowEach run
purge_watchtowerBounds the Competitor Monitor tables, in chunks, so a backlog drains over several runsEach run
sweep_cachesDeletes expired rows from the nine cache tables. Each keeps its own TTL; chunked, so a backlog drains over several runsEach run
higgsfieldRefreshes the image provider's operator token before it expiresEach run
social_schedulePublishes queued social posts whose time has arrived, up to 25 per runEach run
blog_scheduleFlips scheduled blog posts to published once their time has passedEach run
cms_schedulePublishes CMS posts (WordPress, Ghost, Webflow) whose scheduled time has arrivedEach run
billing_syncReconciles stored plans against live Stripe subscriptionsEach run
google_syncPulls Search Console data day by day into gsc_daily, bounded per runEach run
ga4_syncPulls Google Analytics 4 into ga4_daily, bounded per runEach run
gsc_inspectChecks index status for the top pages of each property, 10 per run against Google's 2,000/day capEach run
gsc_vitalsCollects Core Web Vitals field data for the site and its busiest pagesEach run
backlink_watchDiffs the referring-domain set for each website, weekly, and records which domains were gained and lostEach run

Each job is wrapped individually, so one failing cannot abort the others. The handler returns immediately and lets the work continue in the background, exactly as a native cron trigger would.

Requests#

bash
# Run everything that is due
curl -sS -X POST https://metricvaultai.com/api/cron/run \
  -H "x-mv-internal-secret: $MV_INTERNAL_SECRET"

# Run one job
curl -sS -X POST "https://metricvaultai.com/api/cron/run?job=rank_alerts" \
  -H "x-mv-internal-secret: $MV_INTERNAL_SECRET"

# GET form, handy from a browser
curl -sS "https://metricvaultai.com/api/cron/run?key=$MV_INTERNAL_SECRET"

Responses#

HTTPBodyMeaning
200{"ok":true,"triggered":["monitor","schedules",…],"at":"2026-08-06T09:00:00.000Z"}Jobs were started. triggered lists them
400{"error":"Unknown job: <name>","valid":["monitor","schedules","rank_alerts","tier2","crawl","sweep_orphans","purge_results","purge_watchtower","sweep_caches","higgsfield","social_schedule","blog_schedule","cms_schedule","billing_sync","google_sync","ga4_sync","gsc_inspect","gsc_vitals","backlink_watch"]}The ?job= name is not in the registry
401{"error":"Unauthorized internal call"}The secret did not match
503{"error":"Internal jobs are not configured (MV_INTERNAL_SECRET is not set)."}The secret is not configured
Important

Important: A 200 means the jobs were started, not that they finished or succeeded. The response is sent before the work completes by design. Judge success from the admin job console and the error console, not from this response. See Background jobs console and Error log and resolution.

The schedulers#

DriverCadenceRole
The standalone cron Worker in cron-worker/Every 15 minutesPrimary. A real Worker, so cron triggers work. Deployed separately with cd cron-worker && wrangler deploy
The repository workflow .github/workflows/cron.ymlHourly, at 17 minutes pastBackup. Catches up if the primary is undeployed

Running both is harmless: the jobs decide their own real cadence, and calling the endpoint more often than anything is due simply does nothing.

The cron Worker also answers a plain GET as a manual health check, forwarding the secret and returning the Pages response. It holds no business logic and no database binding on purpose.

The workflow can be run by hand from the Actions tab, optionally naming a single job in its job input.


Removed: POST /api/article-process#

This route, together with POST /api/article-start and GET /api/article-status, was removed on 2026-09-01. Nothing in the product called it. The Article Writer runs through POST /api/premium-ai like every other dashboard tool.

It is documented here because the reason it was removed is a rule worth keeping:

Note

An internal secret authorises the call, not the caller.

/api/article-start took a request from anyone — it ran no identity check — and then fetched /api/article-process on the platform's own behalf, presenting MV_INTERNAL_SECRET. That synthetic request was built fresh, so it carried no Authorization header. By the time the AI quota check ran, the only address left anywhere in the call was the one the original, anonymous caller had typed into the request body. The gate accepted it, and the generation was billed to whichever customer the body named.

The secret was doing its job the whole time. It proved the call came from inside the platform, which was true. It could not prove anything about who asked for it, and nothing else in the chain was carrying that.

So: when a handoff needs to run work on a user's behalf, forward something the receiver can verify — the caller's own bearer token, or a signed claim minted after the identity check. A field in the request body is not an identity, and an internal secret does not turn it into one.


POST /api/billing/sync#

PropertyValue
MethodPOST
AuthenticationStandard internal gate
Also runs asThe billing_sync background job
PurposeReconciles stored plan entitlements against live Stripe subscriptions

This is the backstop for the Stripe webhook. It pages through subscriptions 100 at a time, up to 40 pages, so it covers up to 4,000 subscriptions in one run. A subscription counts as entitling when its status is active, trialing or past_due.

Two safety properties matter:

  • Revocations only follow a complete scan. If the scan does not finish, the run grants only and reports revoke_skipped: "incomplete scan — granted only, no revocations".
  • Only Stripe-owned records are revoked. A plan granted manually or comped by support is never touched.
bash
curl -sS -X POST https://metricvaultai.com/api/billing/sync \
  -H "x-mv-internal-secret: $MV_INTERNAL_SECRET"

Response, pretty-printed:

json
{
  "ok": true,
  "scanned": 812,
  "granted": 3,
  "unchanged": 806,
  "revoked": 2,
  "skipped_no_email": 1,
  "errors": [],
  "scan_complete": true
}
HTTPBodyMeaning
200The summary aboveThe run completed
200{"ok":false,"errors":["STRIPE_SECRET_KEY is not set"]}Billing is not configured in this environment
500{"ok":false,"error":"sync failed"} or the underlying messageThe run threw

GET /api/blog/higgsfield/signin and /callback#

PropertyValue
MethodGET
Authentication?key=<MV_INTERNAL_SECRET>, diagnostic-style gate
FailurePlain text 403 Forbidden with Cache-Control: no-store

The blog's AI image provider is authorised as one platform-wide operator session, not per customer. There is a single stored credential row shared by every account. That is exactly why this flow is gated on the operator secret rather than on a customer session: a customer must never be able to reach it.

The sign-in route performs dynamic client registration, generates a PKCE verifier and challenge, stores a random state server-side, and redirects to the provider. States older than 15 minutes are pruned. The callback consumes the single-use state, exchanges the code, stores the tokens, and renders an HTML result page.

Related: the higgsfield background job keeps that token alive rather than racing live user requests, and /api/diag/blog-image?probe=1 reports the stored token's state without refreshing it. See Diagnostic endpoints.


Diagnostic routes#

Every route under /api/diag/ plus /api/dbgdfs sits behind the diagnostic gate. They are documented in full in Diagnostic endpoints, including which ones spend real money when called.


Operational notes#

Rotating the secret#

Rotation is a coordinated change across three systems, and there is no overlap window: the Worker compares against exactly one value.

  1. Generate a new random value of at least 32 characters.
  2. Set it in the Pages project environment and redeploy or wait for propagation.
  3. Set it on the cron Worker with wrangler secret put MV_INTERNAL_SECRET.
  4. Set it in the repository secrets.
  5. Verify with a manual call: curl -sS -X POST https://metricvaultai.com/api/cron/run -H "x-mv-internal-secret: $NEW" should return {"ok":true,…}.

Between steps 2 and 4 the backup scheduler will fail its calls with 401. That is expected and harmless, because the primary scheduler covers the window and the jobs are idempotent.

What breaks if it is unset#

SymptomCause
Competitor Monitor stops detecting changes/api/cron/run returns 503, so no job ever runs
Rank alerts stop firingSame
Scheduled reports never advanceSame
Plans drift from Stripe after a missed webhookbilling_sync never runs
Every diagnostic returns {"ok":false,"error":"forbidden"}The diagnostic gate fails closed

Verifying the whole chain#

bash
# 1. The Pages endpoint accepts the secret
curl -sS -X POST https://metricvaultai.com/api/cron/run \
  -H "x-mv-internal-secret: $MV_INTERNAL_SECRET"

# 2. One job in isolation
curl -sS -X POST "https://metricvaultai.com/api/cron/run?job=monitor" \
  -H "x-mv-internal-secret: $MV_INTERNAL_SECRET"

# 3. The diagnostic gate agrees with the same value
curl -sS "https://metricvaultai.com/api/diag/psi?key=$MV_INTERNAL_SECRET"

If step 1 returns 503, the Pages environment variable is missing. If it returns 401, the value you sent does not match the one deployed. If steps 1 and 3 disagree, you have two different values in play.

Do not add a cron trigger to the Pages configuration#

Adding a [triggers] crons block to the Pages project's wrangler.toml will not work. Cron is a Workers feature, and this is a Pages project. The standalone cron Worker exists precisely because that is true.

See also

Was this article helpful?