programate lift

API Documentation

Developers

Programate Lift API

The API has moved to programate.ai. Update your integration to the new base URL https://programate.ai/wp-json/sm-api/v1 now. Requests to systamesh.com are being proxied for a transition period and will stop working after it. Your API keys are unchanged.

The Programate Lift API lets agents and applications launch brand-visibility probes against AI engines and read back your recommendation rate — fan-outs, conversation turns, entity mentions, sources, and recommendations — with the workspace and Library basics (targets, customer profiles, goals, and result classes) needed to set those runs up.

API scope: Full-account parity. The API covers Runs (launch probes, read back your recommendation rate), Reputation and Alignment checks (trackers, questions, scored reports, alignment-item review, competitor overrides), geo Regions, expanded Library management (targets, personas, goals, result classes — including category, region-of-record, and homepage patches), Analytics, Strategy tasks, Reports (PDF, DOCX and Markdown exports with the monthly allowance), and a composed workspace Overview — an agent can now drive everything the app can. The entire surface has a machine-readable spec at GET /openapi (OpenAPI 3.1, no auth required).

Base URL

https://programate.ai/wp-json/sm-api/v1

All requests must use HTTPS. All request and response bodies are JSON (UTF-8). All timestamps are ISO-8601 in UTC.

Authentication

The API uses bearer tokens. Create a key in your dashboard at programate.ai/api, then send it on every request:

Authorization: Bearer sm_live_a1b2c3d4…wXyZ
  • A key is tied to your account. Creating your first key is what enables API access — there is no separate switch.
  • Eligibility: API access unlocks after your account has purchased at least 1000 credits (the $10 minimum top-up, a refill, or any paid plan) — once, ever. Brand-new free accounts can’t create keys until then. Once unlocked it stays unlocked.
  • A key’s secret is shown once, at creation. Copy it then; we only store a hash and can never show it again. Lost a key? Delete it and create a new one.
  • You can name a key, edit its name, set spend caps, revoke it, and create up to 10 keys from the dashboard. All keys currently have the same permissions.
  • Always send the token in the Authorization header — tokens in the URL/query string are rejected (so they never leak into logs).

Requests with a missing, malformed, revoked, suspended, or unknown token get 401 (a key disabled for abuse returns 403 sm_api_key_suspended):

{ "code": "sm_api_unauthorized", "message": "Missing or invalid API key.", "data": { "status": 401 } }

Workspaces, the library, and runs — how they fit together

  • A workspace is one isolated probe environment. It holds exactly one target (the brand you’re measuring) plus its own customer profiles (personas), goals, and result classes (class definitions). Credits are shared across your whole account; workspaces only partition the work.
  • A run (probe) launches a persona + goal against the workspace’s target across one or more AI engines, for n_runs conversations of up to max_turns turns each. Runs are asynchronous — you create one, then poll for the result.
  • A completed run returns the full data set: per-engine conversations (turns), the entity mentions and sources in each turn, the fan-out queries the engines issued, and a summary. Recommendations (“Insights”) are generated by a separate call on a completed run.

Most calls operate on your active workspace by default. Override per call by passing workspace_id.

Reputation, alignment, and how checks work

  • A tracker is a category × region context — e.g. “CRM software, National” vs. “CRM software, San Francisco Bay Area.” Your target can be scored under one or more trackers, each independently. Calls default to your default tracker; override with tracker_id.
  • A question is a scenario your target gets probed against inside a check. Each has a kind (5: trust, strengths, weaknesses, switching, versus — every one names the brand; the retired unaided kinds discovery/comparison round-trip only as inactive history) and belongs to a tracker. Generate questions from your site + Library context, or write your own; only active questions run, and a tracker holds at most 10 active questions.
  • A check is a reputation run — same async launch/poll lifecycle as a probe (POST /reputation/checks → GET /runs/{id}; it shows up in GET /runs with operation: "reputation"), assembled from three independent pieces you toggle on or off: run_questions (probe the tracker’s active questions across engines), include_rescan (re-crawl your homepage and refresh the alignment/perception picture — requires target.homepage to be set), and generate_strategy (produce a fresh strategy synthesis). Pass at least one of the three.
  • Engines and gear: a check probes up to 6 conversational engines; google_aio (Google AI Overviews) rides on top without counting toward the 6, and can never run alone. gear (full | balanced | aggressive | max, default aggressive) sets the prebaked engine-coverage tier: under aggressive, frontier engines each answer a stratified half-sample of the questions instead of every one (cheaper); full makes every selected engine answer every active question. The same gear rides POST /estimate and the launch, so the quote matches the charge.
  • Three standard recipes:
    • Full check — {"engines":["gemini","openai"], "run_questions":true} — add "gear":"full" for exhaustive engine × question coverage; the default is the sampled aggressive tier
    • Alignment-only — {"engines":[], "run_questions":false, "include_rescan":true}
    • Strategy refresh — {"engines":[], "run_questions":false, "generate_strategy":true}
  • The check run itself terminates with a summary (score, engines, question count). The full scored report (grade, components, coverage, perception gap, alerts, strategy) is a separate read: GET /reputation/report.
  • Polling: a check typically takes 7–10 minutes end to end (multiple questions × engines, plus synthesis). Poll GET /runs/{id} every 30–60 seconds; budget up to 15 minutes before treating it as stuck. The launch response’s poll_after_seconds is a hint for your first poll interval.

Rate limits

Three layers protect the service. All thresholds are tunable; the table shows the defaults.

LimitPAYGProScale
All API calls — per key, every endpoint60 / min120 / min300 / min
Min delay between run launches — per key15 s5 s2 s
Concurrent runs in flight — per account136
  • Per-key, all calls: every request — reads, workspace/library writes, and runs alike — counts toward a single per-minute ceiling. Over it returns 429 sm_api_rate. (So looping create/delete/list/engines calls is limited exactly like everything else.)
  • Run launches additionally honor a minimum delay (429 sm_api_run_interval + a Retry-After header) and a per-account concurrency cap (429 sm_api_concurrent when you already have the max number of runs in flight).
  • POST /reputation/checks draws from the same buckets as POST /runs — run-interval, concurrency, and the per-key hourly/monthly credit caps count probes and checks from one key against a single pool, not two. Mixing the two launch endpoints cannot multiply throughput.
  • Question generation and saves carry their own gates (layered on top of, not instead of, the per-key/per-IP caps): 2 generation slots service-wide (429 sm_api_capacity_busy), 5 generations/hour per account shared with the web app (429 sm_api_generation_limited), and question saves at 20/min per account (429 sm_api_rate). See POST /reputation/questions.
  • Per-IP: independent of any key, each caller IP is capped (default 300/min) and repeated failed-auth requests are cut off quickly (default 20/min). Both return 429. This is the abuse/DOS backstop — it applies even to unauthenticated requests.
{ "code": "sm_api_run_interval", "message": "Too soon since your last run. Wait 5s.", "data": { "status": 429 } }

These limits only smooth traffic; runs are long-running, so real throughput is bounded by credits and concurrency.

Spending caps

Each key can carry an hourly and a monthly credit cap to protect you from runaway spend. They are independent of your account balance — you still can’t spend credits you don’t have.

  • Both default to no maximum (unlimited).
  • If set, the minimum is 1000 credits.
  • The hourly cap is a rolling 60-minute window. The monthly cap is the current calendar month (UTC).
  • A run is checked before it launches: if its estimated cost would push the key’s hour or month spend over the cap, it’s rejected — no credits are held.
{ "code": "sm_api_cap_hour", "message": "This key's hourly credit cap (1000) would be exceeded. Resets in 22m.", "data": { "status": 429 } }

sm_api_cap_month is the monthly equivalent.

We email the account owner on the few high-signal events: a new key is created, a key is auto-disabled for abuse, or a key hits a spend cap. Routine per-call errors (rate limits, validation, etc.) are not emailed — handle those in your integration via the response code.

Endpoints

GET /me

Your account, this key’s caps, and current usage.

curl https://programate.ai/wp-json/sm-api/v1/me \
  -H "Authorization: Bearer sm_live_a1b2c3d4…wXyZ"
{
  "account": { "plan": "pro", "email": "[email protected]" },
  "credits": { "balance": 4200, "held": 50, "spendable": 4150 },
  "key": {
    "label": "Production agent",
    "prefix": "sm_live_a1b2",
    "caps":  { "hourly": 5000, "monthly": null },
    "usage": { "hourly": 1200, "monthly": 18400 }
  },
  "limits": { "run_interval_seconds": 5, "calls_per_minute": 120, "concurrent_runs": 3 }
}

GET /engines

The AI engines you may use in a run, and whether each is currently enabled for your account. Only pass enabled engine keys to POST /runs — an unapproved engine returns 403 sm_api_model_not_approved and a disabled one 400 sm_api_model_disabled.

{
  "engines": [
    { "key": "gemini",    "label": "Google Gemini", "enabled": true },
    { "key": "openai",    "label": "OpenAI",        "enabled": true },
    { "key": "perplexity","label": "Perplexity",    "enabled": false }
  ]
}

Workspaces

GET /workspaces

{
  "active_id": 1,
  "workspaces": [
    { "id": 1, "name": "Workspace 1", "active": true },
    { "id": 4, "name": "Competitor watch", "active": false }
  ]
}

POST /workspaces

{ "name": "New workspace" }

Returns the created workspace. Exceeding your workspace cap (PAYG 1 / Pro 5 / Scale 25, plus any extra workspaces bought on the Plan page) returns 402 sm_api_workspace_cap.

PATCH /workspaces/{id}

{ "name": "Renamed workspace" }

DELETE /workspaces/{id}

Permanent. Requires the workspace’s exact current name as confirmation (mismatch → 400 sm_api_invalid). You can’t delete your only workspace → 409 sm_api_last_workspace.

{ "confirm_name": "Competitor watch" }

Library

Targets, customer profiles (personas), goals, and result classes (class definitions) all go through one endpoint.

KindWhat it isPer-workspace capFields
targetThe brand you measure1name (required), aliases[], negative_aliases[], competitor_seed[], success_class
personaA customer profile20label (required), disposition (required), resteer_examples[]
goalA buying scenario20label (required), scenario (required), guidance
class_defA result class24label (required, fixed once created), definition (required), kind (offering | distractor, default offering)

success_class on a target must be the label/key of one of that workspace’s class definitions.

GET /library?kind=persona&workspace_id=1

{
  "items": [
    { "id": "8f3a1c2d-…", "label": "Budget-conscious founder", "disposition": "Skeptical, price-first", "resteer_examples": ["Anything cheaper?"] }
  ]
}

POST /library

Create, update, or delete a library object.

{
  "action": "create",
  "kind": "persona",
  "workspace_id": 1,
  "fields": {
    "label": "Budget-conscious founder",
    "disposition": "Skeptical, price-first",
    "resteer_examples": ["Anything cheaper?", "Is there a free tier?"]
  }
}
  • action: create | update | delete | list (plus patch, targets only — see Regions & market below).
  • id is required for update and delete.
  • create returns { "id": "8f3a1c2d-…" }. Hitting a kind’s cap returns 409 sm_api_library_limit.
  • Deleting an object still in use (e.g. a result class set as a target’s success_class) returns 409 sm_api_library_in_use. Updating/deleting an unknown id returns 404 sm_api_not_found.
  • class_def.kind marks a result class as a category you compete in (offering, the default) or a decoy/other class (distractor). On PAYG, creating an offering class — or updating a distractor into one — returns 402 sm_api_upgrade_required; updates to a class that is already offering are unaffected, and distractor writes are never gated.
Warning — action:"update" on a target clears on omit. update is a full-row writer: any of aliases, negative_aliases, or competitor_seed you don’t include in a kind:"target" update is wiped to [], not left alone (and name is required on every update). For targeted one-field changes use POST /library action:"patch" (see Regions & market below); reserve update for when you mean to resend the complete target.

Runs

POST /runs

Launch a probe (asynchronous).

{
  "workspace_id": 1,
  "persona_id": "8f3a1c2d-…",
  "goal_id": "2c1d7b4e-…",
  "engines": ["gemini", "openai"],
  "n_runs": 3,
  "max_turns": 4,
  "channel": "web",
  "generate_recommendations": false
}

The target and its success class come from the workspace — you don’t pass them. engines must be enabled for your account (see GET /engines); n_runs is 1–20 and max_turns is 1–8. Returns 202:

{ "run_id": "5f1c8e7a-…", "state": "running", "estimate_credits": 42 }

Possible errors: 402 sm_api_insufficient_credits (buy credits), 409 sm_api_library_incomplete (the workspace has no Target/Persona/Goal yet), 403 sm_api_workspace_over_cap (more workspaces than your plan allows — delete some or upgrade), 403 sm_api_account_suspended, 403 sm_api_forbidden (a workspace you don’t own), 403 sm_api_model_not_approved / 400 sm_api_model_disabled (engine not usable), 429 sm_api_run_interval (too soon), 429 sm_api_concurrent (too many runs in flight), 429 sm_api_capacity (we’re at capacity / upstream provider throttle — not charged), 429 sm_api_cap_hour / sm_api_cap_month.

GET /runs/{id}

Poll for status, then results. While running (200):

{ "run_id": "5f1c8e7a-…", "state": "running", "runs_completed": 1 }

When complete (200):

{
  "run_id": "5f1c8e7a-…",
  "operation": "probe",
  "state": "captured",
  "credits_charged": 40,
  "created_at": "2026-06-22T10:00:00Z",
  "insights_run_id": null,
  "insights_running_run_id": null,
  "params": { "persona_id": "8f3a1c2d-…", "goal_id": "2c1d7b4e-…", "engines": ["gemini","openai"], "n_runs": 3, "max_turns": 4 },
  "results": {
    "summary": {
      "n_runs_completed": 3,
      "recommendation_rate": 0.67,
      "avg_depth_to_rec": 2.1,
      "by_engine": [
        { "engine": "gemini", "recommendation_rate": 0.67,
          "target_tier_histogram": { "absent": 0, "named": 1, "recommended": 2 } }
      ]
    },
    "runs": [
      {
        "engine": "gemini",
        "status": "success",
        "n_turns": 4,
        "depth_to_rec": 2,
        "target_tier": 2,
        "turns": [
          {
            "turn_index": 1,
            "buyer_message": "I need a CRM for a 5-person startup.",
            "engine_answer": "A few good options are …",
            "mentions": [
              { "entity": "Acme CRM", "presence": "recommended", "position_bucket": "top3", "sentiment": "positive" }
            ],
            "sources": [ { "url": "https://example.com/best-crm", "domain": "example.com" } ]
          }
        ]
      }
    ],
    "fanouts": {
      "all":       [ { "query": "best CRM for startups", "occurrences": 3, "distinct_turns": 2 } ],
      "last_turn": [ { "query": "best CRM for startups", "occurrences": 1, "distinct_turns": 1 } ]
    },
    "recommendations": null
  }
}

Terminal state is one of captured (credits charged), released (hold returned, credits_charged: 0 — e.g. a zero-cost or dry run), or failed. An expired run returns 410 sm_api_run_expired.

Recommendations are a SEPARATE job. This run’s own results.recommendations is always null — generating Insights (below) creates a new run, and the recommendations live on that run. Once generated, this run’s insights_run_id points to the Insights job (insights_running_run_id while in flight); poll that run_id for the results.recommendations.

Run-level outcomes (not HTTP errors). A poll can return 200 with a non-success state: failed carries an error object and released means the hold was returned and nothing was charged. Causes of failed include every engine erroring, synthesis failing, the actual cost exceeding your credits at settlement, or a temporary rate limit / capacity issue on our side (our infrastructure or an upstream model provider) — the last case is reported with a clear “temporarily rate-limited, please retry” message (never the provider’s raw error) and is not charged. Always read state before trusting results.

Reputation checks poll here too. A check launched via POST /reputation/checks uses this same endpoint and lifecycle. Its terminal payload carries operation: "reputation", the params echo is operation-aware ({engines, question_ids, max_turns, tracker_id, run_questions, include_rescan, generate_strategy, gear} instead of the probe shape), and results is just the check summary:

{
  "summary": { "n_questions": 9, "engines": ["gemini", "openai"], "score": 62, "tracker_id": "dc58424b-…" },
  "runs": [ { "engine": "gemini", "question_id": "9c1a7e2f-…", "status": "success" } ]
}

The engine’s raw internal report never rides this response — report_available: true is set instead; the canonical read is GET /reputation/report. An alignment-only check (include_rescan with run_questions:false) terminates with { "summary": { "tracker_id": "dc58424b-…" }, "runs": [] }. The probe contract above is unchanged.

GET /runs?workspace_id=1&page=1

List runs in a workspace. Filter with operation=probe or operation=reputation (omit for both). Every item carries operation; reputation items add a check object so you can tell a full check / alignment check (run_questions:false) / strategy refresh apart structurally.

{
  "items": [
    { "run_id": "5f1c8e7a-…", "operation": "probe", "state": "captured", "credits_charged": 40, "created_at": "2026-06-22T10:00:00Z", "label": "Budget-conscious founder / CRM shopping / gemini / 3×4", "source": "api", "insights_run_id": null, "insights_running_run_id": null },
    { "run_id": "7b2e491a-…", "operation": "reputation", "check": { "tracker_id": "dc58424b-…", "run_questions": true, "include_rescan": false, "generate_strategy": false }, "state": "captured", "credits_charged": 413, "created_at": "2026-08-15T09:00:00Z", "label": "Reputation check · CRM software · National", "source": "api", "insights_run_id": null, "insights_running_run_id": null }
  ],
  "total": 47, "page": 1, "pages": 5
}

POST /runs/{id}/recommendations

Runs a recommendations pass over a completed probe. Returns a NEW run_id (the Insights job) — poll THAT run_id, not the original probe. Its results.recommendations holds the output when terminal; the source probe’s own results.recommendations stays null (but its insights_run_id now points here). Errors: 409 sm_api_run_not_complete (the source probe hasn’t finished), 409 sm_api_insights_exists (already generated), 409 sm_api_insights_in_flight (already running), 400 sm_api_unsupported_operation (a reputation run — a check’s strategy is generated by the check itself via generate_strategy, not here), plus all the POST /runs gates (credits, over-cap, suspended, throttle). The recommendations payload:

{
  "summary": "Acme is named but rarely recommended for budget buyers…",
  "onsite":  [ { "title": "Best CRM for small teams", "action": "create", "topics": ["pricing"], "target_queries": ["best CRM for startups"], "rationale": "…" } ],
  "offsite": [ { "title": "G2 Best CRM list", "domain": "g2.com", "action": "Pitch for inclusion", "url": "https://g2.com/…", "rationale": "…" } ]
}

POST /estimate

Preview credit cost before you launch (free). Dry-runs the same validation and cost model as the real launch — no run is created, nothing is charged, and none of the run-launch gates (interval, concurrency, spend caps) apply.

For a reputation estimate, send the exact body of POST /reputation/checks plus "operation": "reputation":

{
  "operation": "reputation",
  "workspace_id": 1,
  "tracker_id": "dc58424b-…",
  "engines": ["gemini", "openai"],
  "run_questions": true,
  "max_turns": 3
}
{
  "operation": "reputation",
  "expected_credits": 413,
  "hold_credits": 460,
  "expected_micro_usd": 826000,
  "suggested_hold_micro_usd": 920000,
  "question_count_resolved": 9
}

gear is honored here exactly as on the launch (default aggressive — see POST /reputation/checks), and so are the engine rules: google_aio never counts toward the 6-engine cap and can’t be estimated alone. The quote is computed at the tier the check would actually run.

For a probe estimate, send the exact body of POST /runs with "operation":"probe" added — the envelope is identical and question_count_resolved comes back null (it only applies to reputation). hold_credits is what the matching launch would hold; the launch sizes through the same code, so the number quoted is the number held.

Possible errors: 400 sm_api_invalid (bad or missing params), 403 sm_api_model_not_approved (engine not usable — this gate fires before quoting, so an unapproved model’s price never leaks), 404 sm_api_not_found (unknown workspace or tracker), 409 sm_api_library_incomplete (a probe estimate in a workspace with no Target yet), 502 sm_api_engine_error (upstream estimate call failed).

Reputation

POST /reputation/checks

Launch a reputation, alignment, or strategy check (asynchronous, charged).

{
  "workspace_id": 1,
  "tracker_id": "dc58424b-…",
  "engines": ["gemini", "openai"],
  "max_turns": 3,
  "gear": "aggressive",
  "run_questions": true,
  "include_rescan": false,
  "generate_strategy": false
}

Returns 202:

{
  "run_id": "7b2e491a-…",
  "state": "running",
  "operation": "reputation",
  "tracker_id": "dc58424b-…",
  "hold_credits": 460,
  "poll_after_seconds": 30
}
  • Pass at least one of run_questions / include_rescan / generate_strategy (only run_questions defaults on). engines may be [] only when run_questions:false. The three recipes from “Reputation, alignment, and how checks work” above apply directly to this body.
  • engines is at most 6 enabled conversational engine keys — google_aio (Google AI Overviews) never counts toward the 6, so a list of 6 plus google_aio (7 items) is valid. google_aio can never run alone: an engines list that resolves to only the Google AI Overviews chain is a 400 (“Google AI Overviews runs only as part of a multi-engine check.”). max_turns is an integer 1–4, default 2 (checks are shorter conversations than probes). tracker_id defaults to your default tracker.
  • gear (optional: full | balanced | aggressive | max, default aggressive) is the prebaked engine-coverage tier: under aggressive, frontier engines each answer a stratified half-sample of the questions instead of every one (cheaper); full makes every selected engine answer every active question. Unknown values coerce to aggressive — never a 400. The gear rides POST /estimate and the launch alike (the quote matches the charge) and is echoed back in GET /runs/{id} params.
  • question_ids (optional, up to 10 UUIDs) runs a subset of the tracker’s questions — each id must be one of its (at most 10) active questions; absent/empty means all its active questions.
  • Poll GET /runs/{id} — same async lifecycle as a probe.

Shares the exact rate-limit buckets as POST /runs — run-interval, concurrency, and hourly/monthly credit caps are per-key totals across probes and checks combined, not two separate pools.

Possible errors: 400 sm_api_invalid (bad params, or all three toggles off), 400 sm_api_no_homepage (include_rescan but the target has no homepage — patch it first), 400 sm_api_model_disabled, 402 sm_api_insufficient_credits (buy credits), 403 sm_api_model_not_approved, 403 sm_api_past_due / sm_api_account_suspended / sm_api_workspace_over_cap, 404 sm_api_not_found (unknown workspace or tracker), 409 sm_api_no_questions (run_questions requested but the tracker has no generated/added questions yet — generate questions first), 429 sm_api_run_interval / sm_api_concurrent / sm_api_capacity / sm_api_cap_hour / sm_api_cap_month.

GET /reputation/report

The scored report for a tracker (free).

GET /reputation/report?workspace_id=1&tracker_id=dc58424b-…
{
  "status": "ok",
  "tracker_id": "dc58424b-…",
  "tracker": { "id": "dc58424b-…", "label": "CRM software · National" },
  "trackers": [ { "id": "dc58424b-…", "label": "CRM software · National", "archived": false } ],
  "questions": [ { "id": "9c1a7e2f-…", "text": "What's the best CRM for a 5-person startup?", "kind": "discovery", "active": true } ],
  "meta": { "positioning": { "claims": ["Fast setup", "No-code workflows"] } },
  "draft": null,
  "report": {
    "model": "gemini-…",
    "score": {
      "value": 62,
      "grade": "solid",
      "components": {
        "preference":  { "value": 55, "weight": 0.20 },
        "endorsement": { "value": 52, "weight": 0.30 },
        "sentiment":   { "value": 68, "weight": 0.30 },
        "alerts":      { "value": 74, "weight": 0.20 }
      }
    },
    "coverage": { "kinds": { "trust": 2, "strengths": 1, "weaknesses": 1, "switching": 0, "versus": 1 }, "missing": [], "legacy": 0, "partial": false, "thin": [] },
    "questions": [ { "question_id": "9c1a7e2f-…", "text": "What's the best CRM for a 5-person startup?", "kind": "discovery", "per_engine": [ { "engine": "gemini", "run_id": "3d9c0f4e-…", "status": "success", "target_presence": "recommended", "on_target": true } ] } ],
    "alignment": { "intended": ["Fast setup"], "perceived": [ { "claim": "Fast setup", "status": "seen" } ], "note": "" },
    "perception_gap": { "ok": true, "gaps": { "personas": [], "goals": [], "offerings": [ { "id": "lo1", "label": "No-code workflows", "verdict": "weakly_supported", "matched_site_items": [], "evidence": "…", "rationale": "…" } ] }, "classes": { "supported": [ { "category": "offering", "id": "lo0", "label": "Fast setup" } ], "weakly_supported": [], "unsupported_in_site": [], "prominent_but_missing": [] }, "score": 71, "cost_micro_usd": 48200, "meta": {} },
    "strategy": { "items": [ { "area": "on_site_edit", "title": "Back the \"Fast setup\" claim with concrete proof", "detail": "…", "kind": "change", "current": "…", "target": "…", "evidence": [ { "source_page": "Home", "quote": "…" } ] } ] },
    "alerts": [ { "url": "https://example.com/crm-comparison", "domain": "example.com", "severity": "medium", "why": "Cites a competitor's pricing table without mentioning you." } ]
  },
  "report_generated_at": "2026-08-15T09:07:41Z",
  "previous_score": 58,
  "previous_generated_at": "2026-08-08T09:03:12Z",
  "last_run": { "run_id": "7b2e491a-…", "created_at": "2026-08-15T09:00:00Z" },
  "discovered_competitors": ["Acme CRM", "Bravo CRM"]
}

Top-level status is always "ok" — read the data-state from report itself: report: null means no completed check yet, and a non-null report.model is what marks a real synthesis (the minimal shapes a strategy-only or alignment-only run leaves behind carry model: null). This endpoint never errors for lack of data.

  • score.components has four weighted components — endorsement (.30), sentiment (.30), alerts (.20), preference (.20, fed by versus questions only) — each {value, weight}; the composite value is their weight-renormalized mean. score.model is v2-branded; a report scored before September 2026 keeps its stored five-component score untouched and reads v1-unaided.
  • meta.site_scan on action:"list" says when the workspace’s cached site scan was last refreshed and by what ({domain, scanned_at, source, pages}, or null before any scan). coverage sits beside score, not inside it: kinds counts distinct scoreable questions per kind, missing lists the floor kinds (trust, strengths, weaknesses) the check lacked, legacy counts retired unaided rows in a pre-September-2026 report, and partial/thin mirror missing.
  • alignment holds the intended-vs-perceived claim pairing ({intended, perceived:[{claim, status}]}); perception_gap is the separate site-support analysis ({ok, gaps:{personas, goals, offerings}, classes:{supported, weakly_supported, unsupported_in_site, prominent_but_missing}, score, cost_micro_usd, meta}). Don’t conflate the two.
  • strategy is {items:[{area, title, detail, kind, current, target, evidence}]} — there is no strategy.summary.
  • Each report.questions[].per_engine[] cell carries the run_id you pass to GET /runs/{id}/transcript.
  • report_generated_at / previous_generated_at timestamp the current and previous syntheses (alongside previous_score and last_run).

Possible errors: 404 sm_api_not_found (unknown workspace or tracker).

GET /reputation/questions

List a tracker’s questions (free).

GET /reputation/questions?workspace_id=1&tracker_id=dc58424b-…
{
  "items": [
    { "id": "9c1a7e2f-…", "text": "What's the best CRM for a 5-person startup?", "kind": "discovery", "active": true, "persona_id": "8f3a1c2d-…", "source": "auto" },
    { "id": "4b6d9a01-…", "text": "Acme CRM vs. Bravo CRM — which handles onboarding better?", "kind": "versus", "active": true, "persona_id": null, "source": "custom" }
  ]
}

POST /reputation/questions

Generate, save, clone, or manage a tracker’s questions (free).

{ "action": "generate", "workspace_id": 1, "tracker_id": "dc58424b-…" }

Poll action:"draft_status" until draft.state reaches "done" (or "error"):

{
  "status": "ok",
  "draft": {
    "id": "5e2f8a91-…",
    "state": "done",
    "proposal": {
      "questions": [
        { "text": "How does Acme CRM compare to Bravo CRM on price?", "kind": "versus", "persona_label": "Budget-conscious founder", "rationale": "Price-first persona, head-to-head kind", "scope": "core", "scope_label": "National", "source": "auto" }
      ]
    },
    "site": "https://acmecrm.com",
    "started_at": "2026-08-15T09:00:04Z",
    "finished_at": "2026-08-15T09:01:12Z"
  }
}
  • action: list | generate | draft_status | save | clone | discard_draft.
  • generate {url?, top_up?} — starts a fresh draft from your site + Library context. top_up:{kinds:["trust","switching"], count:4} instead asks for just a few more, targeted: kinds is any of the 5 kinds, count 1–8. Calling generate while a draft is already pending doesn’t error — you get the in-flight draft back; poll draft_status.
  • save {questions:[{id?, text, kind, active, persona_id?, source?}], meta?} — upserts the tracker’s question set (omit id to create, include it to update); text ≤500 chars, kind is one of the 7 (blank-text or unknown-kind items are dropped); meta.positioning optionally updates the tracker’s stored positioning (claims[] / intended_strengths[], up to 8 × 200 chars, plus tone ≤60). The stored set is capped at 40 items; a payload over 40 is refused with 400 sm_api_question_cap — on this cap the API is stricter than the web app, which silently truncates (an agent must hear the refusal, not lose items). The engine’s active-question cap is 10 — which is why question_ids on a check tops out at 10.
  • clone {tracker_id, from_tracker_id} — copies another tracker’s active questions into tracker_id (handy when you spin up a new region or category tracker). Both ids are required.
  • discard_draft {id?} — throws away a pending or ready draft without saving it.

Draft lifecycle: generate (or top_up) opens a draft → poll draft_status until draft.state reaches "done" ("error" on failure; there is no "ready" state) → save what you want to keep, or discard_draft to bin it. Finished questions live at draft.proposal.questions, not top-level; drafted items carry {text, kind, persona_label, rationale, scope, scope_label, source} — no id and no persona_id until saved (ids exist only after action:"save"; scope/scope_label are the core-vs-aspirational market tier). Only one draft per tracker at a time.

Recommended composition — the API won’t force it. The web console additionally enforces a client-side composition floor (at least one each of trust, strengths, and weaknesses) and a cost confirmation above 9 active questions. The API enforces neither: any composition saves, but sparse compositions degrade scoring — visible in report.coverage.

Generation is gated hard — it runs a real site crawl + LLM synthesis pass:

  • Only 2 generations service-wide run at once (one pool shared with the web app) — the next gets 429 sm_api_capacity_busy with retry_after in data.
  • 5 generations/hour per account, shared with the app’s own UI (same bucket, across every key) — over it is 429 sm_api_generation_limited.
  • save calls are separately capped at 20/min per account — over it is 429 sm_api_rate.

Possible errors: 400 sm_api_invalid (bad action or params), 400 sm_api_question_cap (save would exceed 40), 404 sm_api_not_found (unknown tracker or workspace), 429 sm_api_capacity_busy (both generation slots busy), 429 sm_api_generation_limited (over the 5/hr shared quota), 429 sm_api_rate (save throttle).

GET /reputation/trackers

List your trackers (free).

{
  "items": [
    { "id": "dc58424b-…", "label": "CRM software · National", "category": "CRM software", "reach": "national", "service_areas": [], "archived": false, "default": true },
    { "id": "a71e9b03-…", "label": "CRM software · SF Bay Area", "category": "CRM software", "reach": "regional", "service_areas": ["San Francisco Bay Area"], "archived": false, "default": false }
  ]
}

POST /reputation/trackers

Create, update, or archive a tracker (free). Params are flat on the request body — there is no fields wrapper:

{
  "action": "create",
  "workspace_id": 1,
  "label": "CRM software · SF Bay Area",
  "category": "CRM software",
  "reach": "regional",
  "service_areas": ["San Francisco Bay Area"],
  "check_prefs": { "mode": "comprehensive", "comp_engines": ["gemini", "openai"] }
}
{ "id": "a71e9b03-…" }
  • action: list | create | update | archive. id is required for update/archive.
  • Fields (create/update; on update, omitted fields are left untouched): label (≤80 chars), reach (local|regional|national|global), aspiration (regional|national|global — a broader-than-core market only, no local), service_areas[] (up to 10), segment, category (≤80 — must be one of the workspace’s result-class labels; explicit "" clears it so the tracker falls back to the target’s success_class), check_prefs (an object: {mode: "quick"|"comprehensive", quick_engine, comp_engines[], compare_library, run_questions} — engine keys must be enabled per GET /engines).
  • Your default tracker can’t be archived — attempting it is 400 sm_api_invalid.

Unarchived-tracker cap, by plan (checked on create; a hard service ceiling of 5 applies underneath regardless of plan):

PlanCap
PAYG1
Pro3
Scale5

Possible errors: 400 sm_api_invalid (bad action or fields), 402 sm_api_tracker_cap (over your plan’s cap — data carries {cap, plan}; archive one or upgrade), 404 sm_api_not_found (unknown tracker or workspace).

GET /runs/{id}/transcript

One question × engine cell’s conversation for a check (free).

GET /runs/3d9c0f4e-…/transcript
{
  "status": "ok",
  "run": {
    "run_id": "3d9c0f4e-…",
    "engine": "gemini",
    "status": "success",
    "n_turns": 2,
    "question": { "id": "9c1a7e2f-…", "text": "What's the best CRM for a 5-person startup?", "kind": "discovery" },
    "started_at": "2026-08-15T09:01:30Z",
    "finished_at": "2026-08-15T09:03:05Z"
  },
  "turns": [
    { "turn_index": 1, "buyer_message": "What's the best CRM for a 5-person startup?", "answer": "A few good options are …", "in_class": true, "class_returned": "recommended", "sources": [ { "url": "https://example.com/best-crm", "domain": "example.com", "title": "Best CRM tools" } ] }
  ]
}

The id is a per-cell run id, not the check id. Each question × engine cell of a completed check is its own run, and GET /reputation/report carries each cell’s run_id (report.questions[].per_engine[].run_id) — that is what you pass here. Passing the check-level run_id that POST /reputation/checks returned gets a pointed 400 sm_api_unsupported_operation telling you to use a per-cell id.

The response is a passthrough of the engine’s transcript: the geo-tailed opening buyer message, every re-steer turn, the full answer, and the judge’s per-turn class read (in_class / class_returned), plus each turn’s sources. workspace_id is honored as everywhere.

Possible errors: 400 sm_api_invalid (malformed id), 400 sm_api_unsupported_operation (a check-level run id — use a per-cell run_id from GET /reputation/report), 404 sm_api_not_found (unknown or foreign id — also what a dry run’s cells return, since dry runs produce no turns).

POST /alignment/items

Accept, reject, or restore an alignment item (free).

{
  "action": "set_status",
  "workspace_id": 1,
  "tracker_id": "dc58424b-…",
  "section": "setup_gap",
  "category": "persona",
  "label": "Add a budget-conscious founder persona",
  "status": "added"
}
{ "ok": true }
  • section: setup_gap | strategy. category is required and section-scoped: persona | goal | offering for setup_gap; on_site_edit | on_site_score | off_site for strategy.
  • status for setup_gap: "added" | "rejected" | null. For strategy: "rejected" | null only — a strategy item’s "added" is stamped automatically when you create the corresponding library object (via POST /library).
  • Explicit null restores the item to undecided — it isn’t a delete. The status key itself must be present; an absent status is a 400. label is the item’s exact label, ≤300 chars. tracker_id is optional (default tracker).

Possible errors: 400 sm_api_invalid (bad section, category, status, or label), 404 sm_api_not_found (unknown tracker or workspace).

POST /reputation/competitors

Override whether a discovered entity counts as a competitor (free).

{
  "action": "override_category",
  "workspace_id": 1,
  "tracker_id": "dc58424b-…",
  "name": "Bravo CRM",
  "value": true
}
{ "ok": true }

name (≤120 chars) must match an entity in that tracker’s discovered_competitors (see GET /reputation/report). value is tri-state and its key must be present: true forces the entity in as a competitor, false forces it out, explicit null clears the override back to automatic classification — an absent value is a 400. tracker_id is optional (default tracker).

Possible errors: 400 sm_api_invalid (bad name, or missing/non-boolean-non-null value), 404 sm_api_not_found (unknown competitor, tracker, or workspace).

Regions & market

GET /regions

Your saved geo-region presets (free).

{
  "current": { "reach": "national", "service_areas": [] },
  "items": [
    { "key": "sf-bay-area", "label": "San Francisco Bay Area", "reach": "regional", "service_areas": ["San Francisco Bay Area"] },
    { "key": "national", "label": "National", "reach": "national", "service_areas": [] }
  ]
}

POST /regions

Save your list of region presets (free).

{
  "action": "save",
  "workspace_id": 1,
  "regions": [
    { "reach": "regional", "service_areas": ["San Francisco Bay Area"] },
    { "reach": "local", "service_areas": ["Oakland, CA", "Berkeley, CA"] }
  ]
}
{
  "ok": true,
  "items": [ { "key": "sf-bay-area", "label": "San Francisco Bay Area", "reach": "regional", "service_areas": ["San Francisco Bay Area"] } ]
}
  • action: list | save. Up to 12 regions; each reach must be local|regional|national|global (anything else is a hard 400); each service_areas[] keeps up to 8 entries of ≤80 chars (extras are dropped).
  • national and global are built-in — never stored as custom regions, and don’t count toward the cap.
  • This is your preset list, not your target’s active region — set that with POST /library action:"patch" (market_reach/market_areas, below).

Possible errors: 400 sm_api_invalid (bad reach or over caps), 404 sm_api_not_found (unknown workspace).

POST /library action:"patch"

Change one or two target fields without resending the rest (free). kind:"target" only. Unlike action:"update", patch reads your current target server-side, merges in only the fields you pass, and resends the full row — omitted fields are left untouched.

{
  "action": "patch",
  "kind": "target",
  "workspace_id": 1,
  "fields": { "success_class": "recommended" }
}
{ "id": "3f9a2b71-…", "name": "Acme CRM", "success_class": "recommended", "homepage": "https://acmecrm.com", "market_reach": "regional", "market_areas": ["San Francisco Bay Area"] }

Three recipes:

  • Category switch — {"fields":{"success_class":"recommended"}}
  • Region-of-record — {"fields":{"market_reach":"regional","market_areas":["San Francisco Bay Area"]}}
  • Homepage set — {"fields":{"homepage":"https://acmecrm.com"}}

fields must contain at least one field. Explicit ""/[] still clears a clearable field (homepage, the market_* sub-keys) — patch only skips fields you leave out entirely. success_class can only be changed, never cleared (an explicit "" is a 400), and must match an existing result-class label in the workspace. Two patches racing on the same target is last-write-wins — there’s no optimistic locking.

Possible errors: 400 sm_api_invalid (bad field or unknown class), 404 sm_api_not_found (unknown target or workspace), 409 sm_api_library_incomplete (the workspace has no Target yet).

GET /analytics

Recommendation-rate KPIs and trend for your workspace (free).

GET /analytics?workspace_id=1&category=CRM%20software
{
  "summary": { "recommendation_rate": 0.62, "share_of_voice": 0.34, "avg_position": 2.1 },
  "maturity": { "stage": "established", "n_runs": 128 },
  "byProfile": [ { "persona_id": "8f3a1c2d-…", "recommendation_rate": 0.71 } ],
  "byGoal": [ { "goal_id": "2c1d7b4e-…", "recommendation_rate": 0.58 } ],
  "byModel": [ { "engine": "gemini", "recommendation_rate": 0.67 } ],
  "aio_trigger_rate": 0.41,
  "aio_unsuitable": false,
  "win_rate": 0.55,
  "outcome_histogram": { "absent": 12, "named": 30, "recommended": 86 },
  "trend": [ { "date": "2026-08-01", "recommendation_rate": 0.58 } ],
  "queries": [ { "query": "best CRM for startups", "occurrences": 14 } ],
  "discovery": {
    "score": 71,
    "grade": "B",
    "generated_at": "2026-09-08T10:00:00Z",
    "previous_score": 64,
    "flags": { "no_search": 0, "question_updated": 1, "not_reached": 1, "wrong_class": 0, "recurring": 0, "no_conversational_coverage": 0 }
  }
}

AI-Overview metrics: aio_trigger_rate is the share of your google_aio runs (last 30 days) where Google actually rendered an AI Overview — null when you have no google_aio runs; aio_unsuitable flips true when such runs exist but the overview almost never triggers (the engine is a poor fit for this target).

discovery is the default tracker’s latest Discovery check — score, grade, when it ran, the previous score, and the check’s six flag counts (no_search, question_updated, not_reached, wrong_class, recurring, no_conversational_coverage). Before the first check every field is null; if that read fails the whole block is null (the analytics body survives). Advanced/custom runs never move it — the Discovery score is default-check-only, and the category filter doesn’t scope it.

Doesn’t include onboarding/setup state — that lives in setup_state on GET /overview, kept separate so this stays a pure data feed.

Possible errors: 400 sm_api_invalid (unknown category), 404 sm_api_not_found (unknown workspace).

Reports

Report documents (PDF, DOCX or Markdown) built from the results already stored for a workspace: Alignment, Reputation and Discovery. Generating one runs no AI and starts no check; it renders what is there. Reports are available on Pro and Scale; PAYG gets 403 sm_report_plan.

The allowance. Each workspace includes a number of reports per billing period. Every JSON response from these routes carries an allowance block, so an agent can see what the next report will cost before it asks for one:

"allowance": {
  "period_start": "2026-10-01T00:00:00+00:00",
  "renews_at": "2026-11-01T00:00:00+00:00",
  "used": 3,
  "max": 3,
  "remaining": 0,
  "next_report_credits": 250,
  "credits_spendable": 1200
}
  • used, max and remaining count runs, per workspace, shared by everyone on the team. Another format of a run already counted (the same results as DOCX after a PDF) is free and uses no slot.
  • Deleting a report does not give its slot back.
  • next_report_credits is 0 while remaining is above zero. Past the allowance it is the price of one extra report: Pro 250 credits, Scale 200.
  • renews_at is when the included reports refresh. It is null when no renewal date is known; the window is then the last 30 days, starting at period_start.
  • Before each POST /reports: if remaining is 0, check that credits_spendable covers next_report_credits. If it does not, the POST answers 402 sm_report_overage_credits with the same block in data.allowance, and nothing is generated or charged.

GET /reports/eligibility

Can a report be made right now, and what will it cost (free). Optional workspace_id (default: your active workspace), include (comma list of alignment, reputation, discovery) and tracker_id. Returns which areas have fresh results, the reason for any that don’t, and the allowance.

Possible errors: 404 sm_api_not_found (unknown workspace), 502 sm_api_engine_error.

POST /reports

curl -X POST https://programate.ai/wp-json/sm-api/v1/reports \
  -H "Authorization: Bearer sm_live_a1b2c3d4…wXyZ" \
  -H "Content-Type: application/json" \
  -d '{ "workspace_id": 12, "format": "pdf", "include": ["alignment", "reputation", "discovery"], "question_gaps": "exclude" }'
{
  "report": {
    "id": 481,
    "workspace_id": 12,
    "format": "pdf",
    "display_name": "Acme CRM report 2026-10-12.pdf",
    "bytes": 184320,
    "created_at": "2026-10-12 09:14:03",
    "expires_at": "2027-01-10 09:14:03",
    "download_path": "/reports/481/download"
  },
  "reused": false,
  "rerender": false,
  "charged_credits": 250,
  "allowance": { "used": 4, "max": 3, "remaining": 0, "next_report_credits": 250, "…": "…" }
}

All fields are optional: format pdf (default) / docx / md; include defaults to every area; charts defaults to every chart; question_gaps exclude (default, leaves out incomplete data) or show. The call is synchronous (a PDF takes a second or two) and answers 201. charged_credits is what this report cost: 0 inside the allowance or for a rerender of a run already counted. Sending the same request again returns the existing report with 200 and reused: true, at no charge.

Possible errors: 400 sm_api_invalid (bad format, include, charts or question_gaps), 402 sm_report_overage_credits (past the allowance and the wallet can’t cover the extra report; data.allowance included), 403 sm_report_plan (plan without reports), 403 for a viewer key (viewers can read and download, not generate or delete), 404 sm_api_not_found (unknown workspace or tracker), 409 sm_report_busy (another report for you is being generated), 422 sm_report_not_eligible (an included area has no fresh result; data.stale and data.areas say which), 429 sm_rate_limited / sm_report_blocked (5 generations per hour, then a 1-hour block; data.retry_after), 500 sm_report_render_failed (nothing stored, nothing charged).

GET /reports

Your reports in a workspace, newest first (free). Optional workspace_id. Returns { "items": [ …report… ], "allowance": { … } }.

GET /reports/{id}

One report’s metadata and the allowance of its workspace (free). A report that isn’t yours, or was deleted, returns 404 sm_api_not_found.

GET /reports/{id}/download

The file itself, as an attachment with the format’s content type. A file body can’t carry the JSON block, so the allowance comes in headers: X-SM-Reports-Used, X-SM-Reports-Max, X-SM-Reports-Remaining, X-SM-Reports-Renews-At. Downloads are limited to 60 per 10 minutes.

Possible errors: 404 sm_api_not_found, 410 sm_report_gone (past retention; generate it again, which is free for a run already counted).

DELETE /reports/{id}

Deletes the report’s file and answers 204. The allowance slot stays used. Viewer keys get 403.

Strategy tasks

GET /strategy-tasks

Open next-best-action items for your workspace (free).

{
  "items": [
    {
      "item_key": "3f7db2c1a9…-sha256-…",
      "scope": "site",
      "title": "Best CRM for small teams",
      "description": "Create a pricing-first comparison page for budget buyers.",
      "link": "https://acmecrm.com/blog/best-crm-small-teams",
      "probe_run_id": "5f1c8e7a-…",
      "probe_label": "Budget-conscious founder / CRM shopping / gemini / 3×4",
      "report_run_id": "9d0b3c12-…",
      "report_created_at": "2026-08-14 08:12:03",
      "state": "still_flagged",
      "verify_state": "still_flagged",
      "verified_at": "2026-09-08T10:00:00Z",
      "actor_user_id": 42,
      "actor_name": "Ada Lovelace"
    }
  ],
  "confirmed": [
    { "item_key": "91ac0e77…", "title": "Add a pricing page", "scope": "site", "verified_at": "2026-09-08T10:00:00Z" }
  ],
  "has_strategy": true,
  "resolved_open": 2
}

Items are distilled from your generated Insights reports (onsite recommendations → scope site, offsite → scope offsite), capped at 30. item_key is a stable content hash — the same advice recurring across reports keeps one identity. has_strategy: false means no Insights report exists yet; resolved_open counts items currently hidden because you resolved them.

Each item carries the web console’s task lifecycle: state (open|done|still_flagged|not_verified|confirmed|reopened|dismissed — the same states the console shows), the last check’s adjudication verify_state (confirmed|still_flagged|not_verified|not_checked_this_run, null until adjudicated) with its verified_at timestamp, and who acted (actor_user_id/actor_name). confirmed[] celebrates the items the latest check verified as cleared (newest first, capped at 5).

POST /strategy-tasks

Resolve (dismiss) a task (free).

{ "action": "resolve", "workspace_id": 1, "id": "3f7db2c1a9…" }

Returns the resolution row:

{ "item_key": "3f7db2c1a9…", "report_run_id": "9d0b3c12-…", "report_created_at": "2026-08-14 08:12:03", "resolved_at": "2026-08-19 10:05:41" }

id is the item’s item_key exactly as returned by GET /strategy-tasks. Resolving is idempotent; a resolved item reappears only if a newer report re-raises the same advice.

Possible errors: 400 sm_api_invalid (bad action or id), 404 sm_api_not_found (unknown item or workspace).

GET /overview

The workspace Home page, composed server-side (free).

GET /overview?workspace_id=1
{
  "analytics": { "summary": { "recommendation_rate": 0.62, "share_of_voice": 0.34 }, "maturity": { "stage": "established", "n_runs": 128 } },
  "reputation": {
    "score": { "value": 62, "grade": "solid", "components": { "endorsement": 66, "sentiment": 71, "alerts": 90, "preference": 58 } },
    "previous_score": 58,
    "alerts": [ { "url": "https://example.com/crm-comparison", "domain": "example.com", "severity": "medium" } ],
    "last_run": { "run_id": "7b2e491a-…", "created_at": "2026-08-15T09:00:00Z" },
    "tracker_id": "dc58424b-…"
  },
  "discovery_score": 71,
  "discovery_checked_at": "2026-09-08T10:00:00Z",
  "strategy_tasks": { "items": [ { "item_key": "3f7db2c1a9…", "scope": "site", "title": "Best CRM for small teams" } ], "has_strategy": true, "resolved_open": 2 },
  "recent_runs": [ { "run_id": "7b2e491a-…", "operation": "reputation", "check": { "tracker_id": "dc58424b-…", "run_questions": true, "include_rescan": false, "generate_strategy": false }, "state": "captured", "credits_charged": 413, "created_at": "2026-08-15T09:00:00Z", "label": "Reputation check · CRM software · National", "source": "api", "insights_run_id": null, "insights_running_run_id": null } ],
  "setup_state": {
    "library": { "loaded": true, "counts": { "target": 1, "persona": 3, "goal": 4, "class_def": 6, "homepage": 1 }, "ready": { "class_def": true, "target": true, "persona": true, "goal": true, "homepage": true }, "ready_count": 5, "state": "complete" },
    "completed_runs": 12, "recommendations": 3, "runs_any": 14, "has_runs": true, "library_ready": true
  }
}

One call instead of four. This is the preferred way to build a dashboard — it costs 1 request against your per-minute cap, not four separate hits to analytics, report, strategy-tasks, and runs. analytics and reputation are subsets (full payloads on GET /analytics / GET /reputation/report); strategy_tasks is the exact GET /strategy-tasks payload; recent_runs is the last 10 runs, list-shaped like GET /runs items. Sections degrade independently to null if an upstream read fails — a broken section never errors the whole overview. The Home page’s narrative copy and its merged, capped action list are client-side presentation only and aren’t replicated here — compose your own from strategy_tasks + reputation.alerts.

discovery_score/discovery_checked_at mirror the default tracker’s latest Discovery check — default-check-only, so advanced/custom runs never move it; both are null before the first check or when that read fails.

Possible errors: 404 sm_api_not_found (unknown workspace).

GET /openapi

The machine-readable spec for the entire API (free, no auth).

GET /openapi

Returns the full OpenAPI 3.1 document for every route in this reference — bearer security scheme, the complete error-code enum, run-state/question-kind/reach enums, and every endpoint’s request/response schema (the reputation report shape is defined once and $ref‘d wherever it recurs).

{
  "openapi": "3.1.0",
  "info": { "title": "Programate Lift API", "version": "1.0.0" },
  "servers": [ { "url": "https://programate.ai/wp-json/sm-api/v1" } ],
  "paths": { "/me": { "get": { "summary": "Your account, this key's caps, and current usage" } } }
}

No Authorization header needed — this route is public so agents can introspect the API before they have a key. It’s still subject to the per-IP rate-limit layer.

Possible errors: 429 sm_api_rate (per-IP limit).

Errors

Every error is JSON with an HTTP status and a stable machine-readable code:

{ "code": "sm_api_insufficient_credits", "message": "Not enough credits for this run.", "data": { "status": 402 } }
StatusCodeMeaning
400sm_api_invalidMalformed/invalid parameters (bad id, out-of-range n_runs/max_turns, bad action/kind, blank name)
400sm_api_model_disabledA chosen engine is turned off
400sm_api_no_homepageinclude_rescan requested but the target has no homepage set — patch it first
400sm_api_question_capSaving would exceed the 40-question cap on a tracker
400sm_api_unsupported_operationEndpoint doesn’t apply to this run’s operation or id (e.g. transcript with a launch-level run id — probe or check — instead of a per-cell run_id from GET /reputation/report; recommendations on a reputation run)
401sm_api_unauthorizedMissing or invalid token
402sm_api_insufficient_creditsNot enough credits to run
402sm_api_workspace_capCan’t create another workspace on your plan (plan cap plus extra workspaces)
402sm_api_tracker_capPlan’s unarchived-tracker cap reached (data carries {cap, plan}) — archive one or upgrade
402sm_api_upgrade_requiredPAYG can’t create an offering result class (or promote a distractor to one) — upgrade to add competing categories
403sm_api_workspace_over_capMore workspaces than your plan allows (soft-freeze, e.g. after a downgrade or a removed extra workspace renews off) — delete workspaces, add an extra workspace, or upgrade before running
403sm_api_account_suspendedAccount is suspended
403sm_api_key_suspendedThis key was auto-disabled for abuse — create a new one or contact support
403sm_api_past_dueSubscription payment overdue — runs paused until settled (reads still work)
403sm_api_model_not_approvedA chosen engine isn’t approved for your account
403sm_api_forbiddenAction not permitted
404sm_api_not_foundNo such run/workspace/library object/tracker/task (also returned for resources your key doesn’t own)
409sm_api_key_limitToo many API keys on the account (max 10)
409sm_api_library_limitLibrary kind at its per-workspace cap
409sm_api_library_in_useDeleting a library object still referenced (e.g. a target’s success_class)
409sm_api_library_incompleteWorkspace has no Target/Persona/Goal yet — create them before running
409sm_api_last_workspaceYou must keep at least one workspace
409sm_api_no_questionsA check with run_questions but no generated/added questions on the tracker — generate questions first
409sm_api_run_not_completeRecommendations need a finished probe run
409sm_api_run_not_readyTranscript requested before the check reached a terminal state
409sm_api_insights_exists / sm_api_insights_in_flightRecommendations already generated / already running for that run
410sm_api_run_expiredRun results are no longer available
429sm_api_rateToo many calls (per-key all-calls cap, per-IP, or the questions-save 20/min throttle)
429sm_api_run_interval / sm_api_concurrentRun launched too soon / too many runs already in flight
429sm_api_capacityWe’re at capacity (our server or an upstream model provider) — run not started; no credits charged. Retry shortly
429sm_api_capacity_busyBoth question-generation slots are busy (service-wide) — retry after data.retry_after seconds
429sm_api_generation_limitedOver the 5/hr question-generation quota (shared with the app)
429sm_api_cap_hour / sm_api_cap_monthOver this key’s hourly / monthly credit cap
500sm_api_internalUnexpected server error
502sm_api_engine_errorUpstream engine temporarily unavailable

Quickstart

BASE=https://programate.ai/wp-json/sm-api/v1
KEY="sm_live_a1b2c3d4…wXyZ"

# 1. Launch a run
RUN=$(curl -s "$BASE/runs" -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"persona_id":"8f3a1c2d-…","goal_id":"2c1d7b4e-…","engines":["gemini"],"n_runs":3,"max_turns":4}' \
  | jq -r .run_id)

# 2. Poll until it's done (respect your plan's rate limits)
curl -s "$BASE/runs/$RUN" -H "Authorization: Bearer $KEY" | jq .state

# 3. Read the full results
curl -s "$BASE/runs/$RUN" -H "Authorization: Bearer $KEY" | jq .results.summary

# 4. Launch a reputation check on your default tracker (checks take ~7-10 min)
CHECK=$(curl -s "$BASE/reputation/checks" -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"engines":["gemini"],"run_questions":true}' | jq -r .run_id)

# 5. Poll the check the same way
curl -s "$BASE/runs/$CHECK" -H "Authorization: Bearer $KEY" | jq .state

# 6. Read the scored report
curl -s "$BASE/reputation/report" -H "Authorization: Bearer $KEY" | jq .report.score

Need a key? Head to your dashboard.