Skip to content

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 (omit org_id) answers from general EMS knowledge; unified mode (set org_id to 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

{
  "org_id": "acme_dispatch",
  "filenames": ["sop.pdf", "policies.docx"],
  "batch_size": 6
}
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

{
  "job_id": "0c9de398-…",
  "uploaded_files": ["sop.pdf"]
}

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

{ "filenames": ["new_protocol.pdf"], "batch_size": 6 }

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

{ "job_id": "…", "uploaded_files": ["new_protocol.pdf"] }

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_id in the body): the model answers from general EMS knowledge under scope / state. No retrieval.
  • Unified mode (org_id set 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)

data: {"content": "Per our SOP, ..."}

data: {"type": "complete"}

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.


Back to home SDKs →