Integration reference

Install Emic in one paste.

Emic is user research for products whose users are AI agents. The default install hears them directly: agents file structured reports through a new tool and answer exit surveys that ride back inside that tool's response. It does not wrap existing tools, record their calls, or add passive session capture. A support ticket is one feedback type of eight; agents also file friction on calls that still succeeded, feature requests, docs and schema issues, awkward workflows, and what worked. Integration is plain HTTP POST; there is no SDK to install. Optional permissioned session studies are separate and off by default. This page is the canonical reference; the same content is served as raw markdown at /llms-full.txt for AI coding agents to fetch directly.

01

The install prompt

Paste this into Claude Code, Cursor, or Codex. It tells the agent exactly where the endpoint is, which auth header to use, which packages don't exist, and how to verify the install with a curl that prints HTTP 201.

Install prompt · paste into Claude Code, Cursor, Codex
02

Endpoint

MethodPOST
URLhttps://emic.sh/api/v1/feedback
AuthAuthorization: Bearer <EMIC_API_KEY>
Content-Typeapplication/json
SuccessHTTP 201

Common pitfall: success is 201, not 200. Use Authorization: Bearer, not X-API-Key. The /api/v1/report path still works as an accepted alias.

03

Request body (JSON)

FieldType / ValuesReq
feedback_type"support_ticket" | "bug_or_failure" | "friction" | "feature_request" | "docs_feedback" | "schema_feedback" | "workflow_feedback" | "positive_feedback"yes
tool_or_endpointstring — the exact tool name or API endpoint this feedback is aboutyes
outcome"success" | "partial_success" | "failure" — friction can ride on a successno
agent_goalstring — the higher-level goal the agent was pursuingno
taskstring — the specific task this tool was meant to dono
what_workedstring — what worked well (praise is signal too)no
what_was_confusingstring — what was ambiguous or surprisingno
what_was_missingstring — a capability/field/mode the agent needed but lackedno
what_would_improve_itstring — the concrete change that would make it betterno
expected_behaviorstring — for a bug/failure: what was expectedno
actual_behaviorstring — for a bug/failure: what happened insteadno
error_messagestring — the error/status returned, verbatimno
attempted_callsstring[] — the calls tried, in order; a minimal replayno
failure_typefor a failure, the granular kind: "missing_capability" | "confusing_schema" | "bad_tool_description" | "incomplete_response" | "incorrect_response" | "unhelpful_error" | "authentication_problem" | "pagination_or_truncation" | "rate_limit_or_timeout" | "docs_mismatch" | "wrong_tool_selected" | "other"no
satisfaction_scoreinteger — 1 (bad) to 5 (great)no
severity"blocking" | "high" | "medium" | "low" (derived if omitted)no
serverNamestring — your product name; derive from package.json (recommended)no
surface"mcp" | "rest" | "vercel-ai" | "langchain" | "other" (recommended)no
agentobject — self-reported identity: `{ model }`no

Only feedback_type and tool_or_endpoint are required — send whatever else fits the kind of feedback. A title is generated server-side — you do not send one.

04

Status codes

201Success — feedback filed. Body: { ok, id }.
401Missing or invalid Authorization: Bearer header. Check the key.
422Validation failed — a required field is missing.
429Rate-limited.
05

Example handler (MCP, TypeScript)

// register next to your existing tools — does not wrap or modify them.
server.registerTool(
  'submit_agent_feedback',
  {
    title: 'Leave feedback about <YOUR-PRODUCT>',
    description: 'After you use this product, report your experience: a bug, friction on a call that still worked, a missing capability, confusing docs or schema, an awkward workflow, or simply what worked.',
    inputSchema: {
      // required: feedback_type, tool_or_endpoint. Everything else is optional.
      feedback_type:         { enum: ['support_ticket','bug_or_failure','friction',
                                    'feature_request','docs_feedback','schema_feedback',
                                    'workflow_feedback','positive_feedback'] },
      tool_or_endpoint:      { type: 'string' },
      outcome:               { enum: ['success','partial_success','failure'] }, // optional
      agent_goal:            { type: 'string' },
      task:                  { type: 'string' },
      what_worked:           { type: 'string' },
      what_was_confusing:    { type: 'string' },
      what_was_missing:      { type: 'string' },
      what_would_improve_it: { type: 'string' },
      expected_behavior:     { type: 'string' },
      actual_behavior:       { type: 'string' },
      error_message:         { type: 'string' },
      attempted_calls:       { type: 'array', items: { type: 'string' } },
      failure_type:          { enum: ['missing_capability','confusing_schema',
                                    'bad_tool_description','incomplete_response',
                                    'incorrect_response','unhelpful_error',
                                    'authentication_problem','pagination_or_truncation',
                                    'rate_limit_or_timeout','docs_mismatch',
                                    'wrong_tool_selected','other'] },
      satisfaction_score:    { type: 'integer' }, // optional, 1-5
      severity:              { enum: ['blocking','high','medium','low'] }, // optional
      model:                 { type: 'string' },  // optional
    },
  },
  async (args) => {
    const url = process.env.EMIC_URL || process.env.AGENTRATE_URL || 'https://emic.sh';
    // fire-and-forget — never block the agent path:
    fetch(`${url}/api/v1/feedback`, {
      method: 'POST',
      headers: {
        'content-type': 'application/json',
        authorization: `Bearer ${process.env.EMIC_API_KEY}`,
      },
      body: JSON.stringify({
        serverName: 'my-mcp-server', // derived from package.json
        surface: 'mcp',
        feedback_type: args.feedback_type,
        tool_or_endpoint: args.tool_or_endpoint,
        outcome: args.outcome,
        agent_goal: args.agent_goal,
        task: args.task,
        what_worked: args.what_worked,
        what_was_confusing: args.what_was_confusing,
        what_was_missing: args.what_was_missing,
        what_would_improve_it: args.what_would_improve_it,
        expected_behavior: args.expected_behavior,
        actual_behavior: args.actual_behavior,
        error_message: args.error_message,
        attempted_calls: args.attempted_calls,
        failure_type: args.failure_type,
        satisfaction_score: args.satisfaction_score,
        severity: args.severity,
        agent: { model: args.model },
      }),
    }).catch(() => {});

    return { content: [{ type: 'text', text: 'Feedback recorded — thanks.' }] };
  },
);
06

Example handler (OpenAI function calling)

import OpenAI from 'openai';
const openai = new OpenAI();

const tools = [
  {
    type: 'function',
    function: {
      name: 'submit_agent_feedback',
      description: 'After you use this product, report your experience: a bug, friction on a call that still worked, a missing capability, confusing docs or schema, an awkward workflow, or simply what worked.',
      parameters: {
        type: 'object',
        properties: {
          feedback_type:         { type: 'string', enum: ['support_ticket','bug_or_failure','friction','feature_request','docs_feedback','schema_feedback','workflow_feedback','positive_feedback'] },
          tool_or_endpoint:      { type: 'string', description: 'The exact tool name or API endpoint this feedback is about.' },
          outcome:               { type: 'string', enum: ['success','partial_success','failure'] },
          agent_goal:            { type: 'string', description: 'The higher-level goal you were pursuing.' },
          task:                  { type: 'string', description: 'The specific task this tool was meant to do.' },
          what_worked:           { type: 'string', description: 'What worked well (praise is signal too).' },
          what_was_confusing:    { type: 'string', description: 'What was ambiguous or surprising.' },
          what_was_missing:      { type: 'string', description: 'A capability/field/mode you needed but lacked.' },
          what_would_improve_it: { type: 'string', description: 'The concrete change that would make it better.' },
          expected_behavior:     { type: 'string', description: 'For a bug/failure: what you expected.' },
          actual_behavior:       { type: 'string', description: 'For a bug/failure: what happened instead.' },
          error_message:         { type: 'string' },
          attempted_calls:       { type: 'array', items: { type: 'string' } },
          failure_type:          { type: 'string', enum: ['missing_capability','confusing_schema','bad_tool_description','incomplete_response','incorrect_response','unhelpful_error','authentication_problem','pagination_or_truncation','rate_limit_or_timeout','docs_mismatch','wrong_tool_selected','other'] },
          satisfaction_score:    { type: 'integer' },
          severity:              { type: 'string', enum: ['blocking','high','medium','low'] },
          model:                 { type: 'string' },
        },
        required: ['feedback_type', 'tool_or_endpoint'],
      },
    },
  },
];

// when the model calls submit_agent_feedback, POST the arguments:
async function handleSubmitAgentFeedback(args) {
  await fetch(`${process.env.EMIC_URL || process.env.AGENTRATE_URL || 'https://emic.sh'}/api/v1/feedback`, {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      authorization: `Bearer ${process.env.EMIC_API_KEY}`,
    },
    body: JSON.stringify({
      serverName: 'my-agent-app',
      surface: 'other',
      feedback_type: args.feedback_type,
      tool_or_endpoint: args.tool_or_endpoint,
      outcome: args.outcome,
      agent_goal: args.agent_goal,
      task: args.task,
      what_worked: args.what_worked,
      what_was_confusing: args.what_was_confusing,
      what_was_missing: args.what_was_missing,
      what_would_improve_it: args.what_would_improve_it,
      expected_behavior: args.expected_behavior,
      actual_behavior: args.actual_behavior,
      error_message: args.error_message,
      attempted_calls: args.attempted_calls,
      failure_type: args.failure_type,
      satisfaction_score: args.satisfaction_score,
      severity: args.severity,
      agent: { model: args.model },
    }),
  }).catch(() => {});
}
07

Example handler (Anthropic tool use)

import Anthropic from '@anthropic-ai/sdk';
const anthropic = new Anthropic();

const message = await anthropic.messages.create({
  model: 'claude-opus-4-8',
  max_tokens: 1024,
  messages,
  tools: [
    {
      name: 'submit_agent_feedback',
      description: 'After you use this product, report your experience: a bug, friction on a call that still worked, a missing capability, confusing docs or schema, an awkward workflow, or simply what worked.',
      input_schema: {
        type: 'object',
        properties: {
          feedback_type:         { type: 'string', enum: ['support_ticket','bug_or_failure','friction','feature_request','docs_feedback','schema_feedback','workflow_feedback','positive_feedback'] },
          tool_or_endpoint:      { type: 'string', description: 'The exact tool name or API endpoint this feedback is about.' },
          outcome:               { type: 'string', enum: ['success','partial_success','failure'] },
          agent_goal:            { type: 'string', description: 'The higher-level goal you were pursuing.' },
          task:                  { type: 'string', description: 'The specific task this tool was meant to do.' },
          what_worked:           { type: 'string', description: 'What worked well (praise is signal too).' },
          what_was_confusing:    { type: 'string', description: 'What was ambiguous or surprising.' },
          what_was_missing:      { type: 'string', description: 'A capability/field/mode you needed but lacked.' },
          what_would_improve_it: { type: 'string', description: 'The concrete change that would make it better.' },
          expected_behavior:     { type: 'string', description: 'For a bug/failure: what you expected.' },
          actual_behavior:       { type: 'string', description: 'For a bug/failure: what happened instead.' },
          error_message:         { type: 'string' },
          attempted_calls:       { type: 'array', items: { type: 'string' } },
          failure_type:          { type: 'string', enum: ['missing_capability','confusing_schema','bad_tool_description','incomplete_response','incorrect_response','unhelpful_error','authentication_problem','pagination_or_truncation','rate_limit_or_timeout','docs_mismatch','wrong_tool_selected','other'] },
          satisfaction_score:    { type: 'integer' },
          severity:              { type: 'string', enum: ['blocking','high','medium','low'] },
          model:                 { type: 'string' },
        },
        required: ['feedback_type', 'tool_or_endpoint'],
      },
    },
  ],
});

// when the model emits a tool_use block for submit_agent_feedback, POST its input:
async function handleSubmitAgentFeedback(input) {
  await fetch(`${process.env.EMIC_URL || process.env.AGENTRATE_URL || 'https://emic.sh'}/api/v1/feedback`, {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      authorization: `Bearer ${process.env.EMIC_API_KEY}`,
    },
    body: JSON.stringify({
      serverName: 'my-agent-app',
      surface: 'other',
      feedback_type: input.feedback_type,
      tool_or_endpoint: input.tool_or_endpoint,
      outcome: input.outcome,
      agent_goal: input.agent_goal,
      task: input.task,
      what_worked: input.what_worked,
      what_was_confusing: input.what_was_confusing,
      what_was_missing: input.what_was_missing,
      what_would_improve_it: input.what_would_improve_it,
      expected_behavior: input.expected_behavior,
      actual_behavior: input.actual_behavior,
      error_message: input.error_message,
      attempted_calls: input.attempted_calls,
      failure_type: input.failure_type,
      satisfaction_score: input.satisfaction_score,
      severity: input.severity,
      agent: { model: input.model },
    }),
  }).catch(() => {});
}
08

Verify the install (always)

Run this from the project root. A successful install prints HTTP 201. Any other code means the install is wrong — do not declare done.

# source .env into the shell so $EMIC_API_KEY is populated:
set -a; . ./.env; set +a

curl -sS -o /dev/null -w "HTTP %{http_code}\n" \
  -X POST "https://emic.sh/api/v1/feedback" \
  -H "authorization: Bearer $EMIC_API_KEY" \
  -H "content-type: application/json" \
  -d '{"serverName":"my-product","surface":"mcp","feedback_type":"positive_feedback","tool_or_endpoint":"install_check","outcome":"success","what_worked":"Install verification — safe to ignore."}'
09

Exit surveys ride the response (ask)

When an agent files a report and one of your surveys is due, the 201 body carries the question:

{
  "ok": true,
  "id": "fb_a1b2c3",
  "survey_prompt": "Maintainer follow-up (optional, one reply): the maintainers of my-product have one short question about this session. Answer only from what actually happened in this session; \"cannot tell\" is a valid answer... To answer, call submit_agent_feedback once more with ... survey_id: \"svy_9f2e#v3\", and answers: { \"q1\": \"...\" }.",
  "survey_id": "svy_9f2e#v3"
}

Your tool handler relays survey_prompt as part of the tool's text response (the canonical handler in /llms-full.txt already does). The agent answers with one more submit_agent_feedback call that includes the survey_id it was given — those reports land labeled stated · asked, and collection stops automatically at the weekly target you set. Surveys are written and versioned in Dashboard → Studies.

10

Optional session studies (off by default)

The default install does not add an observer, session recording, or any wrapper around existing calls. A team can separately choose a bounded session study, define its consent and data boundary, and post a compact study record to /api/v1/sessions. This is never required for direct reports or surveys and must not be silently enabled. Any generated session story is labeled inferred, never presented as the agent's own words.

11

Transcript-grounded analysis (inferred)

Analysis requires a separately captured, permissioned study session; no session capture is installed by default. From the inbox, a separate analysis model reads the transcript and answers bounded behavioral questions. It is not the original agent and has no access to that agent's memory or private reasoning. Every finding must carry a source quote, and the UI checks whether that exact text occurs in the transcript. Quote presence shows grounding, not that the interpretation is true. Results are labeled inferred. Analysis runs only for sessions the team explicitly chose to capture.

Need a key? Create or rotate one in Dashboard → Settings → Ingest keys.