# Partner fact ingestion (`POST /v1/ingest`)

This implements the *PlayerPal Data, Analytics & Ingestion Specification v0.1*. The principle: partners send pseudonymous daily **facts**, and PlayerPal computes every **KPI** from them. Ingestion is batch-first; daily or periodic payloads are enough.

**Auth:** OAuth client credentials or PP-HMAC-SHA256 ([hmac-signing.md](hmac-signing.md)) with scope `ingest.write`. `source_id` must be your own party ID, and the schema must match your party type.

## Envelope (§4)

```json
{
  "schema": "playerpal.provider.player_game_daily.v1",
  "schema_version": 1,
  "batch_id": "01JAXAMPLEBATCHID",
  "source_party": "GAME_PROVIDER",
  "source_id": "pp_prv_example",
  "integration_id": "int_example_prod",
  "period_start": "2026-10-12T00:00:00Z",
  "period_end": "2026-10-13T00:00:00Z",
  "generated_at": "2026-10-13T01:15:00Z",
  "records": []
}
```

The response is a `202` receipt: `{ batch_id, schema, received_at, records_received, records_accepted, records_rejected, rejections: [{ index, reason }], replayed }`. You can fetch it again from `GET /v1/ingest/batches/:batchId`.

- **Idempotency.** Re-sending a `batch_id` with the same records replays the receipt. Different records under the same `batch_id` return `409 idempotency_conflict`.
- **Corrections (§15).** Send the corrected facts in a new batch. Records upsert by natural key, `revision` is incremented, and `received_at` is kept separately from the activity date.
- **Per-record validation.** One bad record never rejects the whole batch. If a batch contains duplicates, the last record for each natural key is kept and the earlier ones are rejected as `duplicate_in_batch`.
- **Limits.** At most 10,000 records per batch. Daily facts must fall within `[period_start day, period_end)`.
- **Currency.** v1 accepts `EUR` only, because the canonical currency policy is still open (§18).
- **Pseudonymous refs only.** `player_ref` must be an alias PlayerPal issued to you: `pp_plr_prv_…` for studios through their integrations, `pp_plr_opr_…` for operators. Anything else is rejected as `player_ref_unknown`. Email-shaped and other direct identifiers fail validation.

## v1 schemas (§19)

| Schema | Sender | Natural key | Notes |
|---|---|---|---|
| `playerpal.provider.player_game_daily.v1` | Studio | provider × player × game × day | `sessions`, `rounds`, `active_seconds`, `total_bet`, `total_win`, `jackpot_win`. `operator_ref` (a `pp_opr_…` ID) must be an operator you're integrated with; if omitted, it is inferred from the alias's integration. |
| `playerpal.provider.game_catalogue.v1` | Studio | provider × game | `name`, `category` (SLOTS/LIVE/TABLE/CRASH/INSTANT/OTHER), `release_date`, `rtp_percent`, `volatility`, `active`. |
| `playerpal.operator.player_commercial_daily.v1` | Operator | operator × player × day | Deposits, withdrawals, stake, win, `ggr`, `bonus_cost`, `ngr` (GGR and NGR may be negative), `first_deposit`. |
| `playerpal.affiliate.attribution.v1` | Operator (v1) | affiliate × player | `affiliate_ref` (a `pp_aff_…` ID), `campaign_ref`, `click_ref`, `attributed_at`, `attribution_status` (CLAIMED/OBSERVED/VERIFIED), `attribution_source`. Only operators can send attribution in v1, as the authoritative source (§8). |

Points, rewards, redemptions and missions are native to PlayerPal (§9), so partners don't send them.

## Privacy

Delete Account removes the player's fact rows. Reward redemptions are kept for loyalty accounting, but lose the link to the player.
