Reference

API reference

The clients, their methods and result objects — and the REST endpoints underneath them. Anything the SDKs do, plain HTTP does too.

Clients

ClientMethods
EggAIThe umbrella: .offers, .leads, .surveys, .assortment, plus score(rows), usage(month?), health(), job(id), jobs(product, status?).
OfferOptimizercheapest_accepted / cheapestAccepted — single customer or rows batch; objective "saving"|"winback", guidelines, constraints, idempotency_key, wait, on_progress. → OfferTerms | OfferRun | Job.
LeadGeneratorrank(customers, offer=…)guidelines, tailor_offer/tailorOffer, threshold (0.05–0.95), max_leads/maxLeads, constraints, idempotency_key, wait. → RankedLeads | Job.
SurveySimulator preview ask(question, options?)panel or customers, n, guidelines, wait. → SurveyResult | Job.
AssortmentOptimizerimpact(panel, scenario=…)Impact | Job; preview(panel, scenario=…) → sampled Impact, synchronous, ≤64 rows.
Jobstatus, progress, refresh(), wait(poll?, timeout?, on_progress?), stream(), events(after?), result_rows()/resultRows(), stop().

Constructors take a key string, or options: api_key/apiKey, base_url/baseUrl (default https://api.eggai.tech), timeout, max_retries/maxRetries. With no key, the EGGAI_API_KEY environment variable is read.

Result objects

ObjectFields
OfferTermscustomer, accepted, text, tagline, terms, changed, objection, reason, p, base_text, steps, raw
OfferRunIterable of OfferTerms; accepted; lookup by id (run["c_1"] / run.get('c_1'))
Leadid, rank, score, reason, offer, qualified, raw
RankedLeadsIterable in rank order; above(score), qualified, threshold
SurveyResultdistribution, transcripts, crosstab(trait), n, rows
Impactretained, counts, affected, watchlist, rows, sweep, sampled
ImpactRowid, outcome, p, substitute, spend_frac/spendFrac, reason, affected, segment, store_type/storeType, audience, spend columns, raw

REST: the basics

Base URL        https://api.eggai.tech
Auth            Authorization: Bearer <workspace API key>
Bodies          JSON. Errors: {"detail": {"message": "…"}}
Times           unix seconds

Keys are issued in the EGGai app (Settings → API keys). Practice keys authenticate identically but cap rows per job. Job creation is rate-limited (10 per 10 minutes per workspace) and open jobs per workspace are capped — a 429 tells you which.

Create jobs

POST/v1/offer-optimization/jobs
{
  "rows": [{"id": "c_8813", "profile": "…",
            "offer": "15% off produce, min 500"}],   // free text, or structured:
                                                     // {depth, min_spend, category, validity, subject}
  "objective": "saving",                             // or "winback"
  "guidelines": "discount caps at 12%",
  "constraints": {"max_depth": 12, "min_depth": 0,
                  "min_spend_min": 300, "min_spend_max": 800},
  "idempotency_key": "campaign-2026-08"
}
→ 201 {"job_id": "ooj_…", "status": "queued"}
POST/v1/lead-scoring/jobs
{
  "rows": [{"id": "c_8813", "profile": "…", "offer": "15% off produce"}],
  "guidelines": "hold the 15%",
  "personalize": true,             // the SDKs' tailor_offer switch
  "threshold": 0.5,                // 0.05–0.95
  "max_leads": 400,
  "constraints": null,
  "idempotency_key": null
}
→ 201 {"job_id": "lsj_…", "status": "queued"}
POST/v1/assortment/jobs
{
  "rows": [{"id": "c_1", "profile": "…", "audience": "buyer",
            "segment": "family", "store_type": "upmarket",
            "category_spend": 610, "total_spend": 7900,
            "current_sku": "Tropicana Orange 1L"}],
  "scenario": {"kind": "delist",              // delist | introduce | reset
               "category": "chilled juice",
               "change": "Tropicana Orange 1L 119",
               "remaining": "own-label 1L 79 · Malee 1L 95",
               // reset only:
               "lines_ranked": ["…"], "protected": ["…"],
               "cut_depth": 30, "sweep": [20, 40]},
  "idempotency_key": null
}
→ 201 {"job_id": "asj_…", "status": "queued"}

The same idempotency_key on the same product returns the existing job with "idempotent_replay": true instead of creating a second one.

Job lifecycle

GET/v1/jobs/{job_id}
→ {"job_id", "kind", "status",       // queued|running|done|stopped|failed
   "n_rows", "answered_rows", "output_rows",
   "created_at", "finished_at",
   "progress": {"pct", "stage", "completed", "total",
                "elapsed_s", "eta_s"},   // while running
   "fail": {"title", "detail"},          // when failed/stopped
   "sweep": [...]}                       // assortment reset sweeps
GET/v1/offer-optimization/jobs · /v1/lead-scoring/jobs · /v1/assortment/jobs
?status=running          // optional filter
→ {"jobs": [summary, …]}  // newest first
GET/v1/jobs/{job_id}/events?after=SEQ
→ {"events": [{"type": "progress"|"live"|"twin"|"done"|"stopped"|"error",
               …, "_seq": n}]}   // poll with after=last _seq
GET/v1/jobs/{job_id}/stream
→ text/event-stream       // same events as SSE; snapshot first, keepalives
GET/v1/jobs/{job_id}/results?cursor=0&limit=500
→ {"rows": [...], "next_cursor": 500}   // 409 until status=="done"; limit ≤ 1000
POST/v1/jobs/{job_id}/stop
→ {"ok": true}            // idempotent

Result rows

Offer (and lead — leads add rank and qualified):

{"uid", "who", "basket",
 "base":  {depth, min_spend, category, validity, subject},
 "final": {…} | null,
 "p_base", "p_final", "accepted_base", "accepted",
 "converted",                // an accepted final exists
 "quote_base", "quote",      // the twin's words, before and after
 "steps": [{terms, p, quote, note, change, ok}]}

Assortment:

{"uid", "who", "audience", "segment", "store_type",
 "category_spend", "total_spend", "current_sku",
 "buys_today", "affected", "habit",
 "outcome",      // switches | spends_less | leaves_category | leaves_store |
                 // unaffected | adopts | tries | ignores | enters_category
 "p", "substitute", "spend_frac", "quote"}

Synchronous helpers

POST/v1/score
{"rows": [{"id", "profile", "offer"}]}    // ≤ 64 rows
→ {"p": [0.22, …]}                        // probabilities only, no rationale
POST/v1/assortment/preview
{"rows": [...], "scenario": {...}}        // ≤ 64 rows, nothing persisted
→ {"sampled", "aggregate": {"retained", "affected", "counts"},
   "rows": [{"id", "affected", "outcome", "p",
             "substitute", "spend_frac", "reason"}]}

Both are rate-limited at 60 calls per minute.

Account

GET/v1/usage?month=2026-08
→ {"workspace_id", "month",
   "billable_units": {"lead": n, "offer": n, …},
   "jobs": [per-job usage records, newest first]}
GET/v1/health
→ {"status": "ok", "engine": {…}}         // no auth required

Survey (preview)

POST/v1/survey/jobs preview
{"question": "…", "options": ["…"] | null,
 "panel": "…", "n": 150,                  // or "rows": [{id, profile}]
 "guidelines": "…"}

Enabled per workspace while in preview. Where it is not enabled, the route 404s — the SDKs turn that into FeatureNotEnabledError.

Errors

HTTPSDK exceptionMeaning
401AuthenticationErrorMissing or unknown key.
404NotFoundErrorNo such job or route (survey routes → FeatureNotEnabledError).
409InvalidRequestErrorResults requested before done.
422InvalidRequestErrorBad rows, over the row cap, field out of range — detail.message says which.
429RateLimitErrorToo many calls or open jobs.
5xxAPIErrorThe engine or API failed; reads are retried with backoff before this surfaces.
JobFailedErrorThe job itself finished failed or stopped; carries the job summary.

Every error body is {"detail": {"message": "…"}} and the SDK exceptions carry that message verbatim.