Create your secret websiteID.
Sign in to your Neurvance account, open the Cube dashboard, and subscribe with Stripe. Each subscription allows a maximum of 1,000 API calls per hour per account, shared across all sessions and endpoints in a rolling 60-minute period. Once payment is verified, choose Create API key and copy your secret websiteID. Canceling renewal keeps API access until the paid period ends. If a renewal payment fails, update your payment method in Manage billing to restore access after payment is verified.
- Choose Create API key in the dashboard.
- Copy the complete key when it appears. It is shown only once.
- Save this one secret as SHOP_WEBSITE_ID on your server. It is also your API key.
Your websiteID is your secret API key. These names refer to one credential. It is shown only when created or rotated. The backend keeps its account identifiers internal; you do not need a separate public ID.
How do I generate a replacement credential?
With an active subscription, choose Rotate API key. Copy the new secret websiteID and update SHOP_WEBSITE_ID on your website server. The old credential immediately stops working. Saved observations and sessions are preserved; queued retries keep their original sequence, timestamp, and data while using the replacement credential.
Keep the key on your server.
The browser sends observations to your own server. The secret websiteID/API key stays on that server, and websiteID must be masked in any request viewer. Your server adds the secret key and forwards them to Cube over HTTPS. Never put the key in frontend code, browser storage, URLs, or logs. At finalization, Cube atomically commits count-weighted merged statistics and completion receipts. The only customer-callable business operations are perbuy, optimizechance, recommendproducts, and spendmoretime. There is no generic function-dispatch endpoint and no direct customer uploadData endpoint.
SHOP_API_URL=https://YOUR_CUBE_APP.herokuapp.com
SHOP_WEBSITE_ID=YOUR_SECRET_WEBSITEID_API_KEY
# For the shared Python integration example or local demo:
SHOP_BATCH_MODE=true
SHOP_ENCRYPT_PAYLOADS=trueUse the API origin and secret websiteID supplied with your Cube account. There is no separate API key value to configure. Add these values in your hosting provider’s private environment settings. With Python, install requests and use this helper:
import os
import requests
# Run on YOUR WEBSITE SERVER. Never send this key to a browser.
def cube_request(path, payload):
response = requests.post(
os.environ["SHOP_API_URL"].rstrip("/") + path,
headers={"Authorization": "Bearer " + os.environ["SHOP_WEBSITE_ID"]},
json={**payload, "websiteID": os.environ["SHOP_WEBSITE_ID"]},
timeout=(3, 25),
allow_redirects=False,
)
response.raise_for_status()
return response.json()HTTPS protects the connection, but it is terminated by the platform router before Cube receives the request. Sealing the body reduces accidental exposure in body-only logs. The Bearer credential can derive the encryption key, so a trusted TLS-terminating proxy can still decrypt it; keep authorization headers and credentials out of logs. Send X-Cube-Encryption: v1, keep the key in the Authorization header, and replace the JSON body with one AES-256-GCM envelope whose content key is derived from your own secret websiteID. The plaintext inside the envelope is exactly the JSON shown in the steps below. Cube seals its reply the same way; open it with the same key. Readable bodies are still accepted, and 400 payload_encryption_required means this backend now requires sealed ones.
import base64, json, os, secrets
import requests
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
KEY = os.environ["SHOP_WEBSITE_ID"]
def content_key(salt):
return HKDF(algorithm=hashes.SHA256(), length=32, salt=salt,
info=b"cube-transport-v1").derive(KEY.encode())
def cube_request(path, payload):
salt, iv = secrets.token_bytes(16), secrets.token_bytes(12)
aad = ("POST " + path).encode()
body = json.dumps({**payload, "websiteID": KEY}).encode()
response = requests.post(
os.environ["SHOP_API_URL"].rstrip("/") + path,
headers={"Authorization": "Bearer " + KEY, "X-Cube-Encryption": "v1"},
json={"v": 1, "alg": "A256GCM",
"salt": base64.b64encode(salt).decode(),
"iv": base64.b64encode(iv).decode(),
"ct": base64.b64encode(
AESGCM(content_key(salt)).encrypt(iv, body, aad)).decode()},
timeout=(3, 25),
allow_redirects=False,
)
sealed = response.json()
if response.headers.get("X-Cube-Encryption") != "v1":
response.raise_for_status()
return sealed
opened = AESGCM(content_key(base64.b64decode(sealed["salt"]))).decrypt(
base64.b64decode(sealed["iv"]), base64.b64decode(sealed["ct"]), aad)
response.raise_for_status()
return json.loads(opened)Each envelope is bound to the exact method and path it was sealed for, so it cannot be replayed against another endpoint. A retried sample is the identical plaintext body with a fresh salt and nonce. Install cryptography alongside requests on your server.
Call this helper from your own protected server routes. Bind every visitor session to the visitor who created it, check browser request origins and CSRF tokens, and keep any retry queue only in RAM. Do not expose an unrestricted relay that accepts another visitor’s session ID.
Observe every 10 seconds. Batch every minute.
At entry, create a unique session on your server. Collect the first observation after ten seconds, then one every ten seconds. Keep observations in RAM and use one account-wide scheduler to send POST /api/v1/batch every minute, including starts, ordered samples, lifecycle events and requested calculations. Preserve the original entry time and sample timestamps.
Batching is optional and requires updating your server integration. Existing endpoints remain available. The supplied Python integration example and local demo opt in with SHOP_BATCH_MODE=true; use their shared BatchClient scheduler. Get the integration guide, server example and local demo instructions from the Cube repository. All Cube requests stay authenticated and use the encrypted transport above.
With the existing direct endpoints, call /api/v1/sessions/start at entry. Resume the same identifiers with resume: true and continue from the returned sequence. Its started_at and deadline are Unix seconds. Individual ten-second HTTP uploads consume the allowance quickly.
Create a unique session_id for each visit and a pseudonymous visitor_id. Keep them stable across page navigation. Start sample_seq at 1 and increase it for each new sample.
Send JSON to POST /api/v1/savedata using Authorization: Bearer YOUR_SECRET_WEBSITEID_API_KEY and Content-Type: application/json.
{
"websiteID": "YOUR_SECRET_WEBSITEID_API_KEY",
"session_id": "visit-unique-123",
"visitor_id": "visitor-456",
"sample_seq": 1,
"captured_at": "2026-09-11T14:30:00Z",
"data": {
"avgSpeed": 100,
"mouseClicksTotal": 10,
"colorsSeen": ["green", "blue"],
"productsSeen": ["cup", "vase"],
"productsviews": 4,
"productsSold": 0,
"productsClicked": 2,
"mouseSide": "left",
"mouseUpsideDown": "up",
"time": 10,
"temperature": 20,
"rain": false,
"day": "Friday",
"lastBought": null,
"lastBoughtItem": ""
}
}Replace the example observations with real values from your site and use the sample’s actual timestamp. For direct delivery, pass this payload to cube_request("/api/v1/savedata", payload).
from uuid import uuid4
# sample is the observation object above, with a current captured_at.
# started_at is the original entry time as a timezone-qualified ISO timestamp.
batch = {
"websiteID": os.environ["SHOP_WEBSITE_ID"],
"batch_id": uuid4().hex,
"sessions": [{
"session_id": sample["session_id"],
"visitor_id": sample["visitor_id"],
"start": {"started_at": started_at, "resume": False},
"samples": [{name: sample[name] for name in
("sample_seq", "captured_at", "data")}],
"calculations": ["perbuy", "recommendproducts"]
}]
}
result = cube_request("/api/v1/batch", batch)A batch accepts up to 20 sessions, 120 observations and 64 KiB of plaintext JSON. Pack by encoded byte size. Each session appears once; omit start on later deliveries. All ingestion is validated before any part is applied; an invalid entry rejects ingestion with an indexed error. Calculations run afterward with individual results or errors, without undoing accepted samples. Request calculations while the session is active; omit them from a completion entry. A batch returns its batch ID, status and per-session sample acknowledgments and calculation results.
Keep the same batch_id and plaintext on uncertain retries. A duplicate cannot rotate connections or train twice. Queue until acknowledgment, honor Retry-After, and stop accepting new observations with an explicit capacity error if RAM fills. A local queued response is not a Cube acknowledgment. RAM queues are lost on restart.
For direct /api/v1/savedata, a 201 response with status: buffered means the sample is held only in backend RAM. It is not saved to Supabase yet. Retry with the same sequence, timestamp, and data if delivery is uncertain; identical retries return 200. Changed retries return 409. Keep retries only in RAM until acknowledged. Never write observations to browser storage, files, Redis, or another database. A backend restart loses unfinished observations.
Field types and validation
- Send every field shown in
data. Purchase labels belong in the completion request. - Counts are nonnegative integers up to 1,000,000,000. Speed is finite and nonnegative; temperature is finite, with an absolute limit of 1,000,000,000.
colorsSeenandproductsSeenhold the distinct colors and product IDs encountered so far. Supply both lists, with up to 64 nonempty strings each and 128 characters per string. No control characters. LegacyColorstext is optional when these lists are present.lastBoughtItemallows up to 256 characters without control characters.- Directions are
left/rightandup/down. Time is elapsed seconds on the website, from 0 to 21600; day is a full English weekday. rainis a boolean;lastBoughtis a validYYYY-MM-DDdate ornull.- Visitor/session IDs use 1–128 letters, digits, underscores, dots, colons or hyphens. Each six-hour session accepts at most 2,160 observations.
captured_atneeds a timezone, must be within the last six hours, and cannot be more than five minutes in the future. Requests are limited to 64 KiB.
A visibility-change handler can attempt a final flush to your server, but browser closure and background timers are not reliable completion signals. A sample that never reaches a server cannot be saved by Cube.
Example: 120 seconds → 12 buffered observations → buy 2 items → one contribution at the first observed prefix in each elapsed-time band. Cube commits weighted merged statistics and completion receipts together. One visitor is never counted as twelve independent customers.
04 / FINAL PURCHASE OUTCOMELabel the whole session.
Your server’s order system must verify payment and associate orders with the Cube session. Add up the quantity of products in that session’s paid orders and deduplicate by order ID. Never trust a browser’s claim that an order was paid.
After verified purchase, include an event with type: complete, last_sample_seq and boughtNumber after the samples in a batch. For the compatible direct endpoint, deliver all samples first, then send POST /api/v1/sessions/complete with the same authorization header:
{
"websiteID": "YOUR_SECRET_WEBSITEID_API_KEY",
"session_id": "visit-unique-123",
"last_sample_seq": 12,
"boughtNumber": 2
}boughtNumber is product quantity, not money spent. This example labels all 12 samples didBuy=1 and boughtNumber=2. A confirmed quantity of zero labels every sample zero.
Deliver all samples from 1 through last_sample_seq before completion. Missing samples return 409 samples_incomplete. Identical completion retries succeed; a different definitive outcome returns a conflict.
What if the visitor leaves without buying?
On page departure, your server calls POST /api/v1/sessions/exit with websiteID and session_id. Send the browser-to-server request with keepalive and include the connection_id returned by session start, so a delayed exit from the previous page cannot close the resumed session. Cube waits five seconds for same-site navigation or reload to resume through /api/v1/sessions/start, then labels every buffered sample boughtNumber: 0. Switching tabs alone is not an exit.
Missed exit signals are handled at six hours after session start, even if samples kept arriving: finalize zero and commit the merged statistics. Before an external checkout redirect, your server calls /api/v1/sessions/checkout to suppress exit finalization while payment is processing. The six-hour deadline still applies.
The outcome is frozen when finalization starts. There is no later correction window. A direct 202 completion response means finalization is running; poll /api/v1/sessions/status with the same two identifiers until state: completed. Retries do not reapply successful merges. A restart loses unfinished RAM observations. Committed finalization is atomic; a permanently failed finalization reports interrupted.
With batching, the shared server scheduler handles the five-second navigation grace locally. Resume cancels a pending exit; stale connection IDs cannot close a resumed visit. Queue checkout protection before leaving for payment. The original six-hour deadline remains fixed; delayed delivery does not extend it.
Ask about the current visit.
Use these four calculation endpoints, or request the same names in a scheduled batch. All use the same Bearer key and body:
POST /api/v1/perbuy— buying estimate.POST /api/v1/optimizechance— direction diagnostic and an empty recommendations list.POST /api/v1/recommendproducts— product recommendation.POST /api/v1/spendmoretime— time-spending recommendation.
Each request uses this body:
{
"websiteID": "YOUR_SECRET_WEBSITEID_API_KEY",
"session_id": "visit-unique-123"
}Example buying estimate:
{
"session_id": "visit-unique-123",
"sample_seq": 12,
"probability_percent": 75.0,
"estimated_product_quantity": 2.5
}Example optimization response:
{
"session_id": "visit-unique-123",
"sample_seq": 12,
"direction": "down",
"recommendations": []
}Cube reads the current session from RAM and builds userData server-side; callers cannot submit arbitrary user data. Your request needs only the secret websiteID and session_id. The returned sequence tells you which observation was evaluated. These calls do not save their request or result, and Cube does not provide a local fallback calculation. Unknown operation names and typo aliases such as optimizechacne and uplaodaata are rejected.
Predictions require 500 completed sessions with usable observations, or 500 usable legacy history rows. Readiness remains 70% at that gate and 100% at 1,000. These counts are not added together. New support counts sessions within one elapsed-time band, not prototype rows. Legacy rows have unknown sample weights and cannot supply independent confidence. Optimization still requires two current samples; 422 insufficient_data means more evidence is needed.
Quantity is expected products across all visits: purchase probability multiplied by a smoothed quantity among buyers. Optional reliability metadata reports session support, distance, uncertainty and the chosen estimator. Neighborhood predictions are used only after prospective session-level comparisons improve log loss without worsening Brier score; otherwise Cube uses the smoothed baseline.
Product recommendations require at least five contributing sessions and observed co-view support, and return at most three products. They describe associations, not purchased product identities or causal uplift. optimizechance returns recommendations: [] and spendmoretime returns recommendation: "": mouse movement and longer browsing cannot establish that a store action helps. Never evaluate response strings as code. Synthetic benchmarks do not demonstrate increased sales; ChatGPT reports are not included.
Download your merged history.
Choose Download CSV in the Cube dashboard. The download streams directly from cubeWebsiteData; Cube creates no stored CSV or other observation copy. It contains merged history, so many completed sessions can contribute to one weighted aggregate. Individual session trajectories cannot be reconstructed from the merged export.
Active observations exist only in RAM until their outcome is known. Old migrated history is marked legacy, with unknown old purchase quantities left blank. Secrets never appear in exports.
Handle retries deliberately.
| Response | What to do |
|---|---|
| 401 | Check the secret key. It may be expired, revoked, or replaced. |
| 403 | Check the paid subscription and website ownership. An expired or unpaid subscription returns inactive_subscription; open Manage billing to renew or update your payment method. |
| 409 | Inspect the conflict: changed retry, missing samples, closed session, or conflicting final outcome. |
| 413 | Reduce the body below 64 KiB. |
| 422 | Collect the current sample and enough completed history. |
| 429 | Wait for Retry-After before retrying the same request. |
| 500 / 503 | Retry temporary failures with backoff. Contact support for repeated calculation failures or a capacity error. |
Subscription limit: maximum 1,000 API calls per hour per account across all /api/v1/ endpoints and sessions, measured over a rolling 60-minute period. Session starts, samples, completions, predictions, and retries admitted by the hourly limit all count, even if later validation fails. Calls rejected by the hourly limit do not extend the wait. Key rotation does not reset usage. Dashboard and billing actions do not use this allowance. The supplied minute scheduler uses one normal batch per minute (60 per hour) and up to about 20 calls for urgent events and retries, leaving the rest of the allowance as headroom. Each admitted batch costs one HTTP call. Compact observations for 1, 5 or 10 simultaneous visitors need about 10–11 calls over ten minutes or 30–31 over thirty minutes, including starts and completions but excluding retries. Larger payloads need extra batches and explicit backpressure. On HTTP 429, wait for Retry-After.
Additional short-term limits: 600 collection/completion requests per minute per account, 12 direct sample uploads per minute per session (historical batch delivery is separate), and 60 calculations per minute per account. Embedded calculations and retries count toward the calculation limit. An individual calculation error does not undo acknowledged ingestion.
Calculations use up to 20,000 aggregate prototypes. Retry fingerprints and outcome identifiers expire after seven hours; aggregate business totals remain. Use new session IDs after a restart, and reconcile delivery within the six-hour session window. A session can buffer at most 2,160 samples during its six-hour lifetime. Each account also has a fair share of live session memory (2,000 open visits and 32 MiB of buffered observations); when it is full, Cube returns HTTP 503 account_session_capacity with Retry-After. Capacity errors are explicit; do not discard unacknowledged samples or treat a failed request as a saved observation.