# XR Audience backend ingestion quickstart

XR Audience ingestion is server-to-server. A Flutter app never receives the
Audience HMAC secret and never calls the ingestion host directly. Start with
the reviewed publisher proxy at `https://docs.xaere.io/FLUTTER_BACKEND.js` when
the source event comes from the Xaere Flutter SDK.

## 1. Provision one scoped integration

An organisation owner or administrator creates an integration for one project
in the Xaere Console. The response reveals its `integration_id` and HMAC secret
exactly once. Store both in the backend secret manager. The secret must not
enter logs, URLs, databases outside the approved secret store, client bundles,
mobile assets, XR Codes or source control.

Demo mode permits 100 events per organisation/month with at most 14 days of
retention. Contract and self-service integrations require an active Audience
entitlement and use the plan's server-enforced quota.

## 2. Accept only the closed event

The backend accepts a minimised event from its authenticated application only
after an explicit consent decision. The event contains exactly:

- `event_id`: one random idempotency key retained unchanged across retries;
- `xr_id` and the matching `integration_id`;
- `occurred_at`, `receiver_class` and a rotating `measurement_token`;
- optional two-letter `country_code` when policy permits it;
- the unmodified short-lived `resolution_proof` returned by Core.

The proof binds the XR Code, integration, optional campaign, issue/expiry time
and Core key. Do not add account IDs, device/installation/advertising IDs, raw
IP addresses, precise location, user agent, microphone samples, decoded media
or arbitrary metadata. The published JSON Schema rejects additional fields:
`https://docs.xaere.io/schemas/audience-event-v1.schema.json`.

## 3. Sign the exact bytes once

Create the JSON body once, HMAC those exact UTF-8 bytes with SHA-256, encode the
digest as unpadded base64url, and send the same bytes. Never parse and
re-serialize after calculating the signature.

```js
import { createHmac } from 'node:crypto';

const raw = JSON.stringify(event);
const signature = createHmac(
  'sha256',
  process.env.XAERE_AUDIENCE_HMAC_SECRET,
).update(raw).digest('base64url');

const response = await fetch('https://ingest.audience.xaere.io/v1/events', {
  method: 'POST',
  redirect: 'error',
  headers: {
    'content-type': 'application/json',
    'x-xaere-event-signature': signature,
  },
  body: raw,
});
```

The `Content-Type` header is mandatory. Compute `X-Xaere-Event-Signature`
over the exact UTF-8 bytes sent on the wire, after the JSON document has been
serialized; do not parse and re-serialize it between signing and delivery.
Audience reads at most 16 KiB of raw bytes, preserves chunk boundaries without
text conversion during HMAC verification, and rejects malformed UTF-8 or a
different media type before accepting an event.

Reject missing HMAC configuration before opening a request. Use a bounded
timeout and never reflect the Audience response body to an untrusted client.

## 4. Require the closed acknowledgement

Successful ingestion returns HTTP `202` and exactly these JSON keys:

```json
{"accepted":true,"duplicate":false,"event_id":"evt_example_not_a_credential"}
```

`duplicate:true` is also a successful idempotent acknowledgement. A generic
2xx response, extra key, malformed JSON or mismatched event ID is ambiguous and
must not cause an offline queue to discard the event.

Audience rejects an invalid HMAC before trusting the Core proof. It then
rejects expired, altered, integration-mismatched or replayed proof data. Exact
`event_id` replays, the same XR Code plus rotating token, and reused proof
signatures are deduplicated. Integrity signals are stored only as bounded UTC
day aggregates; they are data-quality indicators, not identified fraud.

## 5. Rotate and revoke

Rotate an exposed HMAC from the Console; the previous value stops working
immediately and the replacement is shown once. Revoke the integration to stop
ingestion. Reducing retention transactionally deletes older live events and
aggregates and cannot restore them later.

For schemas, provisioning, reports and key rotation, use
`https://docs.xaere.io/API_V1.md`. For the consent-bound Flutter proxy, use
`https://docs.xaere.io/FLUTTER_BACKEND.md`.

Audience reports may be filtered with `campaign_id=cmp_...`, `xr_id=xr_...`,
or both after Core validates the organisation/project relationships and binds
the Code to the selected Audience integration. The filters apply to event and
reach aggregates. Integrity signals remain integration-wide for the requested
period because the bounded duplicate counters intentionally store neither
identifier; aggregate exports preserve that scope explicitly.
