Skip to content
Metric VaultHelp Center
Open app

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#

VariableMeaning
MV_API_KEYYour mv_live_ key. Used on /api/v1/analyze
MV_SESSIONA Metric Vault session access token. Used on /api/keys/*
Base URLhttps://metricvaultai.com

Keep both out of source control. See Authentication for how to obtain each one.

Warning

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#

bash
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#

js
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#

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#

bash
curl -sS -X POST https://metricvaultai.com/api/keys/create \
  -H "Content-Type: application/json" \
  -d "{\"token\": \"$MV_SESSION\", \"name\": \"reporting-pipeline\"}"

JavaScript#

js
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#

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 only

List and revoke keys#

curl#

bash
# 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#

js
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#

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#

bash
curl -sS -X POST https://metricvaultai.com/api/usage \
  -H "Content-Type: application/json" \
  -d '{"user_email": "owner@example.com"}'

Python#

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#

js
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#

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, 2

Retry with exponential backoff#

python
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 *= 2

Only 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#

python
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 results

Three 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#

bash
# 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.json

Common mistakes in client code#

MistakeWhat happensDo this instead
Short client timeoutThe call is aborted mid-analysis, and the credit is still spentAllow at least 60 seconds
Retrying a 429 quota_exceededEvery retry fails and wastes wall timeBranch on code and stop
Matching on message textBreaks when a message is localised or rewordedBranch on code
Destructuring analysis fields blindlyThrows when a field is absentRead defensively with defaults
Storing the key in the repositoryThe key spends account credits and cannot be scoped downUse a secret manager, and rotate
Reusing a session token in a scriptIt is an account-wide credential that expiresUse it once for key management, then discard it
Assuming responses are cachedEvery call runs fresh and charges 6 creditsCache on your side if you repeat a domain

See also

Was this article helpful?