← PHI Gate / API
Get a token

Driving PHI Gate from your own code

Everything the page does, over HTTP. Base URL https://api.skillsafe.ai/v1/app-api. Every response is the same envelope — {"data": …} on success, {"error": {"code", "message", "details"}} on failure — and every request carries Authorization: Bearer <token> and Content-Type: application/json.

Three things that are easy to get wrong

1. There is no X-App-Slug header. The slug appears in the POST /guest body and nowhere else. A request carrying that header still returns 200, which is exactly why the mistake survives.

2. The run body is the input object itself. Do not wrap it in an input key. A wrapped body returns 200 with a plausible-looking hold, the model never sees task or facts, and you are billed for a run against a payload the prompt cannot read. There is no error to catch. Comparing estimate holds will not reliably tell you either — the two can come back byte-identical.

3. To check your payload is really landing, send the same body with facts removed and confirm the hold drops materially. If it does not move, your facts are not being priced, which means they are not being read.

The four lanes

One work object — the pasted module — and four reviews over it, selected by the task field. task is the field to get right first: it routes the whole run. If it is absent or unrecognised the model picks the closest lane, sets lane_inferred to true and names the lane it chose, rather than blending two contracts.

tasklanewhat it answers
phiPatient-data exposure auditWhich of the eighteen Safe Harbor identifiers this module holds, where each one goes, and whether the redactor you already have covers it.
hipaaHIPAA Security Rule gateA determination per safeguard - present, partial, absent, asserted-only or contradicted - plus the business-associate and retention decisions.
emrEMR workflow clinical-safety reviewWhat a clinician can do in this module that they should not be able to, and the remediation in the order it has to land.
cdssDecision-support alert reviewWhether a clinician would trust these alerts, and what a missing observation does to a clinical score.

The hold differs per lane, because the prompt sections and output caps differ. Estimate the lane you are about to run, never a different one.

Errors

HTTPerror.codewhat it means here
400VALIDATION_ERRORThe body was not the shape the app expects. Most often facts is missing, or the whole input was wrapped in an input key - it must not be.
401UNAUTHENTICATEDNo token, or a token this app does not accept. Mint a guest token, or sign in for a personal one.
402PAYMENT_REQUIREDThe balance is below min_credits for this run. /estimate is free and tells you the hold in advance, so a client that estimates first never sees this.
403FORBIDDENA guest token on a metered path with sponsorship off. All four lanes are metered.
404NOT_FOUNDWrong slug, or a job id that does not belong to this app.
409CONFLICTAn Idempotency-Key that was already used with a different body.
429RATE_LIMITEDBack off. /similar on the history collection is 30/min per IP; the other data endpoints share 120/min.
503UNAVAILABLEThe model tier is briefly unavailable. Retry with backoff, reusing the same idempotency key.

1 Get a token

A guest token is enough for /me and /estimate. All four lanes are metered, so running one needs a personal token — the token page will mint, reveal and copy yours without a developer console.

2 Check the session and the balance

3 The input shape, one worked example per lane

facts is the measurement the browser computed, and it is required — the module source is never sent. It carries flags_total (read off the flags array itself, so a count can never contradict the array), must_reconcile (the critical and high flag ids the reply must account for one by one), totals, files, identifier_summary, identifier_sample, leak_sample, safeguards, clinical_signals, clinical_scores, business_associates, retention_policy, flags, code_excerpts and sampling.

Each row in leak_sample carries three fields worth reading together: mitigation (none / file / inline) says whether a redactor stands in the way and is what sets the severity; reached_via names the local variable an identifier travelled through when it was not written literally at the call site; and crosses_boundary says whether the value left the host at all. That last one is the question a reviewer asks first, because an identifier sent to an outside party is a disclosure and needs a business associate agreement under 45 CFR 164.308(b)(1), while the same identifier in a local log file does not.

identifier_sample and leak_sample are samples, and sampling states the population, how many rows were sent and how many files and categories the draw covered. The draw is a golden-ratio low-discrepancy sequence, not an every-k-th stride: field declarations run in file order, so a stride can resonate with the per-file field count and land on one file repeatedly while reporting a broad sample.

task: "phi" — Patient-data exposure audit

Which of the eighteen Safe Harbor identifiers this module holds, where each one goes, and whether the redactor you already have covers it.

{
  "task": "phi",
  "project_label": "meridian-ehr / medications service",
  "policy_notes": "All PHI is encrypted at rest with AES-256 and every request between services is TLS-only. PHI is never written to application logs. There is no audit trail for read access yet. Business associate agreements are executed with Acme Reference Labs and Bright Imaging Partners; Northwind Analytics is still pending. Clinical records are retained for about seven years and then destroyed.",
  "notes": "Ships to one pilot hospital in about four weeks.",
  "facts": {
    "...": "the browser measurement, unchanged across lanes"
  }
}

task: "hipaa" — HIPAA Security Rule gate

A determination per safeguard - present, partial, absent, asserted-only or contradicted - plus the business-associate and retention decisions.

{
  "task": "hipaa",
  "project_label": "meridian-ehr / medications service",
  "policy_notes": "All PHI is encrypted at rest with AES-256 and every request between services is TLS-only. PHI is never written to application logs. There is no audit trail for read access yet. Business associate agreements are executed with Acme Reference Labs and Bright Imaging Partners; Northwind Analytics is still pending. Clinical records are retained for about seven years and then destroyed.",
  "notes": "Ships to one pilot hospital in about four weeks.",
  "facts": {
    "...": "the browser measurement, unchanged across lanes"
  }
}

task: "emr" — EMR workflow clinical-safety review

What a clinician can do in this module that they should not be able to, and the remediation in the order it has to land.

{
  "task": "emr",
  "project_label": "meridian-ehr / medications service",
  "policy_notes": "All PHI is encrypted at rest with AES-256 and every request between services is TLS-only. PHI is never written to application logs. There is no audit trail for read access yet. Business associate agreements are executed with Acme Reference Labs and Bright Imaging Partners; Northwind Analytics is still pending. Clinical records are retained for about seven years and then destroyed.",
  "notes": "Ships to one pilot hospital in about four weeks.",
  "facts": {
    "...": "the browser measurement, unchanged across lanes"
  }
}

task: "cdss" — Decision-support alert review

Whether a clinician would trust these alerts, and what a missing observation does to a clinical score.

{
  "task": "cdss",
  "project_label": "meridian-ehr / medications service",
  "policy_notes": "All PHI is encrypted at rest with AES-256 and every request between services is TLS-only. PHI is never written to application logs. There is no audit trail for read access yet. Business associate agreements are executed with Acme Reference Labs and Bright Imaging Partners; Northwind Analytics is still pending. Clinical records are retained for about seven years and then destroyed.",
  "notes": "Ships to one pilot hospital in about four weeks.",
  "facts": {
    "...": "the browser measurement, unchanged across lanes"
  }
}

Building facts yourself is supported but rarely what you want — the scanner that produces it is a few hundred lines of classification and mitigation logic. The practical path is to paste into the page once, use Whole measurement .json on the free panel, and feed that object straight back in as facts.

4 Estimate first - it is free

Assert model_alias == "gpt-terra" and markup_bps == 1000 here. That is the authoritative proof the app is bound to the right model at the right markup, and it costs nothing. Present hold_credits as reserved, never as the price.

5 Run, and poll the job

Always send an Idempotency-Key, and fold the lane into it: two lanes over the same measurement are two distinct runs and must not collide on one key. If you retry a malformed reply, give the retry its own key suffix — a replayed key is answered with the original job even when the body differs, so without the suffix the retry is served the very reply it exists to replace.

6 Or stream it

Trust the done payload over the accumulated deltas; the stream can drop the tail. If it dies mid-object, close the open brackets and render what parsed rather than discarding the whole reply — those findings were paid for.

7 The output contract

One JSON object. The envelope is identical for all four lanes; only body differs. This is the shape the page's own parser enforces, so a client that follows it sees exactly what the page sees.

{
  "lane": "phi | hipaa | emr | cdss",
  "lane_inferred": false,
  "title": "one line naming what was reviewed",
  "posture": "compliant | watch | at-risk | critical",
  "verdict": "one sentence a reviewer could act on",
  "summary": "3-6 sentences",
  "headline_numbers": [
    {
      "label": "of 18 identifier categories",
      "value": "14"
    }
  ],
  "findings": [
    {
      "id": "PG-1",
      "title": "...",
      "severity": "critical | high | medium | low",
      "citation": "45 CFR 164.312(e)(1)",
      "path": "services/analytics/sink.py",
      "identifier": "mrn",
      "evidence": "quoted from facts",
      "why": "...",
      "patient_impact": "...",
      "mitigated_by": "...",
      "action": "...",
      "effort": "hours | days | weeks | quarter | unknown"
    }
  ],
  "coverage_check": [
    {
      "flag_id": "LEAK-01",
      "status": "confirmed | set-aside | superseded | duplicate",
      "finding_id": "PG-1",
      "note": ""
    }
  ],
  "artifact": {
    "kind": "none | text | markdown",
    "filename": "...",
    "content": "..."
  },
  "next_lane": {
    "lane": "hipaa",
    "reason": "..."
  },
  "assumptions": [
    "..."
  ],
  "open_questions": [
    "..."
  ],
  "body": {
    "...": "per-lane, below"
  }
}

Per-lane body

{
  "phi": {
    "exposures": [
      {
        "identifier": "mrn",
        "category": "mrn",
        "sinks": [
          "log",
          "url"
        ],
        "reaches": "...",
        "covered_by_redactor": false,
        "fix": "..."
      }
    ],
    "deidentification": {
      "safe_harbor_blocked_by": [
        "names",
        "medical record numbers"
      ],
      "verdict": "...",
      "route": "..."
    },
    "redactor_gaps": [
      "..."
    ]
  },
  "hipaa": {
    "determinations": [
      {
        "safeguard": "Transmission security",
        "citation": "45 CFR 164.312(e)(1)",
        "measured": "contradicted",
        "determination": "contradicted",
        "agrees_with_measurement": true,
        "evidence_needed": "...",
        "note": "..."
      }
    ],
    "business_associates": [
      {
        "name": "Northwind Analytics",
        "covered": false,
        "action": "..."
      }
    ],
    "blocking": [
      "..."
    ],
    "needs_counsel": [
      "..."
    ]
  },
  "emr": {
    "workflow_steps": [
      {
        "step": "...",
        "stage": "order",
        "guard_present": false,
        "what_can_go_wrong": "...",
        "required_check": "..."
      }
    ],
    "remediation": [
      {
        "seq": 1,
        "change": "...",
        "reason": "...",
        "blocks_release": true,
        "effort": "days",
        "test": "..."
      }
    ],
    "safe_to_ship": [
      "..."
    ]
  },
  "cdss": {
    "alerts": [
      {
        "alert": "...",
        "trigger": "...",
        "tier": "none",
        "interruptive": true,
        "override_captured": false,
        "suppression": "none",
        "fatigue_risk": "high",
        "fix": "..."
      }
    ],
    "scores": [
      {
        "score": "NEWS2",
        "missing_observation_behaviour": "...",
        "bands_documented": false,
        "risk": "critical",
        "fix": "..."
      }
    ],
    "trust_verdict": "...",
    "would_be_ignored_because": [
      "..."
    ]
  }
}
The reconciliation contract

Every id in facts.must_reconcile gets exactly one coverage_check row. A flag the reply neither raises nor explains away is shown on the page as unreviewed — not as cleared — and a coverage_check row naming a flag_id the measurement never produced is reported as unsupported. Both checks run client-side against the measurement, so they hold whether the reply came from the page or from your own client.

must_reconcile spans the whole measurement, not just the lane you asked for. A cdss run is still handed identifier and safeguard flags; the expected handling is a set-aside row naming the lane that owns each one.