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.
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.
| task | lane | what it answers |
|---|---|---|
| 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. |
| hipaa | HIPAA Security Rule gate | A determination per safeguard - present, partial, absent, asserted-only or contradicted - plus the business-associate and retention decisions. |
| 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. |
| cdss | Decision-support alert review | Whether 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
| HTTP | error.code | what it means here |
|---|---|---|
| 400 | VALIDATION_ERROR | The 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. |
| 401 | UNAUTHENTICATED | No token, or a token this app does not accept. Mint a guest token, or sign in for a personal one. |
| 402 | PAYMENT_REQUIRED | The 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. |
| 403 | FORBIDDEN | A guest token on a metered path with sponsorship off. All four lanes are metered. |
| 404 | NOT_FOUND | Wrong slug, or a job id that does not belong to this app. |
| 409 | CONFLICT | An Idempotency-Key that was already used with a different body. |
| 429 | RATE_LIMITED | Back off. /similar on the history collection is 30/min per IP; the other data endpoints share 120/min. |
| 503 | UNAVAILABLE | The model tier is briefly unavailable. Retry with backoff, reusing the same idempotency key. |
1 Get a token
# A GUEST token: enough for /me and /estimate, not enough to run a lane.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/guest" \
-H "Content-Type: application/json" \
-d '{"slug":"phi-gate"}'
# -> {"data":{"token":"aut_...","subject_type":"guest"}}
#
# Note what is NOT here: there is no X-App-Slug header. The slug travels in this
# body and nowhere else. A personal token, which every lane needs, comes from
# signing in - see /tokens.html.
export SKILLSAFE_TOKEN="aut_paste_yours_here"
import json, urllib.request
API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from /tokens.html, or the guest call below
def call(path, body=None, token=None, extra_headers=None):
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = "Bearer " + token
headers.update(extra_headers or {})
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(API + path, data=data, headers=headers,
method="POST" if data else "GET")
with urllib.request.urlopen(req) as resp:
return json.load(resp)
guest = call("/guest", {"slug": "phi-gate"})
print(guest["data"]["subject_type"], guest["data"]["token"][:12] + "...")
const API = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from /tokens.html, or the guest call below
async function call(path, body, opts = {}) {
const res = await fetch(API + path, {
method: body ? "POST" : "GET",
headers: {
"Content-Type": "application/json",
...(TOKEN ? { Authorization: `Bearer ${TOKEN}` } : {}),
...(opts.headers || {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const json = await res.json();
if (!res.ok) throw Object.assign(new Error(json.error?.message || res.statusText),
{ status: res.status, code: json.error?.code });
return json.data;
}
const guest = await call("/guest", { slug: "phi-gate" });
TOKEN = guest.token;
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
const api = "https://api.skillsafe.ai/v1/app-api"
func call(path string, body any, token string) (map[string]any, error) {
var rdr io.Reader
method := http.MethodGet
if body != nil {
b, _ := json.Marshal(body)
rdr = bytes.NewReader(b)
method = http.MethodPost
}
req, _ := http.NewRequest(method, api+path, rdr)
req.Header.Set("Content-Type", "application/json")
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var out struct {
Data map[string]any `json:"data"`
Error map[string]any `json:"error"`
}
json.NewDecoder(res.Body).Decode(&out)
if out.Error != nil {
return nil, fmt.Errorf("%v", out.Error["message"])
}
return out.Data, nil
}
func main() {
token := os.Getenv("SKILLSAFE_TOKEN")
if token == "" {
guest, err := call("/guest", map[string]string{"slug": "phi-gate"}, "")
if err != nil {
panic(err)
}
token = guest["token"].(string)
}
fmt.Println("token acquired")
}
import java.net.URI;
import java.net.http.*;
public class PhiGate {
static final String API = "https://api.skillsafe.ai/v1/app-api";
static final HttpClient HTTP = HttpClient.newHttpClient();
static String token = System.getenv("SKILLSAFE_TOKEN"); // or paste "YOUR_TOKEN"
static String call(String path, String body) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(API + path))
.header("Content-Type", "application/json");
if (token != null) b.header("Authorization", "Bearer " + token);
b.method(body == null ? "GET" : "POST",
body == null ? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(body));
HttpResponse<String> res = HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString());
if (res.statusCode() >= 400) throw new RuntimeException(res.statusCode() + " " + res.body());
return res.body();
}
public static void main(String[] args) throws Exception {
if (token == null) {
System.out.println(call("/guest", "{\"slug\":\"phi-gate\"}"));
}
}
}
require "json"
require "net/http"
API = URI("https://api.skillsafe.ai/v1/app-api")
TOKEN = ENV["SKILLSAFE_TOKEN"] || "YOUR_TOKEN"
def call(path, body = nil, token: TOKEN, headers: {})
uri = URI(API.to_s + path)
req = body ? Net::HTTP::Post.new(uri) : Net::HTTP::Get.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{token}" if token
headers.each { |k, v| req[k] = v }
req.body = JSON.generate(body) if body
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
parsed = JSON.parse(res.body)
raise parsed.dig("error", "message") || res.message if res.code.to_i >= 400
parsed["data"]
end
guest = call("/guest", { slug: "phi-gate" }, token: nil)
puts guest["subject_type"]
<?php
const API = "https://api.skillsafe.ai/v1/app-api";
$token = getenv("SKILLSAFE_TOKEN") ?: "YOUR_TOKEN";
function call(string $path, ?array $body = null, ?string $token = null, array $extra = []): array {
$headers = array_merge(["Content-Type: application/json"], $extra);
if ($token) { $headers[] = "Authorization: Bearer " . $token; }
$ch = curl_init(API . $path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POST => $body !== null,
CURLOPT_POSTFIELDS => $body !== null ? json_encode($body) : null,
]);
$raw = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$json = json_decode($raw, true);
if ($code >= 400) { throw new RuntimeException($json["error"]["message"] ?? "HTTP $code"); }
return $json["data"];
}
$guest = call("/guest", ["slug" => "phi-gate"]);
echo $guest["subject_type"], PHP_EOL;
using System.Net.Http.Json;
using System.Text.Json;
var api = "https://api.skillsafe.ai/v1/app-api";
var http = new HttpClient { BaseAddress = new Uri(api + "/") };
var token = Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN";
async Task<JsonElement> Call(string path, object? body = null, string? bearer = null)
{
var req = new HttpRequestMessage(body is null ? HttpMethod.Get : HttpMethod.Post, path.TrimStart('/'));
if (body is not null) req.Content = JsonContent.Create(body);
if (bearer is not null) req.Headers.Authorization = new("Bearer", bearer);
var res = await http.SendAsync(req);
var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
if (!res.IsSuccessStatusCode)
throw new Exception(doc.RootElement.GetProperty("error").GetProperty("message").GetString());
return doc.RootElement.GetProperty("data").Clone();
}
var guest = await Call("guest", new { slug = "phi-gate" });
Console.WriteLine(guest.GetProperty("subject_type").GetString());
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
curl -s "https://api.skillsafe.ai/v1/app-api/me" -H "Authorization: Bearer $SKILLSAFE_TOKEN"
# -> {"data":{"subject_type":"user","username":"...","credits":123456}}
#
# Free. Call it before every run and compare `credits` against the hold from
# /estimate: a 402 after you submit is a client bug, not a user problem.
me = call("/me", token=TOKEN)["data"]
print(me["subject_type"], me.get("credits"))
const me = await call("/me");
console.log(me.subject_type, me.credits);
me, err := call("/me", nil, token)
if err != nil {
panic(err)
}
fmt.Println(me["subject_type"], me["credits"])
System.out.println(call("/me", null));
me = call("/me")
puts "#{me["subject_type"]} #{me["credits"]}"
$me = call("/me", null, $token);
echo $me["subject_type"], " ", $me["credits"], PHP_EOL;
var me = await Call("me", null, token);
Console.WriteLine($"{me.GetProperty("subject_type").GetString()}");
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
# /estimate is FREE. It runs no job, bills nothing, and is the only way to know
# the hold before you commit. Note the body: the input object ITSELF, never
# wrapped in an "input" key.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/estimate" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" \
-d @payload-phi.json
# -> {"data":{"model":"gpt-5.6-terra","model_alias":"gpt-terra","markup_bps":1000,
# "hold_credits":2130,"min_credits":420,"sponsor_enabled":false}}
#
# Estimate EVERY lane you offer: the hold differs per lane because the prompt
# sections and output caps differ. Showing lane A's hold against lane B's button
# is a lie the user pays for.
#
# A useful sanity check that your payload is landing: send the same body with
# `facts` removed and confirm the hold drops materially. If it does not, your
# facts are not being priced, which means the model is not reading them.
est = call("/estimate", payload("phi"), token=TOKEN)["data"]
assert est["model_alias"] == "gpt-terra"
assert est["markup_bps"] == 1000
print(est["hold_credits"], "reserved;", est["min_credits"], "minimum")
# Prove the facts are actually priced, not silently ignored:
bare = dict(payload("phi"))
bare.pop("facts", None)
print("without facts:", call("/estimate", bare, token=TOKEN)["data"]["hold_credits"])
const est = await call("/estimate", payload("phi"));
console.log(est.model, est.model_alias, est.markup_bps, est.hold_credits);
// Show hold_credits as RESERVED, never as the price. The charge afterwards is
// usually far lower, because the hold prices the full output cap.
est, err := call("/estimate", payload("phi"), token)
if err != nil {
panic(err)
}
fmt.Println(est["model_alias"], est["hold_credits"])
String est = call("/estimate", payloadJson("phi"));
System.out.println(est);
est = call("/estimate", payload("phi"))
puts "#{est["hold_credits"]} reserved on #{est["model"]}"
$est = call("/estimate", payload("phi"), $token);
echo $est["hold_credits"], " reserved on ", $est["model"], PHP_EOL;
var est = await Call("estimate", Payload("phi"), token);
Console.WriteLine(est.GetProperty("hold_credits").GetInt32());
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
# METERED. This spends credits. Always send an Idempotency-Key: a retry after a
# network blip must not bill twice. Fold the lane into the key - two lanes over
# the same measurement are two distinct runs.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/run" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: phigate-phi-$(date +%s)-1" \
-d @payload-phi.json
# -> {"data":{"job_id":"job_..."}}
curl -s "https://api.skillsafe.ai/v1/app-api/jobs/job_..." -H "Authorization: Bearer $SKILLSAFE_TOKEN"
# -> {"data":{"status":"succeeded","output":{"output":"{\"lane\":\"phi\",...}"},
# "charged_credits":874,"truncated":false}}
#
# `output.output` is a STRING containing the JSON object. Parse it, then check
# `truncated`: a truncated reply ran with a reduced output cap and is incomplete,
# so present it as cut short rather than as an answer.
import time
job = call("/run", payload("phi"), token=TOKEN,
extra_headers={"Idempotency-Key": "phigate-phi-%d-1" % int(time.time())})["data"]
while True:
state = call("/jobs/" + job["job_id"], token=TOKEN)["data"]
if state["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(1.5)
if state["status"] != "succeeded":
raise SystemExit(state.get("error") or state["status"])
reply = json.loads(state["output"]["output"])
print(reply["lane"], reply["posture"], len(reply["findings"]), "findings")
print("charged", state["charged_credits"], "truncated", state["truncated"])
# The contract: one coverage_check row per id in facts["must_reconcile"].
covered = {c["flag_id"] for c in reply["coverage_check"]}
missing = [i for i in payload("phi")["facts"]["must_reconcile"] if i not in covered]
if missing:
print("UNREVIEWED - the reply never accounted for:", missing)
const job = await call("/run", payload("phi"), {
headers: { "Idempotency-Key": `phigate-phi-${Date.now()}-1` },
});
let state;
for (;;) {
state = await call(`/jobs/${job.job_id}`);
if (["succeeded", "failed", "cancelled"].includes(state.status)) break;
await new Promise((r) => setTimeout(r, 1500));
}
if (state.status !== "succeeded") throw new Error(state.error || state.status);
const reply = JSON.parse(state.output.output);
const covered = new Set(reply.coverage_check.map((c) => c.flag_id));
const unreviewed = payload("phi").facts.must_reconcile.filter((i) => !covered.has(i));
console.log(reply.posture, reply.findings.length, "findings", unreviewed.length, "unreviewed");
job, err := call("/run", payload("phi"), token)
if err != nil {
panic(err)
}
id := job["job_id"].(string)
for {
state, err := call("/jobs/"+id, nil, token)
if err != nil {
panic(err)
}
status := state["status"].(string)
if status == "succeeded" || status == "failed" || status == "cancelled" {
fmt.Println(status, state["charged_credits"])
break
}
time.Sleep(1500 * time.Millisecond)
}
String job = call("/run", payloadJson("phi"));
// poll /jobs/{id} until status is succeeded, failed or cancelled, then parse
// output.output as JSON. Send an Idempotency-Key header on the POST.
System.out.println(job);
job = call("/run", payload("phi"),
headers: { "Idempotency-Key" => "phigate-phi-#{Time.now.to_i}-1" })
loop do
state = call("/jobs/#{job["job_id"]}")
if %%w[succeeded failed cancelled].include?(state["status"])
reply = JSON.parse(state["output"]["output"])
puts "#{reply["posture"]} #{reply["findings"].length} findings"
break
end
sleep 1.5
end
$job = call("/run", payload("phi"), $token,
["Idempotency-Key: phigate-phi-" . time() . "-1"]);
do {
usleep(1500000);
$state = call("/jobs/" . $job["job_id"], null, $token);
} while (!in_array($state["status"], ["succeeded", "failed", "cancelled"], true));
$reply = json_decode($state["output"]["output"], true);
echo $reply["posture"], " ", count($reply["findings"]), " findings", PHP_EOL;
var job = await Call("run", Payload("phi"), token);
var id = job.GetProperty("job_id").GetString();
JsonElement state;
while (true)
{
state = await Call($"jobs/{id}", null, token);
var s = state.GetProperty("status").GetString();
if (s is "succeeded" or "failed" or "cancelled") break;
await Task.Delay(1500);
}
var reply = JsonDocument.Parse(state.GetProperty("output").GetProperty("output").GetString()!);
Console.WriteLine(reply.RootElement.GetProperty("posture").GetString());
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
# Same billing as /run, but the reply arrives as it is written, which is what
# lets a UI show real progress instead of a spinner.
curl -N -s -X POST "https://api.skillsafe.ai/v1/app-api/run-stream" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: phigate-cdss-$(date +%s)-1" \
-d @payload-cdss.json
# event: job data: {"job_id":"job_..."}
# event: delta data: {"text":"{\"lane\":\"cdss\","}
# event: delta data: {"text":"\"findings\":[..."}
# event: done data: {"charged_credits":902,"truncated":false,"output":{"output":"..."}}
#
# Accumulate the deltas, but trust the `done` payload: the delta stream can drop
# the tail. If the stream dies mid-object, close the open brackets and render what
# parsed - the user paid for those findings.
import urllib.request
req = urllib.request.Request(
API + "/run-stream",
data=json.dumps(payload("cdss")).encode(),
headers={"Content-Type": "application/json",
"Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "phigate-cdss-%d-1" % int(time.time())})
acc, done = "", None
with urllib.request.urlopen(req) as stream:
event = None
for raw in stream:
line = raw.decode().rstrip("\n")
if line.startswith("event: "):
event = line[7:]
elif line.startswith("data: "):
body = json.loads(line[6:])
if event == "delta":
acc += body.get("text", "")
elif event == "done":
done = body
full = (done or {}).get("output", {}).get("output") or acc
reply = json.loads(full)
print(reply["body"]["trust_verdict"])
const res = await fetch(`${API}/run-stream`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${TOKEN}`,
"Idempotency-Key": `phigate-cdss-${Date.now()}-1`,
},
body: JSON.stringify(payload("cdss")),
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "", acc = "", done = null, event = null;
for (;;) {
const { value, done: eof } = await reader.read();
if (eof) break;
buf += dec.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop();
for (const line of lines) {
if (line.startsWith("event: ")) event = line.slice(7);
else if (line.startsWith("data: ")) {
const body = JSON.parse(line.slice(6));
if (event === "delta") acc += body.text || "";
if (event === "done") done = body;
}
}
}
const reply = JSON.parse(done?.output?.output || acc);
console.log(reply.body.trust_verdict);
req, _ := http.NewRequest(http.MethodPost, api+"/run-stream", bytes.NewReader(payloadBytes("cdss")))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", fmt.Sprintf("phigate-cdss-%%d-1", time.Now().Unix()))
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
sc := bufio.NewScanner(res.Body)
var acc strings.Builder
event := ""
for sc.Scan() {
line := sc.Text()
switch {
case strings.HasPrefix(line, "event: "):
event = strings.TrimPrefix(line, "event: ")
case strings.HasPrefix(line, "data: ") && event == "delta":
var d struct{ Text string `json:"text"` }
json.Unmarshal([]byte(strings.TrimPrefix(line, "data: ")), &d)
acc.WriteString(d.Text)
}
}
fmt.Println(len(acc.String()), "characters streamed")
HttpRequest req = HttpRequest.newBuilder(URI.create(API + "/run-stream"))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token)
.header("Idempotency-Key", "phigate-cdss-" + System.currentTimeMillis() + "-1")
.POST(HttpRequest.BodyPublishers.ofString(payloadJson("cdss")))
.build();
StringBuilder acc = new StringBuilder();
HTTP.send(req, HttpResponse.BodyHandlers.ofLines()).body().forEach(line -> {
if (line.startsWith("data: ")) acc.append(line.substring(6));
});
System.out.println(acc.length() + " characters streamed");
uri = URI("https://api.skillsafe.ai/v1/app-api/run-stream")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{TOKEN}"
req["Idempotency-Key"] = "phigate-cdss-#{Time.now.to_i}-1"
req.body = JSON.generate(payload("cdss"))
acc = +""
event = nil
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req) do |res|
res.read_body do |chunk|
chunk.each_line do |line|
event = line[7..].strip if line.start_with?("event: ")
acc << JSON.parse(line[6..])["text"].to_s if line.start_with?("data: ") && event == "delta"
end
end
end
end
puts "#{acc.length} characters streamed"
$ch = curl_init(API . "/run-stream");
$acc = "";
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(payload("cdss")),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"Authorization: Bearer " . $token,
"Idempotency-Key: phigate-cdss-" . time() . "-1",
],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$acc) {
foreach (explode("\n", $chunk) as $line) {
if (str_starts_with($line, "data: ")) {
$d = json_decode(substr($line, 6), true);
if (isset($d["text"])) { $acc .= $d["text"]; }
}
}
return strlen($chunk);
},
]);
curl_exec($ch);
curl_close($ch);
echo strlen($acc), " characters streamed", PHP_EOL;
var req = new HttpRequestMessage(HttpMethod.Post, "run-stream")
{
Content = JsonContent.Create(Payload("cdss")),
};
req.Headers.Authorization = new("Bearer", token);
req.Headers.Add("Idempotency-Key", $"phigate-cdss-{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}-1");
using var res = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
var acc = new StringBuilder();
string? evt = null, line;
while ((line = await reader.ReadLineAsync()) is not null)
{
if (line.StartsWith("event: ")) evt = line[7..];
else if (line.StartsWith("data: ") && evt == "delta")
acc.Append(JsonDocument.Parse(line[6..]).RootElement.GetProperty("text").GetString());
}
Console.WriteLine($"{acc.Length} characters streamed");
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": [
"..."
]
}
}
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.