# Lead Scoring: Omni app-auth integration Provider baseline: `omni-ai-platform-app-auth`, revision 2, 2026-09-30. `openapi.json` is generated from backend routes; `fixtures/` contains examples, not a second validation schema. The existing `v2` directory is retained without legacy compatibility. Cross-service acceptance remains pending until Omni verifies the same artifacts. ## Authentication and identity POST/GET use `Authorization: Bearer ` for one Omni installation/environment. AI Platform resolves a stable `app_id`, requires active state and `lead_scoring` permission. Missing/invalid/inactive/revoked credentials return 401; missing permission returns 403 on both routes. Omni owns user/account/EMS authorization; AI Platform does not register accounts. POST `/v1/lead-scoring/runs` has exactly `request_id`, `account_id`, `subject_ref`, `snapshot`, `scoring_config`, `ai_profile`. Account ID is a nonblank string of 1–255 characters, preserved unchanged. Subject must match the snapshot EMS lead ID. `Idempotency-Key` represents the body request UUID. Self-supplied app/tenant/callback fields return 422. Each `scoring_config.rules[]` may include `description`: an optional string of at most 99 Unicode characters. Omitted or null descriptions preserve legacy normalized requests and replay fingerprints. Empty/whitespace descriptions are accepted but omitted from AI context. Nonblank descriptions are stored with the request, included in its fingerprint and sent as supplemental JSON context in the AI scoring pass, not as evidence or instructions. Rule matching, points and weights are unchanged; rule IDs, conditions, points and weights are not sent to AI. The request fixture includes a described rule and a legacy rule without this field. Deduplication is `(app_id, account_id, request_id)`: 202 and Location for new admission, 200 same canonical body/run, 409 changed body. Summary/detail carry app/account/request/run identity. GET another app's run returns 404. Key rotation preserves ownership/replay. Missing callback configuration blocks new admission with 503 after replay lookup; DB or scheduling unavailability also returns 503. Generate OpenAPI and serializer-derived summary/detail/signature examples with `make contract`; `uv run --no-sync python scripts/export_contract_fixtures.py --check` rejects fixture drift without modifying artifacts. Validate examples in `tests/test_run_contract.py` and `tests/test_notifications.py`. ## Credentials and configuration Operator CLI issues, rotates or revokes keys: `uv run --no-sync ai-platform create-app `, `rotate-app-key `, `revoke-app-key `. Raw key is output only for issuance/rotation and must be transferred securely to Omni backend. Admin Web lists status and revokes access without receiving raw keys. Old tenant provisioning endpoints and provisioning-secret settings are removed. Omni backend configuration: `LEAD_SCORING_API_URL`, `AI_PLATFORM_APP_ID` (expected identity), `AI_PLATFORM_APP_API_KEY`. Provider configuration: `AI_PLATFORM_APP_WEBHOOKS`, JSON mapping app ID to `{"url":"https://omni.example.invalid/api/v1/internal/lead-scoring/notifications","signing_secret":""}`. Each app has its own URL/secret. Only development loopback HTTP is allowed; other environments require HTTPS. URL/secret/key never belong in browser-visible configuration. `X-Tenant-VKey` retains its existing name and RAM-only AIHub role. ## Terminal notification Webhook contains exactly `event_id`, `app_id`, `account_id`, `request_id`, `run_id`, `status` (`completed`/`failed`), `occurred_at` (UTC ISO 8601). It contains no scores, snapshot or evidence. Event ID/body remain stable across retry; `occurred_at` is terminal event time, not send time. `X-Webhook-Timestamp` is UTC ISO 8601 for this delivery attempt. `X-Webhook-Signature` is `sha256=` plus lowercase hex HMAC-SHA256 over UTF-8 bytes `timestamp + "." + raw_request_body`. Verify raw bytes before parsing, compare constant-time, reject timestamps outside ±5 minutes and body above 8192 bytes. `fixtures/webhook-delivery.json` uses the test-only secret `test-only-webhook-secret-32-characters-long` to fix exact bytes/signature. Omni commits the immutable source request before POST and durably accepts callback before ACK 202; an exact duplicate returns 200, conflicting identity/body returns 409. Dedupe `(app_id,event_id)`, bind run atomically even when callback precedes admission response, GET with app key and compare app/account/request/run/subject to the stored source before EMS writeback. Account authorization, enabled/EMS rechecks and latest-run ordering belong to Omni. Provider retries network/non-2xx up to five attempts, then keeps a dead event. `uv run --no-sync ai-platform redeliver-notification ` retries the same event without rescoring. A crash may repeat a previously ACKed event. Webhook transport rejects redirects. GET is the result source of truth. ## Migration and verification boundary The unreleased schema is consolidated in `0001_initial`, creating the current app-auth schema directly. Databases with the old local revisions need separate reconciliation or an explicitly approved rebuild; the baseline does not automatically stamp, reset or map legacy data. Provider tests do not prove Omni request persistence, callback receiver or EMS writeback. Run the shared validation checklist on both applications with mock AI/EMS before enabling real integration.