API Reference

Oracle Simulation API

Generate a complete synthetic respondent-level dataset: submit a survey and quota groups, and the Oracle simulates each respondent answering every question.

Overview

The Simulation API generates a complete synthetic respondent-level dataset. You define who the respondents are (quota groups — target-group cells with explicit counts) and what they answer (a full survey), and the Oracle generates a row per respondent with their demographics and their individual answer to every question. The answer data is returned to you directly — there are no redirect links or external survey tools to reconcile.

Because a run can involve thousands of respondents across up to 30 questions, simulation is an asynchronous job. The lifecycle is:

  1. Create — submit the survey and quota groups; receive a job id.
  2. Track — poll the job, or receive a webhook when it finishes.
  3. Retrieve — pull the respondent-level results.

Base URL: https://app.simsurveys.com/api/v1

Authentication

All requests are authenticated with a Bearer token. Create and manage API keys from the API Dashboard.

Authorization: Bearer sk_live_abc123...
  • Keys are 72 characters long and begin with sk_live_.
  • Keys are shown once at creation and cannot be retrieved again.
  • The key must carry the oracle:simulate scope — select it when creating the key in the dashboard (or contact us to enable Oracle access). A missing, invalid, or expired key returns 401; a key that lacks the scope returns 403.
  • Keep keys server-side. An sk_live_ key is a sensitive credential — never embed it in browser or mobile-client code. The API is called server-to-server and does not support cross-origin (CORS) browser requests.
  • There is no sandbox or test mode — every simulation is billable. Validate integrations against small jobs.

Rate Limiting

Each API key has a configurable rate limit (default 60 requests per minute, range 1–1,000). Every response includes X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers (X-RateLimit-Reset is a Unix epoch-seconds timestamp). Creating a simulation counts as a single request, regardless of respondent count.

When the limit is exceeded, the API returns 429 Too Many Requests with a Retry-After header (in seconds) and the standard typed error envelope:

{
  "error": {
    "type": "rate_limit_error",
    "message": "Rate limit exceeded. Try again later."
  }
}

Job Lifecycle

A simulation job moves through the following statuses:

StatusMeaning
queuedAccepted and waiting to start
processingRespondents are being generated
completedFinished — results are available
failedThe job could not complete; see the error object

Jobs run to completion — there is no cancel operation once a job is created. Simulation is atomic: if any quota group fails to generate, the entire job transitions to failed and no partial results are returned. Once completed, a job's results are retained indefinitely and remain retrievable.

Create a Simulation

POST /api/v1/oracle/simulate

Creates a simulation job and returns immediately with a job id and status: "queued".

Idempotency: because a job is billable, pass an Idempotency-Key header (any unique string) to make retries safe. A replay with the same key returns 200 with the original job at its current status, not a duplicate. Reusing a key with a different request body returns 409 with a conflict_error. A key is bound only once a job is created (2xx) — a request that fails validation does not consume it, so you can retry with a corrected body. Keys are honored for 24 hours.

Request Body

FieldTypeRequiredDescription
model_type string Yes One of: consumer, hcp, social, patient
quota_groups array Yes One or more demographic cells, each with an explicit respondent count. Total sample size N = the sum of all counts (up to 5,000)
survey object Yes The survey instrument. Contains questions (1–30 items)
context string No Study context applied to the whole run. Consumer and patient models only.
webhook_url string No HTTPS URL notified when the job reaches completed or failed. See Webhooks. If omitted, poll for status instead.

Quota Group Object (quota_groups[])

FieldTypeRequiredDescription
count integer Yes Number of respondents to generate for this cell (≥ 1)
demographics object Yes* The demographic profile for this cell (free-form key-value pairs; see the Demographics reference). Consumer, social, and patient models.
medical_specialty string Yes* The cell's specialty — one of the 20 supported specialties. HCP model only (replaces demographics).

* Each group carries the targeting fields appropriate to the chosen model_typedemographics for consumer/social/patient, medical_specialty for HCP.

Patient model — conditioning on a diagnosis: for the patient model, include a demographics key named exactly "Patient Condition" (title case, with a space) in a quota group to ground that cell in a specific condition, e.g. { "count": 100, "demographics": { "age": "55-64", "Patient Condition": "Type 2 Diabetes" } }. This key is case- and spelling-sensitive: because demographics are free-form, a mistyped key (e.g. patient_condition) is silently treated as an ordinary demographic label rather than a diagnosis, with no error.

Question Object (survey.questions[])

FieldTypeRequiredApplies toDescription
idstringYesall Client-defined identifier, unique within the survey; used as the key for this question's answer in each respondent's responses. Duplicate ids are rejected with a validation_error.
typestringYesall One of: single_select, multi_select, grid, numeric, ranking, text
textstringYesall The question wording (max 5,000 characters)
optionsstring[]Yessingle_select, multi_select, ranking, numeric 2–20 answer options. For numeric, each option is a numeric value or range treated as a single choice (e.g. "0", "1-2", "24").
rowsstring[]Yesgrid The row items being rated (2–20)
columnsstring[]Yesgrid The scale/columns each row is rated against (2–20)
max_selectionsintegerNomulti_select Maximum options a respondent may select. Defaults to the number of options (no cap). Respondents always select at least one.

Example Request

curl -X POST https://app.simsurveys.com/api/v1/oracle/simulate \
  -H "Authorization: Bearer sk_live_your_key_here" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7f3a9c1e-2b4d-4a10-9c8e-1f2a3b4c5d6e" \
  -d '{
    "model_type": "consumer",
    "quota_groups": [
      { "count": 150, "demographics": { "age": "25-34", "gender": "Female" } },
      { "count": 150, "demographics": { "age": "35-44", "gender": "Male" } }
    ],
    "survey": {
      "questions": [
        { "id": "q1", "type": "single_select", "text": "Which brand do you prefer?", "options": ["Nike", "Adidas", "New Balance", "Puma"] },
        { "id": "q2", "type": "multi_select", "text": "What matters when buying?", "options": ["Price", "Comfort", "Style", "Brand"], "max_selections": 2 },
        { "id": "q3", "type": "grid", "text": "Rate each factor.", "rows": ["Durability", "Style"], "columns": ["Not important", "Somewhat", "Very important"] },
        { "id": "q4", "type": "numeric", "text": "Pairs bought last year?", "options": ["0", "1-2", "3-5", "6+"] },
        { "id": "q5", "type": "ranking", "text": "Rank these brands.", "options": ["Nike", "Adidas", "New Balance", "Puma"] },
        { "id": "q6", "type": "text", "text": "Describe your last purchase." }
      ]
    }
  }'

Response (202 Accepted)

{
  "id": "sim_a1b2c3d4e5f6a1b2c3d4e5f6",
  "object": "oracle.simulation",
  "status": "queued",
  "model": "consumer",
  "respondent_count": 300,
  "created": 1770940800
}

Retrieve a Simulation

GET /api/v1/oracle/simulate/{id}

Returns the job's current status. Poll this endpoint until status is completed or failed; on completed, read respondents. Large datasets are paginated.

Query Parameters

ParameterTypeDefaultDescription
limitinteger100Respondents per page (1–1,000)
offsetinteger0Number of respondents to skip

Response (while queued or processing)

Before the job finishes, the response carries status metadata only. The respondents and pagination fields are omitted until status is completed:

{
  "id": "sim_a1b2c3d4e5f6a1b2c3d4e5f6",
  "object": "oracle.simulation",
  "status": "processing",
  "model": "consumer",
  "respondent_count": 300,
  "created": 1770940800
}

Response (completed)

{
  "id": "sim_a1b2c3d4e5f6a1b2c3d4e5f6",
  "object": "oracle.simulation",
  "status": "completed",
  "model": "consumer",
  "respondent_count": 300,
  "created": 1770940800,
  "completed": 1770940815,
  "respondents": [
    {
      "respondent_id": "resp_00001",
      "demographics": { "age": "25-34", "gender": "Female" },
      "responses": {
        "q1": "Nike",
        "q2": ["Comfort", "Price"],
        "q3": { "Durability": "Very important", "Style": "Somewhat" },
        "q4": "1-2",
        "q5": ["Nike", "New Balance", "Adidas", "Puma"],
        "q6": "I bought running shoes online during a sale."
      }
    }
  ],
  "pagination": { "limit": 100, "offset": 0, "total": 300 }
}
  • The example shows one respondent for brevity; a page returns up to limit rows.
  • respondents and pagination are present only when status is completed.
  • respondent_count is the requested sample size (the sum of quota-group counts). Because generation is atomic, on a completed job it always equals pagination.total. Page against pagination.total; when offset ≥ total, respondents is an empty array.
  • Each response is keyed by the question id. The model field echoes the request's model_type.
  • created and completed are Unix timestamps in seconds.
  • Completed results are retained indefinitely and can be re-fetched at any time.

Response for HCP and patient jobs

For the hcp model, each respondent row carries medical_specialty in place of demographics. The patient model uses demographics, as above; the input demographic keys are echoed back on each row, including the special "Patient Condition" key when supplied.

{
  "respondent_id": "resp_00001",
  "medical_specialty": "Cardiology & Cardiovascular",
  "responses": { "q1": "Drug A" }
}

List Simulations

GET /api/v1/oracle/simulate

Lists your simulation jobs, most recent first. Because results are retained indefinitely and there is no other lookup, this is how you recover a job id that wasn't persisted. Paginated; optionally filter by status.

Query Parameters

ParameterTypeDefaultDescription
statusstringFilter by status: queued, processing, completed, or failed
limitinteger20Jobs per page (1–100)
offsetinteger0Number of jobs to skip

Response

{
  "object": "list",
  "data": [
    {
      "id": "sim_a1b2c3d4e5f6a1b2c3d4e5f6",
      "object": "oracle.simulation",
      "status": "completed",
      "model": "consumer",
      "respondent_count": 300,
      "created": 1770940800
    }
  ],
  "pagination": { "limit": 20, "offset": 0, "total": 42 }
}

List items are job summaries (no respondents); fetch full results from the retrieve endpoint. When offset ≥ total, data is an empty array. Every request — create, retrieve, and list — counts against your key's rate limit.

Webhooks

If you supply an HTTPS webhook_url when creating a job, the Oracle sends a POST to that URL when the job reaches a terminal status — so you don't have to poll. The payload identifies the job and its outcome; always fetch the data itself from the retrieve endpoint.

POST // to your webhook_url

{
  "id": "evt_9f8e7d6c5b4a3210",
  "object": "oracle.simulation.event",
  "type": "simulation.completed",
  "simulation_id": "sim_a1b2c3d4e5f6a1b2c3d4e5f6",
  "status": "completed",
  "respondent_count": 300,
  "occurred_at": 1770940815
}

The type is simulation.completed or simulation.failed, matching the job status. occurred_at is the Unix-seconds time the event was emitted (distinct from the job's own created).

Verifying signatures

Every delivery is signed so you can confirm it genuinely came from Simsurveys. Two headers are sent:

HeaderValue
X-Simsurveys-TimestampUnix seconds when the request was sent
X-Simsurveys-SignatureHex HMAC-SHA256 of {timestamp}.{raw_body}, keyed with your webhook signing secret

The signing secret is a single account-level secret, found in your dashboard (distinct from your sk_live_ API key). To verify: recompute the lowercase-hex HMAC over timestamp + "." + raw_request_body using that secret, and compare with a constant-time equality against the (lowercase) header value. Reject the request if it doesn't match, or if the timestamp is outside a 5-minute window (replay protection).

import hmac, hashlib, time

def verify(raw_body, timestamp, signature, secret):
    if abs(time.time() - int(timestamp)) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{timestamp}.{raw_body}".encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature.lower())

Treat the webhook as a hint, not the source of truth. After verifying, re-GET the job to read the authoritative status and results.

A simulation.failed event carries the same fields with status: "failed":

{
  "id": "evt_1a2b3c4d5e6f7890",
  "object": "oracle.simulation.event",
  "type": "simulation.failed",
  "simulation_id": "sim_a1b2c3d4e5f6a1b2c3d4e5f6",
  "status": "failed",
  "respondent_count": 300,
  "occurred_at": 1770940815
}

Delivery & retries

  • Respond with any 2xx to acknowledge. Non-2xx or timeout (>10s) triggers retries with exponential backoff for up to 24 hours.
  • Delivery is best-effort: after 24 hours of failed retries the event is dropped. Always keep polling available as the source of truth.
  • Delivery is at-least-once — the same event may arrive more than once. Deduplicate on the event id.
  • Event payloads follow the same versioning contract as API responses — ignore unrecognized fields.

Answer Encoding by Question Type

Each respondent's responses object maps a question id to its answer. The answer's shape depends on the question type:

TypeValue shapeExample
single_selectstring — the chosen option"Nike"
multi_selectstring[] — the chosen options["Comfort", "Price"]
gridobject — { row: column } per row{ "Durability": "Very important", "Style": "Somewhat" }
numericstring — the selected numeric value/range"1-2"
rankingstring[] — options ordered best → worst["Nike", "New Balance", "Adidas", "Puma"]
textstring — open-ended response"I shop online during sales."
  • single_select returns exactly one of the question's options.
  • multi_select returns between 1 and max_selections options (never empty).
  • ranking is exhaustive — it returns all of the question's options, in ranked order.
  • numeric returns one of the question's options (a numeric value or range, e.g. "1-2"), as a string.
  • grid returns every rows item as a key, each mapped to exactly one columns value.
  • text returns a free-form string.

Code Examples

A full run: create the job, poll until it completes, then page through the respondent-level results.

Python

import requests, time

API_KEY = "sk_live_your_key_here"
BASE_URL = "https://app.simsurveys.com/api/v1"
HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}

# 1. Create the simulation
job = requests.post(
    f"{BASE_URL}/oracle/simulate",
    headers=HEADERS,
    json={
        "model_type": "consumer",
        "quota_groups": [
            {"count": 150, "demographics": {"age": "25-34", "gender": "Female"}},
            {"count": 150, "demographics": {"age": "35-44", "gender": "Male"}},
        ],
        "survey": {"questions": [
            {"id": "q1", "type": "single_select",
             "text": "Which brand do you prefer?",
             "options": ["Nike", "Adidas", "New Balance", "Puma"]},
        ]},
    },
).json()

sim_id = job["id"]

# 2. Poll until complete
while job["status"] in ("queued", "processing"):
    time.sleep(2)
    job = requests.get(f"{BASE_URL}/oracle/simulate/{sim_id}", headers=HEADERS).json()

if job["status"] == "failed":
    raise RuntimeError(job["error"]["message"])

# 3. Page through respondent-level results (pagination.total is authoritative)
respondents, offset = [], 0
while True:
    page = requests.get(
        f"{BASE_URL}/oracle/simulate/{sim_id}",
        headers=HEADERS,
        params={"limit": 1000, "offset": offset},
    ).json()
    respondents += page["respondents"]
    offset += len(page["respondents"])
    if offset >= page["pagination"]["total"]:
        break

print(f"Retrieved {len(respondents)} respondents")

JavaScript / Node.js

const API_KEY = "sk_live_your_key_here";
const BASE_URL = "https://app.simsurveys.com/api/v1";
const HEADERS = { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

// 1. Create the simulation
let job = await (await fetch(`${BASE_URL}/oracle/simulate`, {
  method: "POST",
  headers: HEADERS,
  body: JSON.stringify({
    model_type: "consumer",
    quota_groups: [
      { count: 150, demographics: { age: "25-34", gender: "Female" } },
      { count: 150, demographics: { age: "35-44", gender: "Male" } },
    ],
    survey: { questions: [
      { id: "q1", type: "single_select",
        text: "Which brand do you prefer?",
        options: ["Nike", "Adidas", "New Balance", "Puma"] },
    ] },
  }),
})).json();

const simId = job.id;

// 2. Poll until complete
while (["queued", "processing"].includes(job.status)) {
  await sleep(2000);
  job = await (await fetch(`${BASE_URL}/oracle/simulate/${simId}`, { headers: HEADERS })).json();
}
if (job.status === "failed") throw new Error(job.error.message);

// 3. Page through respondent-level results (pagination.total is authoritative)
const respondents = [];
let offset = 0, total = Infinity;
while (offset < total) {
  const page = await (await fetch(
    `${BASE_URL}/oracle/simulate/${simId}?limit=1000&offset=${offset}`,
    { headers: HEADERS },
  )).json();
  respondents.push(...page.respondents);
  offset += page.respondents.length;
  total = page.pagination.total;
}
console.log(`Retrieved ${respondents.length} respondents`);

Errors

Every error response — at any status code — uses the same typed error envelope. Request-time errors (bad body, invalid quotas, too many questions) are returned synchronously from the POST:

{
  "error": {
    "type": "validation_error",
    "message": "survey.questions must contain between 1 and 30 items."
  }
}
TypeHTTP StatusDescription
validation_error400Invalid request body, quota groups, or survey
authentication_error401Missing, invalid, or expired API key
permission_error403Key lacks the oracle:simulate scope
not_found_error404Unknown simulation id
conflict_error409Idempotency-Key reused with a different request body
rate_limit_error429Rate limit exceeded
internal_error500Unexpected server error

A job that fails after being accepted returns status: "failed" with an error object on the GET response. This is a job-level error: the HTTP status is still 200 (the request succeeded; the job did not). It carries the standard job fields plus error, and the error.type is a job-level value (currently generation_error) — a separate, open set from the transport error types above, so handle it independently of HTTP status. respondent_count stays the requested count on a failed job (no rows are produced).

{
  "id": "sim_a1b2c3d4e5f6a1b2c3d4e5f6",
  "object": "oracle.simulation",
  "status": "failed",
  "model": "consumer",
  "respondent_count": 300,
  "created": 1770940800,
  "error": { "type": "generation_error", "message": "A quota group failed to generate; the job was aborted." }
}

HTTP Status Codes

StatusMeaning
202Accepted — simulation job created
200Success — job status/results returned
400Bad request — invalid body, quotas, or survey
401Unauthorized — missing, invalid, or expired API key
403Forbidden — key lacks the oracle:simulate scope
404Not found — unknown simulation id
409Conflict — Idempotency-Key reused with a different request body
429Too many requests — rate limit exceeded
500Internal server error

Limits & Constraints

ConstraintValue
Questions per survey1–30
Answer options per question2–20
Max question length5,000 characters
Respondents per jobUp to 5,000 (sum of all quota-group counts)
Quota groups per jobAt least 1; each count ≥ 1; combined total ≤ 5,000
Grid rows / columns2–20 each
Results pagination100 per page (max 1,000)
Max request body size1 MB
Result retentionIndefinite — completed results never expire
Idempotency-Key window24 hours
Rate limitJob creation counts as one request against your key's limit

Exceeding any of these limits returns a 400 validation_error describing the violated constraint.

Demographics Reference

A quota group's demographics object (consumer, social, and patient models) is free-form: each key-value pair is passed through to the model verbatim. The API validates only that keys and values are strings — it does not enforce a fixed set. The table below is the recommended vocabulary the models were tuned on; using these keys and values gives the most reliable results.

KeyRecommended values
genderMale, Female
age18-24, 25-34, 35-44, 45-54, 55-64, 65-74, 75+
incomeUnder $10k, $10k-15k, $15k-20k, $20k-25k, $25k-30k, $30k-35k, $35k-40k, $40k-45k, $45k-50k, $50k-60k, $60k-75k, $75k-100k, $100k-125k, $125k-150k, $150k-200k, $200k+
locationUrban, Suburban, Rural
race_ethnicityWhite, Black/African American, American Indian/Alaska Native, Asian, Native Hawaiian/Pacific Islander, Other, Multiracial, Hispanic/Latino
educationSome High School or Less, High School Graduate, Some College, College Graduate, Post-Graduate Degree
employmentNot in Labor Force, Employed, Unemployed
childrenNo, Yes
politicalDemocrat, Republican, Libertarian, Green Party, Other
marital_statusMarried, Not Married

For the patient model only, the special key "Patient Condition" conditions the cell on a diagnosis (see Create).

Supported Specialties

For the hcp model, a quota group's medical_specialty must exactly match one of these 20 values:

Allied Health & Other Medical ProfessionalsNeurology & Neurosurgery
Anesthesiology & Pain ManagementNursing & Advanced Practice Providers
Cardiology & CardiovascularOncology & Hematology
Dermatology & Skin ConditionsPathology & Laboratory Medicine
Emergency & Critical CarePediatrics & Child Health
Endocrinology & DiabetesPrimary Care & General Practice
Eye & Vision CarePulmonology & Respiratory Medicine
Gastroenterology & Digestive HealthRadiology & Medical Imaging
Internal Medicine SubspecialtiesSurgical Specialties
Mental Health & PsychiatryWomen's Health & Reproductive Medicine

Versioning

The current API version is v1, reflected in the URL path (/api/v1). Backwards-incompatible changes ship under a new version prefix; additive changes (new optional request fields, new response fields) may appear within v1, so clients should ignore unrecognized response fields. Unrecognized request fields — and fields that are recognized but not applicable to the chosen model_type (e.g. context or demographics sent to the hcp model, or medical_specialty sent to a non-HCP model) — are rejected with a validation_error.

Start building with the Simulation API.

Generate full synthetic respondent-level datasets programmatically. Create an account and generate your first API key in minutes.