# Send daily facts

Fact ingest is how you give PlayerPal the daily figures behind your analytics: gameplay per player and game from studios, commercial figures per player from operators, and affiliate attribution. You send facts; PlayerPal computes every KPI from them, so all partners see metrics defined the same way. Ingest is batch-first: one delivery per day, or per period, is enough.

## Schemas at a glance

| Schema | Who sends it | Natural key | What it feeds |
|---|---|---|---|
| `playerpal.provider.player_game_daily.v1` | Game provider | provider × player × game × day | Gameplay KPIs: active and new players, sessions, active time, retention, players at risk |
| `playerpal.provider.game_catalogue.v1` | Game provider | provider × game | Game names and attributes in game breakdowns and Explore game views |
| `playerpal.operator.player_commercial_daily.v1` | Operator | operator × player × day | Commercial KPIs: depositors, first-time deposits, deposits, GGR, NGR |
| `playerpal.affiliate.attribution.v1` | Operator | affiliate × player | Affiliate views, which count only `VERIFIED` attributions |

A KPI whose feed you have not delivered shows `requires_feed` in the [overview](./kpi-api.mdx), never a zero. Points, rewards, redemptions and missions are native to PlayerPal, so you never send them. Definitions are in [Metrics](../analytics/metrics.mdx).

## Build the envelope

`ingestSubmit` (see the API reference)

A batch is one envelope with records of one schema. PlayerPal checks the envelope first: if it is invalid, the request fails with `400` and nothing is stored.

- `source_party` is `GAME_PROVIDER` for provider schemas and `OPERATOR` for operator and attribution schemas.
- `source_id` must be your own party ID. Any other value returns `403 source_id_must_be_authenticated_party`.
- A schema sent by the wrong party type returns `403 schema_not_allowed_for_party`.
- `batch_id` is 1–128 characters of `A-Z a-z 0-9 _ . : -` and is your idempotency key.
- `period_start` must be before `period_end`. A batch holds at most 10,000 records.

(Parameter table: see the API reference.)

Envelope:

```
{
  "schema": "playerpal.provider.player_game_daily.v1",
  "schema_version": 1,
  "batch_id": "daily-2026-10-12-0001",
  "source_party": "GAME_PROVIDER",
  "source_id": "pp_prv_XXXX",
  "integration_id": null,
  "period_start": "2026-10-12T00:00:00Z",
  "period_end": "2026-10-13T00:00:00Z",
  "generated_at": "2026-10-13T01:15:00Z",
  "records": []
}
```

Invalid envelope:

```
{
  "statusCode": 400,
  "message": "period_invalid",
  "error": "Bad Request"
}
```

## Add records for your schema

After the envelope, every record is validated on its own. Amounts are decimal strings (preferred) or numbers with at most 2 decimal places; counts are non-negative integers. References use only letters, digits and `_ . : -`, up to 128 characters.

  
**player_game_daily**

| Field | Required | Notes |
|---|---|---|
| `date` | Yes | UTC day, `YYYY-MM-DD`, inside the envelope period |
| `player_ref` | Yes | Your provider alias `pp_plr_prv_…` |
| `game_ref` | Yes | Your game ID, matching the game catalogue |
| `operator_ref` | No | Operator ID `pp_opr_…` you are integrated with. Defaults to the alias's integration operator |
| `market` | No | ISO 3166-1 alpha-2, upper case |
| `sessions`, `rounds`, `active_seconds` | At least one of these or `total_bet` | Counts |
| `total_bet`, `total_win`, `jackpot_win` | No | Non-negative amounts |
| `currency` | When any amount is sent | `EUR` |

  
  
**game_catalogue**

| Field | Required | Notes |
|---|---|---|
| `game_ref` | Yes | Your game ID |
| `name` | Yes | 1–200 characters |
| `category` | No | `SLOTS`, `LIVE`, `TABLE`, `CRASH`, `INSTANT` or `OTHER` (default) |
| `release_date` | No | `YYYY-MM-DD` |
| `rtp_percent` | No | Greater than 0, at most 100 |
| `volatility` | No | `LOW`, `MEDIUM`, `HIGH` or `VERY_HIGH` |
| `active` | No | Defaults to `true`; only `false` marks a game inactive |

  
  
**player_commercial_daily**

| Field | Required | Notes |
|---|---|---|
| `date` | Yes | UTC day, `YYYY-MM-DD`, inside the envelope period |
| `player_ref` | Yes | Your operator alias `pp_plr_opr_…` |
| `currency` | Yes | `EUR` |
| `market` | No | ISO 3166-1 alpha-2, upper case |
| `deposits_count`, `withdrawals_count` | No | Counts |
| `deposits_amount`, `withdrawals_amount`, `stake_amount`, `win_amount`, `bonus_cost` | No | Non-negative amounts |
| `ggr`, `ngr` | No | Amounts, may be negative |
| `first_deposit` | No | Boolean |

  
  
**attribution**

| Field | Required | Notes |
|---|---|---|
| `player_ref` | Yes | Your operator alias `pp_plr_opr_…` |
| `affiliate_ref` | Yes | Affiliate party ID `pp_aff_…` |
| `attribution_status` | Yes | `CLAIMED`, `OBSERVED` or `VERIFIED` |
| `attribution_source` | Yes | Short reference, same character rules as `batch_id` |
| `campaign_ref`, `click_ref` | No | Your references |
| `attributed_at` | No | ISO 8601 instant |

Only operators send attribution in this version, as the authoritative source.

  

player_game_daily record:

```
{
  "date": "2026-10-12",
  "player_ref": "pp_plr_prv_XXXX",
  "game_ref": "your-game-ref",
  "market": "MT",
  "currency": "EUR",
  "sessions": 3,
  "rounds": 120,
  "total_bet": "60.00",
  "total_win": "52.40"
}
```

player_commercial_daily record:

```
{
  "date": "2026-10-12",
  "player_ref": "pp_plr_opr_XXXX",
  "currency": "EUR",
  "deposits_count": 1,
  "deposits_amount": "50.00",
  "ggr": "-12.50",
  "first_deposit": false
}
```

## Sign and send the batch

Sign the request with PP-HMAC-SHA256, as in the [Quickstart](./quickstart.mdx), or send an OAuth bearer token. Sign the exact bytes you send: re-serialising the JSON after signing breaks the signature.

PlayerPal answers `202 Accepted` with a receipt. One bad record never rejects the batch: it is listed in `rejections` and the rest are stored.

You get `202` with `records_accepted` equal to the number of records you sent and `replayed: false`.

(Code sample: send-batch. See the page for curl, Node.js, Python, PHP, Java and C#.)

Response:

```
{
  "batch_id": "daily-2026-10-12-0001",
  "schema": "playerpal.provider.player_game_daily.v1",
  "received_at": "2026-10-13T01:15:02.000Z",
  "records_received": 1,
  "records_accepted": 1,
  "records_rejected": 0,
  "rejections": [],
  "replayed": false
}
```

## Read receipts

`ingestReceipt` (see the API reference)

The receipt is stored with the batch, so you can fetch it later by `batch_id` as often as you need. A fetched receipt always has `replayed: true`. An unknown `batch_id` returns `404 batch_not_found`.

`index` in each rejection points into your `records` array. The receipt lists at most 500 rejections; the counts are always complete. Common reasons:

| Reason | Fix |
|---|---|
| `player_ref_invalid` | The value is not a valid reference, for example an email address |
| `player_ref_unknown` | Use an active alias PlayerPal issued to you |
| `date_invalid`, `date_outside_period` | Use `YYYY-MM-DD` inside the envelope period |
| `currency_required`, `currency_unsupported` | Send `EUR` with every amount |
| `activity_indicator_required` | Send `sessions`, `rounds`, `active_seconds` or `total_bet` |
| `operator_ref_not_integrated` | Use an operator you are integrated with, or omit it |
| `affiliate_ref_unknown` | Use the affiliate's PlayerPal party ID |
| `duplicate_in_batch` | Send each natural key once per batch |
| `<field>_invalid`, `record_not_object` | Fix the malformed field or record |

(Parameter table: see the API reference.)

(Code sample: ingest-receipt. See the page for curl, Node.js, Python, PHP, Java and C#.)

A receipt with rejections:

```
{
  "batch_id": "pgd-2026-10-14",
  "schema": "playerpal.provider.player_game_daily.v1",
  "received_at": "2026-10-15T01:15:02.311Z",
  "records_received": 1200,
  "records_accepted": 1197,
  "records_rejected": 3,
  "rejections": [
    { "index": 14, "reason": "player_ref_unknown" },
    { "index": 220, "reason": "date_outside_period" },
    { "index": 301, "reason": "duplicate_in_batch" }
  ],
  "replayed": true
}
```

## Resend and correct data

`batch_id` makes retries safe. PlayerPal compares the `schema` and `records` of a repeated `batch_id` with the stored batch.

- **Same records:** you get the original receipt back with `replayed: true`, and nothing is stored twice. Retry a timed-out call with the identical batch.
- **Different records:** `409 idempotency_conflict`. New data always needs a new `batch_id`.
- **Inside one batch:** the last record for each natural key wins; earlier ones are rejected as `duplicate_in_batch`.

To correct data, send the corrected facts in a **new** batch. Records replace the stored fact for the same natural key, and PlayerPal keeps a revision count and the time it received the correction. Resend the full record, not only the changed fields.

Sending the step 3 batch again returns `202` with `replayed: true`. Changing one record and keeping the `batch_id` returns `409`.

Same batch_id, different records:

```
{
  "statusCode": 409,
  "message": "idempotency_conflict",
  "error": "Conflict"
}
```

Correction batch:

```
{
  "schema": "playerpal.provider.player_game_daily.v1",
  "schema_version": 1,
  "batch_id": "daily-2026-10-12-0001-fix1",
  "source_party": "GAME_PROVIDER",
  "source_id": "pp_prv_XXXX",
  "period_start": "2026-10-12T00:00:00Z",
  "period_end": "2026-10-13T00:00:00Z",
  "generated_at": "2026-10-14T09:00:00Z",
  "records": [
    { "date": "2026-10-12", "player_ref": "pp_plr_prv_XXXX",
      "game_ref": "your-game-ref", "currency": "EUR",
      "rounds": 124, "total_bet": "62.00", "total_win": "52.40" }
  ]
}
```

## Data rules

| Rule | Detail |
|---|---|
| Players | `player_ref` must be an alias PlayerPal issued to you: `pp_plr_prv_…` through your integrations if you are a studio, `pp_plr_opr_…` if you are an operator. An alias for another audience or a revoked alias is rejected as `player_ref_unknown`. An email address or phone number fails as `player_ref_invalid` before it is ever matched |
| Currency | `EUR` only in this version, because the conversion policy is not yet decided. Other currencies are rejected as `currency_unsupported` |
| Dates | Daily records use UTC days. A record's `date` must fall from the UTC day of `period_start` up to, but not including, `period_end` |
| Markets | ISO 3166-1 alpha-2 in upper case, for example `MT` |

## Errors

| Status | Codes |
|---|---|
| `400` | `envelope_required`, `schema_unsupported`, `schema_version_unsupported`, `batch_id_invalid`, `source_party_not_allowed_for_schema`, `source_id_required`, `period_invalid`, `records_required`, `too_many_records` |
| `401` | Signature or token problems. See [Authentication](./authentication.mdx) |
| `403` | `source_id_must_be_authenticated_party`, `schema_not_allowed_for_party`, `partner_scope_missing` |
| `404` | `batch_not_found` (receipt lookup) |
| `409` | `idempotency_conflict` |

Every error code is listed in [Idempotency and errors](./idempotency-and-errors.mdx).

## Related

  - [Read KPIs](./kpi-api.mdx): 
    Query the metrics PlayerPal computes from your facts.
  
  - [Link players](./linking-players.mdx): 
    Get the aliases you send facts for.
  
  - [API reference](/api/partner): 
    Envelope, record and receipt schemas.
