Walker Formulation Academy for developers API reference Error catalog OpenAPI
Sections

#Lead scoring

Status: reading the score in the lead object (GET /leads) is Available; triggering and recording a score through the API is Planned; scoring inside the platform is available. A lead is already scored inside the platform; you cannot trigger scoring or record your own through the API. The grade class and persistence of reason are absent from the current implementation: they will appear together with the public API (see "Notes"). No dates are promised.

Scoring answers the question "how much does this lead look like a buyer". The model reads the recent conversation and assigns a number from 0 to 100; in parallel, a separate analysis sets flags (wants to buy, writes to support, is a supplier). The score helps a manager prioritize and lets automation filter out off-target inquiries.

Access: leads:read to read the score as part of a lead; leads:score to request scoring by our model; leads:write to record the external CRM's score. Paid: POST /leads/{id}/score: yes, charged to the tenant's wallet; PUT /leads/{id}/score: no. PII: not in the response itself; the score is built from a conversation with PII masking.

#What you get

FieldTypeDescription
scoreintegerA score from 0 to 100
gradestringThe lead's class, computed from score: A is 75 and above, B is 50-74, C is 25-49, D is below 25
reasonstringA short rationale, up to 500 characters. Stored together with the score
flagsobjectThe flags buy_intent, support, supplier
modelstringThe model that produced the score
scored_atstring (ISO 8601)When it was produced
sourcestringThe source of the score: ai is our model, external is the external CRM, manual is a manual score in the dashboard

#Flags

Each flag takes one of three values: true, false, null.

FlagMeaning
buy_intentThe person wants to buy
supportThis is a support inquiry, not a new customer
supplierThis is a supplier or a mailing, not a customer

null means "not sure". The analysis yields a probability, and a flag is set only when it is at least 0.8 (true) or at most 0.2 (false); between those values there is no flag. This band was chosen because it is where the analysis's accuracy was measured.

#How the score affects a lead

#What the model sees

The model receives the last 10 messages of the conversation, without staff's internal notes. Before being sent to the model, personal data is removed from the text: phone numbers, emails, card numbers, messenger accounts, passport numbers, SNILS, INN, names, and addresses are replaced with placeholders. If a lead has no conversation or the conversation has no messages, scoring is not possible.

Model providers receive the processed text. More: PII transfer.

#When a lead is scored automatically

Automatic scoring is triggered in these cases:

For leads created through POST /leads without a conversation, automatic scoring is unavailable; such leads can be scored only with an external score (PUT).

#POST /leads/{id}/score

Triggers scoring by our model.

Headers: Authorization, Idempotency-Key (required: a paid operation). Parameters: no body is needed.

The operation runs synchronously, within seconds (the model has a 20-second timeout), and is not wrapped in an operation envelope (Conventions). A repeated request with the same Idempotency-Key does not charge money a second time.

200 OK response:

{
  "lead_id": "5d1b2c7e-3f0a-4a8e-9a3b-1c2d3e4f5a6b",
  "score": 72,
  "grade": "B",
  "reason": "Клиент уточнил сроки старта и спросил о рассрочке.",
  "flags": { "buy_intent": true, "support": false, "supplier": false },
  "model": "model-EXAMPLE",
  "scored_at": "2026-10-11T09:45:12Z",
  "source": "ai",
  "usage": {
    "cost": { "amount": "0.12", "currency": "RUB" },
    "model": "model-EXAMPLE"
  }
}

The usage field follows the "money is visible" principle: the operation's cost is returned in the response as a string, in the currency of the tenant's wallet. Test keys do not charge money. The amount of the cost is not fixed in the design.

Possible failures. The internal reasons of today's scoring are mapped to codes in the catalog; the mapping is preliminary.

ReasonCodeHTTP
lead_not_foundnot_found404
conversation_not_found, no_messagesinvalid_state409
no_keyinsufficient_funds402
model_failed, output_rejected, parse_failedupstream_unavailable503
write_failedservice_unavailable503

Exceeding the spending limit, the limit of concurrent paid operations (5 per tenant), or the maximum cost of a single operation (the equivalent of 100 RUB) returns 402 spend_limit_reached. Retry 503 errors with the same Idempotency-Key.

#Examples

curl -X POST https://formula-cream.pro/api/public/v1/leads/$LEAD_ID/score \
  -H "Authorization: Bearer $API_KEY" \
  -H "Idempotency-Key: 9b2d7a54-1f3c-4e8a-b6d0-5c7a2e9f1b30"
# API_KEY=sap_ru_test_EXAMPLE0KEYID_… (an example, not a real key)
const res = await fetch(
  `https://formula-cream.pro/api/public/v1/leads/${leadId}/score`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.API_KEY}`,
      "Idempotency-Key": crypto.randomUUID(),
    },
  },
);
if (res.status === 402) throw new Error("Top up the wallet");
const result = await res.json();
r = httpx.post(
    f"https://formula-cream.pro/api/public/v1/leads/{lead_id}/score",
    headers={"Authorization": f"Bearer {os.environ['API_KEY']}",
             "Idempotency-Key": str(uuid.uuid4())},
    timeout=30,
)
result = r.json()
$ch = curl_init("https://formula-cream.pro/api/public/v1/leads/$leadId/score");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("API_KEY"),
        "Idempotency-Key: " . bin2hex(random_bytes(16)),
    ],
]);
$result = json_decode(curl_exec($ch), true);

After a successful scoring, the lead.scored event is sent to subscribers.

#PUT /leads/{id}/score

Records your score (the external CRM). It is stored next to ours and does not replace it: in the field ownership table (see modes), the external score is called external_score. The scope is leads:write (not leads:score): writing does not charge the wallet. PUT is idempotent by nature; the Idempotency-Key header is accepted and ignored.

ParameterTypeRequiredConstraintsExample
scoreintegeryes0-10085
reasonstringnoUp to 500 characters"Repeat customer"
modelstringnoA label for the score's source"crm-scoring-v3"

Requires If-Match, like any lead change (428 precondition_required without the header, 412 precondition_failed for a stale version). The 200 OK response is a score object with source: "external" and a computed grade. A value outside 0-100 returns 422 validation_failed (errors[].field = /score).

curl -X PUT https://formula-cream.pro/api/public/v1/leads/$LEAD_ID/score \
  -H "Authorization: Bearer $API_KEY" -H "If-Match: 3" \
  -H "Content-Type: application/json" \
  -d '{"score":85,"reason":"Повторный клиент"}'

#Events

EventWhen
lead.scoredThe lead was scored (by our model or by an external score)

The payload is thin: the lead ID, the version, and the type. Fetch the score itself with GET /leads/{id}; a subscription with include_pii: true delivers the content in the event. Webhooks: the webhooks section.

#Notes

#Open questions