API reference
The clients, their methods and result objects — and the REST endpoints underneath them. Anything the SDKs do, plain HTTP does too.
Clients
| Client | Methods |
|---|---|
EggAI | The umbrella: .offers, .leads,
.surveys, .assortment, plus score(rows),
usage(month?), health(), job(id),
jobs(product, status?). |
OfferOptimizer | cheapest_accepted /
cheapestAccepted — single customer or rows batch; objective
"saving"|"winback", guidelines, constraints,
idempotency_key, wait, on_progress. → OfferTerms
| OfferRun | Job. |
LeadGenerator | rank(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. |
AssortmentOptimizer | impact(panel, scenario=…) →
Impact | Job; preview(panel, scenario=…) → sampled
Impact, synchronous, ≤64 rows. |
Job | status, 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
| Object | Fields |
|---|---|
OfferTerms | customer, accepted,
text, tagline, terms, changed,
objection, reason, p, base_text,
steps, raw |
OfferRun | Iterable of OfferTerms; accepted;
lookup by id (run["c_1"] / run.get('c_1')) |
Lead | id, rank, score,
reason, offer, qualified, raw |
RankedLeads | Iterable in rank order; above(score),
qualified, threshold |
SurveyResult | distribution, transcripts,
crosstab(trait), n, rows |
Impact | retained, counts,
affected, watchlist, rows, sweep,
sampled |
ImpactRow | id, 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
{
"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"}
{
"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"}
{
"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
→ {"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
?status=running // optional filter → {"jobs": [summary, …]} // newest first
→ {"events": [{"type": "progress"|"live"|"twin"|"done"|"stopped"|"error",
…, "_seq": n}]} // poll with after=last _seq
→ text/event-stream // same events as SSE; snapshot first, keepalives
→ {"rows": [...], "next_cursor": 500} // 409 until status=="done"; limit ≤ 1000
→ {"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
{"rows": [{"id", "profile", "offer"}]} // ≤ 64 rows
→ {"p": [0.22, …]} // probabilities only, no rationale
{"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
→ {"workspace_id", "month",
"billable_units": {"lead": n, "offer": n, …},
"jobs": [per-job usage records, newest first]}
→ {"status": "ok", "engine": {…}} // no auth required
Survey (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
| HTTP | SDK exception | Meaning |
|---|---|---|
| 401 | AuthenticationError | Missing or unknown key. |
| 404 | NotFoundError | No such job or route (survey routes →
FeatureNotEnabledError). |
| 409 | InvalidRequestError | Results requested before
done. |
| 422 | InvalidRequestError | Bad rows, over the row cap, field out
of range — detail.message says which. |
| 429 | RateLimitError | Too many calls or open jobs. |
| 5xx | APIError | The engine or API failed; reads are retried with backoff before this surfaces. |
| — | JobFailedError | The job itself finished failed or
stopped; carries the job summary. |
Every error body is {"detail": {"message": "…"}} and the SDK exceptions carry that
message verbatim.