2026 LLM API JSON Sales Call Summaries with Bullets and CRM Action Items A developer building a one-person gaming SaaS has implemented a system that generates LLM sales-call summaries as validated JSON, enforced by a Node.js API, with per-tenant cost metadata attached. The approach uses a strict schema to keep output structured and predictable, and validates responses server-side to treat model output as untrusted input. The developer also recommends counting tokens before generation and splitting long transcripts at speaker-turn boundaries to fit model limits. A one-person gaming SaaS needs each LLM sales-call summary to arrive as structured output: a JSON schema enforced by its Node.js API, with tenant costs attached instead of buried in one monthly total. Short answer: generate each sales-call summary as validated JSON, attach the tenant ID to the returned per-call cost metadata, and write CRM actions only after the contract passes validation. That choice makes the output dull in the best way. A title goes in the title field. Bullets render as bullets. Action items become records instead of prose somebody has to read again. It also keeps the expensive part of the request visible per tenant, which matters when one studio uploads ten calls and another uploads ten thousand. Start at the consumer. The CRM needs a compact title, an overview, a few factual bullets, risks, and action items with owners. It does not need a persuasive paragraph. If the model returns markdown, every downstream destination invents its own parser: the dashboard splits lines, the email job strips markers, and the webhook tries to guess whether “Alex to send the build” is a task or a note. Use one contract instead: type CallSummary = { title: string; overview: string; bullets: string ; risks: string ; action items: Array<{ task: string; owner: string | null; } ; }; This contract is deliberately modest. Due dates, confidence scores, account sentiment, and deal stages sound useful, but each new field creates another promise to the UI and CRM. Ship the fields the product can act on this week. Add another only when it earns its place. The schema is also a trust boundary. A model response is untrusted input even when it is valid JSON. Validate the object on the server, reject extra shapes, and never let an absent action items array silently turn into “no follow-up required.” A missing field and an empty field mean different things. That distinction is easy to lose. For long transcripts, count the prompt, schema, and source text before generation with POST /v1/ai/tokens/count . The exact request shape should come from discovery rather than a copied blog snippet. If the complete input does not fit the selected model's limit, split at speaker-turn boundaries and summarize chunks before producing the final contract. Character slicing is a poor token counter, especially across languages. The example below uses the OpenAI client against an OpenAI-compatible chat surface. It sends a strict schema-shaped instruction, checks the returned JSON without assuming it is correct, retries rate limits with backoff, and retries validation once with a shorter transcript. It is intentionally a single file; separating transport, validation, and persistence is worthwhile after the flow survives real traffic. Install the two runtime dependencies with npm install openai zod . Set INFRAI API KEY and INFRAI BASE URL in the process environment before running the file. python import OpenAI from "openai"; import { z } from "zod"; const SummarySchema = z.object { title: z.string .min 1 , overview: z.string .min 1 , bullets: z.array z.string .min 1 , risks: z.array z.string .min 1 , action items: z.array z.object { task: z.string .min 1 , owner: z.string .min 1 .nullable , } , , } .strict ; type CallSummary = z.infer