---
title: "Ingest one or many evidence items from an external source in a single call. Built for webhook senders: PostHog destinations, Zapier, Intercom, or anything that can POST JSON."
method: POST
path: "/api/v1/evidence/ingest"
tags: ["evidence"]
---

# Ingest one or many evidence items from an external source in a single call. Built for webhook senders: PostHog destinations, Zapier, Intercom, or anything that can POST JSON.

`POST /api/v1/evidence/ingest`

Accepts two shapes against the same path: simple { productId, source, content, ... } for one item, and batch { productId, source, items: [...] } for up to 50. Validation is fail-fast and all-or-nothing: the first invalid item rejects the whole request with 400 and nothing is inserted, and the error names the path that failed (e.g. items[2].content). A body that is not a JSON object, a null batch member, or a non-string sourceUrl, stage or externalId is a 400 rather than a 500. productId must be a UUID: a malformed one is a 400, not the 404 it used to surface as. tags stays deliberately lenient — a non-array becomes [] and non-string members are dropped, since refusing them would break senders that work today. The billing check counts the entire batch, so a request that would cross the evidence_items quota is refused whole rather than partially written. Rows land with status='active', triage_status='inbox', origin='integration' (POST /api/v1/evidence leaves origin unset), and created_by=auth.userId. Fire-and-forget side effects: an activity-log row, a Slack 'evidence_created' notification carrying the first 5 items, and the PostHog 'webhook_evidence_ingested' event. Rate-limited per workspace on the evidenceIngest bucket, charged per item rather than per request: a 50-item batch spends 50 of the 1000/hour budget and a single-item post spends 1, so batching is neither penalised nor a way around the ceiling. The budget is spent after the payload is parsed and before anything is written, and the sliding window admits the request that crosses the ceiling, so a workspace can overshoot by at most one batch. Scope: use evidence:ingest, a scope that opens this endpoint and nothing else, so a credential stored in a third-party tool cannot mutate intents, statuses, share tokens, or verification if that tool leaks it. A write-scoped key is still accepted, because senders configured before the narrow scope existed hold one; prefer evidence:ingest for anything new. Only owners, admins, and editors can mint it — ingest is a write, so it is not available to viewers or commenters. Idempotency: items carrying externalId are deduplicated on (workspace, source, externalId), both within a single batch (first occurrence wins) and against everything the workspace already stored for that source. duplicates counts what was skipped. A request in which every item was a redelivery returns 200 with created 0 and writes nothing — not a 4xx, which would make a well-behaved sender retry forever. Skipped items cost no evidence_items quota, though they do spend the rate-limit budget: the limiter protects the service from volume, the quota bills for stored rows. A unique index settles the case where two identical deliveries race past the lookup. If the duplicate lookup itself fails the request is refused with 500 rather than inserting, so a retry gets a clean second chance. POST /api/webhooks/ingest is a legacy alias that delegates to this handler with an identical contract, kept live for senders already configured against it.

## Query parameters

- `workspace_id` string

## Request body

- object
  - `productId` string, required — Product the evidence lands under. Must belong to the workspace (else 404).
  - `source` string, required — Non-empty label for where the evidence came from, e.g. 'posthog', 'zapier', 'intercom'. Trimmed, then applied to every row in the request. Required here, unlike POST /api/v1/evidence where it is optional.
  - `content` string — Evidence text for the simple single-item shape. Required unless items is given. Max 10000 characters.
  - `items` object[] — Batch shape: up to 50 objects, each accepting content (required) plus the same optional type, severity, sentiment, sourceUrl, tags, and stage fields. Required unless content is given; must be a non-empty array.
  - `type` string — One of friction | quote | observation | metric | request. Defaults to 'observation' when omitted, unlike POST /api/v1/evidence where type is required.
  - `severity` string — low | medium | high | critical. 400 if invalid.
  - `sentiment` string — positive | negative | neutral | mixed. 400 if invalid.
  - `sourceUrl` string — Link back to the originating ticket, survey response, or issue; stored as null if omitted.
  - `tags` string[] — Tag array; non-string members are dropped. Defaults to [].
  - `externalId` string — The sending system's own id for this event (PostHog: {event.uuid}). Supplying it makes redelivery a no-op: the same externalId under the same workspace and source is stored once, and a retry returns 200 with created 0 and duplicates 1 instead of a second row. Max 255 chars, trimmed. Omit it and the item inserts unconditionally, as before.
  - `stage` string — Free-text journey stage; stored as null if omitted.

## Response `201`

Evidence items created; body carries the created count and the created rows.

- object

## Other responses

- `400` — Invalid JSON body; missing productId or source; items not an array, empty, or over 50; neither items nor content given; or a per-item failure (empty content, content over 10000 chars, invalid type/severity/sentiment) reported as items[n]: <reason>.
- `401` — Missing/invalid API key or no valid session cookie.
- `403` — API key lacks write scope; OR billing limit reached (body { error: 'plan_limit_reached', metric: 'evidence_items', current, limit, plan }).
- `404` — productId not found in this workspace.
- `429` — Workspace ingest budget exhausted (1000 items/hour, sliding window). Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset are set on the response.
- `500` — DB or internal error.

---

[API](https://skmtc.dev/pathmode/apis/pathmode-api.md) · [All operations](https://skmtc.dev/pathmode/apis/pathmode-api/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/pathmode/pathmode-api/revisions/e419abb0a327/schema)
