Driving HL7 Desk from your own code
Everything the web app's AI lanes do is available over HTTP. Send one HL7 v2 message and a task, get back one JSON object. The HL7 parser the browser runs for free — the delimiters out of MSH-1 and MSH-2, the field and subcomponent split, the escape decoding, the calendar validation, the resolved message structure, the segment grammar, the code tables, the identifier inventory — is not recomputed server-side. If you drive the API directly you should send your own prescan facts, because that object is what the model is held accountable to.
Base URL and headers
https://api.skillsafe.ai/v1/app-api
One header on every request:
Authorization: Bearer <token>— get one from the token page, no developer console needed.
The token is app-scoped, so the slug is not a header. There is no X-App-Slug header — a token minted for this app addresses this app and nothing else. The slug appears in exactly one place: the body of POST /guest.
The body of /estimate, /run and /run-stream is the input object itself, not wrapped in an input key. A wrapped body returns 200 while hiding task from the model, which is the most confusing failure available here: you get an answer, and it is for the wrong lane.
Handling patient data
An HL7 v2 message off a real interface is patient data. Two things follow, and neither is optional if you are driving this API against production traffic:
- De-identify before you send. The web app does this in the browser; over the API it is your responsibility. Set
deidentified: trueso the prompt knows that stable pseudonyms are pseudonyms and not data-quality defects. - Derive
prescanfrom exactly the text you send. The prescan carries decoded readings of fields, so a reading ofPID-3contains the identifier it is a reading of. Scanning the original while sending a redacted message would put every real identifier back into the request.
The response envelope
Every response has the same two shapes. Branch on error.code, never on the message text — messages are for humans and will change.
// success
{"ok": true, "data": { ... }}
// failure
{"ok": false, "error": {"code": "validation_error", "message": "...", "details": { ... }}}
Error codes
| code | HTTP | What to do |
|---|---|---|
unauthorized | 401 | The token is missing, malformed or expired. Mint a new one from the token page. |
forbidden | 403 | A guest token tried to run. /me and /estimate work for guests; /run and /run-stream need a personal token. |
payment_required | 402 | The balance is below min_credits for this lane. /estimate is free, so check it before submitting. |
validation_error | 400 | The body is not the shape the app expects. Most often task is missing or is not one of the four lane ids, or the input was wrapped in an input key — it must be the input object itself. |
not_found | 404 | The job id does not exist, or belongs to another subject. |
rate_limited | 429 | Back off and retry. Do not tight-loop; the limit is shared. |
internal_error | 500 | Retry once with the same Idempotency-Key. Reusing the key is what stops a retry becoming a second charge. |
Step 1 — a tiny client
Everything below uses this one helper. It does the single thing that matters: it reads the envelope and raises on ok: false, because an error response is still HTTP-shaped JSON and ignoring it turns a 402 into a confusing KeyError three lines later.
# The whole client is two variables and curl.
BASE=https://api.skillsafe.ai/v1/app-api
TOKEN=YOUR_TOKEN
call() { # call <path> [json-body]
if [ -n "$2" ]; then
curl -sS -X POST "$BASE$1" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$2"
else
curl -sS "$BASE$1" -H "Authorization: Bearer $TOKEN"
fi
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # or os.environ.get("HL7_DESK_TOKEN")
def call(path, body=None, token=TOKEN):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(BASE + path, data=data, method="POST" if data else "GET")
req.add_header("Authorization", "Bearer " + token)
if data:
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req) as r:
payload = json.load(r)
except urllib.error.HTTPError as e:
payload = json.load(e) # errors carry a JSON body too
if not payload.get("ok"):
err = payload.get("error", {})
raise RuntimeError("%s: %s" % (err.get("code"), err.get("message")))
return payload["data"]
const BASE = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN";
async function call(path, body) {
const res = await fetch(BASE + path, {
method: body ? "POST" : "GET",
headers: {
Authorization: `Bearer ${TOKEN}`,
...(body ? { "Content-Type": "application/json" } : {})
},
body: body ? JSON.stringify(body) : undefined
});
const payload = await res.json(); // errors carry a JSON body too
if (!payload.ok) {
throw new Error(`${payload.error.code}: ${payload.error.message}`);
}
return payload.data;
}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
const base = "https://api.skillsafe.ai/v1/app-api"
var token = func() string {
if v := os.Getenv("HL7_DESK_TOKEN"); v != "" {
return v
}
return "YOUR_TOKEN"
}()
type envelope struct {
OK bool `json:"ok"`
Data json.RawMessage `json:"data"`
Error *struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func call(path string, body interface{}) (json.RawMessage, error) {
var req *http.Request
var err error
if body != nil {
b, _ := json.Marshal(body)
req, err = http.NewRequest("POST", base+path, bytes.NewReader(b))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
} else {
req, err = http.NewRequest("GET", base+path, nil)
if err != nil {
return nil, err
}
}
req.Header.Set("Authorization", "Bearer "+token)
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
raw, _ := io.ReadAll(res.Body)
var env envelope
if err := json.Unmarshal(raw, &env); err != nil {
return nil, err
}
if !env.OK {
return nil, fmt.Errorf("%s: %s", env.Error.Code, env.Error.Message)
}
return env.Data, nil
}
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
public final class Hl7DeskClient {
private static final String BASE = "https://api.skillsafe.ai/v1/app-api";
private static final String TOKEN = "YOUR_TOKEN"; // or System.getenv("HL7_DESK_TOKEN")
private static final HttpClient HTTP = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(20)).build();
/** Returns the raw JSON body. Parse it with the JSON library you already use. */
static String call(String path, String jsonBody) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.timeout(Duration.ofMinutes(5));
if (jsonBody == null) {
b = b.GET();
} else {
b = b.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody));
}
HttpResponse<String> res = HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString());
// Errors carry a JSON body too - branch on error.code, not on the status.
return res.body();
}
}
require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # or ENV.fetch("HL7_DESK_TOKEN", nil)
def call(path, body = nil)
uri = URI(BASE + path)
req = body ? Net::HTTP::Post.new(uri) : Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
if body
req["Content-Type"] = "application/json"
req.body = JSON.generate(body)
end
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
payload = JSON.parse(res.body) # errors carry a JSON body too
raise "#{payload['error']['code']}: #{payload['error']['message']}" unless payload["ok"]
payload["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // or getenv("HL7_DESK_TOKEN")
function call(string $path, ?array $body = null): array {
$headers = ["Authorization: Bearer " . TOKEN];
$opts = ["http" => ["method" => "GET", "header" => "", "ignore_errors" => true]];
if ($body !== null) {
$headers[] = "Content-Type: application/json";
$opts["http"]["method"] = "POST";
$opts["http"]["content"] = json_encode($body);
}
$opts["http"]["header"] = implode("\r\n", $headers);
$raw = file_get_contents(BASE . $path, false, stream_context_create($opts));
$payload = json_decode($raw, true); // errors carry a JSON body too
if (empty($payload["ok"])) {
throw new RuntimeException($payload["error"]["code"] . ": " . $payload["error"]["message"]);
}
return $payload["data"];
}
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
public static class Hl7DeskClient
{
const string Base = "https://api.skillsafe.ai/v1/app-api";
const string Token = "YOUR_TOKEN"; // or Environment.GetEnvironmentVariable("HL7_DESK_TOKEN")
static readonly HttpClient Http = new HttpClient { Timeout = TimeSpan.FromMinutes(5) };
public static async Task<JsonElement> CallAsync(string path, object body = null)
{
var req = new HttpRequestMessage(body == null ? HttpMethod.Get : HttpMethod.Post, Base + path);
req.Headers.Add("Authorization", "Bearer " + Token);
if (body != null)
{
req.Content = new StringContent(JsonSerializer.Serialize(body),
Encoding.UTF8, "application/json");
}
var res = await Http.SendAsync(req);
var raw = await res.Content.ReadAsStringAsync(); // errors carry a JSON body too
var payload = JsonDocument.Parse(raw).RootElement;
if (!payload.GetProperty("ok").GetBoolean())
{
var e = payload.GetProperty("error");
throw new Exception(e.GetProperty("code").GetString() + ": " +
e.GetProperty("message").GetString());
}
return payload.GetProperty("data");
}
}
Step 2 — get a token
The friendly route is the token page, which shows the token this browser already holds, reveals it, copies it, and can mint a fresh guest one. Programmatically, POST /guest is the whole story:
curl -sS -X POST https://api.skillsafe.ai/v1/app-api/guest \
-H "Content-Type: application/json" \
-d '{"slug": "hl7-desk"}'
# {"ok":true,"data":{"token":"aut_...","guest_id":"gst_...","expires_at":"..."}}
import json, urllib.request
req = urllib.request.Request(
"https://api.skillsafe.ai/v1/app-api/guest",
data=json.dumps({"slug": "hl7-desk"}).encode(),
headers={"Content-Type": "application/json"},
method="POST")
with urllib.request.urlopen(req) as r:
token = json.load(r)["data"]["token"]
print(token) # aut_...
const res = await fetch("https://api.skillsafe.ai/v1/app-api/guest", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug: "hl7-desk" })
});
const { data } = await res.json();
console.log(data.token); // aut_...
body := bytes.NewBufferString(`{"slug":"hl7-desk"}`)
res, err := http.Post("https://api.skillsafe.ai/v1/app-api/guest", "application/json", body)
if err != nil {
panic(err)
}
defer res.Body.Close()
var env struct {
Data struct{ Token string } `json:"data"`
}
json.NewDecoder(res.Body).Decode(&env)
fmt.Println(env.Data.Token) // aut_...
HttpRequest req = HttpRequest.newBuilder(URI.create("https://api.skillsafe.ai/v1/app-api/guest"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("{\"slug\":\"hl7-desk\"}"))
.build();
String body = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString()).body();
System.out.println(body); // {"ok":true,"data":{"token":"aut_...", ...}}
uri = URI("https://api.skillsafe.ai/v1/app-api/guest")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req.body = JSON.generate({ slug: "hl7-desk" })
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)["data"]["token"] # aut_...
<?php
$opts = ["http" => [
"method" => "POST",
"header" => "Content-Type: application/json",
"content" => json_encode(["slug" => "hl7-desk"]),
]];
$raw = file_get_contents("https://api.skillsafe.ai/v1/app-api/guest", false, stream_context_create($opts));
echo json_decode($raw, true)["data"]["token"]; // aut_...
var http = new HttpClient();
var content = new StringContent("{\"slug\":\"hl7-desk\"}", Encoding.UTF8, "application/json");
var res = await http.PostAsync("https://api.skillsafe.ai/v1/app-api/guest", content);
var raw = await res.Content.ReadAsStringAsync();
Console.WriteLine(raw); // {"ok":true,"data":{"token":"aut_...", ...}}
A guest token is enough for /me and /estimate. Running any of the four lanes is metered and needs a personal token, which comes from signing in on the token page.
Step 3 — who am I, and can I afford this
GET /me returns subject_type (user or guest) and, for a signed-in user, credits. Compare that against min_credits from step 5 before submitting: a 402 after submit is a failure of your client, not of the user.
call /me
# {"ok":true,"data":{"subject_type":"user","username":"...","credits":48210}}
me = call("/me")
print(me["subject_type"], me.get("credits"))
const me = await call("/me");
console.log(me.subject_type, me.credits);
raw, err := call("/me", nil)
if err != nil {
panic(err)
}
var me struct {
SubjectType string `json:"subject_type"`
Credits int `json:"credits"`
}
json.Unmarshal(raw, &me)
fmt.Println(me.SubjectType, me.Credits)
System.out.println(Hl7DeskClient.call("/me", null));
// {"ok":true,"data":{"subject_type":"user","credits":48210}}
me = call("/me")
puts "#{me['subject_type']} #{me['credits']}"
<?php
$me = call("/me");
echo $me["subject_type"], " ", $me["credits"] ?? 0, "\n";
var me = await Hl7DeskClient.CallAsync("/me");
Console.WriteLine(me.GetProperty("subject_type").GetString());
Step 4 — the input object
task comes first, because it selects the lane and therefore the entire shape of body in the reply.
| field | type | required | meaning |
|---|---|---|---|
task | string | yes | One of decode, conformance, ack, fhir. If it is absent or unrecognised the prompt picks the closest lane and sets task_inferred: true in the reply rather than blending two contracts. |
message_text | string | yes | The whole HL7 v2 message. Segments separated by \r (the standard) or \n (what a log paste usually gives you) — both are accepted and the prescan reports which arrived. |
profile | string | yes | The conformance basis: hl7-v2-generic, ihe-pam, us-core-adt or unknown. Steers the conformance and ACK lanes. |
receiver_note | string | no | Free text — what the receiving system actually said. This is the single most useful field you can populate: "rejected with segment sequence error" turns a general review into a diagnosis. |
deidentified | boolean | no | True when the message you are sending has had its identifiers replaced. Tells the prompt not to treat stable pseudonyms as data-quality defects. |
prescan | object | yes | The parser facts. Send {"ok": false} if you have none — the lanes still work, they simply have nothing to be reconciled against. See below. |
prior_conformance | object | no | The conformance lane's own verdict and findings, for the ACK and FHIR lanes. This is what the web app's handoff button sends. |
clip_note | string | no | Say so if you truncated the message, and cut on whole-segment boundaries. A half segment parses into wrong values rather than failing. |
Sending your own prescan
The reply's reconciliation array is required to carry exactly one entry per distinct flag_id you send in prescan.flags. So the prescan is not decoration: it is the mechanism that stops the model quietly ignoring a defect. The shape the app sends is:
{
"ok": true,
"message": {"code": "ADT", "trigger": "A01", "trigger_label": "...",
"declared_structure": "ADT_A01", "resolved_structure": "ADT_A01",
"version": "2.5", "control_id": "...", "processing_id": "P",
"sending_application": "...", "receiving_application": "...",
"timestamp": "20260318142530", "timestamp_reading": "..."},
"encoding": {"field": "|", "component": "^", "repetition": "~",
"escape": "\\", "subcomponent": "&",
"is_default": true, "terminator": "CR"},
"counts": {"segments": 7, "populated_fields": 49, ...},
"segments": [{"name": "MSH", "position": 1, "fields": 12,
"known": true, "purpose": "..."}],
"grammar": {"structure": "ADT_A01", "checked": true,
"required": ["MSH", "EVN", "PID", "PV1"],
"missing_required": [], "out_of_order": ["AL1"]},
"key_fields": [{"location": "PID-5", "label": "Patient Name",
"raw": "...", "reading": "..."}],
"phi": {"count": 11, "locations": ["PID-3", "PID-5", ...]},
"flags": [{"id": "HL7-TYPE-DATE", "severity": "high",
"location": "PID-7", "label": "..."}]
}
key_fields[].reading is the decoded expansion and is empty whenever there is nothing to expand - a plain string like a sending application name has no expansion, so its reading is "" while its raw carries the value. An empty reading never means the field is empty; only an empty raw does.
If you are building your own prescan, the one thing worth copying is how the structure is resolved: HL7 groups many trigger events onto one message structure, so A04, A08 and A13 all use ADT_A01 and A28 and A31 use ADT_A05. Deriving it as message code plus trigger event invents names like ADT_A08 that no published grammar matches.
Step 5 — estimate, free
POST /estimate creates no job, spends nothing, and is the authoritative check that your input shape and the model binding are both right. hold_credits is a reservation sized for the full output cap, not the price; the charge is usually far lower. The hold differs per lane, so re-estimate when you change task.
INPUT='{
"task": "conformance",
"message_text": "MSH|^~\\&|EPICADT|MERCYGEN|IEENGINE|MERCYGEN|20260318142530||ADT^A01^ADT_A01|MSG00001|P|2.5\nEVN|A01|20260318142530\nPID|1||MRN448120^^^MERCYMPI^MR||SURNAME^GIVEN||19710904|M\nPV1|1|I|WARD^ROOM^BED",
"profile": "ihe-pam",
"receiver_note": "",
"deidentified": false,
"prescan": {"ok": false}
}'
call /estimate "$INPUT"
# {"ok":true,"data":{"model":"gpt-5.6-terra","model_alias":"gpt-terra",
# "markup_bps":1000,"hold_credits":1740,"min_credits":310}}
MESSAGE = "\r".join([
"MSH|^~\\&|EPICADT|MERCYGEN|IEENGINE|MERCYGEN|20260318142530||ADT^A01^ADT_A01|MSG00001|P|2.5",
"EVN|A01|20260318142530",
"PID|1||MRN448120^^^MERCYMPI^MR||SURNAME^GIVEN||19710904|M",
"PV1|1|I|WARD^ROOM^BED",
])
payload = {
"task": "conformance",
"message_text": MESSAGE,
"profile": "ihe-pam",
"receiver_note": "",
"deidentified": False,
"prescan": {"ok": False}, # see "sending your own prescan" below
}
est = call("/estimate", payload)
print(est["model"], est["model_alias"], est["hold_credits"], est["min_credits"])
const MESSAGE = [
"MSH|^~\\&|EPICADT|MERCYGEN|IEENGINE|MERCYGEN|20260318142530||ADT^A01^ADT_A01|MSG00001|P|2.5",
"EVN|A01|20260318142530",
"PID|1||MRN448120^^^MERCYMPI^MR||SURNAME^GIVEN||19710904|M",
"PV1|1|I|WARD^ROOM^BED"
].join("\r");
const payload = {
task: "conformance",
message_text: MESSAGE,
profile: "ihe-pam",
receiver_note: "",
deidentified: false,
prescan: { ok: false }
};
const est = await call("/estimate", payload);
console.log(est.model, est.hold_credits, est.min_credits);
message := strings.Join([]string{
`MSH|^~\&|EPICADT|MERCYGEN|IEENGINE|MERCYGEN|20260318142530||ADT^A01^ADT_A01|MSG00001|P|2.5`,
`EVN|A01|20260318142530`,
`PID|1||MRN448120^^^MERCYMPI^MR||SURNAME^GIVEN||19710904|M`,
`PV1|1|I|WARD^ROOM^BED`,
}, "\r")
payload := map[string]interface{}{
"task": "conformance",
"message_text": message,
"profile": "ihe-pam",
"receiver_note": "",
"deidentified": false,
"prescan": map[string]interface{}{"ok": false},
}
raw, err := call("/estimate", payload)
if err != nil {
panic(err)
}
fmt.Println(string(raw))
String message = String.join("\r",
"MSH|^~\\&|EPICADT|MERCYGEN|IEENGINE|MERCYGEN|20260318142530||ADT^A01^ADT_A01|MSG00001|P|2.5",
"EVN|A01|20260318142530",
"PID|1||MRN448120^^^MERCYMPI^MR||SURNAME^GIVEN||19710904|M",
"PV1|1|I|WARD^ROOM^BED");
// Build the body with your JSON library; escaping HL7 by hand is a bug factory.
String body = mapper.writeValueAsString(Map.of(
"task", "conformance",
"message_text", message,
"profile", "ihe-pam",
"receiver_note", "",
"deidentified", false,
"prescan", Map.of("ok", false)));
System.out.println(Hl7DeskClient.call("/estimate", body));
message = [
'MSH|^~\\&|EPICADT|MERCYGEN|IEENGINE|MERCYGEN|20260318142530||ADT^A01^ADT_A01|MSG00001|P|2.5',
'EVN|A01|20260318142530',
'PID|1||MRN448120^^^MERCYMPI^MR||SURNAME^GIVEN||19710904|M',
'PV1|1|I|WARD^ROOM^BED'
].join("\r")
payload = {
task: "conformance",
message_text: message,
profile: "ihe-pam",
receiver_note: "",
deidentified: false,
prescan: { ok: false }
}
est = call("/estimate", payload)
puts "#{est['model']} hold=#{est['hold_credits']} min=#{est['min_credits']}"
<?php
$message = implode("\r", [
'MSH|^~\\&|EPICADT|MERCYGEN|IEENGINE|MERCYGEN|20260318142530||ADT^A01^ADT_A01|MSG00001|P|2.5',
'EVN|A01|20260318142530',
'PID|1||MRN448120^^^MERCYMPI^MR||SURNAME^GIVEN||19710904|M',
'PV1|1|I|WARD^ROOM^BED',
]);
$payload = [
"task" => "conformance",
"message_text" => $message,
"profile" => "ihe-pam",
"receiver_note" => "",
"deidentified" => false,
"prescan" => ["ok" => false],
];
$est = call("/estimate", $payload);
echo $est["model"], " ", $est["hold_credits"], " ", $est["min_credits"], "\n";
var message = string.Join("\r", new[] {
@"MSH|^~\&|EPICADT|MERCYGEN|IEENGINE|MERCYGEN|20260318142530||ADT^A01^ADT_A01|MSG00001|P|2.5",
"EVN|A01|20260318142530",
"PID|1||MRN448120^^^MERCYMPI^MR||SURNAME^GIVEN||19710904|M",
"PV1|1|I|WARD^ROOM^BED"
});
var payload = new {
task = "conformance",
message_text = message,
profile = "ihe-pam",
receiver_note = "",
deidentified = false,
prescan = new { ok = false }
};
var est = await Hl7DeskClient.CallAsync("/estimate", payload);
Console.WriteLine(est.GetProperty("hold_credits").GetInt32());
If the balance sits between min_credits and hold_credits the run still executes with a reduced output cap and the done event carries "truncated": true. Surface that rather than presenting a clipped answer as complete.
Step 6 — run and poll
POST /run returns a job_id; poll GET /jobs/{id} until status is terminal. data.output.output is a string containing the one JSON object the prompt returns — parse it a second time.
Always send an Idempotency-Key. Hash the task, the message and an attempt counter. A network blip or an automatic reformat retry must reuse the key derived from the same input, or you pay twice for one answer.
# 1. submit
JOB=$(call /run "$INPUT" | python3 -c 'import json,sys; print(json.load(sys.stdin)["data"]["job_id"])')
# 2. poll until terminal
while :; do
OUT=$(call "/jobs/$JOB")
STATUS=$(printf '%s' "$OUT" | python3 -c 'import json,sys; print(json.load(sys.stdin)["data"]["status"])')
case "$STATUS" in
succeeded) printf '%s' "$OUT"; break ;;
failed|cancelled) echo "job $STATUS" >&2; exit 1 ;;
*) sleep 2 ;;
esac
done
import time
job = call("/run", payload)["job_id"]
while True:
j = call("/jobs/" + job)
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
if j["status"] != "succeeded":
raise RuntimeError("job " + j["status"])
result = json.loads(j["output"]["output"]) # the one JSON object the prompt returns
print(result["task"], result["verdict"])
for c in result["body"]["checks"]:
print(c["status"], "-", c["area"])
const { job_id } = await call("/run", payload);
let job;
for (;;) {
job = await call(`/jobs/${job_id}`);
if (["succeeded", "failed", "cancelled"].includes(job.status)) break;
await new Promise(r => setTimeout(r, 2000));
}
if (job.status !== "succeeded") throw new Error(`job ${job.status}`);
const result = JSON.parse(job.output.output);
console.log(result.verdict);
for (const c of result.body.checks) console.log(c.status, "-", c.area);
raw, err := call("/run", payload)
if err != nil {
panic(err)
}
var submitted struct{ JobID string `json:"job_id"` }
json.Unmarshal(raw, &submitted)
for {
raw, err = call("/jobs/"+submitted.JobID, nil)
if err != nil {
panic(err)
}
var job struct {
Status string `json:"status"`
Output struct{ Output string `json:"output"` } `json:"output"`
}
json.Unmarshal(raw, &job)
if job.Status == "succeeded" {
fmt.Println(job.Output.Output) // the one JSON object the prompt returns
break
}
if job.Status == "failed" || job.Status == "cancelled" {
panic("job " + job.Status)
}
time.Sleep(2 * time.Second)
}
String submitted = Hl7DeskClient.call("/run", body);
String jobId = /* read data.job_id out of `submitted` with your JSON library */ "";
String job;
while (true) {
job = Hl7DeskClient.call("/jobs/" + jobId, null);
String status = /* read data.status */ "";
if (status.equals("succeeded")) break;
if (status.equals("failed") || status.equals("cancelled")) {
throw new IllegalStateException("job " + status);
}
Thread.sleep(2000);
}
// data.output.output is a STRING containing the one JSON object the prompt returns.
System.out.println(job);
job_id = call("/run", payload)["job_id"]
job = nil
loop do
job = call("/jobs/#{job_id}")
break if %w[succeeded failed cancelled].include?(job["status"])
sleep 2
end
raise "job #{job['status']}" unless job["status"] == "succeeded"
result = JSON.parse(job["output"]["output"])
puts result["verdict"]
result["body"]["checks"].each { |c| puts "#{c['status']} - #{c['area']}" }
<?php
$job_id = call("/run", $payload)["job_id"];
do {
$job = call("/jobs/$job_id");
if (in_array($job["status"], ["succeeded", "failed", "cancelled"], true)) break;
sleep(2);
} while (true);
if ($job["status"] !== "succeeded") {
throw new RuntimeException("job " . $job["status"]);
}
$result = json_decode($job["output"]["output"], true);
echo $result["verdict"], "\n";
foreach ($result["body"]["checks"] as $c) {
echo $c["status"], " - ", $c["area"], "\n";
}
var submitted = await Hl7DeskClient.CallAsync("/run", payload);
var jobId = submitted.GetProperty("job_id").GetString();
JsonElement job;
while (true)
{
job = await Hl7DeskClient.CallAsync("/jobs/" + jobId);
var status = job.GetProperty("status").GetString();
if (status == "succeeded") break;
if (status == "failed" || status == "cancelled") throw new Exception("job " + status);
await Task.Delay(2000);
}
var text = job.GetProperty("output").GetProperty("output").GetString();
var result = JsonDocument.Parse(text).RootElement;
Console.WriteLine(result.GetProperty("verdict").GetString());
Step 7 — run-stream, for progress
POST /run-stream is the same call over server-sent events. Accumulate the delta events' text, then parse the concatenation. Keep whatever arrived if the stream dies: the web app parses a truncated reply and renders the sections that made it, rather than discarding an answer the user paid for.
curl -sS -N -X POST https://api.skillsafe.ai/v1/app-api/run-stream \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: hl7-desk:conformance:$(printf '%s' "$INPUT" | shasum -a 256 | cut -c1-32):a1" \
-d "$INPUT"
# event: delta
# data: {"text":"{\"task\": \"conform"}
# ...
# event: done
# data: {"job_id":"job_...","charged_credits":612,"truncated":false}
import hashlib
key = "hl7-desk:%s:%s:a1" % (
payload["task"],
hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest()[:32])
req = urllib.request.Request(
BASE + "/run-stream",
data=json.dumps(payload).encode(),
headers={"Authorization": "Bearer " + TOKEN,
"Content-Type": "application/json",
"Idempotency-Key": key},
method="POST")
chunks = []
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().rstrip("\n")
if line.startswith("data: "):
evt = json.loads(line[6:])
if "text" in evt:
chunks.append(evt["text"])
result = json.loads("".join(chunks))
print(result["verdict"])
const key = `hl7-desk:${payload.task}:${Date.now().toString(36)}:a1`;
const res = await fetch(`${BASE}/run-stream`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": key
},
body: JSON.stringify(payload)
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "", text = "";
for (;;) {
const { done, value } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop();
for (const line of lines) {
if (!line.startsWith("data: ")) continue;
const evt = JSON.parse(line.slice(6));
if (evt.text) text += evt.text;
}
}
const result = JSON.parse(text);
console.log(result.verdict);
b, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(b))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "hl7-desk:conformance:abc123:a1")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var text strings.Builder
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 0, 64*1024), 1024*1024)
for sc.Scan() {
line := sc.Text()
if !strings.HasPrefix(line, "data: ") {
continue
}
var evt struct{ Text string `json:"text"` }
if json.Unmarshal([]byte(line[6:]), &evt) == nil && evt.Text != "" {
text.WriteString(evt.Text)
}
}
fmt.Println(text.String())
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", "hl7-desk:conformance:abc123:a1")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
StringBuilder text = new StringBuilder();
HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofLines())
.body()
.filter(l -> l.startsWith("data: "))
.forEach(l -> {
// each event is {"text": "..."} for a delta, or the done payload
text.append(l.substring(6));
});
System.out.println(text);
uri = URI("#{BASE}/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = "hl7-desk:conformance:abc123:a1"
req.body = JSON.generate(payload)
text = +""
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|
next unless line.start_with?("data: ")
evt = JSON.parse(line[6..].strip) rescue next
text << evt["text"] if evt["text"]
end
end
end
end
puts JSON.parse(text)["verdict"]
<?php
$opts = ["http" => [
"method" => "POST",
"header" => implode("\r\n", [
"Authorization: Bearer " . TOKEN,
"Content-Type: application/json",
"Idempotency-Key: hl7-desk:conformance:abc123:a1",
]),
"content" => json_encode($payload),
]];
$fh = fopen(BASE . "/run-stream", "r", false, stream_context_create($opts));
$text = "";
while (($line = fgets($fh)) !== false) {
if (strncmp($line, "data: ", 6) !== 0) continue;
$evt = json_decode(substr($line, 6), true);
if (isset($evt["text"])) $text .= $evt["text"];
}
fclose($fh);
echo json_decode($text, true)["verdict"], "\n";
var req = new HttpRequestMessage(HttpMethod.Post, Base + "/run-stream");
req.Headers.Add("Authorization", "Bearer " + Token);
req.Headers.Add("Idempotency-Key", "hl7-desk:conformance:abc123:a1");
req.Content = new StringContent(JsonSerializer.Serialize(payload),
Encoding.UTF8, "application/json");
var res = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
var text = new StringBuilder();
string line;
while ((line = await reader.ReadLineAsync()) != null)
{
if (!line.StartsWith("data: ")) continue;
var evt = JsonDocument.Parse(line.Substring(6)).RootElement;
if (evt.TryGetProperty("text", out var t)) text.Append(t.GetString());
}
Console.WriteLine(text.ToString());
The output contract, lane by lane
Every lane returns the same outer envelope and differs only in body. The envelope:
{
"task": "decode | conformance | ack | fhir",
"task_inferred": false,
"title": "...",
"verdict": "conformant | minor-issues | non-conformant | unparseable",
"summary": "...",
"assumptions": [],
"open_questions": [],
"findings": [{"id": "HD-001", "severity": "critical | high | medium | low",
"location": "PID-7", "title": "...", "why": "...", "fix": "..."}],
"reconciliation": [{"flag_id": "...", "status": "confirmed | noted | set-aside | superseded",
"note": "..."}],
"next_lane": {"lane": "...", "reason": "..."},
"body": { ... }
}
verdict describes the message in every lane, including fhir — not the quality of the answer. That is what lets four lanes share one envelope and one history record.
Every location the model emits is an HL7 coordinate in the message you sent: a segment name, SEG-n, SEG-n.c, line N, or the literal message. The web app checks each one against the parsed message and marks any that does not exist; if you are consuming the API, do the same — a cited field the message does not contain is the shape an invented finding takes.
task: "decode" — Decode (Read stage)
What the message says, segment by segment.
Request:
{
"task": "decode",
"message_text": "<the whole HL7 v2 message>",
"profile": "ihe-pam",
"receiver_note": "",
"deidentified": false,
"prescan": { ... }
}
Reply:
{
"task": "decode",
"task_inferred": false,
"title": "ADT^A01 admit from EPICADT to IEENGINE",
"verdict": "non-conformant",
"summary": "...",
"assumptions": [],
"open_questions": [],
"findings": [
{"id": "HD-001", "severity": "high", "location": "PID-7",
"title": "...", "why": "...", "fix": "..."}
],
"reconciliation": [
{"flag_id": "HL7-TYPE-DATE", "status": "confirmed", "note": ""}
],
"next_lane": {"lane": "conformance", "reason": "..."},
"body": {
"message_kind": "...",
"narrative": "...",
"segment_readings": [
{"location": "MSH", "label": "Message Header", "reading": "..."}
],
"notable_fields": [
{"location": "PV1-2", "label": "Patient Class",
"reading": "...", "significance": "..."}
],
"downstream_note": "..."
}
}
task: "conformance" — Conformance (Verify stage)
Nine check areas, plus a corrected message.
Request:
{
"task": "conformance",
"message_text": "<the whole HL7 v2 message>",
"profile": "ihe-pam",
"receiver_note": "the engine rejected this with 'segment sequence error'",
"deidentified": false,
"prescan": { ... }
}
Reply:
{
"task": "conformance",
... the same envelope ...
"body": {
"standard_basis": "...",
"checks": [
{"area": "framing and delimiters", "status": "pass", "note": "..."},
{"area": "message header", "status": "pass", "note": "..."},
{"area": "segment grammar", "status": "fail", "note": "..."},
{"area": "required fields", "status": "pass", "note": "..."},
{"area": "data types", "status": "fail", "note": "..."},
{"area": "code tables", "status": "fail", "note": "..."},
{"area": "cross-field consistency","status": "fail", "note": "..."},
{"area": "identity and identifiers","status": "pass","note": "..."},
{"area": "profile-specific", "status": "warn", "note": "..."}
],
"corrected_message": "MSH|^~\\&|...\nEVN|...\n...",
"correction_notes": [
{"location": "PID-8", "change": "..."}
]
}
}
task: "ack" — ACK (Respond stage)
The acknowledgment the receiver should return.
Request:
{
"task": "ack",
"message_text": "<the whole HL7 v2 message>",
"profile": "ihe-pam",
"receiver_note": "",
"deidentified": false,
"prescan": { ... },
"prior_conformance": {
"verdict": "non-conformant",
"standard_basis": "...",
"findings": [{"id": "HD-001", "severity": "high",
"location": "PID-7", "title": "..."}]
}
}
Reply:
{
"task": "ack",
... the same envelope ...
"body": {
"ack_code": "AE",
"ack_code_reason": "...",
"ack_message": "MSH|^~\\&|IEENGINE|MERCYGEN|EPICADT|MERCYGEN|...||ACK|NEWID|P|2.5\nMSA|AE|MSG00001|...\nERR|...",
"err_segments": [
{"location": "PID-7", "hl7_error_code": "102",
"hl7_error_text": "Data type error", "severity": "E",
"user_message": "..."}
],
"sender_action": "...",
"receiver_action": "..."
}
}
task: "fhir" — FHIR (Produce stage)
The same message as FHIR R4, with a mapping table.
Request:
{
"task": "fhir",
"message_text": "<the whole HL7 v2 message>",
"profile": "us-core-adt",
"receiver_note": "",
"deidentified": true,
"prescan": { ... }
}
Reply:
{
"task": "fhir",
... the same envelope ...
"body": {
"bundle_type": "transaction",
"resources": [
{"resource_type": "Patient", "purpose": "...",
"from_locations": ["PID-3", "PID-5", "PID-7", "PID-8"]}
],
"mapping_rows": [
{"hl7_location": "PID-3.1", "hl7_label": "Patient Identifier List",
"fhir_path": "Patient.identifier.value",
"transform": "direct", "note": ""}
],
"bundle_json": "{\n \"resourceType\": \"Bundle\", ...\n}",
"unmapped": [
{"location": "EVN-5", "why": "..."}
]
}
}
Lane-specific rules worth knowing before you consume the output
conformancereturns exactly ninechecksentries, one per area, in a fixed order: framing and delimiters, message header, segment grammar, required fields, data types, code tables, cross-field consistency, identity and identifiers, profile-specific.corrected_messagenever invents a value to fill an empty required field — the field stays empty and the need for it appears infindings.ackreverses the MSH direction: the ACK's MSH-3/MSH-4 are the original's MSH-5/MSH-6.MSA-2echoes the original's MSH-10 exactly, and MSH-10 of the ACK is a new placeholder because only a real receiver can mint one. Everyhl7_error_codeis a value from HL7 table 0357.fhirreturnsbundle_jsonas a string. Parse it; if it does not parse, treat it as text and not as a Bundle — the web app shows a visible warning in exactly that case rather than presenting unpostable JSON as postable.- All lanes honour the delimiters the message declares. If
prescan.encoding.is_defaultis false, any HL7 text in the reply uses the message's own characters, not pipe and caret.
What this API will not do
- It does not give clinical advice. The prompt forbids interpreting a diagnosis, a result value or an allergy clinically. A clinical value is a field value.
- It does not fetch anything. There is no lookup against a terminology server, an MPI or a FHIR endpoint. Everything in a reply comes from the message you sent.
- It does not remember. Each run is independent. Continuity between lanes is something you pass in, via
prior_conformance.