# Sweat AI — Deep Investigations Sweat AI is an AI-native BPO for banks and fintechs. Our current wedge is onboarding and fraud queues. This is the contract for its investigation API. Evidence investigation as an API. You state what must be established; the engine plans the work, gathers evidence, applies deterministic validation, and publishes its findings, sources and unresolved gaps. Publication does not include a separate independent review. Read the recorded evidence and limits. Base URL: https://check.getsweat.ai The resource is a CHECK. `/v1/checks/...` is the current surface; `/v1/investigations/...` is a permanent alias for the same handlers, so either path works and neither will be withdrawn. ## Authentication Authorization: Bearer fcrm_ User-Agent: YourApplication/1.0 Send a descriptive User-Agent. The edge currently rejects the default Python urllib user agent with Cloudflare error 1010 before the request reaches application authentication. For Python urllib, set the request header explicitly; ordinary curl and Node fetch clients also work. Inspect the response body to distinguish an edge rejection from the application's scope/authentication errors. An API key belongs to exactly one organisation; investigations you create are filed there. Required scopes: investigations:write create, continue, cancel, recheck investigations:read read an investigation, its events and its result Without a key every endpoint below returns 401 {"error":"unauthenticated"}. A key missing a scope returns 403 {"error":"scope_denied","required":"..."}. Private checks are visible to their owner and workspace administrators; an API key also needs the explicit investigations:review scope to override private ownership. Automated monitor children preserve their originating owner's access. Workspace viewers can read permitted checks but cannot change settings, record review decisions or mutate evidence. Human review decisions require a signed-in owner, administrator or member; API keys cannot impersonate a human reviewer. Only signed-in owners and administrators can change Check settings. ## Create an investigation POST /v1/checks (alias: POST /v1/investigations) Authorization: Bearer fcrm_... Idempotency-Key: Content-Type: application/json { "organization_id": "", "title": "Investigation — Acme Trading Ltd", "objective": { "prompt": "Enhanced due diligence at onboarding for Acme Trading Ltd.", "subject": { "legal_name": "Acme Trading Ltd", "domains": ["acme.example"] } }, "access": [{ "type": "public_web", "mode": "read" }], "constraints": { "max_cost_usd": 20, "max_wall_clock_minutes": 30, "privacy": "organization", "allowed_actions": ["read", "transform", "compute"] }, "deliverables": { "artifacts": ["report"], "verification": "deterministic" } } → 202 { "investigation": { "id": "", "status": "queued", ... }, "run_id": "", "task_id": "", "replayed": false, "status_url": "...", "events_url": "...", "result_url": "..." } Status codes on this endpoint: 202 accepted; a new run was created 200 replay of an Idempotency-Key you already used; `replayed` is true 400 malformed body, or a missing Idempotency-Key 409 that Idempotency-Key was already used for a DIFFERENT request body Notes that matter: - `deliverables.verification: "deterministic"` is the current request contract and the default when omitted. It validates structure and source support; it does not schedule a second worker. An explicit `"independent"` request is rejected with `independent_review_unavailable` because that assurance is unavailable. Published packets record `not_completed` with reason `independent_review_not_run` for the separate-review field. - `constraints.execution_profile` selects `fast` (default, five minutes) or `deep` (29 minutes). The wall-clock limit includes publication. Queue admission is bounded by workspace, account, and shared capacity. - `objective.prompt` is the whole instruction. Say what must be ESTABLISHED, not what to search for; the engine plans its own steps. - `constraints.workflow_scope` can explicitly select `standard_edd`, `focused_research` or `partner_evidence`. Use `focused_research` for bounded factual questions that do not ask for merchant onboarding clearance. `constraints.applicant_requests: "forbidden"` forbids applicant requests. `constraints.source_urls_only` supplies 1–64 exact public HTTPS URLs; embedded credentials and private destinations are rejected. These restrictions are part of the recorded request and persist through continuation and recheck. `objective.questions` optionally names up to 20 questions as `[{"id":"q1","text":"What fact must be established?"}]`; IDs must be unique. - Dates use `YYYY-MM-DD` (UTC midnight) or a full ISO timestamp with seconds and an explicit timezone, such as `2026-09-16T15:30:00Z`. Impossible dates, locale-dependent dates and timezone-free timestamps are rejected. The same rule applies to `objective.as_of` and recheck `as_of`. A deadline may also be a positive hours/minutes/seconds duration such as `PT5M`. Supplied `inputs` must be an array, including on continuation. Required text must remain nonempty after normalization. - `subject.legal_name` and `subject.domains` are claims the investigation TESTS, never assumptions it inherits. A name that appears in a hostname is not evidence of control. - Give the engine everything the applicant supplied, in the place it reads it. `subject.legal_name` and `trade_names` supply company screening names — no declared name, no company screen. `subject.jurisdiction` (`US-DE`, `US-NY`, `GB`…) and `subject.registration_number` choose the registry. The subject's website binds as first-party evidence only as an input with a role: `"inputs": [{ "type": "url", "url": "https://acme.example/", "role": "subject_url" }]` — this works without a legal name; `subject.domains` needs one. Without a bound host the engine records the subject's own site as "source unavailable" and never cites it. Declare people to screen with `"guidance": { "known_context": ["principal: Full Name"] }`. A principal declaration requests a name screen; it does not establish their role or ownership. General prose in `claims_to_test` is a claim to research, not an explicit principal screening declaration. - Documents the applicant supplied (formation certificate, operating agreement, cap table) ride in the SAME run as public-web research. Upload each one first — `POST /v1/investigation-assets` returns an asset id and a signed upload target; PUT the bytes there, then `POST /v1/investigation-assets/{id}/complete` — and reference it as `"inputs": [{ "type": "asset", "asset_id": "", "role": "subject" }]`. The engine keeps the two sides apart per model session: a document is read only through the id-scoped artifact tool on a private agent with no web tool, public-web cells never see the document or its storage URL, and a packet citing a storage URL as a web source is refused. PDFs are read as extracted text (poppler); a scanned, image-only PDF is stored but unreadable and the evidence manifest says so (`text_extraction`). - `GET /v1/investigations/{id}/requirements?provider=stripe_connect` reads the provider's onboarding requirements off the published packet: Stripe Connect's own field paths (`company.name`, `company.tax_id`, `company.owners_provided`, `representative`…) each marked satisfied, partial (on the applicant's word only), missing, discrepant or platform_collected, with the claim and citation ids behind it and the one thing to ask the applicant for. Computed on read; nothing is submitted to the provider. 202 until a packet is published. - `GET /v1/investigations/{id}/requirements?provider=bridge` explicitly selects the Bridge KYB evidence review. B01–B22 index the supplied onboarding guide and documented updates; they are not Bridge API field names. The review uses subject-matched findings and captured, hash-bound citations, excludes policy documents as evidence about the applicant, and returns partial, missing or discrepant status with evidence links and limits. Public findings do not complete protected identity checks, ownership attestations or tax-identifier verification. This is not provider approval and does not submit an application. Choose the provider explicitly; the compatibility default remains `stripe_connect`. - `Idempotency-Key` is REQUIRED, not optional: without one the request is rejected 400. Reusing a key returns the same investigation with 200 instead of starting a second paid run. A previously committed key whose old input no longer passes validation returns `409 legacy_receipt_requires_review` with an authorized existing case ID; no new work is admitted. Read that case before intentionally using a new key. - Cost is checked before new work using estimated invocation reservations. Settled provider usage can differ, so `max_cost_usd` is not an exact billing guarantee. Read the recorded usage and boundary or `run_failure.code`. - `max_wall_clock_minutes` covers the whole run from its recorded start, including retries. The worker stops scheduling and aborts active research and model work at the earliest run or explicit deadline, reserving time for synthesis and publication. Process shutdown, network settlement and terminal state propagation are not instantaneous. An elapsed absolute deadline rejects new admissions; an already committed idempotent receipt can still replay. A terminal packet does not establish that all requested work finished. - Every run costs money. There is no free or sandbox mode. ## Be called back instead of polling Send a callback with the create request and we POST to it when the case reaches a state you would wait for. POST /v1/checks { ..., "callback_url": "https://your.app/hooks/sweat", "callback_secret": "<16-200 chars, optional>", "external_ref": "" } The response echoes `callback_set: true` and your `external_ref`; the secret is never echoed by any route. `callback_url` must be an absolute https URL on a public host. Events: investigation.completed a packet was published investigation.failed the run failed investigation.cancelled the case was cancelled investigation.decision_recorded a reviewer decided, or a workspace rule did investigation.changed a scheduled re-check found something different Each POST carries the event, your `external_ref`, the case id and status, the result id and a link — never the packet. Read the evidence back through the API under your own key. X-Foresyn-Event the event name X-Foresyn-Delivery a UUID, the same across the one retry X-Foresyn-Timestamp unix seconds, also inside the signature X-Foresyn-Signature t=,v1= Verify it (the signed string is the timestamp, a dot, then the exact body): const [, t, v1] = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(req.header('X-Foresyn-Signature')) const expected = crypto.createHmac('sha256', SECRET).update(`${t}.${rawBody}`).digest('hex') const ok = crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected)) Delivery is one attempt plus one retry on a network error or 5xx, ten seconds each. It is a nudge to read the case, not the record: a delivery you never received is not a case that never finished. Poll if it matters. ## Poll it GET /v1/investigations/{id} the case, its latest packet and the recent events (compact; add compact=false for every event and include_history=true for prior runs) Authorization: Bearer fcrm_... → 200 { "investigation": { "id", "status", "current_run_id", "constraints", ... }, "plan_revisions": [ { "revision", "plan": { "steps": [...] } } ], "events": [ { "seq", "kind", "stage", "message", "created_at" } ], "result": null | , "run_failure": null | { "code", "role", ... } } Poll every 5–15s. Events are append-only and carry a monotonic `seq`, so you can resume from the last one you saw rather than re-reading the whole list: GET /v1/investigations/{id}/events?after=&limit=100 → 200 { "events": [...], "next_cursor": } Related reads: GET /v1/investigations/{id}/events the event stream alone GET /v1/investigations/{id}/result the packet alone, once one is published — see below for what "published" does and does not mean ## Statuses queued accepted, not yet started planning deciding what would establish the objective investigating gathering and testing evidence waiting_for_input BLOCKING — it needs an answer or a record from you verifying legacy independent-review workflow status ready TERMINAL — packet published with no recorded open gaps; this does not mean independently reviewed complete_with_gaps TERMINAL — packet published, gaps named in the packet; check verification.status before relying on it failed TERMINAL — see run_failure.code cancelled TERMINAL — stopped before finishing; run_failure.code says by whom or by what (see "When a run stops") Terminal: ready, complete_with_gaps, failed, cancelled. Stop polling there. `complete_with_gaps` means publication finished with recorded gaps; it does not establish that the requested work succeeded. Read `answer.question_outcomes`, requested-source outcomes, findings and processing diagnostics. A truthful evidence limitation can be useful, while a readable source discarded during processing or an answer missing a requested fact still needs investigation. Check every requested question against its actual evidence before accepting the result. Neither terminal status nor an empty gap list supplies that review. ## Answer a blocking question When status is `waiting_for_input`, the newest events say what is missing. POST /v1/investigations/{id}/continue Authorization: Bearer fcrm_... Idempotency-Key: { "organization_id": "", "message": "The applicant is the Beirut entity, LB-402118." } When the run is waiting, it resumes from where it paused and prior evidence is preserved. When it is NOT waiting — status `planning`, `investigating` or `verifying` — a continuation CANCELS the current run and starts a new one from the plan, with your message folded in (`mutation.restarted` is true in the response). That is a second paid run. If you only meant to stop, cancel; if you only meant to add context, wait for the run to finish and `recheck`. ## Stop or re-run POST /v1/investigations/{id}/cancel stop now, keep the evidence so far POST /v1/investigations/{id}/recheck run it again against current records A recheck or terminal-case continuation returns `409 investigation_family_busy` if another run in the same case family is still open, including waiting for input. Monitoring uses the same admission lock and requires an enabled cadence and a current approval; a newer human decision prevents a stale monitor admission. ## Human decisions and applicant requests GET /v1/investigations/{id}/decision → { "decisions": [...], "can_review": true, "revision": 0 } The authenticated browser records a decision with: POST /v1/investigations/{id}/decision { "organization_id": "", "contract_version": 2, "decision": "request_documents", "result_id": "", "expected_decision_revision": 0, "idempotency_key": "", "note": "Internal reviewer rationale", "applicant_message": "Message the applicant is allowed to read", "requested": ["The specific record needed"], "applicant_email": "" } Choices are approve, decline, escalate and request_documents. Decisions bind to the currently published result and current decision revision; stale submissions return 409. Retry the same intent with the same key to read its receipt. Changing the intent requires a new key. Request documents requires contract_version 2. `note` remains internal; only `applicant_message` appears on the public page. A request without applicant_email creates a link without sending email. Delivery status is sent only after provider confirmation; pending or unconfirmed is not proof of delivery. Replaying a receipt never intentionally sends a second email. A request expires after 30 days, and a later decision supersedes its link. An authorized reviewer may also POST `/v1/investigations/{id}/requests/{request_id}/revoke`. Revocation is idempotent. Public `/r/{token}` pages show only active requests and applicant-facing fields; closed, expired and unknown tokens share the same unavailable response. The page provides contact instructions; it does not accept uploads or resume a run. ## Publication and recorded review A packet is served by `GET …/result` as soon as the investigation is `ready` or `complete_with_gaps`. That is not the same as verified. Read `result.verification.status`: not_completed no separate independent review was completed passed historical recorded review; inspect checked_claims passed_with_caveats historical recorded review with residual_risks pending legacy candidate placeholder, not a promised review Current candidate submission publishes directly and sets `verification.reason: "independent_review_not_run"`. Its findings are the investigator's recorded conclusions. Deterministic validation and source checks are not a separate independent review. Historical boundary packets may instead name a cost, time or review failure in `verification.reason`. A published packet can be useful without an independent review. Assess its claims, sources, gaps and coverage; do not describe publication as verification. ## Reading the packet `result` is a structured packet, not prose. The parts an agent should use: assessment OPTIONAL typed recommendation — see below answer.claim_ids decision-grade findings supporting the answer answer.context_claim_ids OPTIONAL admitted source-context findings; their citations support the qualified context summary findings[] each with supporting_citations[] pointing at sources[] sources[] each with citation_id, url, content_hash, captured_at gaps[] what could not be established, and why entities resolved graph, with unresolved nodes LABELLED provenance model, prompt and policy versions, costs ### assessment — a recommended disposition for your decision Prose containing a verdict is rejected on submission, so the engine expresses a view on disposition in exactly one typed place: assessment.recommended_disposition approve | approve_with_conditions | escalate | pause_pending_documents | decline | not_assessed assessment.overall_risk unknown | low | moderate | high | critical assessment.identity_confidence high | medium | low assessment.hard_stop boolean, optional assessment.primary_reason one sentence saying why assessment.conditions[] required when approve_with_conditions Read it as an assessment and recommended disposition for your decision; the final decision is always yours. The engine never makes the decision. Cross-field rules the packet enforces, which you can rely on: - `not_assessed` is a neutral focused-research result: it answers the requested factual questions without making a merchant onboarding recommendation - a bare `approve` cannot coexist with any deferred check — outstanding work is what `approve_with_conditions` exists to carry - `identity_confidence: "high"` requires a government-registry or official regulatory citation; a subject's own site cannot establish it - `hard_stop: true` confines the disposition to escalate, pause or decline - `overall_risk: "unknown"` permits only pause or escalate, never approval or decline; missing coverage does not establish a risk level `assessment` can be absent. Absent is not `low` or `unknown`; never invent an assessment. A bounded run can record an `unknown` risk assessment with a pause or escalation while preserving useful source-context findings. But PRESENT does not mean evidenced, and you must check rather than assume. Stored packets retain their original content. Before you act on a disposition, inspect the packet in your hand: answer.claim_ids empty means no decision-grade claim underwrites it answer.context_claim_ids source observations, not disposition authority sources empty means no evidence was admitted at all Context findings can substantiate a qualified public-source observation while leaving identity, ownership or risk unresolved. They do not upgrade a pause or escalation into a risk finding, and cannot justify approval or decline. Rules the packet keeps, which you can rely on when consuming it: - A claim without a citation is not present. If it is stated, it is sourced. - An allegation is distinguished from a finding. A dismissed charge is reported as dismissed, not as a hit. - A source that was reached and holds no record is recorded differently from a source that could not be reached. Absence of evidence is never rendered as evidence of absence. - Citations address the stable published location of a record, not the transport URL used to fetch it. They stay resolvable. ## Coverage, stated plainly What an investigation can reach today. Nothing here is a promise about a particular run: coverage is what the engine may dispatch, not what it found. sanctions Configured name screens can read the OFAC SDN primary and alternate-name files, UN Security Council Consolidated, current UK Sanctions List (FCDO) and EU Consolidated Financial Sanctions List. OFAC's separate consolidated non-SDN lists are not included in this screen. Each list appears as a checks_performed[].screenings[] record (list, capture date + content hash, rows read, matches, completeness); a list that could not be read is recorded as partial/not_run with the reason, never silently. Matching is whole-name equality after normalisation and against the names recorded for the screen; a partial overlap is a labelled lead, never a match. registries US: Delaware (ICIS, through a rendered browser session), New York (DOS public inquiry API) and SEC EDGAR (by CIK, ticker or exact legal name) are dispatched from the declared jurisdiction; other states are reached only through public web fetches. UK Companies House (with persons with significant control) needs a company number or a GB jurisdiction. None is guaranteed to be reached. PEP STRUCTURED, NOT SCREENED. There is no licence-clear PEP database behind this. A PEP claim is only accepted when the packet records the person, the office and the relationship with citations for the office linkage; a same-name profile with no evidenced office scores zero. adverse media STRUCTURED, NOT LICENSED. Fetched articles are clustered into stories with an independent_publisher_count, so fifteen syndicated copies of one wire story count as one source. There is no commercial aggregator behind it. Two things follow that an agent must not get wrong: - A zero-match screen is a recorded observation, never a conclusion about the subject. "No match on the lists we read" is not "unsanctioned". - `status: "ready"` means the dossier has no open gaps. It does not mean the subject is acceptable, and it is not a clearance. ## When a run stops `run_failure.code` is an OPEN vocabulary, not an enum. Branch on the ones you care about and treat anything unrecognised as a terminal stop — do not assume a code you have not seen means the run is still alive. These are the codes observed in production: Ceilings — the run was stopped by a limit you set: deadline_exceeded hit max_wall_clock_minutes cost_budget_exhausted hit max_cost_usd investigation_time_budget_exceeded same stop, written by the claim RPC investigation_budget_exceeded same stop, written by the claim RPC Historical verification and rework codes (the standalone verifier is retired): rework_generation_limit the verifier kept rejecting the packet identical_rework_candidate rework returned the packet unchanged candidate_no_longer_admissible the packet stopped validating before the verifier opened it verification_evidence_unavailable the verifier could not reopen the evidence it was asked to re-check Packet contract: invalid_structured_result the draft failed publication validation; this can include evidence and task checks result_contains_decision_language it stated a verdict where an evidence rationale belonged, and was refused invalid_result_checks_performed its checks_performed ledger was rejected invalid_result_observation_boundary observation and inference were not separated invalid_continuation_gap_closure the continuation did not close the gap it claimed to Worker boundaries — the engine refused to continue: forbidden_native_tool_activity it reached for a tool it may not use artifact_read_outside_manifest it read outside the authorised manifest invalid_capability_receipt the capability receipt did not verify producer_claim_limit the research worker restarted too often verifier_claim_limit the verification worker restarted too often openclaw_exit_nonzero the investigator process exited unusably openclaw_timeout the investigator response exceeded its allowed time openclaw_aborted the investigator response was interrupted prompt_envelope_exceeded the evidence exceeded this run's report analysis envelope interaction_invalid_json an interaction payload did not parse unexpected_error unclassified Human stops. These land on `status: "cancelled"`, and the two are NOT interchangeable with the boundary codes above — a cancelled investigation is not necessarily one a person cancelled: operator_cancelled a person stopped it operator_reset a person cleared the run The evidence, plan and events collected before the stop are preserved and readable. A stopped run may also carry a partial `result` packet; inspect its run identity, limits and missing work, and check `verification.status` before you rely on anything in it. A failed run is still worth reading; a packet's presence does not mean the run completed. ## Check a document at upload Separate from investigations: a synchronous answer to "is this the right document for this slot?", for an onboarding form to call the moment a merchant uploads a file. It rejects the documents a partner would refuse — the wrong type (bylaws for a certificate of incorporation), an expired ID, a card without its back, a bill older than 90 days, a name or tax number that does not match what was typed — with a message to show the merchant. GET /v1/document-checks/requirements (no key) requirement sets, slots, sources of each rule POST /v1/document-checks one check, ~12 s GET /v1/document-checks/packages/ what one onboarding still needs Scope: documents:check (a key with investigations:write also works). curl -s https://check.getsweat.ai/v1/document-checks \ -H "Authorization: Bearer $key" -H 'Content-Type: application/json' \ -d '{ "reference": "merchant-4812", "requirement_set": "bridge:KE:limited_company", "slot": "business_formation", "declared": { "legal_name": "Acme Traders Limited" }, "file": "data:application/pdf;base64,…" }' The answer's `check.verdict` is accepted, rejected (show `check.merchant_message` and ask for another file), needs_review (let the merchant continue; a person should look) or unverified (the reader was unreachable; you choose fail-open or fail-closed). Identity slots take `front` and `back` instead of `file`, plus `person`: { key, full_name, roles }. Files are base64 data URIs (PDF, JPEG, PNG, WebP, HEIC, TIFF; 15 MB each). Identifiers in the answer are masked to their last four characters. ## Minimal loop key="fcrm_…" org="" body=$(jq -nc \ --arg org "$org" \ --arg prompt "Enhanced due diligence at onboarding for Acme Trading Ltd." \ '{ organization_id: $org, objective: { prompt: $prompt }, access: [{ type: "public_web", mode: "read" }], constraints: { max_cost_usd: 20, max_wall_clock_minutes: 30 }, deliverables: { artifacts: ["report"], verification: "deterministic" } }') id=$(curl -s https://check.getsweat.ai/v1/investigations \ -H "Authorization: Bearer $key" \ -H "Idempotency-Key: $(uuidgen)" \ -H 'Content-Type: application/json' \ -d "$body" | jq -r .investigation.id) while :; do s=$(curl -s "https://check.getsweat.ai/v1/investigations/$id" \ -H "Authorization: Bearer $key" | jq -r .investigation.status) echo "$s" case "$s" in ready|complete_with_gaps|failed|cancelled) break;; esac sleep 10 done curl -s "https://check.getsweat.ai/v1/investigations/$id/result" \ -H "Authorization: Bearer $key" | jq . ## Getting a key API keys are issued per organisation from the Sweat AI console. If you are an agent and have no key, ask the human you are working for; keys are not self-serve and this page will never issue one.