Walker Formulation Academy for developers API reference Error catalog OpenAPI
Sections

#Agent actions and action requests

Status: Planned. Action requests (lead.action_requested) and the reply to them (ack) are not implemented on the platform. Today, agents write to our CRM directly. This page describes the intent; the action parameters are preliminary. No dates are promised.

Several AI agents work in the platform's CRM. They do not just read leads, they change them: they set a score, move a deal, assign tasks, and send reminders to a customer. When you have an external CRM and it is the source of truth (the external mode, after a lead is accepted), agents must not change a lead silently. Instead, the agent files an action request; you receive it by webhook and decide: carry it out or decline.

Access: the lead.action_requested webhook (an integration subscription, webhooks:manage); leads:write to reply. Paid: no. PII: the event payload is thin; the reason and evidence are requested through the API unless the subscription has the include_pii flag enabled (PII transfer).

#Who the agents are

AgentWhat it does with a lead
AI sellerConducts the conversation, creates a lead, sets a reminder (an "AI" type task), asks to bring in an operator
CRM linkerAfter each reply from the seller, refines the contact data (email, phone, tags), links or creates a deal, sets the intent and the lead's extracted data, rescores
Automations (tenant rules)Move a deal through the funnel, set tags, score a lead, assign an operator, set priority, close a conversation
NudgeReminds a cooled-off lead or a customer who did not answer an offer
StrategistOnly reads funnel counters, writes nothing

#How it works

The agent decides on an action
        |
        v
internal mode ----------> the action runs on our side, lead.updated event
both mode --------------> runs on our side, fields per the ownership table
external mode, accepted -> action request (lead.action_requested)
                                   |
                                   v
                 Your CRM: ack = accept  -> the action runs, lead.updated event
                           ack = reject  -> the action does not run, the agent takes the refusal into account
                           no reply in time -> the request expires (expired)

Before a lead is accepted (the external mode), actions run on our side, as in the internal mode: the lead is still ours.

#Request states

StateMeaning
requestedThe request is created, no reply yet
ackedAccepted by your CRM
rejectedDeclined by your CRM
expiredThe time ran out without a reply, or the tenant switched the mode from external to internal (CRM: modes)

#Action types

TypeWhat the agent asks for
Change statusMove the lead to a canonical status (for example, qualified) or mark a loss with a reason
AssignAssign a responsible staff member
Create a taskCreate a task on the lead
Send a nudgeSend a reminder to the customer
Hand off to an operatorBring a human into the conversation

A notification about the handoff of the conversation to an operator itself arrives as a separate event, conversation.handoff_requested (with lead_id if the conversation has a lead); there is no lead.handoff_requested event.

Every request carries a reason (why the agent decided so) and evidence (what it relied on: a phrase from the conversation, the length of silence, a score), so that a person in your CRM can make a well-informed decision.

#Request object

A preliminary schema; the final one is fixed at release.

FieldTypeDescription
action_idstring (uuid)The request ID
lead_idstring (uuid)The lead
lead_versionintegerThe lead's version at the time of the request
typestringThe action type (the table above)
paramsobjectThe action parameters
reasonstringThe reason
evidencearrayEvidence: short descriptions and links to the source
agentstringWhich agent is asking
statestringrequested, acked, rejected, expired
requested_at, expires_atstring (ISO 8601), UTCCreated, and the reply deadline

params examples:

TypeParameters
Change status{"status": "qualified"} or {"status": "lost", "lost_reason": "..."}
Assign{"assignee_id": "<staff member uuid>"}
Create a task{"title": "...", "due_at": "..."}
Send a nudge{"channel": "telegram", "text": "..."}
Hand off to an operator{}

#The lead.action_requested event

It arrives at the subscribed webhook address in the v2 envelope. By default the payload is "thin" (ID, version, type).

{
  "id": "evt_EXAMPLE02",
  "type": "lead.action_requested",
  "api_version": "2026-10-01",
  "created_at": "2026-10-11T10:02:41Z",
  "livemode": true,
  "data": {
    "lead_id": "5d1b2c7e-3f0a-4a8e-9a3b-1c2d3e4f5a6b",
    "action_id": "a3d2f1c0-8b7e-4c55-9d2a-6e1f0b9c7d34",
    "lead_version": 4
  }
}

The signature in the Webhook-Signature header (JWS, Ed25519), delivery retries, and deduplication by Webhook-Event-Id are in the webhooks section. Get the content of the request with GET /leads/{id} or, with include_pii: true, right in the event; a separate endpoint for reading a request is not named in the design (see "Open questions").

#POST /leads/{id}/actions/{action_id}/ack

A reply to a request: accept or decline.

Scope: leads:write. Headers: Idempotency-Key (recommended), Content-Type: application/json.

ParameterTypeRequiredConstraintsExample
decisionstringyesaccept or reject"accept"
reasonstringnoThe reason for a refusal; visible to the agent and in the log"The lead is already being handled by a manager"

With accept, you report that the action was applied (or will be applied) on your side. The platform reflects the result in the lead's projection.

200 OK response: the request with its new state.

{
  "action_id": "a3d2f1c0-8b7e-4c55-9d2a-6e1f0b9c7d34",
  "lead_id": "5d1b2c7e-3f0a-4a8e-9a3b-1c2d3e4f5a6b",
  "state": "acked"
}
SituationResponse
The request is already closed (acked, rejected, expired)409 invalid_state
No decision, or the value is not accept/reject422 validation_failed (errors[].field = /decision)
Someone else's lead or request404 not_found
A repeat with the same Idempotency-Key and bodyThe same response
The same Idempotency-Key, a different body409 idempotency_conflict

Other errors: the error code catalog.

#Examples

curl -X POST \
  https://formula-cream.pro/api/public/v1/leads/$LEAD_ID/actions/$ACTION_ID/ack \
  -H "Authorization: Bearer $API_KEY" \
  -H "Idempotency-Key: 4f6c1b9a-0d7e-4a23-8e5f-2b3c9d1a7e60" \
  -H "Content-Type: application/json" \
  -d '{"decision":"accept"}'
# API_KEY=sap_ru_test_EXAMPLE0KEYID_… (an example, not a real key)
await fetch(
  `https://formula-cream.pro/api/public/v1/leads/${leadId}/actions/${actionId}/ack`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.API_KEY}`,
      "Idempotency-Key": crypto.randomUUID(),
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ decision: "reject", reason: "Лид уже в работе" }),
  },
);
httpx.post(
    f"https://formula-cream.pro/api/public/v1/leads/{lead_id}/actions/{action_id}/ack",
    headers={"Authorization": f"Bearer {os.environ['API_KEY']}",
             "Idempotency-Key": str(uuid.uuid4())},
    json={"decision": "accept"},
)
$ch = curl_init("https://formula-cream.pro/api/public/v1/leads/$leadId/actions/$actionId/ack");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("API_KEY"),
        "Idempotency-Key: " . bin2hex(random_bytes(16)),
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS => json_encode(["decision" => "accept"]),
]);

#An example handler in an external CRM

The handler verifies the signature, takes the event, reads the lead, applies the action on its side, and replies to the platform. For signature verification and replay protection, see the webhooks section.

// An Express-style handler, a sketch. The signature is verified before the body is parsed.
app.post("/webhooks/platform", express.raw({ type: "application/json" }), async (req, res) => {
  if (!verifyWebhookSignature(req.headers["webhook-signature"], req.body, WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }
  res.sendStatus(200); // acknowledge receipt quickly, do the work afterwards

  const event = JSON.parse(req.body);
  if (event.type !== "lead.action_requested") return;
  if (await alreadyProcessed(event.id)) return; // delivery is retried

  const { lead_id, action_id } = event.data;
  const lead = await api(`/leads/${lead_id}`); // a GET with the key
  const decision = await applyInCrm(lead, action_id); // your logic
  await api(`/leads/${lead_id}/actions/${action_id}/ack`, "POST", {
    decision: decision.ok ? "accept" : "reject",
    reason: decision.reason,
  });
});

verifyWebhookSignature, alreadyProcessed, applyInCrm, and api are functions of your system; here they show the order of steps.

#Sending a nudge

If a "send a nudge" action is requested, the mode and the tenant's settings decide:

The reason is that the text of our school is not suitable for a tenant's customer in any language.

#Open questions