Anchored Growth — API Guide
Anchored Growth gives AI sales agents calibrated outcome predictions: Bayesian per-channel posteriors, drift detection, per-contact decision history, recommendation profiles, and a tamper-evident audit trail.
You send us your outbound outcomes through a REST ingestion endpoint; you read calibrated decisions back through a set of Model Context Protocol (MCP) tools that any MCP-capable agent (Claude Desktop, Claude Code, your own client) can call.
This guide takes you from "I have an API key" to "I just made a calibrated tool call" in about five minutes.
What you get
Seven read-only MCP tools:
| Tool | What it answers | Required args |
|---|---|---|
get_campaign_health |
"How is this campaign performing?" | tenant_id, campaign_id |
get_campaign_recommendations |
"What does AG recommend?" (tone, day-of-week, word count, …) | tenant_id |
get_bandit_state |
"What's the Bayesian state behind those recommendations?" (Beta posteriors per channel/position) | tenant_id |
get_drift_state |
"Is the model still calibrated?" (CUSUM drift) | tenant_id |
get_workflow_decisions |
"Show me the actual per-contact decisions." | tenant_id |
get_recipient_intelligence |
"Tell me about this contact — decisions + position guidance." | tenant_id, contact_id |
get_action_audit_log |
"Prove every prior tool call was logged." | tenant_id |
Every response is locked to schema_version: "1.0.0". New fields may be added
(non-breaking); existing fields are never renamed or removed before a major
version bump.
Endpoints
| Surface | URL |
|---|---|
| MCP server (streamable-http) | https://mcp.anchoredgrowth.io/mcp |
| MCP health check (public, no auth) | https://mcp.anchoredgrowth.io/healthz |
| Outcome ingestion — JSON | https://c3ltly5r5j.execute-api.us-east-2.amazonaws.com/prod/ingest/events |
| Outcome ingestion — CSV | https://c3ltly5r5j.execute-api.us-east-2.amazonaws.com/prod/ingest/csv |
All calls authenticate with the same key:
Authorization: Bearer ag_YOUR_KEY_HERE
Authentication
You authenticate with an Anchored Growth API key in the format
ag_<48 chars>_<8-char checksum>. The same key works for both the MCP tools and
the ingestion endpoints, scoped to a single tenant_id (your company ID).
Getting a key: mint it yourself at https://app.anchoredgrowth.io →
AI Agent Access → Generate API key (requires the manage_billing
permission). The screen also shows your tenant_id and config snippets. The key is shown
exactly once — store it securely.
Step 1 — Confirm the server is reachable
curl https://mcp.anchoredgrowth.io/healthz
Expected:
{"status":"ok","transport":"streamable-http"}
If you don't get 200 OK, your network is blocking the host — stop here and
contact us.
Step 2 — Connect your client
Option A — Claude Desktop
Claude Desktop launches MCP servers over stdio, so a remote streamable-http
server needs the mcp-remote bridge (Node.js, run via npx).
Edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or
%APPDATA%\Claude\claude_desktop_config.json (Windows):
{
"mcpServers": {
"anchored-growth": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://mcp.anchoredgrowth.io/mcp",
"--header",
"Authorization:Bearer ag_YOUR_KEY_HERE",
"--transport",
"http-only"
]
}
}
}
Restart Claude Desktop. The seven Anchored Growth tools appear in the MCP tool menu.
Option B — Python (official MCP SDK)
pip install mcp
import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
MCP_URL = "https://mcp.anchoredgrowth.io/mcp"
API_KEY = "ag_YOUR_KEY_HERE"
async def main():
headers = {"Authorization": f"Bearer {API_KEY}"}
async with streamablehttp_client(MCP_URL, headers=headers) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print([t.name for t in tools.tools])
result = await session.call_tool(
"get_action_audit_log",
{"tenant_id": "YOUR_TENANT_ID", "limit": 5},
)
print(result.content[0].text)
asyncio.run(main())
Option C — curl (protocol-level debugging)
Every MCP request is JSON-RPC 2.0. The streamable-http transport replies with
Server-Sent Events, so the Accept header must include both content types:
KEY="ag_YOUR_KEY_HERE"
MCP_URL="https://mcp.anchoredgrowth.io/mcp"
curl -X POST $MCP_URL \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{
"protocolVersion":"2025-11-25",
"capabilities":{},
"clientInfo":{"name":"curl","version":"0.0.1"}
}
}'
The JSON-RPC payload comes back on the SSE data: line.
Step 3 — Make your first tool call
get_action_audit_log is the fastest tool to try: it returns immediately whether
or not you've sent us any data, and the shape is identical on a fresh tenant.
curl -X POST $MCP_URL \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{
"name":"get_action_audit_log",
"arguments":{"tenant_id":"YOUR_TENANT_ID","limit":5}
}
}'
Expected (excerpt — text is a JSON-encoded string per the MCP content-block contract):
{
"schema_version": "1.0.0",
"tenant_id": "YOUR_TENANT_ID",
"scope": "company",
"entry_count": 0,
"entries": [],
"summary": "No audit entries for this tenant.",
"alerts": ["no_audit_entries_available", "payloads_not_stored_only_sha256_hashes"],
"as_of": "2026-06-09T00:00:00Z"
}
Call it a second time and entry_count increments — the audit trail is
self-referential by design.
Step 4 — Send us your outcome data
The tools become meaningful once they have outcomes to reason over. POST outbound outcomes to the JSON ingestion endpoint:
curl -X POST https://c3ltly5r5j.execute-api.us-east-2.amazonaws.com/prod/ingest/events \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{
"events": [{
"channel": "email",
"sequence_step": 0,
"recipient_id": "prospect-001",
"recipient_email": "demo@example.com",
"recipient_position": "VP of Engineering",
"sent_at": "2026-06-01T15:30:00Z",
"outcome_at": "2026-06-02T09:12:00Z",
"outcome": "responded",
"tone": "direct",
"word_count": 87
}]
}'
Expected: HTTP 202 with body {"accepted":1,"rejected":0}.
Required fields:
| Field | Type | Notes |
|---|---|---|
channel |
string | email or linkedin |
sequence_step |
int | 0-indexed step (0–50). This is the step number, not a job title. |
recipient_id |
string | Your opaque prospect identifier |
sent_at |
ISO8601 | When the message was sent |
outcome_at |
ISO8601 | When the outcome was observed |
outcome |
string | One of responded, no_response, bounced, unsubscribed |
Useful optional fields:
| Field | Notes |
|---|---|
event_id |
UUID for client-side idempotency |
recipient_email |
RFC 5321 address; used by reply-detection |
recipient_position |
Job title (e.g. "VP of Engineering") — feeds position bucketing |
thread_id |
If omitted, derived deterministically from (tenant, recipient, campaign) |
tone |
One of direct, conversational, professional, empathetic, questioning. casual is not accepted — use conversational. |
word_count, subject, body_excerpt, body, industry |
Content features. body is opt-in, capped at 10 000 chars. |
metadata |
Arbitrary JSON, ≤2 KB serialized. metadata.campaign_id / metadata.sequence_id contributes to thread derivation. |
Limits: max 500 events per request. Use {"events":[...]} for batches, or POST
a single event object (no wrapper) for one-offs.
CSV alternative: POST /ingest/csv accepts a header row + up to 500 data rows
(5 MB body cap, same field names as columns).
Step 5 — Get a calibrated read
After your first batch lands, the analytics pipeline starts populating per-tenant
state. get_bandit_state exposes the Bayesian per-(channel, position) posteriors
that drive AG's decisions:
curl -X POST $MCP_URL \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc":"2.0","id":3,"method":"tools/call",
"params":{
"name":"get_bandit_state",
"arguments":{"tenant_id":"YOUR_TENANT_ID","top_n":5}
}
}'
The intelligence pipeline runs once daily; arms populate after the first batch
is ingested and processed. Each populated arm carries alpha, beta,
posterior_mean, posterior_variance, and a data_source flag
(migration = bootstrapped from history, live = streamed).
Deleting data
# Delete a single event
curl -X DELETE \
"https://c3ltly5r5j.execute-api.us-east-2.amazonaws.com/prod/ingest/events/{event_id}" \
-H "Authorization: Bearer $KEY"
# Delete every event for a recipient
curl -X DELETE \
"https://c3ltly5r5j.execute-api.us-east-2.amazonaws.com/prod/ingest/events?recipient_id=prospect-001" \
-H "Authorization: Bearer $KEY"
Both are tenant-scoped — there is no path that deletes events outside your company. Ingested events also auto-delete via TTL 24 months after ingestion.
Troubleshooting
| Status | Meaning | Fix |
|---|---|---|
401 missing_token |
Authorization header absent/empty |
Send exactly Authorization: Bearer ag_... |
401 invalid_token / self_serve_auth_failed |
Token doesn't match a credential. Body's reason says why: INVALID_FORMAT (truncation/whitespace), TOKEN_NOT_FOUND, TOKEN_INACTIVE (revoked), TOKEN_EXPIRED / TOKEN_LOCKED, TOKEN_INVALID (typo) |
Re-check the key; contact us if revoked/expired |
429 QUOTA_EXCEEDED |
Monthly tool-call quota exhausted | Body has quotaLimit, currentUsage, resetAt. Contact us for a higher limit |
429 TOO_MANY_FAILED_ATTEMPTS |
5 bad auth attempts (same IP+key) in 1 hour | Wait (Retry-After: 3600) or use a different IP |
406 Not Acceptable |
Accept header missing a type |
Use Accept: application/json, text/event-stream |
200 but body looks like an SSE stream |
Expected — streamable-http frames JSON-RPC inside SSE | The JSON is on the data: line; SDKs parse it transparently |
Privacy & audit
Every tool call writes one row to our audit log, and the response blocks on that write (the audit is the canonical record, not a nice-to-have). Each row contains:
- The API key ID (never the raw key)
- SHA-256 hashes of the input and output (never the values themselves)
- Latency, timestamp, success/error type, and the calling actor
We do not store message bodies, recipient emails, or other payload data beyond what's needed to compute a response. Hashed audit rows are the only persistent record of a call — sufficient to prove a call happened and verify byte-equality, insufficient to reconstruct content.
Schema versioning
Every response includes schema_version: "1.0.0". We increment the major
version for any breaking change. Until 2.0, all responses are
forward-compatible: new fields may appear, but existing fields are never renamed
or removed.
Support
Keys are self-serve at https://app.anchoredgrowth.io → AI Agent Access.
Bug reports, product feedback and quota increases: jchasecrafton@anchoredgrowth.io.
When reporting an issue, include the as_of timestamp from any tool response, the
HTTP status code, and the full error body with the API key redacted.