# Scoring

### Native scoring framework

Configure how Saber turns signal results into structured, explainable **fit** and **urgency** scores (0–100) for companies and contacts. Scoring sits on top of the signal layer — the engine is signal-agnostic, so any data the signal layer can ingest becomes scoreable.

#### How it fits together

1. **Profile** — a named, org-scoped configuration bound to a single object type (`company` or `contact`). E.g. *"EMEA Enterprise"*, *"Mid-Market SaaS"*. A single object can be assigned multiple profiles of the matching type.
2. **Rule** — within a profile, maps one signal template to point values for one dimension (`fit` or `urgency`). The shape of `pointValues` depends on the signal's answer type (boolean, number/percentage/currency ranges, or list choices).
3. **Assignment** — links a profile to a specific company or contact. Triggers an immediate recompute so the score appears without waiting for the next signal run.
4. **Score result** — the computed score for one `(profile, object, dimension)` triple. Includes a per-rule contribution breakdown so the number is always explainable, plus the previous score for delta views.

#### Computation semantics

Score is `(earned / maxPossible) * 100`. **All rules in a profile/dimension count toward the denominator regardless of whether a signal answer exists** — unanswered rules earn 0 points but their max still counts. This keeps low-coverage scores conservatively low so they don't falsely look high. `signalCoverage` and `totalRules` are returned alongside the score so consumers can see how complete it is.

#### When scores recompute

In v1 scoring is manual-trigger:

* Assigning a profile to an object → immediate recompute
* Upserting or deleting a rule → recompute every object assigned to the profile
* Calling `POST /v1/scoring/compute` directly

Auto-trigger on signal completion is on the v1.5 roadmap.

## List scoring profiles

> List all scoring profiles for the authenticated organisation.

```json
{"openapi":"3.2.0","info":{"title":"Saber Platform API","version":"1.0.0"},"tags":[{"name":"Scoring","summary":"Scoring","kind":"nav","description":"## Native scoring framework\n\nConfigure how Saber turns signal results into structured, explainable **fit** and\n**urgency** scores (0–100) for companies and contacts. Scoring sits on top of the\nsignal layer — the engine is signal-agnostic, so any data the signal layer can ingest\nbecomes scoreable.\n\n### How it fits together\n\n1. **Profile** — a named, org-scoped configuration bound to a single object type\n   (`company` or `contact`). E.g. *\"EMEA Enterprise\"*, *\"Mid-Market SaaS\"*. A single\n   object can be assigned multiple profiles of the matching type.\n2. **Rule** — within a profile, maps one signal template to point values for one\n   dimension (`fit` or `urgency`). The shape of `pointValues` depends on the signal's\n   answer type (boolean, number/percentage/currency ranges, or list choices).\n3. **Assignment** — links a profile to a specific company or contact. Triggers an\n   immediate recompute so the score appears without waiting for the next signal run.\n4. **Score result** — the computed score for one `(profile, object, dimension)`\n   triple. Includes a per-rule contribution breakdown so the number is always\n   explainable, plus the previous score for delta views.\n\n### Computation semantics\n\nScore is `(earned / maxPossible) * 100`. **All rules in a profile/dimension count\ntoward the denominator regardless of whether a signal answer exists** — unanswered\nrules earn 0 points but their max still counts. This keeps low-coverage scores\nconservatively low so they don't falsely look high. `signalCoverage` and `totalRules`\nare returned alongside the score so consumers can see how complete it is.\n\n### When scores recompute\n\nIn v1 scoring is manual-trigger:\n\n- Assigning a profile to an object → immediate recompute\n- Upserting or deleting a rule → recompute every object assigned to the profile\n- Calling `POST /v1/scoring/compute` directly\n\nAuto-trigger on signal completion is on the v1.5 roadmap.\n"}],"servers":[{"url":"https://api.saber.app","description":"Production server"}],"security":[{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","bearerFormat":"API Key","description":"API key authentication using Bearer token. Format: sk_live_ followed by a secure random string."}},"schemas":{"ScoringProfile":{"type":"object","description":"Named, org-scoped scoring configuration that groups rules for a specific object type.","required":["id","organizationId","type","name","createdAt","updatedAt"],"properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string"},"type":{"type":"string","enum":["company","contact"],"description":"Object type this profile scores. Immutable after creation."},"name":{"type":"string"},"description":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"ErrorResponse":{"type":"object","description":"Standard error envelope returned by every endpoint that flows through the global error handler. Note: the global rate-limit middleware (HTTP 429 from per-API-key throttling) returns a different, flat shape — see `RateLimitErrorResponse`.\n","required":["error"],"properties":{"error":{"type":"object","required":["type","code","requestId"],"properties":{"type":{"type":"string","description":"Error category. Maps 1:1 to HTTP status (e.g. VALIDATION → 422, UNAUTHORIZED → 401).","enum":["BAD_REQUEST","VALIDATION","UNPROCESSABLE_ENTITY","NOT_FOUND","CONFLICT","UNAUTHORIZED","FORBIDDEN","PAYMENT_REQUIRED","PAYLOAD_TOO_LARGE","INTERNAL","EXTERNAL","TIMEOUT"]},"code":{"type":"string","description":"Stable machine-readable error code. Safe to switch on in client code."},"message":{"type":"string","description":"Human-readable error message. Wording may change; do not match against this string."},"errorCode":{"type":"string","description":"Optional public error code propagated from a downstream service (e.g. NestJS, LinkedIn)."},"errorAction":{"type":"string","description":"Optional user action guidance (e.g. \"reconnect Sales Navigator\")."},"details":{"type":"object","additionalProperties":true,"description":"Optional structured fields forwarded from a downstream service. Shape depends on the source."},"requestId":{"type":"string","format":"uuid","description":"Correlation ID for this request. Include when reporting issues."},"fields":{"type":"array","description":"Per-field validation errors (populated when `type` is `VALIDATION` and the cause is a struct-tag validator).","items":{"type":"object","required":["field","message"],"properties":{"field":{"type":"string"},"message":{"type":"string"}}}},"debug":{"type":"object","description":"Debug information. Only present in development environments.","properties":{"cause":{"type":"string"}}}}}}}}},"paths":{"/v1/scoring/profiles":{"get":{"summary":"List scoring profiles","description":"List all scoring profiles for the authenticated organisation.","operationId":"listScoringProfiles","tags":["Scoring"],"responses":{"200":{"description":"Profiles retrieved","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ScoringProfile"}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}}}
```

## Create a scoring profile

> Create a named, org-scoped scoring profile. A profile groups scoring rules for a\
> single object type (\`company\` or \`contact\`) and produces fit/urgency scores\
> for any object assigned to it. A single object can be assigned to multiple\
> profiles of the matching type.<br>

```json
{"openapi":"3.2.0","info":{"title":"Saber Platform API","version":"1.0.0"},"tags":[{"name":"Scoring","summary":"Scoring","kind":"nav","description":"## Native scoring framework\n\nConfigure how Saber turns signal results into structured, explainable **fit** and\n**urgency** scores (0–100) for companies and contacts. Scoring sits on top of the\nsignal layer — the engine is signal-agnostic, so any data the signal layer can ingest\nbecomes scoreable.\n\n### How it fits together\n\n1. **Profile** — a named, org-scoped configuration bound to a single object type\n   (`company` or `contact`). E.g. *\"EMEA Enterprise\"*, *\"Mid-Market SaaS\"*. A single\n   object can be assigned multiple profiles of the matching type.\n2. **Rule** — within a profile, maps one signal template to point values for one\n   dimension (`fit` or `urgency`). The shape of `pointValues` depends on the signal's\n   answer type (boolean, number/percentage/currency ranges, or list choices).\n3. **Assignment** — links a profile to a specific company or contact. Triggers an\n   immediate recompute so the score appears without waiting for the next signal run.\n4. **Score result** — the computed score for one `(profile, object, dimension)`\n   triple. Includes a per-rule contribution breakdown so the number is always\n   explainable, plus the previous score for delta views.\n\n### Computation semantics\n\nScore is `(earned / maxPossible) * 100`. **All rules in a profile/dimension count\ntoward the denominator regardless of whether a signal answer exists** — unanswered\nrules earn 0 points but their max still counts. This keeps low-coverage scores\nconservatively low so they don't falsely look high. `signalCoverage` and `totalRules`\nare returned alongside the score so consumers can see how complete it is.\n\n### When scores recompute\n\nIn v1 scoring is manual-trigger:\n\n- Assigning a profile to an object → immediate recompute\n- Upserting or deleting a rule → recompute every object assigned to the profile\n- Calling `POST /v1/scoring/compute` directly\n\nAuto-trigger on signal completion is on the v1.5 roadmap.\n"}],"servers":[{"url":"https://api.saber.app","description":"Production server"}],"security":[{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","bearerFormat":"API Key","description":"API key authentication using Bearer token. Format: sk_live_ followed by a secure random string."}},"schemas":{"CreateScoringProfileRequest":{"type":"object","required":["profileType","name"],"properties":{"profileType":{"type":"string","enum":["company","contact"],"description":"Object type this profile will score"},"name":{"type":"string"},"description":{"type":["string","null"]}}},"ScoringProfile":{"type":"object","description":"Named, org-scoped scoring configuration that groups rules for a specific object type.","required":["id","organizationId","type","name","createdAt","updatedAt"],"properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string"},"type":{"type":"string","enum":["company","contact"],"description":"Object type this profile scores. Immutable after creation."},"name":{"type":"string"},"description":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"ErrorResponse":{"type":"object","description":"Standard error envelope returned by every endpoint that flows through the global error handler. Note: the global rate-limit middleware (HTTP 429 from per-API-key throttling) returns a different, flat shape — see `RateLimitErrorResponse`.\n","required":["error"],"properties":{"error":{"type":"object","required":["type","code","requestId"],"properties":{"type":{"type":"string","description":"Error category. Maps 1:1 to HTTP status (e.g. VALIDATION → 422, UNAUTHORIZED → 401).","enum":["BAD_REQUEST","VALIDATION","UNPROCESSABLE_ENTITY","NOT_FOUND","CONFLICT","UNAUTHORIZED","FORBIDDEN","PAYMENT_REQUIRED","PAYLOAD_TOO_LARGE","INTERNAL","EXTERNAL","TIMEOUT"]},"code":{"type":"string","description":"Stable machine-readable error code. Safe to switch on in client code."},"message":{"type":"string","description":"Human-readable error message. Wording may change; do not match against this string."},"errorCode":{"type":"string","description":"Optional public error code propagated from a downstream service (e.g. NestJS, LinkedIn)."},"errorAction":{"type":"string","description":"Optional user action guidance (e.g. \"reconnect Sales Navigator\")."},"details":{"type":"object","additionalProperties":true,"description":"Optional structured fields forwarded from a downstream service. Shape depends on the source."},"requestId":{"type":"string","format":"uuid","description":"Correlation ID for this request. Include when reporting issues."},"fields":{"type":"array","description":"Per-field validation errors (populated when `type` is `VALIDATION` and the cause is a struct-tag validator).","items":{"type":"object","required":["field","message"],"properties":{"field":{"type":"string"},"message":{"type":"string"}}}},"debug":{"type":"object","description":"Debug information. Only present in development environments.","properties":{"cause":{"type":"string"}}}}}}}}},"paths":{"/v1/scoring/profiles":{"post":{"summary":"Create a scoring profile","description":"Create a named, org-scoped scoring profile. A profile groups scoring rules for a\nsingle object type (`company` or `contact`) and produces fit/urgency scores\nfor any object assigned to it. A single object can be assigned to multiple\nprofiles of the matching type.\n","operationId":"createScoringProfile","tags":["Scoring"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateScoringProfileRequest"}}}},"responses":{"201":{"description":"Profile created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScoringProfile"}}}},"401":{"description":"Unauthorized — invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}}}
```

## GET /v1/scoring/profiles/{profileId}

> Get a scoring profile

```json
{"openapi":"3.2.0","info":{"title":"Saber Platform API","version":"1.0.0"},"tags":[{"name":"Scoring","summary":"Scoring","kind":"nav","description":"## Native scoring framework\n\nConfigure how Saber turns signal results into structured, explainable **fit** and\n**urgency** scores (0–100) for companies and contacts. Scoring sits on top of the\nsignal layer — the engine is signal-agnostic, so any data the signal layer can ingest\nbecomes scoreable.\n\n### How it fits together\n\n1. **Profile** — a named, org-scoped configuration bound to a single object type\n   (`company` or `contact`). E.g. *\"EMEA Enterprise\"*, *\"Mid-Market SaaS\"*. A single\n   object can be assigned multiple profiles of the matching type.\n2. **Rule** — within a profile, maps one signal template to point values for one\n   dimension (`fit` or `urgency`). The shape of `pointValues` depends on the signal's\n   answer type (boolean, number/percentage/currency ranges, or list choices).\n3. **Assignment** — links a profile to a specific company or contact. Triggers an\n   immediate recompute so the score appears without waiting for the next signal run.\n4. **Score result** — the computed score for one `(profile, object, dimension)`\n   triple. Includes a per-rule contribution breakdown so the number is always\n   explainable, plus the previous score for delta views.\n\n### Computation semantics\n\nScore is `(earned / maxPossible) * 100`. **All rules in a profile/dimension count\ntoward the denominator regardless of whether a signal answer exists** — unanswered\nrules earn 0 points but their max still counts. This keeps low-coverage scores\nconservatively low so they don't falsely look high. `signalCoverage` and `totalRules`\nare returned alongside the score so consumers can see how complete it is.\n\n### When scores recompute\n\nIn v1 scoring is manual-trigger:\n\n- Assigning a profile to an object → immediate recompute\n- Upserting or deleting a rule → recompute every object assigned to the profile\n- Calling `POST /v1/scoring/compute` directly\n\nAuto-trigger on signal completion is on the v1.5 roadmap.\n"}],"servers":[{"url":"https://api.saber.app","description":"Production server"}],"security":[{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","bearerFormat":"API Key","description":"API key authentication using Bearer token. Format: sk_live_ followed by a secure random string."}},"schemas":{"ScoringProfile":{"type":"object","description":"Named, org-scoped scoring configuration that groups rules for a specific object type.","required":["id","organizationId","type","name","createdAt","updatedAt"],"properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string"},"type":{"type":"string","enum":["company","contact"],"description":"Object type this profile scores. Immutable after creation."},"name":{"type":"string"},"description":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"ErrorResponse":{"type":"object","description":"Standard error envelope returned by every endpoint that flows through the global error handler. Note: the global rate-limit middleware (HTTP 429 from per-API-key throttling) returns a different, flat shape — see `RateLimitErrorResponse`.\n","required":["error"],"properties":{"error":{"type":"object","required":["type","code","requestId"],"properties":{"type":{"type":"string","description":"Error category. Maps 1:1 to HTTP status (e.g. VALIDATION → 422, UNAUTHORIZED → 401).","enum":["BAD_REQUEST","VALIDATION","UNPROCESSABLE_ENTITY","NOT_FOUND","CONFLICT","UNAUTHORIZED","FORBIDDEN","PAYMENT_REQUIRED","PAYLOAD_TOO_LARGE","INTERNAL","EXTERNAL","TIMEOUT"]},"code":{"type":"string","description":"Stable machine-readable error code. Safe to switch on in client code."},"message":{"type":"string","description":"Human-readable error message. Wording may change; do not match against this string."},"errorCode":{"type":"string","description":"Optional public error code propagated from a downstream service (e.g. NestJS, LinkedIn)."},"errorAction":{"type":"string","description":"Optional user action guidance (e.g. \"reconnect Sales Navigator\")."},"details":{"type":"object","additionalProperties":true,"description":"Optional structured fields forwarded from a downstream service. Shape depends on the source."},"requestId":{"type":"string","format":"uuid","description":"Correlation ID for this request. Include when reporting issues."},"fields":{"type":"array","description":"Per-field validation errors (populated when `type` is `VALIDATION` and the cause is a struct-tag validator).","items":{"type":"object","required":["field","message"],"properties":{"field":{"type":"string"},"message":{"type":"string"}}}},"debug":{"type":"object","description":"Debug information. Only present in development environments.","properties":{"cause":{"type":"string"}}}}}}}}},"paths":{"/v1/scoring/profiles/{profileId}":{"get":{"summary":"Get a scoring profile","operationId":"getScoringProfile","tags":["Scoring"],"responses":{"200":{"description":"Profile retrieved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScoringProfile"}}}},"404":{"description":"Profile not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}}}
```

## Update a scoring profile

> Rename or re-describe a profile. Profile type is immutable.

```json
{"openapi":"3.2.0","info":{"title":"Saber Platform API","version":"1.0.0"},"tags":[{"name":"Scoring","summary":"Scoring","kind":"nav","description":"## Native scoring framework\n\nConfigure how Saber turns signal results into structured, explainable **fit** and\n**urgency** scores (0–100) for companies and contacts. Scoring sits on top of the\nsignal layer — the engine is signal-agnostic, so any data the signal layer can ingest\nbecomes scoreable.\n\n### How it fits together\n\n1. **Profile** — a named, org-scoped configuration bound to a single object type\n   (`company` or `contact`). E.g. *\"EMEA Enterprise\"*, *\"Mid-Market SaaS\"*. A single\n   object can be assigned multiple profiles of the matching type.\n2. **Rule** — within a profile, maps one signal template to point values for one\n   dimension (`fit` or `urgency`). The shape of `pointValues` depends on the signal's\n   answer type (boolean, number/percentage/currency ranges, or list choices).\n3. **Assignment** — links a profile to a specific company or contact. Triggers an\n   immediate recompute so the score appears without waiting for the next signal run.\n4. **Score result** — the computed score for one `(profile, object, dimension)`\n   triple. Includes a per-rule contribution breakdown so the number is always\n   explainable, plus the previous score for delta views.\n\n### Computation semantics\n\nScore is `(earned / maxPossible) * 100`. **All rules in a profile/dimension count\ntoward the denominator regardless of whether a signal answer exists** — unanswered\nrules earn 0 points but their max still counts. This keeps low-coverage scores\nconservatively low so they don't falsely look high. `signalCoverage` and `totalRules`\nare returned alongside the score so consumers can see how complete it is.\n\n### When scores recompute\n\nIn v1 scoring is manual-trigger:\n\n- Assigning a profile to an object → immediate recompute\n- Upserting or deleting a rule → recompute every object assigned to the profile\n- Calling `POST /v1/scoring/compute` directly\n\nAuto-trigger on signal completion is on the v1.5 roadmap.\n"}],"servers":[{"url":"https://api.saber.app","description":"Production server"}],"security":[{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","bearerFormat":"API Key","description":"API key authentication using Bearer token. Format: sk_live_ followed by a secure random string."}},"schemas":{"UpdateScoringProfileRequest":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"description":{"type":["string","null"]}}},"ScoringProfile":{"type":"object","description":"Named, org-scoped scoring configuration that groups rules for a specific object type.","required":["id","organizationId","type","name","createdAt","updatedAt"],"properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string"},"type":{"type":"string","enum":["company","contact"],"description":"Object type this profile scores. Immutable after creation."},"name":{"type":"string"},"description":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"ErrorResponse":{"type":"object","description":"Standard error envelope returned by every endpoint that flows through the global error handler. Note: the global rate-limit middleware (HTTP 429 from per-API-key throttling) returns a different, flat shape — see `RateLimitErrorResponse`.\n","required":["error"],"properties":{"error":{"type":"object","required":["type","code","requestId"],"properties":{"type":{"type":"string","description":"Error category. Maps 1:1 to HTTP status (e.g. VALIDATION → 422, UNAUTHORIZED → 401).","enum":["BAD_REQUEST","VALIDATION","UNPROCESSABLE_ENTITY","NOT_FOUND","CONFLICT","UNAUTHORIZED","FORBIDDEN","PAYMENT_REQUIRED","PAYLOAD_TOO_LARGE","INTERNAL","EXTERNAL","TIMEOUT"]},"code":{"type":"string","description":"Stable machine-readable error code. Safe to switch on in client code."},"message":{"type":"string","description":"Human-readable error message. Wording may change; do not match against this string."},"errorCode":{"type":"string","description":"Optional public error code propagated from a downstream service (e.g. NestJS, LinkedIn)."},"errorAction":{"type":"string","description":"Optional user action guidance (e.g. \"reconnect Sales Navigator\")."},"details":{"type":"object","additionalProperties":true,"description":"Optional structured fields forwarded from a downstream service. Shape depends on the source."},"requestId":{"type":"string","format":"uuid","description":"Correlation ID for this request. Include when reporting issues."},"fields":{"type":"array","description":"Per-field validation errors (populated when `type` is `VALIDATION` and the cause is a struct-tag validator).","items":{"type":"object","required":["field","message"],"properties":{"field":{"type":"string"},"message":{"type":"string"}}}},"debug":{"type":"object","description":"Debug information. Only present in development environments.","properties":{"cause":{"type":"string"}}}}}}}}},"paths":{"/v1/scoring/profiles/{profileId}":{"put":{"summary":"Update a scoring profile","description":"Rename or re-describe a profile. Profile type is immutable.","operationId":"updateScoringProfile","tags":["Scoring"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateScoringProfileRequest"}}}},"responses":{"200":{"description":"Profile updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScoringProfile"}}}},"404":{"description":"Profile not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}}}
```

## Delete a scoring profile

> Deletes the profile and cascades to its rules, assignments, and score results.<br>

```json
{"openapi":"3.2.0","info":{"title":"Saber Platform API","version":"1.0.0"},"tags":[{"name":"Scoring","summary":"Scoring","kind":"nav","description":"## Native scoring framework\n\nConfigure how Saber turns signal results into structured, explainable **fit** and\n**urgency** scores (0–100) for companies and contacts. Scoring sits on top of the\nsignal layer — the engine is signal-agnostic, so any data the signal layer can ingest\nbecomes scoreable.\n\n### How it fits together\n\n1. **Profile** — a named, org-scoped configuration bound to a single object type\n   (`company` or `contact`). E.g. *\"EMEA Enterprise\"*, *\"Mid-Market SaaS\"*. A single\n   object can be assigned multiple profiles of the matching type.\n2. **Rule** — within a profile, maps one signal template to point values for one\n   dimension (`fit` or `urgency`). The shape of `pointValues` depends on the signal's\n   answer type (boolean, number/percentage/currency ranges, or list choices).\n3. **Assignment** — links a profile to a specific company or contact. Triggers an\n   immediate recompute so the score appears without waiting for the next signal run.\n4. **Score result** — the computed score for one `(profile, object, dimension)`\n   triple. Includes a per-rule contribution breakdown so the number is always\n   explainable, plus the previous score for delta views.\n\n### Computation semantics\n\nScore is `(earned / maxPossible) * 100`. **All rules in a profile/dimension count\ntoward the denominator regardless of whether a signal answer exists** — unanswered\nrules earn 0 points but their max still counts. This keeps low-coverage scores\nconservatively low so they don't falsely look high. `signalCoverage` and `totalRules`\nare returned alongside the score so consumers can see how complete it is.\n\n### When scores recompute\n\nIn v1 scoring is manual-trigger:\n\n- Assigning a profile to an object → immediate recompute\n- Upserting or deleting a rule → recompute every object assigned to the profile\n- Calling `POST /v1/scoring/compute` directly\n\nAuto-trigger on signal completion is on the v1.5 roadmap.\n"}],"servers":[{"url":"https://api.saber.app","description":"Production server"}],"security":[{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","bearerFormat":"API Key","description":"API key authentication using Bearer token. Format: sk_live_ followed by a secure random string."}},"schemas":{"ErrorResponse":{"type":"object","description":"Standard error envelope returned by every endpoint that flows through the global error handler. Note: the global rate-limit middleware (HTTP 429 from per-API-key throttling) returns a different, flat shape — see `RateLimitErrorResponse`.\n","required":["error"],"properties":{"error":{"type":"object","required":["type","code","requestId"],"properties":{"type":{"type":"string","description":"Error category. Maps 1:1 to HTTP status (e.g. VALIDATION → 422, UNAUTHORIZED → 401).","enum":["BAD_REQUEST","VALIDATION","UNPROCESSABLE_ENTITY","NOT_FOUND","CONFLICT","UNAUTHORIZED","FORBIDDEN","PAYMENT_REQUIRED","PAYLOAD_TOO_LARGE","INTERNAL","EXTERNAL","TIMEOUT"]},"code":{"type":"string","description":"Stable machine-readable error code. Safe to switch on in client code."},"message":{"type":"string","description":"Human-readable error message. Wording may change; do not match against this string."},"errorCode":{"type":"string","description":"Optional public error code propagated from a downstream service (e.g. NestJS, LinkedIn)."},"errorAction":{"type":"string","description":"Optional user action guidance (e.g. \"reconnect Sales Navigator\")."},"details":{"type":"object","additionalProperties":true,"description":"Optional structured fields forwarded from a downstream service. Shape depends on the source."},"requestId":{"type":"string","format":"uuid","description":"Correlation ID for this request. Include when reporting issues."},"fields":{"type":"array","description":"Per-field validation errors (populated when `type` is `VALIDATION` and the cause is a struct-tag validator).","items":{"type":"object","required":["field","message"],"properties":{"field":{"type":"string"},"message":{"type":"string"}}}},"debug":{"type":"object","description":"Debug information. Only present in development environments.","properties":{"cause":{"type":"string"}}}}}}}}},"paths":{"/v1/scoring/profiles/{profileId}":{"delete":{"summary":"Delete a scoring profile","description":"Deletes the profile and cascades to its rules, assignments, and score results.\n","operationId":"deleteScoringProfile","tags":["Scoring"],"responses":{"204":{"description":"Profile deleted"},"404":{"description":"Profile not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}}}
```

## GET /v1/scoring/profiles/{profileId}/rules

> List scoring rules for a profile

```json
{"openapi":"3.2.0","info":{"title":"Saber Platform API","version":"1.0.0"},"tags":[{"name":"Scoring","summary":"Scoring","kind":"nav","description":"## Native scoring framework\n\nConfigure how Saber turns signal results into structured, explainable **fit** and\n**urgency** scores (0–100) for companies and contacts. Scoring sits on top of the\nsignal layer — the engine is signal-agnostic, so any data the signal layer can ingest\nbecomes scoreable.\n\n### How it fits together\n\n1. **Profile** — a named, org-scoped configuration bound to a single object type\n   (`company` or `contact`). E.g. *\"EMEA Enterprise\"*, *\"Mid-Market SaaS\"*. A single\n   object can be assigned multiple profiles of the matching type.\n2. **Rule** — within a profile, maps one signal template to point values for one\n   dimension (`fit` or `urgency`). The shape of `pointValues` depends on the signal's\n   answer type (boolean, number/percentage/currency ranges, or list choices).\n3. **Assignment** — links a profile to a specific company or contact. Triggers an\n   immediate recompute so the score appears without waiting for the next signal run.\n4. **Score result** — the computed score for one `(profile, object, dimension)`\n   triple. Includes a per-rule contribution breakdown so the number is always\n   explainable, plus the previous score for delta views.\n\n### Computation semantics\n\nScore is `(earned / maxPossible) * 100`. **All rules in a profile/dimension count\ntoward the denominator regardless of whether a signal answer exists** — unanswered\nrules earn 0 points but their max still counts. This keeps low-coverage scores\nconservatively low so they don't falsely look high. `signalCoverage` and `totalRules`\nare returned alongside the score so consumers can see how complete it is.\n\n### When scores recompute\n\nIn v1 scoring is manual-trigger:\n\n- Assigning a profile to an object → immediate recompute\n- Upserting or deleting a rule → recompute every object assigned to the profile\n- Calling `POST /v1/scoring/compute` directly\n\nAuto-trigger on signal completion is on the v1.5 roadmap.\n"}],"servers":[{"url":"https://api.saber.app","description":"Production server"}],"security":[{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","bearerFormat":"API Key","description":"API key authentication using Bearer token. Format: sk_live_ followed by a secure random string."}},"schemas":{"ScoringRule":{"type":"object","description":"Maps one signal template to point values for a given dimension within a profile.","required":["id","profileId","signalTemplateId","dimension","pointValues","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"profileId":{"type":"string","format":"uuid"},"signalTemplateId":{"type":"string","description":"Stable parent template ID (not the version ID). Resolved at compute time across versions."},"dimension":{"type":"string","enum":["fit","urgency"]},"selector":{"type":["string","null"],"description":"Reserved for future selector-based scoring (jsonpath, list filters).\nAlways `null` in v1; multi-selector rules become possible in a later\nrelease without a follow-up migration.\n"},"pointValues":{"$ref":"#/components/schemas/ScoringPointValues"},"createdAt":{"type":"string","format":"date-time"}}},"ScoringPointValues":{"type":"object","description":"Point-value mapping for one rule. The shape varies by the answer type of the\nreferenced signal template — exactly one of `true`/`false`, `ranges`, or `choices`\nis populated.\n","properties":{"true":{"type":"number","description":"Points awarded when a boolean signal answers `true`. Used by `boolean` answer types."},"false":{"type":"number","description":"Points awarded when a boolean signal answers `false`. Used by `boolean` answer types."},"ranges":{"type":"array","description":"Used by `number`, `percentage`, and `currency` answer types. Upper bound is exclusive.","items":{"type":"object","required":["min","max","points"],"properties":{"min":{"type":"number"},"max":{"type":"number"},"points":{"type":"number"}}}},"choices":{"type":"object","description":"Used by `list` answer types. Map of allowed list value → points awarded if present in the answer.","additionalProperties":{"type":"number"}},"mode":{"type":"string","enum":["additive","best-match","contains-all","contains-none","exact-match"],"description":"Optional. Controls how a `list` rule combines matched choices. Defaults\nto `additive` (sum matched, clamp to highest single choice) — the v1\nbehaviour. Other modes:\n\n* `best-match` — award only the highest-scoring matched choice.\n* `contains-all` — award `max(choices)` only if every configured choice appears.\n* `contains-none` — award `max(choices)` only if none of the configured choices appear.\n* `exact-match` — award `sum(choices)` only when the answer set equals the configured set exactly.\n"}}},"ErrorResponse":{"type":"object","description":"Standard error envelope returned by every endpoint that flows through the global error handler. Note: the global rate-limit middleware (HTTP 429 from per-API-key throttling) returns a different, flat shape — see `RateLimitErrorResponse`.\n","required":["error"],"properties":{"error":{"type":"object","required":["type","code","requestId"],"properties":{"type":{"type":"string","description":"Error category. Maps 1:1 to HTTP status (e.g. VALIDATION → 422, UNAUTHORIZED → 401).","enum":["BAD_REQUEST","VALIDATION","UNPROCESSABLE_ENTITY","NOT_FOUND","CONFLICT","UNAUTHORIZED","FORBIDDEN","PAYMENT_REQUIRED","PAYLOAD_TOO_LARGE","INTERNAL","EXTERNAL","TIMEOUT"]},"code":{"type":"string","description":"Stable machine-readable error code. Safe to switch on in client code."},"message":{"type":"string","description":"Human-readable error message. Wording may change; do not match against this string."},"errorCode":{"type":"string","description":"Optional public error code propagated from a downstream service (e.g. NestJS, LinkedIn)."},"errorAction":{"type":"string","description":"Optional user action guidance (e.g. \"reconnect Sales Navigator\")."},"details":{"type":"object","additionalProperties":true,"description":"Optional structured fields forwarded from a downstream service. Shape depends on the source."},"requestId":{"type":"string","format":"uuid","description":"Correlation ID for this request. Include when reporting issues."},"fields":{"type":"array","description":"Per-field validation errors (populated when `type` is `VALIDATION` and the cause is a struct-tag validator).","items":{"type":"object","required":["field","message"],"properties":{"field":{"type":"string"},"message":{"type":"string"}}}},"debug":{"type":"object","description":"Debug information. Only present in development environments.","properties":{"cause":{"type":"string"}}}}}}}}},"paths":{"/v1/scoring/profiles/{profileId}/rules":{"get":{"summary":"List scoring rules for a profile","operationId":"listScoringRules","tags":["Scoring"],"responses":{"200":{"description":"Rules retrieved","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ScoringRule"}}}}},"404":{"description":"Profile not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}}}
```

## Upsert a scoring rule

> Create or replace the rule for \`(profileId, signalTemplateId, dimension)\`. Triggers\
> a recompute of every object assigned to the profile so existing scores reflect\
> the new rule immediately.\
> \
> The shape of \`pointValues\` depends on the answer type of the referenced\
> signal template:\
> \
> \* \*\*boolean\*\* — \`{ "true": 20, "false": -5 }\`\
> \* \*\*number / percentage / currency\*\* — \`{ "ranges": \[{ "min": 0, "max": 500, "points": 15 }] }\` (upper bound is exclusive)\
> \* \*\*list\*\* — \`{ "choices": { "Salesforce": 10, "HubSpot": 8 }, "mode": "additive" }\` — \`mode\` is optional; supported values are \`additive\` (default), \`best-match\`, \`contains-all\`, \`contains-none\`, and \`exact-match\`.<br>

```json
{"openapi":"3.2.0","info":{"title":"Saber Platform API","version":"1.0.0"},"tags":[{"name":"Scoring","summary":"Scoring","kind":"nav","description":"## Native scoring framework\n\nConfigure how Saber turns signal results into structured, explainable **fit** and\n**urgency** scores (0–100) for companies and contacts. Scoring sits on top of the\nsignal layer — the engine is signal-agnostic, so any data the signal layer can ingest\nbecomes scoreable.\n\n### How it fits together\n\n1. **Profile** — a named, org-scoped configuration bound to a single object type\n   (`company` or `contact`). E.g. *\"EMEA Enterprise\"*, *\"Mid-Market SaaS\"*. A single\n   object can be assigned multiple profiles of the matching type.\n2. **Rule** — within a profile, maps one signal template to point values for one\n   dimension (`fit` or `urgency`). The shape of `pointValues` depends on the signal's\n   answer type (boolean, number/percentage/currency ranges, or list choices).\n3. **Assignment** — links a profile to a specific company or contact. Triggers an\n   immediate recompute so the score appears without waiting for the next signal run.\n4. **Score result** — the computed score for one `(profile, object, dimension)`\n   triple. Includes a per-rule contribution breakdown so the number is always\n   explainable, plus the previous score for delta views.\n\n### Computation semantics\n\nScore is `(earned / maxPossible) * 100`. **All rules in a profile/dimension count\ntoward the denominator regardless of whether a signal answer exists** — unanswered\nrules earn 0 points but their max still counts. This keeps low-coverage scores\nconservatively low so they don't falsely look high. `signalCoverage` and `totalRules`\nare returned alongside the score so consumers can see how complete it is.\n\n### When scores recompute\n\nIn v1 scoring is manual-trigger:\n\n- Assigning a profile to an object → immediate recompute\n- Upserting or deleting a rule → recompute every object assigned to the profile\n- Calling `POST /v1/scoring/compute` directly\n\nAuto-trigger on signal completion is on the v1.5 roadmap.\n"}],"servers":[{"url":"https://api.saber.app","description":"Production server"}],"security":[{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","bearerFormat":"API Key","description":"API key authentication using Bearer token. Format: sk_live_ followed by a secure random string."}},"schemas":{"UpsertScoringRuleRequest":{"type":"object","required":["signalTemplateId","dimension","answerType","pointValues"],"properties":{"signalTemplateId":{"type":"string"},"dimension":{"type":"string","enum":["fit","urgency"]},"answerType":{"type":"string","enum":["boolean","number","percentage","currency","list"],"description":"Drives shape validation for `pointValues`. Must match the referenced\nsignal template's answer type — mismatch surfaces as an\n`INVALID_POINT_VALUES` 422 at write time rather than a silent\ncompute failure later.\n"},"pointValues":{"$ref":"#/components/schemas/ScoringPointValues"}}},"ScoringPointValues":{"type":"object","description":"Point-value mapping for one rule. The shape varies by the answer type of the\nreferenced signal template — exactly one of `true`/`false`, `ranges`, or `choices`\nis populated.\n","properties":{"true":{"type":"number","description":"Points awarded when a boolean signal answers `true`. Used by `boolean` answer types."},"false":{"type":"number","description":"Points awarded when a boolean signal answers `false`. Used by `boolean` answer types."},"ranges":{"type":"array","description":"Used by `number`, `percentage`, and `currency` answer types. Upper bound is exclusive.","items":{"type":"object","required":["min","max","points"],"properties":{"min":{"type":"number"},"max":{"type":"number"},"points":{"type":"number"}}}},"choices":{"type":"object","description":"Used by `list` answer types. Map of allowed list value → points awarded if present in the answer.","additionalProperties":{"type":"number"}},"mode":{"type":"string","enum":["additive","best-match","contains-all","contains-none","exact-match"],"description":"Optional. Controls how a `list` rule combines matched choices. Defaults\nto `additive` (sum matched, clamp to highest single choice) — the v1\nbehaviour. Other modes:\n\n* `best-match` — award only the highest-scoring matched choice.\n* `contains-all` — award `max(choices)` only if every configured choice appears.\n* `contains-none` — award `max(choices)` only if none of the configured choices appear.\n* `exact-match` — award `sum(choices)` only when the answer set equals the configured set exactly.\n"}}},"ScoringRule":{"type":"object","description":"Maps one signal template to point values for a given dimension within a profile.","required":["id","profileId","signalTemplateId","dimension","pointValues","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"profileId":{"type":"string","format":"uuid"},"signalTemplateId":{"type":"string","description":"Stable parent template ID (not the version ID). Resolved at compute time across versions."},"dimension":{"type":"string","enum":["fit","urgency"]},"selector":{"type":["string","null"],"description":"Reserved for future selector-based scoring (jsonpath, list filters).\nAlways `null` in v1; multi-selector rules become possible in a later\nrelease without a follow-up migration.\n"},"pointValues":{"$ref":"#/components/schemas/ScoringPointValues"},"createdAt":{"type":"string","format":"date-time"}}},"ErrorResponse":{"type":"object","description":"Standard error envelope returned by every endpoint that flows through the global error handler. Note: the global rate-limit middleware (HTTP 429 from per-API-key throttling) returns a different, flat shape — see `RateLimitErrorResponse`.\n","required":["error"],"properties":{"error":{"type":"object","required":["type","code","requestId"],"properties":{"type":{"type":"string","description":"Error category. Maps 1:1 to HTTP status (e.g. VALIDATION → 422, UNAUTHORIZED → 401).","enum":["BAD_REQUEST","VALIDATION","UNPROCESSABLE_ENTITY","NOT_FOUND","CONFLICT","UNAUTHORIZED","FORBIDDEN","PAYMENT_REQUIRED","PAYLOAD_TOO_LARGE","INTERNAL","EXTERNAL","TIMEOUT"]},"code":{"type":"string","description":"Stable machine-readable error code. Safe to switch on in client code."},"message":{"type":"string","description":"Human-readable error message. Wording may change; do not match against this string."},"errorCode":{"type":"string","description":"Optional public error code propagated from a downstream service (e.g. NestJS, LinkedIn)."},"errorAction":{"type":"string","description":"Optional user action guidance (e.g. \"reconnect Sales Navigator\")."},"details":{"type":"object","additionalProperties":true,"description":"Optional structured fields forwarded from a downstream service. Shape depends on the source."},"requestId":{"type":"string","format":"uuid","description":"Correlation ID for this request. Include when reporting issues."},"fields":{"type":"array","description":"Per-field validation errors (populated when `type` is `VALIDATION` and the cause is a struct-tag validator).","items":{"type":"object","required":["field","message"],"properties":{"field":{"type":"string"},"message":{"type":"string"}}}},"debug":{"type":"object","description":"Debug information. Only present in development environments.","properties":{"cause":{"type":"string"}}}}}}}}},"paths":{"/v1/scoring/profiles/{profileId}/rules":{"put":{"summary":"Upsert a scoring rule","description":"Create or replace the rule for `(profileId, signalTemplateId, dimension)`. Triggers\na recompute of every object assigned to the profile so existing scores reflect\nthe new rule immediately.\n\nThe shape of `pointValues` depends on the answer type of the referenced\nsignal template:\n\n* **boolean** — `{ \"true\": 20, \"false\": -5 }`\n* **number / percentage / currency** — `{ \"ranges\": [{ \"min\": 0, \"max\": 500, \"points\": 15 }] }` (upper bound is exclusive)\n* **list** — `{ \"choices\": { \"Salesforce\": 10, \"HubSpot\": 8 }, \"mode\": \"additive\" }` — `mode` is optional; supported values are `additive` (default), `best-match`, `contains-all`, `contains-none`, and `exact-match`.\n","operationId":"upsertScoringRule","tags":["Scoring"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertScoringRuleRequest"}}}},"responses":{"200":{"description":"Rule upserted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScoringRule"}}}},"404":{"description":"Profile not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}}}
```

## Delete a scoring rule

> Removes the rule and triggers a recompute of every object assigned to the\
> profile so scores never silently go stale.<br>

```json
{"openapi":"3.2.0","info":{"title":"Saber Platform API","version":"1.0.0"},"tags":[{"name":"Scoring","summary":"Scoring","kind":"nav","description":"## Native scoring framework\n\nConfigure how Saber turns signal results into structured, explainable **fit** and\n**urgency** scores (0–100) for companies and contacts. Scoring sits on top of the\nsignal layer — the engine is signal-agnostic, so any data the signal layer can ingest\nbecomes scoreable.\n\n### How it fits together\n\n1. **Profile** — a named, org-scoped configuration bound to a single object type\n   (`company` or `contact`). E.g. *\"EMEA Enterprise\"*, *\"Mid-Market SaaS\"*. A single\n   object can be assigned multiple profiles of the matching type.\n2. **Rule** — within a profile, maps one signal template to point values for one\n   dimension (`fit` or `urgency`). The shape of `pointValues` depends on the signal's\n   answer type (boolean, number/percentage/currency ranges, or list choices).\n3. **Assignment** — links a profile to a specific company or contact. Triggers an\n   immediate recompute so the score appears without waiting for the next signal run.\n4. **Score result** — the computed score for one `(profile, object, dimension)`\n   triple. Includes a per-rule contribution breakdown so the number is always\n   explainable, plus the previous score for delta views.\n\n### Computation semantics\n\nScore is `(earned / maxPossible) * 100`. **All rules in a profile/dimension count\ntoward the denominator regardless of whether a signal answer exists** — unanswered\nrules earn 0 points but their max still counts. This keeps low-coverage scores\nconservatively low so they don't falsely look high. `signalCoverage` and `totalRules`\nare returned alongside the score so consumers can see how complete it is.\n\n### When scores recompute\n\nIn v1 scoring is manual-trigger:\n\n- Assigning a profile to an object → immediate recompute\n- Upserting or deleting a rule → recompute every object assigned to the profile\n- Calling `POST /v1/scoring/compute` directly\n\nAuto-trigger on signal completion is on the v1.5 roadmap.\n"}],"servers":[{"url":"https://api.saber.app","description":"Production server"}],"security":[{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","bearerFormat":"API Key","description":"API key authentication using Bearer token. Format: sk_live_ followed by a secure random string."}},"schemas":{"ErrorResponse":{"type":"object","description":"Standard error envelope returned by every endpoint that flows through the global error handler. Note: the global rate-limit middleware (HTTP 429 from per-API-key throttling) returns a different, flat shape — see `RateLimitErrorResponse`.\n","required":["error"],"properties":{"error":{"type":"object","required":["type","code","requestId"],"properties":{"type":{"type":"string","description":"Error category. Maps 1:1 to HTTP status (e.g. VALIDATION → 422, UNAUTHORIZED → 401).","enum":["BAD_REQUEST","VALIDATION","UNPROCESSABLE_ENTITY","NOT_FOUND","CONFLICT","UNAUTHORIZED","FORBIDDEN","PAYMENT_REQUIRED","PAYLOAD_TOO_LARGE","INTERNAL","EXTERNAL","TIMEOUT"]},"code":{"type":"string","description":"Stable machine-readable error code. Safe to switch on in client code."},"message":{"type":"string","description":"Human-readable error message. Wording may change; do not match against this string."},"errorCode":{"type":"string","description":"Optional public error code propagated from a downstream service (e.g. NestJS, LinkedIn)."},"errorAction":{"type":"string","description":"Optional user action guidance (e.g. \"reconnect Sales Navigator\")."},"details":{"type":"object","additionalProperties":true,"description":"Optional structured fields forwarded from a downstream service. Shape depends on the source."},"requestId":{"type":"string","format":"uuid","description":"Correlation ID for this request. Include when reporting issues."},"fields":{"type":"array","description":"Per-field validation errors (populated when `type` is `VALIDATION` and the cause is a struct-tag validator).","items":{"type":"object","required":["field","message"],"properties":{"field":{"type":"string"},"message":{"type":"string"}}}},"debug":{"type":"object","description":"Debug information. Only present in development environments.","properties":{"cause":{"type":"string"}}}}}}}}},"paths":{"/v1/scoring/profiles/{profileId}/rules/{ruleId}":{"delete":{"summary":"Delete a scoring rule","description":"Removes the rule and triggers a recompute of every object assigned to the\nprofile so scores never silently go stale.\n","operationId":"deleteScoringRule","tags":["Scoring"],"responses":{"204":{"description":"Rule deleted"},"404":{"description":"Rule or profile not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}}}
```

## List profile assignments for an object

> Returns every profile a given object is assigned to.

```json
{"openapi":"3.2.0","info":{"title":"Saber Platform API","version":"1.0.0"},"tags":[{"name":"Scoring","summary":"Scoring","kind":"nav","description":"## Native scoring framework\n\nConfigure how Saber turns signal results into structured, explainable **fit** and\n**urgency** scores (0–100) for companies and contacts. Scoring sits on top of the\nsignal layer — the engine is signal-agnostic, so any data the signal layer can ingest\nbecomes scoreable.\n\n### How it fits together\n\n1. **Profile** — a named, org-scoped configuration bound to a single object type\n   (`company` or `contact`). E.g. *\"EMEA Enterprise\"*, *\"Mid-Market SaaS\"*. A single\n   object can be assigned multiple profiles of the matching type.\n2. **Rule** — within a profile, maps one signal template to point values for one\n   dimension (`fit` or `urgency`). The shape of `pointValues` depends on the signal's\n   answer type (boolean, number/percentage/currency ranges, or list choices).\n3. **Assignment** — links a profile to a specific company or contact. Triggers an\n   immediate recompute so the score appears without waiting for the next signal run.\n4. **Score result** — the computed score for one `(profile, object, dimension)`\n   triple. Includes a per-rule contribution breakdown so the number is always\n   explainable, plus the previous score for delta views.\n\n### Computation semantics\n\nScore is `(earned / maxPossible) * 100`. **All rules in a profile/dimension count\ntoward the denominator regardless of whether a signal answer exists** — unanswered\nrules earn 0 points but their max still counts. This keeps low-coverage scores\nconservatively low so they don't falsely look high. `signalCoverage` and `totalRules`\nare returned alongside the score so consumers can see how complete it is.\n\n### When scores recompute\n\nIn v1 scoring is manual-trigger:\n\n- Assigning a profile to an object → immediate recompute\n- Upserting or deleting a rule → recompute every object assigned to the profile\n- Calling `POST /v1/scoring/compute` directly\n\nAuto-trigger on signal completion is on the v1.5 roadmap.\n"}],"servers":[{"url":"https://api.saber.app","description":"Production server"}],"security":[{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","bearerFormat":"API Key","description":"API key authentication using Bearer token. Format: sk_live_ followed by a secure random string."}},"schemas":{"ProfileAssignment":{"type":"object","description":"Links a scoring profile to one company or contact.","required":["id","profileId","organizationId","objectType","objectId","assignedAt"],"properties":{"id":{"type":"string","format":"uuid"},"profileId":{"type":"string","format":"uuid"},"organizationId":{"type":"string"},"objectType":{"type":"string","enum":["company","contact"]},"objectId":{"type":"string","description":"For companies, the domain (e.g. `acme.com`); for contacts, the LinkedIn profile URL."},"assignedAt":{"type":"string","format":"date-time"}}},"ErrorResponse":{"type":"object","description":"Standard error envelope returned by every endpoint that flows through the global error handler. Note: the global rate-limit middleware (HTTP 429 from per-API-key throttling) returns a different, flat shape — see `RateLimitErrorResponse`.\n","required":["error"],"properties":{"error":{"type":"object","required":["type","code","requestId"],"properties":{"type":{"type":"string","description":"Error category. Maps 1:1 to HTTP status (e.g. VALIDATION → 422, UNAUTHORIZED → 401).","enum":["BAD_REQUEST","VALIDATION","UNPROCESSABLE_ENTITY","NOT_FOUND","CONFLICT","UNAUTHORIZED","FORBIDDEN","PAYMENT_REQUIRED","PAYLOAD_TOO_LARGE","INTERNAL","EXTERNAL","TIMEOUT"]},"code":{"type":"string","description":"Stable machine-readable error code. Safe to switch on in client code."},"message":{"type":"string","description":"Human-readable error message. Wording may change; do not match against this string."},"errorCode":{"type":"string","description":"Optional public error code propagated from a downstream service (e.g. NestJS, LinkedIn)."},"errorAction":{"type":"string","description":"Optional user action guidance (e.g. \"reconnect Sales Navigator\")."},"details":{"type":"object","additionalProperties":true,"description":"Optional structured fields forwarded from a downstream service. Shape depends on the source."},"requestId":{"type":"string","format":"uuid","description":"Correlation ID for this request. Include when reporting issues."},"fields":{"type":"array","description":"Per-field validation errors (populated when `type` is `VALIDATION` and the cause is a struct-tag validator).","items":{"type":"object","required":["field","message"],"properties":{"field":{"type":"string"},"message":{"type":"string"}}}},"debug":{"type":"object","description":"Debug information. Only present in development environments.","properties":{"cause":{"type":"string"}}}}}}}}},"paths":{"/v1/scoring/assignments":{"get":{"summary":"List profile assignments for an object","description":"Returns every profile a given object is assigned to.","operationId":"listProfileAssignments","tags":["Scoring"],"parameters":[{"name":"objectType","in":"query","required":true,"description":"Object type (`company` or `contact`)","schema":{"type":"string","enum":["company","contact"]}},{"name":"objectId","in":"query","required":true,"description":"For `company` use the domain (e.g. `acme.com`); for `contact` use the LinkedIn profile URL","schema":{"type":"string"}}],"responses":{"200":{"description":"Assignments retrieved","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProfileAssignment"}}}}},"422":{"description":"Missing or invalid query parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}}}
```

## Assign a profile to a single object

> Links one company or contact to a scoring profile. Triggers immediate score\
> computation so the score appears without waiting for the next signal run.\
> Returns 422 if the object type does not match the profile's configured type.<br>

```json
{"openapi":"3.2.0","info":{"title":"Saber Platform API","version":"1.0.0"},"tags":[{"name":"Scoring","summary":"Scoring","kind":"nav","description":"## Native scoring framework\n\nConfigure how Saber turns signal results into structured, explainable **fit** and\n**urgency** scores (0–100) for companies and contacts. Scoring sits on top of the\nsignal layer — the engine is signal-agnostic, so any data the signal layer can ingest\nbecomes scoreable.\n\n### How it fits together\n\n1. **Profile** — a named, org-scoped configuration bound to a single object type\n   (`company` or `contact`). E.g. *\"EMEA Enterprise\"*, *\"Mid-Market SaaS\"*. A single\n   object can be assigned multiple profiles of the matching type.\n2. **Rule** — within a profile, maps one signal template to point values for one\n   dimension (`fit` or `urgency`). The shape of `pointValues` depends on the signal's\n   answer type (boolean, number/percentage/currency ranges, or list choices).\n3. **Assignment** — links a profile to a specific company or contact. Triggers an\n   immediate recompute so the score appears without waiting for the next signal run.\n4. **Score result** — the computed score for one `(profile, object, dimension)`\n   triple. Includes a per-rule contribution breakdown so the number is always\n   explainable, plus the previous score for delta views.\n\n### Computation semantics\n\nScore is `(earned / maxPossible) * 100`. **All rules in a profile/dimension count\ntoward the denominator regardless of whether a signal answer exists** — unanswered\nrules earn 0 points but their max still counts. This keeps low-coverage scores\nconservatively low so they don't falsely look high. `signalCoverage` and `totalRules`\nare returned alongside the score so consumers can see how complete it is.\n\n### When scores recompute\n\nIn v1 scoring is manual-trigger:\n\n- Assigning a profile to an object → immediate recompute\n- Upserting or deleting a rule → recompute every object assigned to the profile\n- Calling `POST /v1/scoring/compute` directly\n\nAuto-trigger on signal completion is on the v1.5 roadmap.\n"}],"servers":[{"url":"https://api.saber.app","description":"Production server"}],"security":[{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","bearerFormat":"API Key","description":"API key authentication using Bearer token. Format: sk_live_ followed by a secure random string."}},"schemas":{"CreateProfileAssignmentRequest":{"type":"object","required":["profileId","objectType","objectId"],"properties":{"profileId":{"type":"string","format":"uuid"},"objectType":{"type":"string","enum":["company","contact"]},"objectId":{"type":"string","minLength":1,"maxLength":500,"description":"For `company`, the domain; for `contact`, the LinkedIn profile URL."}}},"ProfileAssignment":{"type":"object","description":"Links a scoring profile to one company or contact.","required":["id","profileId","organizationId","objectType","objectId","assignedAt"],"properties":{"id":{"type":"string","format":"uuid"},"profileId":{"type":"string","format":"uuid"},"organizationId":{"type":"string"},"objectType":{"type":"string","enum":["company","contact"]},"objectId":{"type":"string","description":"For companies, the domain (e.g. `acme.com`); for contacts, the LinkedIn profile URL."},"assignedAt":{"type":"string","format":"date-time"}}},"ErrorResponse":{"type":"object","description":"Standard error envelope returned by every endpoint that flows through the global error handler. Note: the global rate-limit middleware (HTTP 429 from per-API-key throttling) returns a different, flat shape — see `RateLimitErrorResponse`.\n","required":["error"],"properties":{"error":{"type":"object","required":["type","code","requestId"],"properties":{"type":{"type":"string","description":"Error category. Maps 1:1 to HTTP status (e.g. VALIDATION → 422, UNAUTHORIZED → 401).","enum":["BAD_REQUEST","VALIDATION","UNPROCESSABLE_ENTITY","NOT_FOUND","CONFLICT","UNAUTHORIZED","FORBIDDEN","PAYMENT_REQUIRED","PAYLOAD_TOO_LARGE","INTERNAL","EXTERNAL","TIMEOUT"]},"code":{"type":"string","description":"Stable machine-readable error code. Safe to switch on in client code."},"message":{"type":"string","description":"Human-readable error message. Wording may change; do not match against this string."},"errorCode":{"type":"string","description":"Optional public error code propagated from a downstream service (e.g. NestJS, LinkedIn)."},"errorAction":{"type":"string","description":"Optional user action guidance (e.g. \"reconnect Sales Navigator\")."},"details":{"type":"object","additionalProperties":true,"description":"Optional structured fields forwarded from a downstream service. Shape depends on the source."},"requestId":{"type":"string","format":"uuid","description":"Correlation ID for this request. Include when reporting issues."},"fields":{"type":"array","description":"Per-field validation errors (populated when `type` is `VALIDATION` and the cause is a struct-tag validator).","items":{"type":"object","required":["field","message"],"properties":{"field":{"type":"string"},"message":{"type":"string"}}}},"debug":{"type":"object","description":"Debug information. Only present in development environments.","properties":{"cause":{"type":"string"}}}}}}}}},"paths":{"/v1/scoring/assignments":{"post":{"summary":"Assign a profile to a single object","description":"Links one company or contact to a scoring profile. Triggers immediate score\ncomputation so the score appears without waiting for the next signal run.\nReturns 422 if the object type does not match the profile's configured type.\n","operationId":"createProfileAssignment","tags":["Scoring"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProfileAssignmentRequest"}}}},"responses":{"201":{"description":"Assignment created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileAssignment"}}}},"404":{"description":"Profile not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Object already assigned to this profile","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Validation error (including profile/object type mismatch)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}}}
```

## Assign a profile to many objects at once

> Links many company or contact IDs to a single profile in one call. Skips\
> duplicates (returns only newly created rows) and triggers an immediate\
> score computation per newly assigned object.<br>

```json
{"openapi":"3.2.0","info":{"title":"Saber Platform API","version":"1.0.0"},"tags":[{"name":"Scoring","summary":"Scoring","kind":"nav","description":"## Native scoring framework\n\nConfigure how Saber turns signal results into structured, explainable **fit** and\n**urgency** scores (0–100) for companies and contacts. Scoring sits on top of the\nsignal layer — the engine is signal-agnostic, so any data the signal layer can ingest\nbecomes scoreable.\n\n### How it fits together\n\n1. **Profile** — a named, org-scoped configuration bound to a single object type\n   (`company` or `contact`). E.g. *\"EMEA Enterprise\"*, *\"Mid-Market SaaS\"*. A single\n   object can be assigned multiple profiles of the matching type.\n2. **Rule** — within a profile, maps one signal template to point values for one\n   dimension (`fit` or `urgency`). The shape of `pointValues` depends on the signal's\n   answer type (boolean, number/percentage/currency ranges, or list choices).\n3. **Assignment** — links a profile to a specific company or contact. Triggers an\n   immediate recompute so the score appears without waiting for the next signal run.\n4. **Score result** — the computed score for one `(profile, object, dimension)`\n   triple. Includes a per-rule contribution breakdown so the number is always\n   explainable, plus the previous score for delta views.\n\n### Computation semantics\n\nScore is `(earned / maxPossible) * 100`. **All rules in a profile/dimension count\ntoward the denominator regardless of whether a signal answer exists** — unanswered\nrules earn 0 points but their max still counts. This keeps low-coverage scores\nconservatively low so they don't falsely look high. `signalCoverage` and `totalRules`\nare returned alongside the score so consumers can see how complete it is.\n\n### When scores recompute\n\nIn v1 scoring is manual-trigger:\n\n- Assigning a profile to an object → immediate recompute\n- Upserting or deleting a rule → recompute every object assigned to the profile\n- Calling `POST /v1/scoring/compute` directly\n\nAuto-trigger on signal completion is on the v1.5 roadmap.\n"}],"servers":[{"url":"https://api.saber.app","description":"Production server"}],"security":[{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","bearerFormat":"API Key","description":"API key authentication using Bearer token. Format: sk_live_ followed by a secure random string."}},"schemas":{"BulkCreateProfileAssignmentsRequest":{"type":"object","required":["profileId","objectType","objectIds"],"properties":{"profileId":{"type":"string","format":"uuid"},"objectType":{"type":"string","enum":["company","contact"]},"objectIds":{"type":"array","minItems":1,"maxItems":500,"items":{"type":"string","minLength":1,"maxLength":500},"description":"Capped at 500 per request. Paginate larger lists by issuing\nmultiple requests.\n"}}},"ProfileAssignment":{"type":"object","description":"Links a scoring profile to one company or contact.","required":["id","profileId","organizationId","objectType","objectId","assignedAt"],"properties":{"id":{"type":"string","format":"uuid"},"profileId":{"type":"string","format":"uuid"},"organizationId":{"type":"string"},"objectType":{"type":"string","enum":["company","contact"]},"objectId":{"type":"string","description":"For companies, the domain (e.g. `acme.com`); for contacts, the LinkedIn profile URL."},"assignedAt":{"type":"string","format":"date-time"}}},"ErrorResponse":{"type":"object","description":"Standard error envelope returned by every endpoint that flows through the global error handler. Note: the global rate-limit middleware (HTTP 429 from per-API-key throttling) returns a different, flat shape — see `RateLimitErrorResponse`.\n","required":["error"],"properties":{"error":{"type":"object","required":["type","code","requestId"],"properties":{"type":{"type":"string","description":"Error category. Maps 1:1 to HTTP status (e.g. VALIDATION → 422, UNAUTHORIZED → 401).","enum":["BAD_REQUEST","VALIDATION","UNPROCESSABLE_ENTITY","NOT_FOUND","CONFLICT","UNAUTHORIZED","FORBIDDEN","PAYMENT_REQUIRED","PAYLOAD_TOO_LARGE","INTERNAL","EXTERNAL","TIMEOUT"]},"code":{"type":"string","description":"Stable machine-readable error code. Safe to switch on in client code."},"message":{"type":"string","description":"Human-readable error message. Wording may change; do not match against this string."},"errorCode":{"type":"string","description":"Optional public error code propagated from a downstream service (e.g. NestJS, LinkedIn)."},"errorAction":{"type":"string","description":"Optional user action guidance (e.g. \"reconnect Sales Navigator\")."},"details":{"type":"object","additionalProperties":true,"description":"Optional structured fields forwarded from a downstream service. Shape depends on the source."},"requestId":{"type":"string","format":"uuid","description":"Correlation ID for this request. Include when reporting issues."},"fields":{"type":"array","description":"Per-field validation errors (populated when `type` is `VALIDATION` and the cause is a struct-tag validator).","items":{"type":"object","required":["field","message"],"properties":{"field":{"type":"string"},"message":{"type":"string"}}}},"debug":{"type":"object","description":"Debug information. Only present in development environments.","properties":{"cause":{"type":"string"}}}}}}}}},"paths":{"/v1/scoring/assignments/bulk":{"post":{"summary":"Assign a profile to many objects at once","description":"Links many company or contact IDs to a single profile in one call. Skips\nduplicates (returns only newly created rows) and triggers an immediate\nscore computation per newly assigned object.\n","operationId":"bulkCreateProfileAssignments","tags":["Scoring"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkCreateProfileAssignmentsRequest"}}}},"responses":{"201":{"description":"Assignments created (duplicates omitted from the response)","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProfileAssignment"}}}}},"422":{"description":"Validation error (including profile/object type mismatch)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}}}
```

## Remove a profile assignment

> Removes the assignment and cleans up any score results computed for this\
> \`(profile, object)\` pair.<br>

```json
{"openapi":"3.2.0","info":{"title":"Saber Platform API","version":"1.0.0"},"tags":[{"name":"Scoring","summary":"Scoring","kind":"nav","description":"## Native scoring framework\n\nConfigure how Saber turns signal results into structured, explainable **fit** and\n**urgency** scores (0–100) for companies and contacts. Scoring sits on top of the\nsignal layer — the engine is signal-agnostic, so any data the signal layer can ingest\nbecomes scoreable.\n\n### How it fits together\n\n1. **Profile** — a named, org-scoped configuration bound to a single object type\n   (`company` or `contact`). E.g. *\"EMEA Enterprise\"*, *\"Mid-Market SaaS\"*. A single\n   object can be assigned multiple profiles of the matching type.\n2. **Rule** — within a profile, maps one signal template to point values for one\n   dimension (`fit` or `urgency`). The shape of `pointValues` depends on the signal's\n   answer type (boolean, number/percentage/currency ranges, or list choices).\n3. **Assignment** — links a profile to a specific company or contact. Triggers an\n   immediate recompute so the score appears without waiting for the next signal run.\n4. **Score result** — the computed score for one `(profile, object, dimension)`\n   triple. Includes a per-rule contribution breakdown so the number is always\n   explainable, plus the previous score for delta views.\n\n### Computation semantics\n\nScore is `(earned / maxPossible) * 100`. **All rules in a profile/dimension count\ntoward the denominator regardless of whether a signal answer exists** — unanswered\nrules earn 0 points but their max still counts. This keeps low-coverage scores\nconservatively low so they don't falsely look high. `signalCoverage` and `totalRules`\nare returned alongside the score so consumers can see how complete it is.\n\n### When scores recompute\n\nIn v1 scoring is manual-trigger:\n\n- Assigning a profile to an object → immediate recompute\n- Upserting or deleting a rule → recompute every object assigned to the profile\n- Calling `POST /v1/scoring/compute` directly\n\nAuto-trigger on signal completion is on the v1.5 roadmap.\n"}],"servers":[{"url":"https://api.saber.app","description":"Production server"}],"security":[{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","bearerFormat":"API Key","description":"API key authentication using Bearer token. Format: sk_live_ followed by a secure random string."}},"schemas":{"ErrorResponse":{"type":"object","description":"Standard error envelope returned by every endpoint that flows through the global error handler. Note: the global rate-limit middleware (HTTP 429 from per-API-key throttling) returns a different, flat shape — see `RateLimitErrorResponse`.\n","required":["error"],"properties":{"error":{"type":"object","required":["type","code","requestId"],"properties":{"type":{"type":"string","description":"Error category. Maps 1:1 to HTTP status (e.g. VALIDATION → 422, UNAUTHORIZED → 401).","enum":["BAD_REQUEST","VALIDATION","UNPROCESSABLE_ENTITY","NOT_FOUND","CONFLICT","UNAUTHORIZED","FORBIDDEN","PAYMENT_REQUIRED","PAYLOAD_TOO_LARGE","INTERNAL","EXTERNAL","TIMEOUT"]},"code":{"type":"string","description":"Stable machine-readable error code. Safe to switch on in client code."},"message":{"type":"string","description":"Human-readable error message. Wording may change; do not match against this string."},"errorCode":{"type":"string","description":"Optional public error code propagated from a downstream service (e.g. NestJS, LinkedIn)."},"errorAction":{"type":"string","description":"Optional user action guidance (e.g. \"reconnect Sales Navigator\")."},"details":{"type":"object","additionalProperties":true,"description":"Optional structured fields forwarded from a downstream service. Shape depends on the source."},"requestId":{"type":"string","format":"uuid","description":"Correlation ID for this request. Include when reporting issues."},"fields":{"type":"array","description":"Per-field validation errors (populated when `type` is `VALIDATION` and the cause is a struct-tag validator).","items":{"type":"object","required":["field","message"],"properties":{"field":{"type":"string"},"message":{"type":"string"}}}},"debug":{"type":"object","description":"Debug information. Only present in development environments.","properties":{"cause":{"type":"string"}}}}}}}}},"paths":{"/v1/scoring/assignments/{assignmentId}":{"delete":{"summary":"Remove a profile assignment","description":"Removes the assignment and cleans up any score results computed for this\n`(profile, object)` pair.\n","operationId":"deleteProfileAssignment","tags":["Scoring"],"responses":{"204":{"description":"Assignment deleted"},"404":{"description":"Assignment not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}}}
```

## Read scores for one or more objects

> Returns one row per \`(profile, object, dimension)\` triple. Pass \`objectId\`\
> multiple times to read scores for several objects in a single call. The\
> response includes the per-rule contribution breakdown so the score is always\
> explainable.<br>

```json
{"openapi":"3.2.0","info":{"title":"Saber Platform API","version":"1.0.0"},"tags":[{"name":"Scoring","summary":"Scoring","kind":"nav","description":"## Native scoring framework\n\nConfigure how Saber turns signal results into structured, explainable **fit** and\n**urgency** scores (0–100) for companies and contacts. Scoring sits on top of the\nsignal layer — the engine is signal-agnostic, so any data the signal layer can ingest\nbecomes scoreable.\n\n### How it fits together\n\n1. **Profile** — a named, org-scoped configuration bound to a single object type\n   (`company` or `contact`). E.g. *\"EMEA Enterprise\"*, *\"Mid-Market SaaS\"*. A single\n   object can be assigned multiple profiles of the matching type.\n2. **Rule** — within a profile, maps one signal template to point values for one\n   dimension (`fit` or `urgency`). The shape of `pointValues` depends on the signal's\n   answer type (boolean, number/percentage/currency ranges, or list choices).\n3. **Assignment** — links a profile to a specific company or contact. Triggers an\n   immediate recompute so the score appears without waiting for the next signal run.\n4. **Score result** — the computed score for one `(profile, object, dimension)`\n   triple. Includes a per-rule contribution breakdown so the number is always\n   explainable, plus the previous score for delta views.\n\n### Computation semantics\n\nScore is `(earned / maxPossible) * 100`. **All rules in a profile/dimension count\ntoward the denominator regardless of whether a signal answer exists** — unanswered\nrules earn 0 points but their max still counts. This keeps low-coverage scores\nconservatively low so they don't falsely look high. `signalCoverage` and `totalRules`\nare returned alongside the score so consumers can see how complete it is.\n\n### When scores recompute\n\nIn v1 scoring is manual-trigger:\n\n- Assigning a profile to an object → immediate recompute\n- Upserting or deleting a rule → recompute every object assigned to the profile\n- Calling `POST /v1/scoring/compute` directly\n\nAuto-trigger on signal completion is on the v1.5 roadmap.\n"}],"servers":[{"url":"https://api.saber.app","description":"Production server"}],"security":[{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","bearerFormat":"API Key","description":"API key authentication using Bearer token. Format: sk_live_ followed by a secure random string."}},"schemas":{"ScoreResult":{"type":"object","description":"Latest computed score for one `(profile, object, dimension)` triple. Includes the\nper-rule contribution breakdown so the score is always explainable, plus a delta\nview of the previous score.\n","required":["id","profileId","organizationId","objectType","objectId","dimension","score","contributions","signalCoverage","totalRules","computedAt","version"],"properties":{"id":{"type":"string","format":"uuid"},"profileId":{"type":"string","format":"uuid"},"organizationId":{"type":"string"},"objectType":{"type":"string","enum":["company","contact"]},"objectId":{"type":"string"},"dimension":{"type":"string","enum":["fit","urgency"]},"score":{"type":"number","minimum":0,"maximum":100,"description":"Score in `[0, 100]` computed as `(earned / maxPossible) * 100`. **All rules\ncount toward the denominator** regardless of whether a signal answer exists,\nso low-coverage scores stay conservatively low.\n"},"previousScore":{"type":["number","null"],"description":"Score from the previous compute, or `null` on first compute."},"contributions":{"type":"array","items":{"$ref":"#/components/schemas/ScoreContribution"}},"previousContributions":{"type":["array","null"],"items":{"$ref":"#/components/schemas/ScoreContribution"}},"signalCoverage":{"type":"integer","description":"Number of rules with a signal answer available at compute time."},"totalRules":{"type":"integer","description":"Total rules in the profile/dimension."},"computedAt":{"type":"string","format":"date-time"},"version":{"type":"integer","description":"Increments on every recompute."}}},"ScoreContribution":{"type":"object","description":"How a single rule contributed to a dimension score.","required":["ruleId","signalTemplateId","matchedValue","pointsEarned","maxPoints"],"properties":{"ruleId":{"type":"string","format":"uuid"},"signalTemplateId":{"type":"string"},"matchedValue":{"type":"string","description":"Human-readable summary of which choice/range/boolean fired for this rule.\nFormat depends on the rule's answer type and (for list rules) `mode`:\n\n* boolean — `\"true\"` / `\"false\"`\n* number/percentage/currency — `\"250 (100–500)\"` (value plus matched range)\n* list `additive` — comma-joined matched choices, e.g. `\"Salesforce, HubSpot\"`, or `\"no match\"`\n* list `best-match` — the single highest-scoring matched choice, e.g. `\"VP\"`, or `\"no match\"`\n* list `contains-all` — `\"A, B, C\"` when satisfied, `\"missing: B, C\"` (alphabetically sorted) when not\n* list `contains-none` — `\"none present\"` when satisfied, `\"found: X, Y\"` when not\n* list `exact-match` — comma-joined matched choices when the answer set equals the configured set, otherwise `\"set mismatch\"`\n"},"pointsEarned":{"type":"number","description":"Points this rule contributed. Always in `[0, maxPoints]`."},"maxPoints":{"type":"number","description":"Maximum points this rule could have contributed."}}},"ErrorResponse":{"type":"object","description":"Standard error envelope returned by every endpoint that flows through the global error handler. Note: the global rate-limit middleware (HTTP 429 from per-API-key throttling) returns a different, flat shape — see `RateLimitErrorResponse`.\n","required":["error"],"properties":{"error":{"type":"object","required":["type","code","requestId"],"properties":{"type":{"type":"string","description":"Error category. Maps 1:1 to HTTP status (e.g. VALIDATION → 422, UNAUTHORIZED → 401).","enum":["BAD_REQUEST","VALIDATION","UNPROCESSABLE_ENTITY","NOT_FOUND","CONFLICT","UNAUTHORIZED","FORBIDDEN","PAYMENT_REQUIRED","PAYLOAD_TOO_LARGE","INTERNAL","EXTERNAL","TIMEOUT"]},"code":{"type":"string","description":"Stable machine-readable error code. Safe to switch on in client code."},"message":{"type":"string","description":"Human-readable error message. Wording may change; do not match against this string."},"errorCode":{"type":"string","description":"Optional public error code propagated from a downstream service (e.g. NestJS, LinkedIn)."},"errorAction":{"type":"string","description":"Optional user action guidance (e.g. \"reconnect Sales Navigator\")."},"details":{"type":"object","additionalProperties":true,"description":"Optional structured fields forwarded from a downstream service. Shape depends on the source."},"requestId":{"type":"string","format":"uuid","description":"Correlation ID for this request. Include when reporting issues."},"fields":{"type":"array","description":"Per-field validation errors (populated when `type` is `VALIDATION` and the cause is a struct-tag validator).","items":{"type":"object","required":["field","message"],"properties":{"field":{"type":"string"},"message":{"type":"string"}}}},"debug":{"type":"object","description":"Debug information. Only present in development environments.","properties":{"cause":{"type":"string"}}}}}}}}},"paths":{"/v1/scoring/scores":{"get":{"summary":"Read scores for one or more objects","description":"Returns one row per `(profile, object, dimension)` triple. Pass `objectId`\nmultiple times to read scores for several objects in a single call. The\nresponse includes the per-rule contribution breakdown so the score is always\nexplainable.\n","operationId":"getScores","tags":["Scoring"],"parameters":[{"name":"objectType","in":"query","required":true,"schema":{"type":"string","enum":["company","contact"]}},{"name":"objectId","in":"query","required":true,"description":"Repeatable. For `company` use the domain; for `contact` use the LinkedIn profile URL.","schema":{"type":"array","items":{"type":"string"}},"style":"form","explode":true}],"responses":{"200":{"description":"Scores retrieved (empty array if no scores exist for the given objects)","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ScoreResult"}}}}},"422":{"description":"Missing or invalid query parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}}}
```

## Trigger score recomputation

> Triggers score recomputation for the given objects against the latest\
> signal data. Idempotent — duplicate triggers for the same object are\
> deduplicated automatically.\
> \
> Returns \`202 Accepted\` with \`{queued, failed}\` counts as soon as\
> dispatches finish (recomputation runs asynchronously; results become\
> available via \`GET /v1/scoring/scores\`). A non-zero \`failed\` means\
> some dispatches failed; if \*every\* dispatch fails the endpoint\
> returns 502 instead.<br>

```json
{"openapi":"3.2.0","info":{"title":"Saber Platform API","version":"1.0.0"},"tags":[{"name":"Scoring","summary":"Scoring","kind":"nav","description":"## Native scoring framework\n\nConfigure how Saber turns signal results into structured, explainable **fit** and\n**urgency** scores (0–100) for companies and contacts. Scoring sits on top of the\nsignal layer — the engine is signal-agnostic, so any data the signal layer can ingest\nbecomes scoreable.\n\n### How it fits together\n\n1. **Profile** — a named, org-scoped configuration bound to a single object type\n   (`company` or `contact`). E.g. *\"EMEA Enterprise\"*, *\"Mid-Market SaaS\"*. A single\n   object can be assigned multiple profiles of the matching type.\n2. **Rule** — within a profile, maps one signal template to point values for one\n   dimension (`fit` or `urgency`). The shape of `pointValues` depends on the signal's\n   answer type (boolean, number/percentage/currency ranges, or list choices).\n3. **Assignment** — links a profile to a specific company or contact. Triggers an\n   immediate recompute so the score appears without waiting for the next signal run.\n4. **Score result** — the computed score for one `(profile, object, dimension)`\n   triple. Includes a per-rule contribution breakdown so the number is always\n   explainable, plus the previous score for delta views.\n\n### Computation semantics\n\nScore is `(earned / maxPossible) * 100`. **All rules in a profile/dimension count\ntoward the denominator regardless of whether a signal answer exists** — unanswered\nrules earn 0 points but their max still counts. This keeps low-coverage scores\nconservatively low so they don't falsely look high. `signalCoverage` and `totalRules`\nare returned alongside the score so consumers can see how complete it is.\n\n### When scores recompute\n\nIn v1 scoring is manual-trigger:\n\n- Assigning a profile to an object → immediate recompute\n- Upserting or deleting a rule → recompute every object assigned to the profile\n- Calling `POST /v1/scoring/compute` directly\n\nAuto-trigger on signal completion is on the v1.5 roadmap.\n"}],"servers":[{"url":"https://api.saber.app","description":"Production server"}],"security":[{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","bearerFormat":"API Key","description":"API key authentication using Bearer token. Format: sk_live_ followed by a secure random string."}},"schemas":{"ComputeScoresRequest":{"type":"object","required":["objectType","objectIds"],"properties":{"objectType":{"type":"string","enum":["company","contact"]},"objectIds":{"type":"array","minItems":1,"maxItems":500,"items":{"type":"string","minLength":1,"maxLength":500},"description":"For `company`, domains; for `contact`, LinkedIn profile URLs.\nCapped at 500 per request — paginate larger lists.\n"}}},"ComputeScoresAcceptedResponse":{"type":"object","description":"Returned by `POST /v1/scoring/compute` with status 202 once recompute\nrecompute jobs have been queued. `failed > 0` means some dispatches\nfailed (logged server-side); the request as a whole is still \"accepted\"\nsince at least one job was queued. If every dispatch fails the endpoint\nreturns 502 instead.\n","required":["queued","failed"],"properties":{"queued":{"type":"integer","minimum":0,"description":"Number of objects whose recompute job was successfully queued."},"failed":{"type":"integer","minimum":0,"description":"Number of objects whose recompute dispatch failed."}}},"ErrorResponse":{"type":"object","description":"Standard error envelope returned by every endpoint that flows through the global error handler. Note: the global rate-limit middleware (HTTP 429 from per-API-key throttling) returns a different, flat shape — see `RateLimitErrorResponse`.\n","required":["error"],"properties":{"error":{"type":"object","required":["type","code","requestId"],"properties":{"type":{"type":"string","description":"Error category. Maps 1:1 to HTTP status (e.g. VALIDATION → 422, UNAUTHORIZED → 401).","enum":["BAD_REQUEST","VALIDATION","UNPROCESSABLE_ENTITY","NOT_FOUND","CONFLICT","UNAUTHORIZED","FORBIDDEN","PAYMENT_REQUIRED","PAYLOAD_TOO_LARGE","INTERNAL","EXTERNAL","TIMEOUT"]},"code":{"type":"string","description":"Stable machine-readable error code. Safe to switch on in client code."},"message":{"type":"string","description":"Human-readable error message. Wording may change; do not match against this string."},"errorCode":{"type":"string","description":"Optional public error code propagated from a downstream service (e.g. NestJS, LinkedIn)."},"errorAction":{"type":"string","description":"Optional user action guidance (e.g. \"reconnect Sales Navigator\")."},"details":{"type":"object","additionalProperties":true,"description":"Optional structured fields forwarded from a downstream service. Shape depends on the source."},"requestId":{"type":"string","format":"uuid","description":"Correlation ID for this request. Include when reporting issues."},"fields":{"type":"array","description":"Per-field validation errors (populated when `type` is `VALIDATION` and the cause is a struct-tag validator).","items":{"type":"object","required":["field","message"],"properties":{"field":{"type":"string"},"message":{"type":"string"}}}},"debug":{"type":"object","description":"Debug information. Only present in development environments.","properties":{"cause":{"type":"string"}}}}}}}}},"paths":{"/v1/scoring/compute":{"post":{"summary":"Trigger score recomputation","description":"Triggers score recomputation for the given objects against the latest\nsignal data. Idempotent — duplicate triggers for the same object are\ndeduplicated automatically.\n\nReturns `202 Accepted` with `{queued, failed}` counts as soon as\ndispatches finish (recomputation runs asynchronously; results become\navailable via `GET /v1/scoring/scores`). A non-zero `failed` means\nsome dispatches failed; if *every* dispatch fails the endpoint\nreturns 502 instead.\n","operationId":"triggerScoreCompute","tags":["Scoring"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComputeScoresRequest"}}}},"responses":{"202":{"description":"Accepted — at least one recompute job was queued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ComputeScoresAcceptedResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"502":{"description":"All recompute dispatches failed.\nError code is `RECOMPUTE_DISPATCH_FAILED`. Safe to retry.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}}}
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://developers.saber.app/scoring.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
