Code examples
Working curl, JavaScript and Python for every public API call, plus error handling, retries and batch processing patterns.
Last updated 2026-08-06
Summary#
Copy-paste examples for every call in the public API, in curl, JavaScript and Python, plus the patterns you need around them: error handling that branches on code, a retry policy that respects the two different 429 meanings, and a batch loop that checks the credit budget before it starts. There is no official SDK; these snippets are the SDK.
Overview#
Conventions used below#
| Variable | Meaning |
|---|---|
MV_API_KEY | Your mv_live_ key. Used on /api/v1/analyze |
MV_SESSION | A Metric Vault session access token. Used on /api/keys/* |
| Base URL | https://metricvaultai.com |
Keep both out of source control. See Authentication for how to obtain each one.
Warning: Every example that calls /api/v1/analyze spends 6 credits per call. Run the batch examples against a short list first.
Analyze a domain#
curl#
curl -sS -X POST https://metricvaultai.com/api/v1/analyze \
-H "Authorization: Bearer $MV_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "example.com"}'JavaScript#
async function analyze(domain, apiKey) {
const res = await fetch('https://metricvaultai.com/api/v1/analyze', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ url: domain })
});
const body = await res.json();
if (!res.ok) {
const err = new Error(body.error || `HTTP ${res.status}`);
err.status = res.status;
err.code = body.code || null;
err.details = body;
throw err;
}
return body.analysis;
}
const analysis = await analyze('example.com', process.env.MV_API_KEY);
console.log(analysis.seoScore, analysis.threatLevel);Python#
import os
import requests
BASE = "https://metricvaultai.com"
class MetricVaultError(Exception):
def __init__(self, message, status=None, code=None, details=None):
super().__init__(message)
self.status = status
self.code = code
self.details = details or {}
def analyze(domain, api_key, timeout=90):
res = requests.post(
f"{BASE}/api/v1/analyze",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
json={"url": domain},
timeout=timeout,
)
body = res.json()
if not res.ok:
raise MetricVaultError(
body.get("error", f"HTTP {res.status_code}"),
status=res.status_code,
code=body.get("code"),
details=body,
)
return body["analysis"]
analysis = analyze("example.com", os.environ["MV_API_KEY"])
print(analysis["seoScore"], analysis["threatLevel"])Note the 90-second timeout. The call runs a model analysis inside the request.
Create an API key#
curl#
curl -sS -X POST https://metricvaultai.com/api/keys/create \
-H "Content-Type: application/json" \
-d "{\"token\": \"$MV_SESSION\", \"name\": \"reporting-pipeline\"}"JavaScript#
const res = await fetch('https://metricvaultai.com/api/keys/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: sessionToken, name: 'reporting-pipeline' })
});
const { ok, key, name } = await res.json();
// `key` is returned exactly once. Store it now.Python#
res = requests.post(
f"{BASE}/api/keys/create",
json={"token": session_token, "name": "reporting-pipeline"},
timeout=30,
)
data = res.json()
if not res.ok:
raise MetricVaultError(data.get("error"), status=res.status_code, code=data.get("code"))
print(data["key"]) # shown once onlyList and revoke keys#
curl#
# List
curl -sS -X POST https://metricvaultai.com/api/keys/list \
-H "Content-Type: application/json" \
-d "{\"token\": \"$MV_SESSION\"}"
# Revoke by id
curl -sS -X POST https://metricvaultai.com/api/keys/revoke \
-H "Content-Type: application/json" \
-d "{\"token\": \"$MV_SESSION\", \"id\": 7}"JavaScript#
async function listKeys(sessionToken) {
const res = await fetch('https://metricvaultai.com/api/keys/list', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: sessionToken })
});
const { keys } = await res.json();
return keys;
}
async function revokeKey(sessionToken, id) {
await fetch('https://metricvaultai.com/api/keys/revoke', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: sessionToken, id })
});
}
// Revoke every key that has never been used
const keys = await listKeys(sessionToken);
for (const k of keys.filter(k => !k.revoked && !k.last_used_at)) {
await revokeKey(sessionToken, k.id);
}Python#
def list_keys(session_token):
res = requests.post(f"{BASE}/api/keys/list", json={"token": session_token}, timeout=30)
return res.json().get("keys", [])
def revoke_key(session_token, key_id):
requests.post(
f"{BASE}/api/keys/revoke",
json={"token": session_token, "id": key_id},
timeout=30,
)
for k in list_keys(session_token):
if not k["revoked"] and not k["last_used_at"]:
revoke_key(session_token, k["id"])Check the credit budget before a job#
POST /api/usage is an internal application route rather than part of the public API, so its shape carries no compatibility promise. It is nonetheless the only way to read your remaining credits programmatically, and it is useful enough to include.
curl#
curl -sS -X POST https://metricvaultai.com/api/usage \
-H "Content-Type: application/json" \
-d '{"user_email": "owner@example.com"}'Python#
def remaining_credits(email):
res = requests.post(
f"{BASE}/api/usage",
json={"user_email": email},
timeout=30,
)
body = res.json()
return body["remaining"], body["quota"], body["days_until_reset"]
remaining, quota, days = remaining_credits("owner@example.com")
print(f"{remaining} of {quota} credits left, resets in {days} days")Useful fields on the response: plan, month, used, quota, remaining, percent_used, near_limit, over_limit, days_until_reset and breakdown_by_tool.
Error handling that branches on code#
Match on code, never on the message text. Messages are localised in the product and can be reworded; codes are stable.
JavaScript#
function classify(err) {
switch (err.code) {
case 'hourly_rate_limit':
return { retry: true, waitMs: (err.details.reset_in_minutes || 5) * 60_000 };
case 'quota_exceeded':
case 'reco_quota_exceeded':
return { retry: false, reason: `Out of credits until ${err.details.reset_date}` };
case 'plan_check_failed':
case 'quota_check_failed':
return { retry: true, waitMs: 2000 }; // transient, nothing was charged
case 'upgrade_required':
return { retry: false, reason: `Needs the ${err.details.required_plan} plan` };
case 'account_suspended':
return { retry: false, reason: 'Account suspended' };
default:
return { retry: err.status >= 500, waitMs: 2000 };
}
}Python#
def classify(err: MetricVaultError):
code = err.code
if code == "hourly_rate_limit":
return True, err.details.get("reset_in_minutes", 5) * 60
if code in ("quota_exceeded", "reco_quota_exceeded"):
return False, 0 # do not retry before reset_at
if code in ("plan_check_failed", "quota_check_failed"):
return True, 2 # transient, nothing was charged
if code in ("upgrade_required", "account_suspended"):
return False, 0
return (err.status or 0) >= 500, 2Retry with exponential backoff#
import time
def analyze_with_retry(domain, api_key, attempts=3):
delay = 2
for attempt in range(1, attempts + 1):
try:
return analyze(domain, api_key)
except MetricVaultError as err:
retry, wait = classify(err)
if not retry or attempt == attempts:
raise
time.sleep(wait or delay)
delay *= 2Only retry the transient cases. A 401, 403 or 400 will fail identically however many times you send it, and a quota_exceeded will fail until the month rolls over.
Batch a list of domains safely#
import time
def analyze_batch(domains, api_key, owner_email, pause=2.0):
cost_per_call = 6
remaining, quota, days = remaining_credits(owner_email)
needed = len(domains) * cost_per_call
if needed > remaining:
raise RuntimeError(
f"Job needs {needed} credits, only {remaining} of {quota} remain "
f"(resets in {days} days)"
)
results = {}
for domain in domains:
try:
results[domain] = analyze_with_retry(domain, api_key)
except MetricVaultError as err:
results[domain] = {"error": str(err), "code": err.code}
if err.code == "quota_exceeded":
break # stop the whole run, do not burn attempts
time.sleep(pause)
return resultsThree things this does that a naive loop does not: it prices the job before starting, it stops the whole run the moment credits are exhausted rather than failing every remaining domain, and it records the failure alongside the successes so a partial run is still useful.
Shell one-liners#
# Just the SEO score
curl -sS -X POST https://metricvaultai.com/api/v1/analyze \
-H "Authorization: Bearer $MV_API_KEY" -H "Content-Type: application/json" \
-d '{"url":"example.com"}' | jq -r '.analysis.seoScore'
# Compact one line per domain, from a file
while read -r d; do
curl -sS -X POST https://metricvaultai.com/api/v1/analyze \
-H "Authorization: Bearer $MV_API_KEY" -H "Content-Type: application/json" \
-d "{\"url\":\"$d\"}" \
| jq -c --arg d "$d" '{domain:$d, da:.analysis.domainAuthority, threat:.analysis.threatLevel}'
sleep 2
done < domains.txt
# Convert a batch of results to CSV
jq -r '[.url, .analysis.domainAuthority, .analysis.seoScore, .analysis.threatLevel] | @csv' results.jsonCommon mistakes in client code#
| Mistake | What happens | Do this instead |
|---|---|---|
| Short client timeout | The call is aborted mid-analysis, and the credit is still spent | Allow at least 60 seconds |
Retrying a 429 quota_exceeded | Every retry fails and wastes wall time | Branch on code and stop |
| Matching on message text | Breaks when a message is localised or reworded | Branch on code |
Destructuring analysis fields blindly | Throws when a field is absent | Read defensively with defaults |
| Storing the key in the repository | The key spends account credits and cannot be scoped down | Use a secret manager, and rotate |
| Reusing a session token in a script | It is an account-wide credential that expires | Use it once for key management, then discard it |
| Assuming responses are cached | Every call runs fresh and charges 6 credits | Cache on your side if you repeat a domain |
See also
Was this article helpful?
Thanks — feedback noted for the docs team.