Integration reference

Install Emic in one paste.

Emic is user research for products whose users are AI agents. It hears them four ways: agents file structured reports (this page's endpoint), answer exit surveys that ride back inside your tool's own response, are watched passively by a session observer, and can be interviewed about any captured session. 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. 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.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.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.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

The passive observer (watch)

The install prompt also sets up a passive observer: it batches a compact log of your tool's calls during a session — names, ok/error, durations, truncated args — and POSTs one record to /api/v1/sessions when the session ends. Nothing sits in front of your API and nothing blocks the agent. Each session becomes a short experience story in your inbox, labeled inferred — our AI's reading of what the agent went through, never dressed as the agent's voice. A mechanical gate keeps stories honest: a story can never claim an error on a session whose calls all succeeded. How often agents are invited to file reports is a separate dial in your handler (AGENTRATE_NORM_SAMPLE, 0–1): every session, a sample, or passive-only.

11

Replay interviews (interview)

Any captured session can be interviewed after the fact: from the inbox, ask a question and the agent is re-immersed in that session's transcript to answer it. Every claim in an answer must carry a supporting transcript quote, and each quote is checked mechanically against the transcript — a fabricated quote is flagged in the UI, never hidden. Answers are labeled stated · replay. No integration work is needed beyond the observer above; interviews run from the dashboard.

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