# Lead Scoring: client contract `openapi.json` is generated from the backend routes (`make contract`). `fixtures/run-request.json` and `fixtures/score-response.json` are generated examples; `scripts/export_contract_fixtures.py --check` rejects drift. The service knows no specific client: client-owned data travels in `metadata`. ## Request `POST /v1/lead-scoring/score`, synchronous. | Header | Meaning | |---|---| | `Authorization: Bearer ` | App credential (one per installation). 401 if invalid or revoked, 403 without the `lead_scoring` permission. | | `Idempotency-Key` | Required UUID chosen by the client. Same key with the same body replays the stored result; same key with a different body returns 422. | | `X-AI-Key` | Optional AI provider key for AIHub profiles. Held in memory for the call only, never stored or logged. Max 4096 chars, no CR/LF (else 422). | Body: `snapshot` (lead profile with `external_id`, conversations, custom fields), `scoring_config` (`rule_weight`, `ai_weight`, `rules[]`) and optional `metadata` (any JSON object up to 10KB, stored and returned unchanged). Rule `path` may be `lead_profile.`, `custom_fields.` or `metrics.`. ## Response (200) `run_id`, `final_score`, `rule_score`, `ai_score`, `result_mode`, `error_code`, `breakdown[]`, `ai {summary, suggested_action, signals[]}`, `metrics`, `metadata`. `result_mode` is `rules_only`, `hybrid` or `rules_only_fallback`. In the fallback case `error_code` says why (`ai_timeout`, `ai_key_missing`, `ai_profile_missing`, or a provider error code) and `final_score` equals `rule_score`. The AI model, prompt and timeout belong to the active AI profile, not the request. ## Boundaries No webhook: the caller waits for the response (the AI deadline is the profile `timeout_seconds`, default 50). Provider tests do not prove client persistence or writeback; verify both sides with a mock AI before enabling a real integration.