HTTP API reference¶
The InCheck public surface is a thin REST API behind a single gateway. Two resource families:
/documents/*— manage Pods, your per-tenant document collections./chat— two-mode chat. EMS mode (omitorg_id) answers from general EMS knowledge; unified mode (setorg_idto a Pod) grounds the answer in your documents.
Every request needs Authorization: Bearer <your-api-key>.
| Environment | Base URL |
|---|---|
production |
https://api.incheck.ai |
staging |
https://api-acceptance.incheck.ai |
Namespace scoping
Your API key is bound to one InCheck organization. The gateway
derives a slug-form namespace (lowercase alphanumeric) from
your subdomain, and every org_id you push to or chat against
must start with <namespace>_. Cross-namespace requests are
rejected with 403.
Documents¶
POST /documents/initiate-upload¶
Request a presigned S3 POST for one or more files.
Body
| Field | Type | Notes |
|---|---|---|
org_id |
string | Hierarchical ^[a-z0-9]+(_[a-z0-9]+){1,9}$; first segment must be your namespace. |
filenames |
string[] | 1–20 entries. Allowed extensions: .pdf .docx .pptx .xlsx. |
batch_size |
integer | Chunk-batch size, 1–20. Default 6. |
Response 201
{
"job_id": "0c9de398-…",
"org_name": "acme",
"org_id": "acme_dispatch",
"version": "20260511_204108",
"s3_folder": "acme/acme_dispatch/2026/05/11/20260511_204108",
"upload_urls": [
{
"filename": "sop.pdf",
"upload_url": "https://…s3.amazonaws.com/",
"upload_fields": { "key": "…", "x-amz-algorithm": "…", "…": "…" },
"s3_key": "acme/acme_dispatch/.../originals/sop.pdf"
}
],
"expires_in": 3600,
"created_at": "2026-05-11T20:41:08Z"
}
To upload, POST the file to upload_url with every field from
upload_fields as multipart form data, plus file as the binary
payload. S3 returns 204 No Content on success.
POST /documents/complete-upload¶
Confirm the files landed and trigger processing.
Body
Response 200
{
"job_id": "0c9de398-…",
"status": "pending",
"org_name": "acme",
"org_id": "acme_dispatch",
"version": "20260511_204108",
"s3_folder": "acme/acme_dispatch/...",
"files_confirmed": ["sop.pdf"],
"created_at": "2026-05-11T20:41:10Z"
}
PUT /documents/orgs/{org_id}/documents/initiate¶
Add or replace files in an existing org_id. Files already in the
current version that you don't list are kept; files you list are
overwritten.
Body
Response 201
Returns the same shape as initiate-upload, plus
existing_documents_to_keep.
PUT /documents/orgs/{org_id}/documents/complete¶
Confirm an update and trigger processing of the merged version.
Body
GET /documents/orgs¶
List the org_ids you own.
Response 200
{
"org_ids": [
{
"org_id": "acme_dispatch",
"org_name": "acme",
"current_version": "20260511_204108",
"document_count": 1,
"last_updated": "2026-05-11T20:54:20Z"
}
],
"total_count": 1,
"filtered_by": "acme"
}
filtered_by is your derived namespace — use it to validate config
before issuing other calls.
GET /documents/orgs/{org_id}/documents¶
List documents in the current version, with short-lived presigned GET URLs.
Response 200
{
"org_id": "acme_dispatch",
"version": "20260511_204108",
"document_count": 1,
"documents": [
{
"filename": "sop.pdf",
"size_bytes": 4364,
"last_modified": "2026-05-11T20:41:10Z",
"presigned_url": "https://…",
"url_expires_in": 3600
}
],
"job_id": "…",
"s3_folder": "acme/acme_dispatch/..."
}
GET /documents/orgs/{org_id}/version¶
Returns the current version pointer for an org_id.
DELETE /documents/orgs/{org_id}/versions/{version}¶
Delete a specific version. Irreversible.
DELETE /documents/orgs/{org_id}¶
Delete the entire org_id and every version under it. Irreversible.
GET /documents/job/{job_id}¶
Status snapshot for a processing job.
Response 200
{
"job_id": "0c9de398-…",
"status": "completed",
"org_name": "acme",
"org_id": "acme_dispatch",
"version": "20260511_204108",
"progress": {
"total_documents": 1,
"total_pages": 3,
"processed_pages": 3
},
"error": null,
"created_at": "2026-05-11T20:41:10Z",
"completed_at": "2026-05-11T20:43:00Z"
}
status values: initiated, pending, processing, completed,
failed. Poll until terminal; typical small-PDF runs take ~90–120
seconds.
Metadata¶
GET /states-and-scopes¶
Return the canonical list of state and scope values that /chat
accepts. The list is owned by the gateway and editable server-side —
fetch it at startup (or on demand) instead of hard-coding values that
might drift.
Response 200
{
"default_state": "California-LAC",
"default_scope": "ALS",
"states": [
{"value": "California-LAC", "label": "California - LA County"},
{"value": "California-OC", "label": "California - Orange County"}
],
"scopes": [
{"value": "ALS", "label": "ALS"},
{"value": "BLS", "label": "BLS"}
],
"scopes_by_state": {
"_default": ["ALS", "BLS", "AEMT", "CCT"],
"California-LAC": ["ALS", "BLS"]
}
}
| Field | Type | Notes |
|---|---|---|
default_state |
string | Sensible default for a state picker. |
default_scope |
string | Sensible default for a scope picker. |
states |
object[] | Every valid state value, with a human-readable label. |
scopes |
object[] | Every valid scope value, with a human-readable label. |
scopes_by_state |
object | Map of state → allowed scopes. Use the "_default" key as the fallback for states not listed explicitly. |
Send value, not label
Only the value field is part of the wire contract — that's what
you pass on /chat's state and scope fields. label is for
human display (UI dropdowns, picker text) and is never accepted
by the API.
Chat¶
POST /chat¶
Send a chat message. Two modes:
- EMS mode (no
org_idin the body): the model answers from general EMS knowledge underscope/state. No retrieval. - Unified mode (
org_idset to one or more Pods you've onboarded): the model answers grounded in the documents in those Pods. Pass a single string to query one Pod, or a list of strings to fan retrieval across multiple Pods in one request.
Body — EMS mode
{
"conversation_id": "ems-1",
"user_id": "alice@hospital.org",
"streaming": false,
"content": "Adult dose of atropine for symptomatic bradycardia?",
"scope": "ALS",
"state": "California-LAC"
}
Body — Unified mode (single Pod)
{
"conversation_id": "uni-1",
"user_id": "alice@hospital.org",
"org_id": "acme_dispatch",
"streaming": false,
"content": "Summarize our dispatch escalation SOP.",
"scope": "ALS",
"state": "California-LAC"
}
Body — Unified mode (multi-Pod fan-out)
{
"conversation_id": "uni-2",
"user_id": "alice@hospital.org",
"org_id": ["acme_dispatch", "acme_wilderness"],
"streaming": false,
"content": "Compare hazmat escalation between dispatch and wilderness ops.",
"scope": "ALS",
"state": "California-LAC"
}
Body — multi-turn (works in both modes)
Carry prior turns as messages, the OpenAI/Anthropic Messages shape.
The current user turn stays in content and is appended by the
gateway before forwarding upstream.
{
"conversation_id": "uni-3",
"user_id": "alice@hospital.org",
"org_id": "acme_dispatch",
"streaming": false,
"content": "And for a 6-year-old?",
"scope": "ALS",
"state": "California-LAC",
"messages": [
{"role": "user", "content": "Adult atropine dose for bradycardia?"},
{"role": "assistant", "content": "1 mg IV/IO q3–5 min, max 3 mg."}
]
}
| Field | Type | Required | Notes |
|---|---|---|---|
conversation_id |
string | yes | Your conversation identifier (≥3 chars). |
user_id |
string | yes | Free-form end-user id (audit trail only). |
content |
string | yes | The user message (1–10000 chars). |
scope |
string | yes | EMS scope — "ALS", "BLS", etc. Fetch the full enumeration from GET /states-and-scopes. |
state |
string | yes | State / jurisdiction value for state-specific protocols. Fetch the full enumeration from GET /states-and-scopes. |
org_id |
string | string[] | no | If present → unified mode. Hierarchical; first segment of each id must be your namespace. Pass a list to fan retrieval across multiple Pods. Omit or pass an empty list → EMS mode. |
streaming |
bool | no (default true) |
When false, response is one final SSE event; when true, streamed. |
messages |
object[] | no | Prior turns as {role: "user" \| "assistant", content: string}. Must alternate starting with user and ending with assistant. Mutually exclusive with conversation_hx (sending both → 422). |
conversation_hx |
string | null | no | Deprecated. Legacy single-string history. Prefer messages. |
Response 200 (text/event-stream)
When streaming=true you receive many data: chunks before the final
{"type": "complete"} marker. When streaming=false you still receive
SSE — but the content is delivered in one event.
Mode hint on errors¶
Missing or invalid required fields come back as 400 with a hint that
spells out the contract — you don't have to consult docs to fix the
call:
{"detail":"scope: Field required; state: Field required.
Required fields: content, conversation_id, user_id, scope, state.
org_id is optional — include it (hierarchical, starting with your
namespace) to run in unified mode with retrieval against your
ingested documents; omit it for EMS mode (general protocol answers)."}
Errors¶
Errors come back as JSON: {"detail": "<message>"}.
| Status | When |
|---|---|
400 |
The body failed schema validation (malformed org_id, etc.) |
401 |
Missing / invalid / revoked API key |
403 |
Namespace mismatch on org_id, or your org isn't configured for the customer API |
404 |
Job, version, or org_id not found |
429 |
Per-key rate limit — respect Retry-After |
5xx |
Upstream failure — safe to retry with backoff |
Rate limits¶
Per-key rate limits are enforced on every endpoint, keyed by your API
key's underlying tenant. 429 responses include a Retry-After header.