REST API

The widget POSTs to these; you can too. All paths under /v1/* accept the project Bearer key via headers:

x-earshot-project: prj_xxxxxxxxxxxx
x-earshot-key:     pk_xxxxxxxxxxxxxxxx

Missing or wrong key → 401. Missing project header → 400. Constant-time comparison; per-project audit logged in production.

POST /v1/feedback

Submit feedback. One-shot — there are no follow-up rounds.

POST https://app.earshotbot.com/v1/feedback
Content-Type: multipart/form-data

audio:      <File>   optional, audio/webm or audio/mp4
screenshot: <File>   optional, image/png
payload:    <JSON>   required

Payload schema:

{
  sessionId: string;        // UUID v7 from the widget's session cookie
  transcript: string;       // empty if audio is being sent (server transcribes)
  context: CapturedContext; // see "Captured context" below
  identity: Identity | null;
  audioDurationMs?: number; // recorded duration of the audio, if any
}

Returns 202 immediately after the upload completes:

{ "sessionId": "<uuid>", "status": "processing" }

The transcription + LLM enrichment + ingest run in a background Inngest worker. The widget treats the 202 as "filed" and doesn't normally wait; a client that wants progress can poll the state endpoint below.

GET /v1/feedback/session/[sessionId]/state

Poll the async progress of a prior POST. Same auth headers as POST.

// Still working
{ "status": "processing" }

// Finished — feedback row exists
{ "status": "done", "id": "fb_abc123...", "transcript": "..." }

// Anything failed (no API key, transcription failed, etc.)
{ "status": "error", "error": "transcribe failed" }

Returns 404 once the session has aged out (1 hour TTL).

POST /v1/identify

Backfill identity onto every feedback row in the session that's still anonymous.

POST https://app.earshotbot.com/v1/identify
Content-Type: application/json
x-earshot-project, x-earshot-key

{
  "sessionId": "<uuid>",
  "identity": {
    "userId": "u_123",
    "email": "user@host.com",
    "name": "Pat User",
    "traits": { "plan": "pro" }
  }
}

Response: { "ok": true, "updated": N } — count of rows backfilled. Linked tracker issues + future "resolve" emails pick up the identity automatically.

PATCH /v1/feedback/[id]

Update status, priority, resolution note, or labels.

PATCH https://app.earshotbot.com/v1/feedback/fb_abc123
Content-Type: application/json
x-earshot-project, x-earshot-key

{
  "status": "done",                  // optional
  "priority": 1,                     // 0-4 (0 = highest)
  "resolutionNote": "Fixed in v1.4", // cleared when status moves back to non-resolved
  "labels": ["bug", "auth"]          // replaces the list
}

Setting status to done or canceled:

  • Stamps resolved_at.
  • Mirrors the resolution as a comment on the linked Linear issue.
  • Emails the submitter (if identity.email is set + RESEND_API_KEY configured).

Reverting clears those.

GET + POST /v1/feedback/[id]/comments

GET  → list comments oldest-first
POST → append one comment

POST body:

{
  body: string;                                 // 1-8000 chars
  authorType: 'human' | 'agent' | 'submitter';  // default 'human'
  authorId?: string | null;                      // free-form, e.g. 'claude-opus-4-7'
  notifySubmitter?: boolean;                     // emails identity.email
  mirrorToTracker?: boolean;                     // appends to linked Linear issue
}

Captured context

Auto-populated by the widget, validated server-side via Zod:

{
  dom: { format: 'rrweb-full-snapshot-v2', data: unknown, capturedAt: string } | null,
  console: Array<{ level, message, stack, timestamp }> // max 50
  network: Array<{ url, method, status, durationMs, initiator, timestamp }>, // max 10
  device: { userAgent, platform, language, viewport: {width, height}, devicePixelRatio, timezone },
  page: { url, referrer, title, customAttrs },
  screenshotUrl: string | null,
  annotationLayer: unknown | null,
  capturedAt: string, // ISO timestamp
}

CORS

/v1/* accepts cross-origin POST/PATCH/GET with Access-Control-Allow-Origin echoing the request origin. Headers allowed: content-type, x-earshot-project, x-earshot-key, authorization. Methods: GET, POST, PATCH, DELETE, OPTIONS.

Per-project origin allowlist enforcement ships in Sprint 5 — until then any origin can POST with a valid key.

Rate limiting

None enforced today. Documented limits will be:

  • 100 submissions / minute per project (sliding window)
  • 10 MB per audio file, 5 MB per screenshot
  • 50 MB cumulative storage per submission

In production: 429 with Retry-After.