From successful API call to reliable production system.
The first request is easy. This guide covers what changes when you run Sentiqra inside a real pipeline.
Failure behavior
Sentiqra documents exactly what happens when something goes wrong - including whether you were billed.
| Situation | HTTP | Engine called? | Billed? | Customer action |
|---|---|---|---|---|
| Invalid JSON | 400 | No | No | Fix request |
| Unsupported analyzer | 400 | No | No | Fix analyzer |
| Language no requested analyzer covers | 400 unsupported_language | No | No | Check the analyzer's language matrix |
| Analyzer does not cover the declared language | 200 partial | Not that analyzer | No | Route on not_applicable — do not treat as a negative finding |
| No analyzable letters (emoji-only, numeric-only, symbols) | 422 / 200 partial | No | No | Skip analysis; the input carries no signal |
| Analyzer not entitled | 403 | No | No | Change entitlement |
| Rate limit | 429 | No | No | Back off |
| One of two analyzers fails | 200 partial | Partially | Successful runs only | X-Retry-Failed child / route |
| In-flight concurrency cap (per-key fair share or fleet pool) | 503 detector_unavailable | No | No | Retry-After included - retry with backoff; shed load is free |
| All analyzers unavailable | 503 | No successful run | No | Retry |
| Valid idempotent replay | 200 | No - recorded response returned | No new charge | Use response |
| Replay with different body | 409 idempotency_conflict | No | No | Use a fresh key |
X-Retry-Failed child retry | 200 retry_child | Failed analyzers only | Newly-completed runs only | Use merged response |
Partial execution
When you run two analyzers, one can succeed while the other fails. The request still returns 200 - check meta.analyzers per-analyzer status.
"meta": {
"analyzers": [
{ "name": "trust-safety", "status": "completed", "latency_ms": 412, "signal_count": 3, "billed": true },
{ "name": "cybersecurity", "status": "failed", "latency_ms": 30001, "signal_count": 0, "billed": false,
"error": "analyzer timeout" }
]
},
"usage": { "analyzer_runs": 1 }
Rule
A completed analyzer's result is usable. A failed analyzer's result is unknown - do not treat unknown as safe.
Policy example
If the secondary analyzer is optional, continue with the primary result. If it is safety-critical, retry or route to review.
"No signals" is not "safe" — and not every empty result is an analysis
Three independent dimensions tell you what a response means: status (did it execute), applicability (could this input produce a meaningful result), and assessment (what a completed run established). Route on all three — never on whether the signals array is empty.
| Condition | Where reported | What it means | Safe to infer |
|---|---|---|---|
| signals_detected | meta.analyzers[].assessment | At least one signal met the reporting criteria. | Signals were observed — not proof of the underlying condition. |
| no_signals | meta.analyzers[].assessment | The analyzer completed; nothing crossed the documented thresholds. | Nothing was detected — not proof of safety or absence. |
| abstained | meta.analyzers[].assessment | A validated rejection rule found the input off-domain for this analyzer. | The analyzer declined to report — route like no_signals, not like a verdict. |
| insufficient_content | meta.analyzers[].applicability (status not_applicable); 422 on single-analyzer endpoints | No analyzable letters — emoji-only, numeric-only, punctuation-only, symbol-only. | Nothing ran. The input carries no analyzable signal. |
| unsupported_language | meta.analyzers[].applicability (status not_applicable); 400 when no requested analyzer covers the code | The analyzer has no coverage for the declared language. | Nothing ran — no finding either way. Check the per-analyzer language matrix. |
| language_fallback | meta.analyzers[].applicability + effective_language | The analyzer ran on its English coverage because the declared language is not a dedicated coverage language for this analyzer. | Signals are real but coverage is shallower — validate thresholds before acting on them. |
- •Language is declared, not detected. Sentiqra analyzes text under the language you declare (or the documented English default — reported as
language_source: "default"). Mixed-language text is analyzed under the single declared language; an incorrect declaration produces a valid result against the wrong coverage. Declare accurately. - •Fragments and encoded content. Short fragments are analyzed as-is — weak evidence yields
no_signalsorabstained, not a guess. Encoded payloads (base64, hex) are analyzed as literal text; decode before submitting when the payload is the content of interest. - •Routing rule. Treat
not_applicable,no_signals, andabstainedas "no actionable finding" — none of them prove the content is benign. If the case is safety-critical, route to human review or an analyzer with coverage for the declared language.
Idempotency
Send an Idempotency-Key header to make a request safe to retry. The key is scoped to your account and the canonical request identity - the request fields including detection_config when you pin one.
POST /v1/analyze
Idempotency-Key: msg_92a8...
- •Same key + same canonical request → returns the recorded response verbatim, marked
X-Sentiqra-Idempotent-Replay: true, with no additional charge. The storedusagefields describe the recorded operation's billing - the replay itself charged nothing. Records live 24 hours and hold the key, a request hash, and the response (which includes per-analyzer outcomes) - your submitted text is never persisted. If the current configuration rolls out inside the TTL and you did not pindetection_config, the replay still returns the recorded response - a replay repeats the recorded operation, not the latest defaults. - •Same key + different request →
409 idempotency_conflict. The canonical identity includesdetection_config- pinning a different configuration under the same key conflicts rather than silently switching semantics. - •Partial results are preserved → a replay echoes the recorded per-analyzer outcomes, including failures. A failed analyzer is never silently retried into a success - replaying cannot expand the original outcome.
- •Duplicate concurrent submissions → one request owns the key; concurrent duplicates get
409 idempotency_in_progressuntil it completes. Concurrent retries cannot create duplicate charges - usage accounting is atomic. - •Fresh key → a new logical operation: executes and bills normally. Use a fresh key when the previous operation's outcome is no longer the one you want.
- •Generate a key per logical unit of work (message ID, ticket ID) - essential for queue consumers and financial/safety pipelines.
Retrying failed analyzers
After a partial result, resubmit the same key and body with X-Retry-Failed: true - an explicit child operation that extends the recorded outcome instead of replacing it.
POST /v1/analyze
Idempotency-Key: msg_92a8...
X-Retry-Failed: true
- •Only analyzers recorded as
failedoroverloadedre-execute; completed runs are preserved and never re-billed.not_applicableis a permanent property of the input and is never retried. - •The merged response is marked
retry_child: true; retried entries carryretried: trueinmeta.analyzers[].usage.billed_runsis exactly what this request charged. - •If a retried analyzer fails again it stays
failed- a furtherX-Retry-Failedchild can retry it again. Once nothing is left to retry the endpoint returns409 no_failed_analyzers. - •A failed child leaves the parent's recorded outcome intact - previously billed runs can never be re-charged.
- •A
completedanalyzer that returned zero signals is a successful run - distinct fromfailed, billed, and never retried by a child operation.
Retries & timeouts
- •Set a client timeout; retry only on 429, 5xx, and network errors.
- •Use exponential backoff with jitter.
- •Always pair retries with an idempotency key.
Rate limits & capacity
- •Limits vary by plan; 429 responses include rate-limit headers.
- •Throttle at the client and queue bursts rather than firing them.
- •Size capacity in analyzer runs, not requests.
- •Each key has a concurrent in-flight fair-share cap (default 8 analyzer runs) beneath a fleet-wide pool.
503 detector_unavailablewithRetry-Aftermeans you're over it - retryable and never billed.
Observability
Record the evidence behind every call so you can debug and audit later.
- •Log
request_idplus the completeverificationreceipt per request - all seven fields (version,configuration,fingerprint,input_commitment,signature,key_id,binding_key_id). A partial receipt cannot be verified later. - •Track latency, error rate, and per-analyzer
statusas metrics. - •Record your own policy version and input-preparation version alongside the analysis evidence - see the continuity package for the full evidence list.
Measuring production quality — don't learn only from flagged cases
Reviewed alerts, customer complaints, and successful appeals are a self-selected slice of traffic, not a sample of it — the selective-labels problem. If only flagged cases receive labels, "production accuracy" describes only the flagged stratum, and "no complaint received" is not evidence of correct classification.
- •Sample every stratum: flagged, unflagged, abstained, and failed cases can each be wrong. A representative audit draws from all four, with additional deliberate oversampling of rare or important cases where justified.
- •Record selection probabilities: weight = 1/p lets population estimates correct for deliberate oversampling. Weighting cannot recover cases — or outcomes — that were never observable.
- •Pending is not negative: a case awaiting its label or its delayed business outcome stays pending. It is never counted as a miss or silently dropped from the report.
- •Blind the review: for reference audits, hide the analyzer's prediction until the reviewer has formed an initial judgment.
- •Keep audit content yours: labeled audit samples stay in your environment — they are not shared into Sentiqra development datasets unless your commitments explicitly permit it.
The sentiqra-audit.py toolkit implements this contract: plan draws the deterministic stratified sample (with --blind review support), and report emits a quality report that discloses the sampling design, missing and pending labels, the population it can speak for — and emits full-population accuracy only when every stratum contributed labels.
Trust separation — public site vs. authenticated console
Browser storage is shared within an origin, so hosting applications with different security needs on one origin is a known risk pattern (OWASP). Today the marketing pages, console, and API gateway all live on sentiqra.tech; the isolation that protects the console is enforced in the browser surface itself:
- •Host-only session cookie:
__Host-sentiqra-sessionis Secure, HttpOnly, SameSite=Lax, Path=/ with no Domain attribute — the__Host-prefix means no sibling subdomain can set or overwrite it. - •No third-party scripts on sensitive surfaces: CSP allows scripts only from
'self'(nonce-bound) and Stripe'sjs.stripe.com— there are no tag managers or marketing pixels anywhere, and the allowlist is the enforcement point if one is ever proposed. - •No sensitive browser storage: localStorage/sessionStorage carry only theme, analytics consent, and a UX-only rate-limit note — never credentials, API keys, or analysis content. There is no service worker.
- •No ambient cross-origin access: CORS grants no origins; API clients authenticate with Bearer tokens, not cookies, so cross-origin browser reads are denied by default.
If third-party marketing scripts or console growth justifies it, the planned split is sentiqra.tech (marketing), console.sentiqra.tech (console), api.sentiqra.tech (API) — a proposed structure, not the current deployment. A console move re-authenticates users once but never breaks Bearer API clients (no cookies involved). Subdomain separation does not remove the need for CSRF protection or server-side authorization — both stay mandatory either way.
Production checklist
Sentiqra provides the signals. Your system owns the decision - and the evidence trail behind it.