Classify Text Data Using Embeddings API in Node.js: Supplier Invoice Guardrails A developer outlined a Node.js approach to classifying supplier-invoice fields using an embeddings API, matching each field against pre-labeled reference examples via cosine similarity and accepting a label only when it clears both a similarity threshold and a margin over the runner-up, otherwise returning review_required. The writeup emphasizes retention-aware caching of reference embeddings keyed by content digest and label-set version, and an idempotent audit record keyed on document identifier, field name, label-set version, model identifier and normalized-text digest so retries resolve to the same decision. The developer notes that a 0.86 versus 0.84 score split is weakly separated and that a lower score with a larger margin can be safer for a narrow label set. Short answer: use embeddings to classify supplier-invoice fields by comparing the new field with pre-labeled examples, then require both a similarity threshold and a margin over the runner-up before accepting a tag. In a Node.js service, the HTTP call is the easy part. The durable decision is a versioned, reviewable record that can be reconciled later. For a media business, the labels might be invoice number , rights period , territory , currency , and net amount . Suppliers rarely use the same wording. “Term ending March 31” and “licensed window: Q1 2026” can describe the same business field even though a phrase matcher sees little overlap. An embedding turns each text into a vector; cosine similarity supplies an ordering among candidate labels. The acceptance rule should be conservative. Pick the highest-scoring example only when its score clears a threshold and its lead over the second-best score clears a margin. Otherwise, return review required . That single branch protects the ledger-like property that matters here: a questionable classification must remain visible instead of being silently rewritten by the next retry. The dominant operational term is repeated embedding work. Suppose one month contains 20,000 invoice fields and the label set has 12 labels with five reference examples each. The reference corpus is 60 vectors; the changing workload is still 20,000 input embeddings, before retries, reprocessing, or human-review traffic. This is why the first control is retention-aware caching, not a clever similarity loop. The vector is evidence, not a verdict. Cache reference embeddings by a content digest and label-set version. Embed each normalized input once for a classification attempt, and attach the resulting decision to a stable key made from the document identifier, field name, label-set version, model identifier, and normalized-text digest. If the worker retries, the audit write must resolve to the same logical record. A payment backend would call this an exactly-once mindset; the transport itself does not provide exactly-once execution. The practical trap is allowing a retry to write a second row with a new timestamp: downstream reconciliation then sees two plausible labels, and a later operator cannot tell whether the supplier changed the field or the worker repeated it. Enforce uniqueness on the logical key, record the original request identifier, and make a conflict return the existing decision rather than silently applying a second one. Keep the audit record smaller than the source document. It should contain the source document identifier, field name, selected label, top score, runner-up score, threshold, margin, label-set version, model identifier, request identifier, and a digest of normalized text. Retain raw invoice text only according to the applicable policy. This is an intentional trade-off: after raw text expires, the digest and scores can explain which decision was made, but they cannot recreate the supplier sentence. If reconstruction is mandatory, retain an encrypted source reference under the organization’s retention rules. The cost-estimate route is useful when planning bulk inputs, but it does not replace an operational ledger. Count reference refreshes, retries, and review traffic as well as first-pass inputs. Cost is a control signal; classification quality and auditability remain the decision criteria. Cosine similarity ranks candidates; it does not establish that the winning candidate is correct. A rights period score of 0.86 against an invoice date score of 0.84 is a weakly separated result, even though 0.86 looks precise. A lower score with a large margin can be safer for a narrow, well-evaluated label set. Build an evaluation set with positive examples, neighboring labels, incomplete fields, supplier-specific wording, and malformed input. Version it with the label descriptions. When a supplier changes its template, add examples and run the same set again. Do not move a production threshold because one batch looked plausible; that is how an audit trail becomes a history of unrecorded policy changes. { "field": "term ending", "label": "rights period", "similarity": 0.86, "runner up similarity": 0.84, "decision": "review required", "label set version": "media-invoice-2026-04" } The record is deliberately plain. A repeated request with the same source digest and label-set version should resolve to the same decision; a changed model or label set should produce a new attributable result rather than mutate history. The openai npm package or a direct HTTPS request can call /v1/embeddings . The application still owns normalization, reference-data versioning, similarity, thresholds, review routing, and persistence. A provider client should not decide whether a media invoice field is safe to post. The following Go code contains the classification core. It is intentionally independent of a provider response shape, so a Node.js worker can use the same state transition after decoding its embedding response. Dimension mismatches, empty vectors, and empty fields become reviewable errors rather than accidental labels. package classifier import "crypto/sha256" "encoding/hex" "errors" "math" "strings" type Example struct { Label string Vector float64 } func cosine a, b float64 float64, error { if len a == 0 || len a = len b { return 0, errors.New "embedding dimensions do not match" } var dot, aa, bb float64 for i := range a { dot += a i b i aa += a i a i bb += b i b i } if aa == 0 || bb == 0 { return 0, errors.New "zero vector" } return dot / math.Sqrt aa math.Sqrt bb , nil } func classify text string, input float64, examples Example, threshold, margin float64 string, float64, string, string, error { normalized := strings.TrimSpace text if normalized == "" { return "", 0, "review required", "", errors.New "empty field" } best, second, label := -1.0, -1.0, "" for , example := range examples { score, err := cosine input, example.Vector if err = nil { return "", 0, "review required", "", err } if score best { second, best, label = best, score, example.Label } else if score second { second = score } } decision := "review required" if best = threshold && best-second = margin { decision = "accepted" } digest := sha256.Sum256 byte normalized return label, best, decision, hex.EncodeToString digest : , nil } For a larger label set, k-means or a FAISS-backed index can reduce comparisons. That optimization changes the failure surface: a nearest centroid is not automatically a business label, and an index refresh must be tied to a label-set version. For a dozen invoice fields, a linear scan is easier to inspect and can be the better quality-latency trade. The network boundary should make failure explicit. The key belongs in an environment variable, the method must be POST , non-success responses must expose their bodies, and a 429 response should honor Retry-After while applying exponential backoff. The audit write, which is a state-changing operation even if the embedding request is not, needs an idempotency key enforced by the persistence layer. package embeddings import "bytes" "fmt" "io" "net/http" "os" "strconv" "time" func Request body byte, logicalID string byte, error { key := os.Getenv "INFRAI API KEY" if key == "" { return nil, fmt.Errorf "INFRAI API KEY is required" } for attempt := 0; attempt < 3; attempt++ { baseURL := os.Getenv "EMBEDDINGS BASE URL" if baseURL == "" { return nil, fmt.Errorf "EMBEDDINGS BASE URL is required" } req, err := http.NewRequest "POST", baseURL+"/v1/embeddings", bytes.NewReader body if err = nil { return nil, err } req.Header.Set "Authorization", "Bearer "+key req.Header.Set "Content-Type", "application/json" req.Header.Set "Idempotency-Key", logicalID resp, err := http.DefaultClient.Do req if err = nil { return nil, err } data, readErr := io.ReadAll resp.Body resp.Body.Close if readErr = nil { return nil, readErr } if resp.StatusCode = 200 && resp.StatusCode < 300 { return data, nil } if resp.StatusCode = http.StatusTooManyRequests { return nil, fmt.Errorf "embeddings: HTTP %d: %s", resp.StatusCode, data } delay := time.Duration 1<