API Keys

API keys are how external software identifies itself to WALDO. You generate a key in the dashboard, hand it to your integrator (or paste it into Claude / ChatGPT), and from that point forward they can read your analyses, trigger new ones, and manage your webhooks on your behalf — with no further input from you. Each key is yours alone, scoped to the permissions you choose, and revocable at any time.

TL;DR for CRE professionals: API keys turn WALDO into a back-end service for whatever tools you already use. Want a custom dashboard pulling live numbers from your latest analysis? Want Claude to "summarize the location quotient on my Cuyahoga County analysis"? Want to trigger a fresh analysis from a Slack slash command? You generate a key in Settings → API Keys, hand it to the integrator, and that's it.


What you get from this

  • Plug WALDO into anything. Any tool that can make an HTTPS request — n8n, Zapier, Make, Pipedream, your custom CRM, Cursor, Claude Desktop, ChatGPT, a Slack app, a Retool dashboard — can read and write to WALDO with an API key.
  • Granular permissions. A key issued to "the n8n flow that just emails reports" doesn't need permission to delete webhooks or run new analyses. You pick scopes per key.
  • Audit trail. Every API call is logged with the key that made it, the route, the response, and the time. If something looks off, you can see exactly which integration is responsible.
  • Revocable in one click. If a laptop walks off, if an integrator leaves, if Claude got a little too autonomous — revoke the key, and that channel is shut down immediately. The rest of your integrations keep working.

Common use cases

What you want to doWhich scopes the key needs
Pull analysis results into a Google Sheet on a schedule
text
read:analyses
Have Claude / ChatGPT read your latest analysis and summarize it
text
read:analyses
+
text
mcp:invoke
Trigger a new analysis from an external system (Slack command, n8n flow, custom dashboard)
text
write:analyses
Build a custom dashboard that lists your analyses and shows status
text
read:analyses
Wire up webhooks programmatically from Terraform / CI
text
read:webhooks
+
text
write:webhooks
Connect Claude Desktop / Cursor to WALDO via MCP
text
mcp:invoke

Step-by-step: creating your first API key

  1. Sign in to WALDO as a Professional or Enterprise user. (API access isn't included on Basic — upgrade in Settings → Billing if needed.)
  2. Go to Settings → API Keys in the left sidebar.
  3. Click "New key." Give it a recognizable name (e.g. n8n production, Cursor MCP, Retool dashboard) so you'll remember what each key does.
  4. Choose the scopes the key needs. Grant the minimum. A read-only key for a dashboard should not have
    text
    write:analyses
    .
  5. Click "Create key." The full key (
    text
    waldo_live_...
    ) will be revealed — copy it now, it won't be shown again. (If you lose it, revoke it and create a new one.)
  6. Paste the key into your integrator's environment variables (e.g.
    text
    WALDO_API_KEY
    in
    text
    .env
    , or directly into the secrets section of your automation tool). Never paste it into a public chat, GitHub, or a Loom video.

Available scopes

ScopeWhat it allows
text
read:analyses
List the caller's analyses; fetch any phase result (economic base, employment / shift-share, market metrics, supply); export a finished analysis as PDF / Excel / PowerPoint; search the markets directory.
text
write:analyses
Trigger a new analysis. Returns a job ID immediately; the analysis runs in the background and fires
text
analysis.completed
when done.
text
read:webhooks
List your webhook endpoints; view recent delivery history.
text
write:webhooks
Create, update, rotate, and delete webhook endpoints; replay deliveries.
text
mcp:invoke
Allow MCP clients (Claude Desktop, Cursor, custom MCP integrations) to call WALDO tools on your behalf.

You can combine multiple scopes on one key. Example: a single "n8n production" key with

text
read:analyses
+
text
write:analyses
+
text
read:webhooks
covers the typical workflow of triggering an analysis, polling for completion, and logging webhook receipts.


Key hygiene — the short version

  • Treat keys like passwords. They grant access to your data and can run analyses on your behalf (which costs money under your plan).
  • One key per integration. When you offboard an integration, you revoke just that one key. If five tools share one key and the laptop walks off, you have to rotate everything.
  • Don't paste keys into chat / Slack / GitHub. Use environment variables, secrets managers (1Password, Doppler, Vault), or your platform's secrets UI.
  • Rotate periodically. For long-lived integrations, plan to rotate the key every 6–12 months: create a new key, update the integration, revoke the old one.
  • Watch the audit trail. If you see calls from a key you don't recognize, revoke it immediately.

Rate limits and quotas

WALDO enforces two layers of rate limiting:

  • Short-window (per user, per minute) — protects against runaway loops and abuse. If your integrator gets a
    text
    429 Too Many Requests
    , slow down.
  • Monthly cost-units (per user, per billing period) — different endpoints cost different amounts (read endpoints = 1 unit, write endpoints = 50 units, MCP calls = 5 units). Quota varies by tier.

Check your current usage in Settings → Billing. If you're hitting quota, the upgrade prompt there shows the next tier up.


When something goes wrong

The most common API key issues:

  • text
    401 Unauthorized
    — The key is missing, mistyped, or revoked. Confirm the
    text
    Authorization: Bearer waldo_live_...
    header is present and matches a non-revoked key.
  • text
    403 Forbidden
    — The key is valid but lacks the scope for the operation. Either add the scope to that key (you'll need to revoke + recreate — scopes aren't editable on existing keys) or use a different key.
  • text
    404 Not Found
    on a resource you own
    — The resource ID is wrong, or it belongs to a different user. WALDO returns 404 instead of 403 for cross-user access to avoid leaking ownership.
  • text
    429 Too Many Requests
    — You've hit the rate limit. Back off and retry; the response includes a
    text
    Retry-After
    header.

Every API response includes an

text
X-Request-ID
. When contacting support, include that ID — it's how we locate the audit-log row for your call.


For your integrator (or AI agent)

Everything below is the technical reference. Paste a link to this page into Claude / ChatGPT / Cursor and ask the AI to wire up the integration — it has all the information it needs.

Authentication

All calls to

text
/api/v1/*
require:

text
Authorization: Bearer waldo_live_<32-base64url-bytes>

Keys are looked up by SHA-256 hash. The full key value is shown to the user once at creation and never stored in plaintext on our side.

Server-to-server only. The public API does not set CORS headers — calling it from a browser would expose the key in client JavaScript. Keys belong on a server (or in an environment variable consumed by a server-side tool).

Key shape

text
waldo_live_<32 base64url bytes, no padding>

Example:

text
waldo_live_3xY7pQ-cN8mZv4_HhKfWAa1bRsTu2EgD0iLoP9qVwXy

The dashboard displays

text
waldo_live_X…3xYz
(12-char prefix + last 4) for identification. The full value never appears after creation.

Scopes (technical)

Scope strings (storage layer):

text
read:analyses
,
text
write:analyses
,
text
read:webhooks
,
text
write:webhooks
,
text
mcp:invoke
.

Stored on

text
api_keys.scopes TEXT[]
. Enforced by the
text
withApiAuth({ requireScopes: [...] })
middleware on every
text
/api/v1/*
route. Multiple scopes on a single key are AND-checked at the route —
text
withApiAuth({ requireScopes: ['read:analyses'] })
accepts any key that includes
text
read:analyses
in its array.

Tier gating (technical)

API access is paid-tier only. After key + scope check,

text
withApiAuth
resolves the user's
text
subscription_tier
and rejects basic-tier users with:

json
{
  "type": "https://api.waldocre.io/errors/forbidden",
  "title": "API access requires a paid plan",
  "status": 403,
  "detail": "Upgrade to Professional or Enterprise to use the API.",
  "instance": "/api/v1/...",
  "request_id": "..."
}

Rate limits (technical)

Two layers:

  1. Sliding-window per
    text
    user_id
    (Upstash Redis): default 100 requests / 60 seconds. Tier-overridable.
  2. Monthly cost-units per
    text
    user_id
    (counted on
    text
    api_audit_log.cost_units
    ): per-tier cap. Read = 1, write = 50, MCP = 5 per call.

Rate-limit responses are RFC 9457 problem-details with

text
429
and a
text
Retry-After
header.

Per-call audit log

Every authenticated call writes one row to

text
api_audit_log
:

text
request_id, key_id, user_id, method, route, status, latency_ms, cost_units, ts

You can correlate any error response back to its row using the

text
X-Request-ID
header value.

Endpoint reference

All under

text
https://<your-waldo-host>/api/v1/
. Full Bearer auth required on each.

Analyses

MethodPathScopeCostDescription
GET
text
/analyses
text
read:analyses
1List caller's analyses (cursor-paginated).
GET
text
/analyses/:id
text
read:analyses
1Summary: status, market, completion, top-line metrics.
GET
text
/analyses/:id/economic-base
text
read:analyses
1Full economic-base phase result.
GET
text
/analyses/:id/employment/shift-share
text
read:analyses
1Full shift-share decomposition.
GET
text
/analyses/:id/market-metrics
text
read:analyses
1Full market-metrics phase result.
GET
text
/analyses/:id/supply
text
read:analyses
1Full supply phase result.
GET
text
/analyses/:id/export?format=pdf|excel|pptx
text
read:analyses
1Binary export.
GET
text
/analyses/:id/status
text
read:analyses
1Async-job status (current_phase, completion_percentage).
POST
text
/analyses
text
write:analyses
50Trigger a new analysis. Requires
text
Idempotency-Key
header. Returns
text
202 Accepted
+
text
{ analysis_id, status_url }
.

Markets

MethodPathScopeCostDescription
GET
text
/markets/search?q=...&type=county|msa
(none)1Search the markets directory.

Webhooks

MethodPathScopeCost
GET
text
/webhooks
text
read:webhooks
1
POST
text
/webhooks
text
write:webhooks
5
GET
text
/webhooks/:id
text
read:webhooks
1
PATCH
text
/webhooks/:id
text
write:webhooks
5
DELETE
text
/webhooks/:id
text
write:webhooks
5
POST
text
/webhooks/:id/rotate
text
write:webhooks
5
GET
text
/webhooks/:id/deliveries
text
read:webhooks
1
POST
text
/webhook-deliveries/:id/replay
text
write:webhooks
5

See the Webhooks docs for the full payload contract.

Idempotency

Write endpoints (currently

text
POST /v1/analyses
) require an
text
Idempotency-Key
header. Send the same key for the same logical request and we'll replay the original response instead of creating a duplicate. Keys are scoped per-user and stored for 24 hours.

text
POST /api/v1/analyses
Authorization: Bearer waldo_live_...
Idempotency-Key: 3f9c-batch-march-cuyahoga
Content-Type: application/json

{ "market_code": "39035", "market_type": "county", ... }

Pagination

List endpoints use opaque cursor pagination:

text
GET /api/v1/analyses?limit=50&cursor=eyJ1cGRhdGVkX2F0IjoiMjAyNi0w...

Cursors encode

text
(updated_at, id)
.
text
limit
is capped at 100. The response includes
text
next_cursor
(or
text
null
when exhausted).

Error format (RFC 9457)

json
{
  "type": "https://api.waldocre.io/errors/invalid-request",
  "title": "Invalid request",
  "status": 400,
  "detail": "events must be a non-empty array",
  "instance": "/api/v1/webhooks",
  "request_id": "..."
}

text
request_id
is also returned as the
text
X-Request-ID
response header.

MCP (Claude / Cursor / agents)

The

text
mcp:invoke
scope unlocks
text
/api/mcp
— a stateless MCP transport that exposes WALDO's read + write surface as typed tools. See the MCP docs for the Claude Desktop / Cursor config snippets and per-tool reference.

Quickstart — Node.js

js
const WALDO = 'https://app.waldocre.io/api/v1'

async function listAnalyses() {
  const res = await fetch(`${WALDO}/analyses?limit=20`, {
    headers: { Authorization: `Bearer ${process.env.WALDO_API_KEY}` },
  })
  if (!res.ok) throw new Error(`${res.status} ${res.statusText}`)
  return res.json()
}

Quickstart — Python

python
import os, requests

WALDO = "https://app.waldocre.io/api/v1"

def list_analyses():
    r = requests.get(
        f"{WALDO}/analyses",
        params={"limit": 20},
        headers={"Authorization": f"Bearer {os.environ['WALDO_API_KEY']}"},
    )
    r.raise_for_status()
    return r.json()

Quickstart — curl

bash
curl -H "Authorization: Bearer $WALDO_API_KEY" \
     https://app.waldocre.io/api/v1/analyses?limit=20