# Ingest Payload Schema

Detailed specification for the `POST /v1/ingest` request and response.

## Request schema

### Capture identity and retry safety

The SDK sends `schemaVersion: 1` and a UUID `captureId` generated once per captured report. Keep the entire accepted payload unchanged across retries. An identical retry in the authenticated project returns the original acceptance, including `occurrenceId`, without creating another occurrence, event or charge. The response includes `projectId` derived from the current authenticated project, including when replaying older receipts that did not store that field. A changed payload with that ID returns409 `CAPTURE_ID_CONFLICT`. Fresh API-key checks still apply. Legacy clients without an ID retain their previous submission behavior.

Optional fields: `capturedAt` (ISO8601 with timezone), `sdkName`, `sdkVersion`, `captureSource`, `osFamily`, `framework`, `runtime`. The server adds `receivedAt`. `applicationId`, `buildId` and `installationId` must refer to server registry records in the key's project; build/install IDs also require the matching application. These labels do not grant CI/agent authority. Registry management routes are not yet released.

### Top-level fields

| Field | Type | Required | Max size | Default | Description |
|-------|------|----------|----------|---------|-------------|
| `title` | `string` | **Yes** | 500 chars | — | Bug report title |
| `description` | `string` | No | 10,000 chars | `""` | Detailed description |
| `severity` | `enum` | No | — | `"medium"` | `"critical"`, `"high"`, `"medium"`, `"low"` |
| `url` | `string` | No | 2,000 chars | — | Page URL where the bug occurred |
| `userAgent` | `string` | No | 500 chars | — | Browser user agent string |
| `viewport` | `string` | No | 50 chars | — | Viewport dimensions, e.g. `"1920x1080"` |
| `events` | `Event[]` | No | 1,000 items | `[]` | Captured browser events |
| `screenshot` | `any` | No | — | `null` | Legacy field; ignored. Occurrence records `not_stored`. Never a ready file |
| `audio` | `any` | No | — | `null` | Legacy field; ignored. Occurrence records `not_stored`. Never a ready file |
| `aiSummary` | `object` | No | — | `null` | AI-generated analysis |
| `encrypted` | `object` | No | — | — | Unsupported end-to-end; do not send secrets here |
| `reporterEmail` | `string` | No | valid email | — | Reporter email for attribution |
| `reporterId` | `string` | No | 200 chars | — | App-specific reporter identifier |
| `sessionId` | `string` | No | 200 chars | — | Session identifier for occurrence grouping |
| `userId` | `string` | No | 200 chars | — | End-user or account identifier |
| `release` | `string` | No | 200 chars | — | Release, build, or deploy version (app version on mobile). Omitted release is a structured miss (`release_missing`), never HTTP 400 |
| `stack` | `string` | No | 10,000 chars | — | Optional stack for server / non-browser captures. Stored on occurrence `metadata.stack` and as an error event when `events` omit stack. Omitted stack is a structured miss (`stack_missing`), never HTTP 400 |
| `frames` | `object[]` | No | 200 items | — | Optional parsed frames; stored on occurrence `metadata.frames` when present |
| `mcp_timeline` | `object[]` | No | 500 items | — | Optional MCP timeline retained on occurrence `metadata.mcp_timeline` and kept when a failure pack is stored without its own timeline. Ingest does not create packs |
| `environment` | `string` | No | 100 chars | — | Environment label such as `production` or `staging` |
| `platform` | `string` | No | 40 chars | inferred | Capture platform: `web`, `ios`, `android`, `react-native`, `flutter`, `server`, `other`. Aliases (`iPhone`, `node`, `expo`, …) are normalised. Inferred from `userAgent` when omitted (browser UA → `web`, CFNetwork → `ios`, no UA → `server`). Never part of the fingerprint — one issue can span platforms |
| `device` | `string` | No | 120 chars | — | Device model, e.g. `iPhone15,3` (stored in occurrence `metadata.device`) |
| `osVersion` | `string` | No | 60 chars | — | OS version, e.g. `17.4.1` (stored in occurrence `metadata.osVersion`) |
| `metadata` | `object` | No | — | `{}` | Additional occurrence metadata for live feed and triage |

### Event schema

Each event in the `events` array:

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `type` | `string` | Yes | `"unknown"` | Event type identifier |
| `timestamp` | `number` | No | now | Unix timestamp in milliseconds |
| `data` | `any` | No | — | Event-specific payload |

### Common event types and their data shapes

#### Error events

```typescript
// type: "error"
{
  message: string;     // Error message
  filename: string;    // Source file URL
  lineno: number;      // Line number
  colno: number;       // Column number
  stack: string;       // Full stack trace
}

// type: "error:unhandledrejection"
{
  message: string;
  stack: string;
}
```

#### Console events

```typescript
// type: "console:log" | "console:warn" | "console:error" | "console:info" | "console:debug"
{
  level: string;
  args: string[];      // Serialized arguments (max 2,000 chars each)
}
```

#### Network events

```typescript
// type: "network" (resource timing)
{
  url: string;
  duration: number;    // milliseconds
  transferSize: number;
  initiatorType: string;
}

// type: "network:fetch"
{
  url: string;
  method: string;      // GET, POST, etc.
  status: number;      // HTTP status code
  duration: number;
}

// type: "network:fetch:error"
{
  url: string;
  method: string;
  error: string;
  duration: number;
}

// type: "network:xhr"
{
  url: string;
  method: string;
  status: number;
  duration: number;
}
```

#### Rage click events

```typescript
// type: "rage-click"
{
  x: number;
  y: number;
  clicks: number;
  target: string;      // CSS selector or tag name
  url: string;
  innerText: string;
}
```

#### DOM events

```typescript
// type: "dom"
// rrweb event objects — see https://github.com/rrweb-io/rrweb
// These are opaque to the API and stored as-is.
```

### Evidence and encryption limits

This intake stores structured report fields and events. It does not persist the legacy `screenshot`, `audio` or `encrypted` fields as retrievable artifacts. Offered values are dropped and recorded on the occurrence as `metadata.media.<field> = { stored: false, reason: "not_stored" }`. `is_encrypted` is never set from a payload flag — ciphertext and key ownership do not exist on this path. A 201 receipt includes `media: { ready: false, stored: false, notStored: [...] }` and is not proof of media storage. The SDK sends approved PNG bytes through the separate evidence reservation/upload lifecycle only when capability discovery admits PNG. That path has local end-to-end qualification; hosted media storage remains unavailable (`GET /v1/evidence/capabilities` stays `storage_unavailable` until G9 is MET). Report and image retry identities stay fixed, and SDK success waits for a ready artifact. See the SDK README for storage limits. Client `encryptionKey` is rejected by the SDK because end-to-end ciphertext storage/read/decryption is not supported.

## Response schema

### Success (201 Created)

```typescript
{
  id: string;              // UUID — stable report identifier
  projectId: string;       // UUID derived from the authenticated capture key
  occurrenceId: string;    // UUID of the accepted occurrence
  schemaVersion?: 1;       // Present with capture identity
  captureId?: string;      // Echoes the accepted capture UUID
  receivedAt?: string;     // Original server acceptance time
  fingerprint: string;     // 16-char hex — dedup key
  deduplicated: boolean;   // true if grouped with existing open bug
  regression: boolean;     // true if reopened a resolved bug
  is_regression: boolean;  // alias for regression
  media: {                 // Ingest never stores screenshot/audio/encrypted
    ready: false;
    stored: false;
    notStored: Array<'screenshot' | 'audio' | 'encrypted'>;
  };
}
```

### Error responses

#### 400 — Validation failed

```json
{
  "error": "Validation failed",
  "details": [
    { "code": "too_small", "minimum": 1, "type": "string", "inclusive": true, "exact": false, "message": "String must contain at least 1 character(s)", "path": ["title"] }
  ]
}
```

#### 400 — Malformed JSON

```json
{ "error": "Invalid JSON in request body" }
```

#### 401 — Auth failure

```json
{ "error": "Missing or invalid API key" }
```

#### 402 — Plan limit exceeded

```json
{
  "error": "Plan limit reached",
  "limit": 100,
  "current": 100,
  "plan": "free",
  "upgradeUrl": "/console/#settings"
}
```

#### 429 — Rate limited

```json
{
  "error": "Too many requests",
  "retryAfter": 45
}
```

## Field size and truncation guidance

| Field | Max size | Truncation behavior |
|-------|----------|---------------------|
| `title` | 500 chars | Truncated to 500 characters |
| `description` | 10,000 chars | Truncated to 10,000 characters |
| `url` | 2,000 chars | Truncated to 2,000 characters |
| `userAgent` | 500 chars | Rejected if > 500 |
| `viewport` | 50 chars | Rejected if > 50 |
| `events` | 1,000 items | First 1,000 accepted event objects retained |
| Event `data` | No hard limit | Stored as JSONB; keep individual event data under 100KB |
| Total body | 10 MB | Request rejected if body exceeds limit |
| Console args | 2,000 chars/arg | Truncated by SDK at capture time |

**Recommendation:** If you buffer more than 1,000 events, prioritize errors, network failures, and console errors. Drop DOM mutation events first, as they are the most voluminous.

## Deduplication behavior

Reports are deduplicated by fingerprint:

```
SHA256(errorType + ":" + normalizedMessage + ":" + urlPattern).slice(0, 16)
```

**Normalization rules:**
- Hex addresses (`0xABCD`) → `<hex>`
- Timestamps (13-digit numbers) → `<timestamp>`
- UUIDs → `<uuid>`
- IP addresses → `<ip>`
- Port numbers → `:<port>`
- Large numbers (8+ digits) → `<num>`
- URL path segments with numeric IDs → `<id>`

**Matching behavior:**
- Same fingerprint + open bug → increments `report_count` (response: `deduplicated: true`)
- Same fingerprint + resolved bug → reopens the bug (response: `regression: true`)
- New fingerprint → creates a new bug (response: `deduplicated: false, regression: false`)

## Plan limits

| Plan | Bugs per month | Projects | Integrations | Team members |
|------|---------------|----------|--------------|-------------|
| Free | 100 | 1 | 1 | 3 |
| Pro | 1,000 | 5 | 10 | 10 |
| Team | 5,000 | 20 | 50 | 50 |
| Enterprise | Unlimited | Unlimited | Unlimited | Unlimited |

When the monthly bug limit is reached, the API returns `402 Payment Required`.
