API & MCP Reference

The Maneuvr API exposes your workspace's competitors, signals, battlecards, runs, and digests over a small, predictable REST surface — plus webhooks for push and an MCP endpoint for AI assistants like Claude and Cursor.

Quickstart

Three calls to prove the surface works.

# 1. List the competitors in your workspace
curl https://your-workspace.maneuvr.app/api/v1/entities \
  -H "Authorization: Bearer mk_live_YOUR_KEY"

# 2. Ingest a signal you heard about
curl -X POST https://your-workspace.maneuvr.app/api/v1/events:ingest \
  -H "Authorization: Bearer mk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "entity_id": "00000000-0000-0000-0000-000000000000",
    "type": "pricing",
    "title": "Acme raised Enterprise tier to $50k",
    "url": "https://acme.com/pricing"
  }'

# 3. Kick off a refresh run
curl -X POST https://your-workspace.maneuvr.app/api/v1/runs \
  -H "Authorization: Bearer mk_live_YOUR_KEY"

Authentication

All /v1 endpoints take a Bearer token. Workspace API keys (recommended) or a Supabase session JWT both work.

Authorization: Bearer mk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Scopes
API keys are scope-gated. Grant the minimum needed; admin scopes are for keys that manage other keys or webhooks.
read:entities
read:events
read:battlecards
read:digests
read:runs
read:deal_outcomes
write:events
write:runs
write:entities
write:deal_outcomes
admin:webhooks
admin:keys
mcp:read
mcp:write

Create and revoke keys in Settings → API Keys. The plaintext value is shown once at creation.

Response envelope

Every endpoint returns the same shape — even errors. SDKs and webhook consumers can parse one schema.

{
  "data": { /* the resource, or an array of resources */ },
  "error": null,
  "meta": {
    "request_id": "req_8f4c…",
    "page": { "next_cursor": "eyJjcmVhdGVkX2F0Ij…" }
  }
}

request_id is also returned as the X-Request-Id response header. Include it when reporting issues.

Pagination

Cursor-based, opaque. Pass the cursor from meta.page.next_cursor back as ?cursor=…

curl "https://your-workspace.maneuvr.app/api/v1/events?limit=50&cursor=eyJjcmVhdGVkX2F0Ij…" \
  -H "Authorization: Bearer mk_live_YOUR_KEY"

Default limit is 25, max 100. When next_cursor is null, you've reached the end.

Idempotency

Send an Idempotency-Key on POSTs to safely retry. We cache the first response for 24 hours and replay it byte-for-byte.

curl -X POST https://your-workspace.maneuvr.app/api/v1/events:ingest \
  -H "Authorization: Bearer mk_live_YOUR_KEY" \
  -H "Idempotency-Key: 7c2b5f48-…" \
  -H "Content-Type: application/json" \
  -d '{ "entity_id": "...", "type": "launch", "title": "..." }'

If you send a different body with a reused key, the request is rejected with 409 conflict.

Errors & rate limits

Errors share the envelope; status is mapped from error.code.

{
  "data": null,
  "error": {
    "code": "validation",
    "message": "Invalid body",
    "details": { "fieldErrors": { "title": ["Required"] } }
  },
  "meta": { "request_id": "req_…" }
}

unauthorized → 401

forbidden → 403

not_found → 404

conflict → 409

validation → 422

rate_limited → 429

internal → 500

Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (ISO 8601). Back off until the reset timestamp on 429.

Endpoints

Full schemas live in the OpenAPI spec. Required scope shown on the right.

MethodPathSummaryScope
GET
/api/v1/entitiesList entitiesread:entities
POST
/api/v1/entitiesCreate an entitywrite:entities
GET
/api/v1/entities/{id}Get an entityread:entities
PATCH
/api/v1/entities/{id}Update an entitywrite:entities
DELETE
/api/v1/entities/{id}Archive an entitywrite:entities
POST
/api/v1/entities/{id}/autofillAuto-discover sources and seed signalswrite:entities
GET
/api/v1/eventsList events (newest first)read:events
GET
/api/v1/events/{id}Get an event with impact briefread:events
POST
/api/v1/events:ingestIngest a competitor signalwrite:events
GET
/api/v1/deal-outcomesList deal outcomes (newest close first)read:deal_outcomes
POST
/api/v1/deal-outcomesLog a won, lost, or no-decision dealwrite:deal_outcomes
GET
/api/v1/deal-outcomes/{id}Get a deal outcomeread:deal_outcomes
GET
/api/v1/battlecards/{entityId}Get the battlecard for an entityread:battlecards
POST
/api/v1/battlecards/{entityId}/regenerateRegenerate a battlecard from latest signalswrite:entities
GET
/api/v1/runsList recent research runsread:runs
POST
/api/v1/runsKick off a new research runwrite:runs
GET
/api/v1/runs/{id}Get a run by idread:runs
GET
/api/v1/digestsList digestsread:digests
GET
/api/v1/digests/{id}Get a digestread:digests
GET
/api/v1/webhooksList subscriptionsadmin:webhooks
POST
/api/v1/webhooksCreate a subscription (returns signing secret once)admin:webhooks
GET
/api/v1/webhooks/{id}Get a subscriptionadmin:webhooks
PATCH
/api/v1/webhooks/{id}Pause, resume, or change a subscriptionadmin:webhooks
DELETE
/api/v1/webhooks/{id}Delete a subscriptionadmin:webhooks
GET
/api/v1/webhooks/{id}/deliveriesList recent delivery attemptsadmin:webhooks
POST
/api/v1/webhooks/deliveries/{deliveryId}/replayReplay a failed deliveryadmin:webhooks
POST
/api/v1/mcpModel Context Protocol — Streamable HTTP transportmcp:read

Webhooks

Subscribe a URL to one or more event types. We POST a thin envelope; fetch the full resource via REST if you need it.

entity.created
entity.updated
event.created
event.impact_generated
run.started
run.completed
run.failed
battlecard.updated
digest.published
deal_outcome.created
deal_outcome.updated

Payload

{
  "id": "evt_…",
  "type": "event.impact_generated",
  "workspace_id": "…",
  "occurred_at": "2026-06-11T11:30:00Z",
  "data": {
    "id": "…",
    "kind": "event",
    "links": { "self": "/v1/events/…" }
  }
}

Signature

We sign every delivery with X-Maneuvr-Signature: t=<unix-ts>,v1=<hex> where hex = HMAC-SHA256(secret, `$${ts}.${rawBody}`). Reject if the timestamp is more than 5 minutes old.

import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody: string, header: string, secret: string): boolean {
  const parts = Object.fromEntries(
    header.split(",").map((kv) => kv.split("=") as [string, string]),
  );
  const t = parts.t;
  const v1 = parts.v1;
  if (!t || !v1) return false;
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");
  return timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}

Retries

5 attempts at 1m, 5m, 30m, 2h, 12h. After the final failure the delivery is marked dead; replay any delivery from Settings → Webhooks.

MCP (Model Context Protocol)

Point Claude Desktop, Cursor, or any MCP client at /api/v1/mcp to give your AI assistant scoped access to this workspace.

https://your-workspace.maneuvr.app/api/v1/mcp
Setup wizard

Claude Desktop

{
  "mcpServers": {
    "maneuvr": {
      "url": "https://your-workspace.maneuvr.app/api/v1/mcp",
      "headers": { "Authorization": "Bearer mk_live_YOUR_KEY" }
    }
  }
}

Available tools

Read: list_competitors, get_competitor, search_signals, get_event, list_recent_changes, get_latest_digest. Write (scope-gated): trigger_run, ingest_signal.

OpenAPI 3.1 spec

Machine-readable spec for code generation, Postman, or import into any OpenAPI tool.