# POST /v1/ingest/*

Three ingest paths. All route through the conflict resolver before landing.

| Endpoint | Input | When to use |
| --- | --- | --- |
| `POST /v1/ingest/fact` | one structured `{entityRef, predicate, object, validFrom, …}` | You already know the predicate / object pair. Structured event streams, CRM exports, programmatic writes. |
| `POST /v1/ingest/mention` | raw text + `sourceVertical` | NLU-extract entities + facts from natural language. Inbox messages, transcripts, reviews. |
| `POST /v1/ingest/link` | typed edge `{from, to, kind}` | Cross-vertical merges (`identity_of`), graph relationships (`paid_for`, `mentioned_in`). |

## POST /v1/ingest/fact

```ts
{
  entityRef: { vertical: string, id: string } | { entityId: string }
  predicate: string                    // e.g. 'tier', 'name', 'address'
  object: string                       // canonicalised string
  validFrom: string                    // ISO 8601
  validUntil?: string
  confidence?: number                  // 0..1, default 0.95 for structured
  source: { vertical: string, messageId?: string, recorder?: string }
  authority?: number                   // 0..1, default 0 — set high for human-curated
}
```

Response carries `factId` and the resolver outcome:

```ts
{
  factId: string
  outcome: 'INSERTED' | 'SUPERSEDED' | 'COMPETING' | 'REJECTED'
  margin?: number
  contradicting?: string[]   // factIds of the rivals
}
```

## POST /v1/ingest/mention

```ts
{
  text: string
  sourceVertical: string
  source: { messageId?: string, ... }
  entityHints?: Array<{ vertical: string, id: string }>  // anchor extraction
  asOf?: string  // when the mention happened (default: now)
}
```

Returns a batch result — every entity the NLU found, every fact emitted, per-fact resolver outcome:

```ts
{
  entities: Array<{ entityId: string, externalRefs: [...] }>
  facts: Array<{ factId: string, outcome: ... }>
  cost: { promptTokens: number, completionTokens: number }
}
```

The extractor is `gpt-4o-mini-2024-07-18` (snapshot-pinned). Cost is tracked per call; the operator playbook covers per-tenant budget alerts.

## POST /v1/ingest/link

```ts
{
  from: { vertical: string, id: string } | { entityId: string }
  to:   { vertical: string, id: string } | { entityId: string }
  kind: string    // 'identity_of', 'paid_for', 'mentioned_in', 'manages', ...
  validFrom?: string
  source: { vertical: string, ... }
}
```

`kind=identity_of` is the cross-vertical merge primitive — when the rent vertical and the inbox vertical both have an `alice@example.com`, they're the same person. Issuing the link triggers re-attribution: existing facts get merged onto the canonical entity.

## Idempotency

All three accept an `Idempotency-Key` header. Same key + same body within 24h returns the original outcome, doesn't re-run the resolver. Different body with the same key returns 409.

## Errors

| Status | Meaning |
| --- | --- |
| `400` | Schema validation failed — error body lists field paths |
| `401` | Missing / invalid bearer key |
| `403` | Scope `brain:write` required |
| `409` | Idempotency-Key reused with a different body |
| `422` | Vocabulary unknown predicate — register it first or use one from the standard set |
| `429` | Per-tenant rate limit hit |

## Related

- [Conflict resolution](/en/docs/concepts/conflict-resolution) — what the resolver does
- [Predicate vocabulary](/en/docs/concepts/predicates) — which predicates are valid
- [POST /v1/facts/:id/retract](/en/docs/api/retract) — the inverse operation
