Run ICE Card from your own code
Information, not medical advice. ICE Card copies, orders and translates what a person wrote about their own health so a stranger can read it in an emergency. It never advises on treatment. The structure lane never adds anything the notes do not say - no medicine, dose, allergy, reaction, condition, implant, number or phone number that is not in the notes - and asks a question instead of guessing. A translation keeps every medicine name, dose and phone number exactly as written (and people's names, dates and numbers too). Check the printed card with your doctor or pharmacist.
Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page reads the card and checks it for free - it orders it the way the source skill asks (P0 severe allergies, high-alert medicines and implants that matter for scans or shocks; P1 the other medicines, conditions and implants; P2 the rest), picks the card type (child, adult, older adult, severe allergy) and checks what a first responder needs: a contact with a phone number, allergies with their reaction, medicines with a dose and how often, no error-prone abbreviations, no medicine from the class of a listed allergy, a recent update date. That is icscan.js, and every finding carries its rule id, line and fix. The run structures free notes into a clean card ("structure") or translates a finished card for a trip ("translate"); the page then holds the reply to the person's own notes (icreply.js): every rule answered, nothing on the card that the notes do not say, nothing changed in translation that must stay exact.
Base URL and the response shape
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response has one shape:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }
The token is minted for this app (the guest call takes {"slug":"ice-card"}), so no slug header is needed afterwards. Send it as Authorization: Bearer .... The run body is the input object, sent bare.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a field the run needs (task, notes, facts) is missing or not a string. Send every field as a string; language may be "" for "structure". |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: both lanes are metered and need a personal sign-in. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field comes first
Two lanes - "structure" (free notes or a rough sheet turned into a clean card the page can print: identity, allergies with reaction and severity, medicines with dose, how often and what for, conditions, implants, contacts and the person's own emergency instructions word for word, plus the questions to confirm before printing and anything left off) and "translate" (a finished card in the sheet format translated into language for a responder abroad, with the card's headings and one to three emergency phrases). The IC- rules belong to both. A missing or unknown task is answered as structure, with a caveat saying so. Both lanes send the same fields.
The input
Every field is a string.
| Field | Required | What |
|---|---|---|
task | yes | "structure" or "translate". The model answers that task and only that task. |
notes | yes | what the person pasted: a card in the card sheet format or free notes in their own words. The page sends up to 12,000 characters - the medical summary, not whole letters. "translate" needs a card in the sheet format (run "structure" first to make one from free notes). |
language | for translate | the language to translate into, as a plain name ("French", "Spanish", "Chinese (Simplified)"); the page offers 17 and clips it to 40 characters. Send "" for "structure". |
facts | yes | the page's own reading of the same notes: every allergy (with its reaction and a severity level 1-4 when the words say it), medicine (name, dose, how often, what for, high-alert), condition, implant and contact, the top-of-card (P0) lines, and every finding as rule | severity | line | where | text. See the facts format. |
retry_note | no | present only on a reformat retry: the previous reply could not be read as one JSON object, and this says how to answer again. Leave it out otherwise. |
The card sheet format
notes is plain text. The sheet the page reads (and the one "structure" produces) has two parts:
- Header lines - one
Key: valueper line:Name,Born,Sex,Blood type,Weight,Language,Doctor,School(for a child),Updated(the date the card was last checked) andNotes(the person's own emergency instructions). - Lists - a heading line, then one bullet per item:
Allergies:(- Penicillin: anaphylaxis, or- No known allergies- a blank is read as unknown, not as none),Medicines:(name, strength, how often, and what for in round brackets:- Warfarin 3 mg once a day (atrial fibrillation)),Conditions:,Implants:andContacts:(name, relationship in round brackets, then the phone number:- Ada Okoro (partner): +44 7700 900612).
Name: Margaret Ellis
Born: 1948-05-12
Blood type: A+
Doctor: Dr Okafor, Riverside Surgery, +44 20 7946 0321
Updated: 2024-02-10
Allergies:
- Penicillin: anaphylaxis
Medicines:
- Warfarin 3 mg once a day in the evening (atrial fibrillation)
- Metformin 500 mg twice a day with meals (type 2 diabetes)
Conditions:
- Atrial fibrillation
Contacts:
- David Ellis (son): +44 7700 900481
Free notes ("leo turner, he's 9 ... severe peanut allergy ... mum is Sara Turner 07700 900456") are fine for "structure": the page finds no sheet sections in them and the model builds the sheet. Every person, date and number on this page is invented for the samples.
The free checks
The page reads the sheet and checks it in the browser, for free, before any run:
- Ordering by the source skill's priorities. P0, the top of the card: severe allergies (anaphylaxis or "severe" in the words - level 3-4), high-alert medicines (blood thinners such as warfarin or apixaban, insulin, long-term steroids, anti-seizure medicines, lithium, digoxin, methotrexate and a few others -
IC-HIGHALERT) and implants that matter for scans or shocks (pacemaker, defibrillator, cochlear implant, neurostimulator, insulin pump and so on -IC-IMPLANT). P1 the other medicines, conditions and implants; P2 blood type, weight, languages, doctor. - Card type: severe allergy (any level-4 allergy), child (under 18), older adult (65 and over) or adult, from
Born:. - What a responder needs: at least one contact (
IC-NOCONTACT) and a phone number for each (IC-NOPHONE); a name (IC-NONAME); allergies stated, or "No known allergies" (IC-NOALLERGY), each with its reaction (IC-ALLERGYREACT); every medicine with a dose (IC-NODOSE) and how often (IC-NOFREQ), once (IC-DUP). - Error-prone abbreviations (
IC-ABBREV, the ISMP list): qd, qod, OD, U and IU, cc, trailing zeros (5.0 mg), leading decimals (.5 mg) and drug-name abbreviations (MSO4, MgSO4, HCTZ). - Allergy class (
IC-CLASS): a listed medicine that belongs to the class of a listed allergy - penicillins, cephalosporins, sulfonamides, NSAIDs, macrolides, tetracyclines (not exhaustive). The page never says which line is wrong; it is a pair to check with a doctor or pharmacist. - Currency: no
Updated:date (IC-NODATE) or one over a year old (IC-STALE), and too much for a wallet card (IC-LONG). The two date findings depend on today's date, so they show on the page but stay out offacts.
The card is drawn as a wallet card (74 x 105 mm), an A4 sheet or large print, with a plain-text copy and a findings CSV - all free.
The facts format
facts is the page's own reading, written as plain text. The page builds it from the notes with icscan.js, and the model checks its reply against it: it answers every error and warn rule id it finds there with one prescan entry per id, keeps a class conflict on the card and raises it as a question, and the page then holds the reply to the same reading. So an API client should send its own facts in the page's reading line format - from icscan.js run under node (Build the input yourself), or written by your own code in the same form - not an empty string. The format, line by line:
<Name|Born|Sex|Blood type|Weight|Language|Doctor|School|Updated|Notes>: <as written - only the lines the sheet gives>
Read by the page: <n> allergies[ (no known allergies stated)], <n> medicines, <n> conditions, <n> implants, <n> contacts[; a severe (anaphylaxis-level) allergy - severe-allergy card].
- allergy | <allergen> | reaction: <reaction|-> | level <1-4|unknown>
- medicine | <name> | dose: <dose|-> | how often: <frequency|-> | for: <reason|->[ | high-alert]
- condition | <text>
- implant | <text>
- contact | <name> | <relation|-> | phone: <phone|->
Top of the card (P0): <Allergy: X - reaction; Takes Y; Implant: Z>. (when there is one)
Translate into: <language>. (translate only)
Findings (rule | severity | line | where | text):
- <rule id> | <error|warn|info> | line <n> | <item|-> | <title>[ (note)][ | the line as written]
Notes with no findings get the single line - none. The allergy level is 4 for anaphylaxis or life-threatening words (adrenaline, throat swelling), 3 for "severe", 2 for swelling or hives, 1 for a rash, itch or intolerance. A real one - the facts the page sends for its "older adult's card with problems" sample (Margaret Ellis, invented) on the "structure" lane:
Name: Margaret Ellis
Born: 1948-05-12
Blood type: A+
Language: English
Doctor: Dr Okafor, Riverside Surgery, +44 20 7946 0321
Updated: 2024-02-10
Read by the page: 2 allergies, 4 medicines, 2 conditions, 1 implant, 2 contacts; a severe (anaphylaxis-level) allergy - severe-allergy card.
- allergy | Penicillin | reaction: anaphylaxis | level 4
- allergy | Sticking plasters | reaction: - | level unknown
- medicine | Warfarin | dose: 3 mg | how often: once a day in the evening | for: atrial fibrillation | high-alert
- medicine | Metformin | dose: 500 mg | how often: twice a day with meals | for: type 2 diabetes
- medicine | Bisoprolol | dose: 5.0 mg | how often: qd | for: heart rate
- medicine | Amoxicillin | dose: 500 mg | how often: three times a day | for: chest infection, started this week
- condition | Atrial fibrillation
- condition | Type 2 diabetes
- implant | Pacemaker, fitted 2021
- contact | David Ellis | son | phone: +44 7700 900481
- contact | Ruth Barnes | neighbour | phone: -
Top of the card (P0): Allergy: Penicillin - anaphylaxis; Takes Warfarin; Implant: Pacemaker, fitted 2021.
Findings (rule | severity | line | where | text):
- IC-CLASS | error | line 16 | Amoxicillin | A listed medicine belongs to the class of a listed allergy (allergy: Penicillin (penicillin class)) | Amoxicillin 500 mg three times a day (chest infection, started this week)
- IC-NOPHONE | error | line 27 | Ruth Barnes | Contact without a phone number | Ruth Barnes (neighbour)
- IC-ALLERGYREACT | warn | line 10 | Sticking plasters | Allergy without its reaction | Sticking plasters
- IC-ABBREV | warn | line 15 | Bisoprolol | Error-prone abbreviation (qd) | Bisoprolol 5.0 mg qd (heart rate)
- IC-ABBREV | warn | line 15 | Bisoprolol | Error-prone abbreviation (trailing zero) | Bisoprolol 5.0 mg qd (heart rate)
- IC-HIGHALERT | info | line 13 | Warfarin | High-alert medicine (Warfarin) | Warfarin 3 mg once a day in the evening (atrial fibrillation)
- IC-IMPLANT | info | line 23 | Pacemaker, fitted 2021 | Implant that matters for scans and shocks (Pacemaker) | Pacemaker, fitted 2021
Rule ids the check uses, with their severity and area:
| Rule | Severity | Area | Title |
|---|---|---|---|
IC-NOCONTACT | error | contacts | No emergency contact |
IC-NOPHONE | error | contacts | Contact without a phone number |
IC-NONAME | warn | identity | No name |
IC-NOALLERGY | warn | allergies | Allergies not stated |
IC-ALLERGYREACT | warn | allergies | Allergy without its reaction |
IC-CLASS | error | allergies | A listed medicine belongs to the class of a listed allergy |
IC-NODOSE | warn | medicines | Medicine without a dose |
IC-NOFREQ | warn | medicines | Medicine without how often |
IC-ABBREV | warn | medicines | Error-prone abbreviation |
IC-DUP | warn | medicines | The same medicine twice |
IC-HIGHALERT | info | medicines | High-alert medicine |
IC-IMPLANT | info | implants | Implant that matters for scans and shocks |
IC-NOBLOOD | info | identity | Blood type not given |
IC-STALE | warn | identity | Card not updated for over a year |
IC-NODATE | info | identity | No update date |
IC-LONG | info | identity | Too much for a wallet card |
A point the check cannot see (a dose that reads oddly, a missing strength, a contact who may not be reachable abroad) comes back as a finding with the rule NEW.
The standard the model applies
The source skill - @huifer/emergency-card (huifer/wellally-health, MIT) - is the only standard:
- The card serves a stranger in an emergency: P0 first (severe allergies, high-alert medicines, implants that matter for scans or shocks), then P1 (the other medicines, conditions and implants), then P2 (blood type, weight, languages, doctor).
- Every allergy says what happens and how bad; "No known allergies" is written only when the person says so - a blank is not the same as none.
- Every medicine has its name, strength, how often and what it is for, as the person wrote them. Error-prone abbreviations are written out when their meaning is certain (qd = once a day, bd = twice a day, U = units, 5.0 mg = 5 mg, .5 mg = 0.5 mg); when it is not certain, the model asks.
- Every emergency contact has a name, a relationship and a full phone number; implants say type, maker and model when known; the card carries the date it was last checked.
- The card is information, not instructions to treat - except the person's own emergency instructions ("use the adrenaline pen and call 999"), which are copied word for word.
House rules
- Never add, change or infer anything medical: no medicine, dose, frequency, allergy, reaction, condition, implant or number the notes do not state. Unclear or conflicting notes are kept as written and asked about in
questions. - Never advise: no "you should", no dose changes, nothing to start, stop or switch. A medicine in the class of a listed allergy (
IC-CLASS) keeps both lines exactly and is raised as something to check with a doctor or pharmacist. - Every
errorandwarnfinding in the facts is answered by its rule id inprescan(callagreeordisagree, with a note);infoids need no entry. - Names of people and medicines, doses, numbers and phone numbers are copied exactly.
- The reply is ONE JSON object and nothing else - no prose around it, no Markdown fences.
The replies
Both lanes carry task, summary, card, findings (rule - an IC- id from the facts or NEW - severity high / medium / low, title, where, why, fix), prescan (rule, call, note) and caveats. The card has the same fields in both: name, born, sex, blood_type, weight, languages, doctor, school, updated, no_known_allergies (a boolean), allergies (each allergen, reaction, severity), medications (each name, dose, frequency, reason), conditions (strings), implants (each type, detail), contacts (each name, relation, phone) and notes. The rest differs by lane.
structure
The notes turned into a clean card the page can print. A field the notes do not give is left empty rather than guessed.
summary- one or two sentences: who the card is for and what is at the top of it. No advice.card-severityisanaphylaxis,severe,moderate,mildorunknown; abbreviations are written out when their meaning is certain;updatedis only a date the notes give;notesholds the person's own emergency instructions word for word.questions- what the person must confirm or add before printing (a missing phone number, an unclear dose, a class conflict, no update date), one question each.left_out- anything in the notes not put on the card, eachitemandwhy.findings,prescanandcaveats.
{
"task": "structure",
"summary": "",
"card": {"name": "", "born": "", "sex": "", "blood_type": "", "weight": "", "languages": "", "doctor": "", "school": "", "updated": "", "no_known_allergies": false, "allergies": [{"allergen": "", "reaction": "", "severity": "anaphylaxis"}], "medications": [{"name": "", "dose": "", "frequency": "", "reason": ""}], "conditions": [""], "implants": [{"type": "", "detail": ""}], "contacts": [{"name": "", "relation": "", "phone": ""}], "notes": ""},
"questions": [""],
"left_out": [{"item": "", "why": ""}],
"findings": [],
"prescan": [],
"caveats": [""]
}
translate
The card in notes translated into language for a responder abroad.
languageandsummary- one sentence in English: what was translated and anything that did not translate cleanly.card- reactions, frequencies, reasons, conditions, relationships, implant types and notes in the target language. Every medicine name stays exactly as written (with the local name in brackets only when the model is certain of it); every dose, number, date and phone number stays exactly; people's names stay exactly; the allergen is given in the target language with the original in brackets.labels- the card's headings in the target language:title(the "in case of emergency - medical information" heading),born,allergies,medicines,conditions,implants,contacts,blood_type,doctor,updated,no_known_allergies,notes,school.phrases- one to three short sentences a responder needs most, eachtextin the target language andenglish, built only from the card.findings,prescanandcaveats(such as a term with no exact local equivalent).
{
"task": "translate",
"language": "",
"summary": "",
"card": {"name": "", "born": "", "sex": "", "blood_type": "", "weight": "", "languages": "", "doctor": "", "school": "", "updated": "", "no_known_allergies": false, "allergies": [{"allergen": "", "reaction": "", "severity": ""}], "medications": [{"name": "", "dose": "", "frequency": "", "reason": ""}], "conditions": [""], "implants": [{"type": "", "detail": ""}], "contacts": [{"name": "", "relation": "", "phone": ""}], "notes": ""},
"labels": {"title": "", "born": "", "allergies": "", "medicines": "", "conditions": "", "implants": "", "contacts": "", "blood_type": "", "doctor": "", "updated": "", "no_known_allergies": "", "notes": "", "school": ""},
"phrases": [{"text": "", "english": ""}],
"findings": [],
"prescan": [],
"caveats": [""]
}
The page holds either reply to the person's own notes: every error and warning answered by rule id, no rule id the page did not report other than NEW, and a contact on the card when the notes give one. For "structure" it wants every medicine, allergen, condition, implant and contact name found in the notes, every number and phone number found in the notes, no "no known allergies" the notes never state, no advice, nothing from the notes dropped without a word in left_out, and the card re-checked: a class conflict or a contact without a phone number still on it must be raised in questions, and no error-prone abbreviation may be left. For "translate" it wants the language asked for, the same number of allergies, medicines, conditions, implants and contacts as the sheet, medicine names, doses, numbers, names, dates of birth and phone numbers kept exactly, the allergen kept in brackets, the text really translated (not more than half the phrases back unchanged), every heading in labels (notes and school only when the card has them), and at least one phrase with its English meaning. Each miss is shown next to the reply as a note.
Reading the reply
The job's output.output is text that should be exactly one JSON object. Parse it; if a model ever wraps it in prose or a fence, take the text from the first { to the last } and parse that (the page does the same in icreply.js). If it still does not parse, send the same input again with a retry_note such as "Your previous reply was not usable. Reply again with ONLY the JSON object for task 'translate' - no prose, no code fences." and a new attempt number in the key (see step 5). A reply's card prints as the emergency card: the page draws it as a wallet card, an A4 sheet or large print, with the P0 lines at the top, and a translated card uses the reply's labels as its headings. Step 5 prints the card, then the phrases (translate) or the questions and what was left out (structure). Have the person read every line before the card is printed or carried.
Structure, then translate
The two lanes chain. A "structure" reply's card is written back in the sheet format (IcScan.sheet(reply.card): the header lines, then Allergies:, Medicines:, Conditions:, Implants:, Contacts: and Notes:) and becomes the new notes. The page checks that sheet again for free; once the person has answered the questions, the same sheet goes to "translate" with a language and fresh facts built from it.
Worked sample - translating Sam's card into French
A clean card for travel: Sam Okoro (invented, like every person, doctor and phone number in the samples), type 1 diabetes on two insulins, no known allergies, two contacts. The check finds only the two IC-HIGHALERT notes (insulin goes to the top of the card), both info, so nothing needs a prescan answer. The input below is exactly what the page sends for the "translate" lane into French; for "structure" only task changes to "structure", language becomes "" and the Translate into: line leaves the facts. Input:
{
"task": "translate",
"notes": "Name: Sam Okoro\nBorn: 1991-08-30\nSex: male\nBlood type: O-\nLanguage: English\nDoctor: Dr Lindqvist, Harbour Health Centre, +44 131 496 0702\nUpdated: 2026-09-15\n\nAllergies:\n- No known allergies\n\nMedicines:\n- Insulin glargine 22 units once a day at bedtime (type 1 diabetes)\n- Insulin aspart by carbohydrate count with each meal (type 1 diabetes)\n\nConditions:\n- Type 1 diabetes since 2004 - may have a hypo: if confused or drowsy, check blood glucose\n\nContacts:\n- Ada Okoro (partner): +44 7700 900612\n- Chidi Okoro (brother): +44 7700 900147",
"language": "French",
"facts": "Name: Sam Okoro\nBorn: 1991-08-30\nSex: male\nBlood type: O-\nLanguage: English\nDoctor: Dr Lindqvist, Harbour Health Centre, +44 131 496 0702\nUpdated: 2026-09-15\nRead by the page: 0 allergies (no known allergies stated), 2 medicines, 1 condition, 0 implants, 2 contacts.\n- medicine | Insulin glargine | dose: 22 units | how often: once a day at bedtime | for: type 1 diabetes | high-alert\n- medicine | Insulin aspart | dose: by carbohydrate count | how often: with each meal | for: type 1 diabetes | high-alert\n- condition | Type 1 diabetes since 2004 - may have a hypo: if confused or drowsy, check blood glucose\n- contact | Ada Okoro | partner | phone: +44 7700 900612\n- contact | Chidi Okoro | brother | phone: +44 7700 900147\nTop of the card (P0): Takes Insulin glargine; Takes Insulin aspart.\nTranslate into: French.\nFindings (rule | severity | line | where | text):\n- IC-HIGHALERT | info | line 13 | Insulin glargine | High-alert medicine (Insulin) | Insulin glargine 22 units once a day at bedtime (type 1 diabetes)\n- IC-HIGHALERT | info | line 14 | Insulin aspart | High-alert medicine (Insulin) | Insulin aspart by carbohydrate count with each meal (type 1 diabetes)"
}
Reply - the real saved reply the page shows for this sample, abbreviated: long strings are cut and end in ..., and phrases keeps only its first two entries (the full reply has 3 phrases, 1 caveat, 2 medicines and 2 contacts). Text in French is shown with \u escapes, as JSON.stringify with ASCII-only output writes it (\u00e9 is é, \u00e8 is è, \u00e0 is à):
{
"task": "translate",
"language": "French",
"summary": "Translated Sam Okoro's emergency card into French; medicine names, doses, dates, phone numbers and names are ...",
"card": {
"name": "Sam Okoro",
"born": "1991-08-30",
"sex": "masculin",
"blood_type": "O-",
"weight": "",
"languages": "anglais",
"doctor": "Dr Lindqvist, Harbour Health Centre, +44 131 496 0702",
"school": "",
"updated": "2026-09-15",
"no_known_allergies": true,
"allergies": [],
"medications": [
{
"name": "Insulin glargine",
"dose": "22 unit\u00e9s",
"frequency": "une fois par jour, au coucher",
"reason": "diab\u00e8te de type 1"
},
{
"name": "Insulin aspart",
"dose": "selon le comptage des glucides",
"frequency": "\u00e0 chaque repas",
"reason": "diab\u00e8te de type 1"
}
],
"conditions": [
"Diab\u00e8te de type 1 depuis 2004 - risque d'hypoglyc\u00e9mie : en cas de ..."
],
"implants": [],
"contacts": [
{
"name": "Ada Okoro",
"relation": "partenaire",
"phone": "+44 7700 900612"
},
{
"name": "Chidi Okoro",
"relation": "fr\u00e8re",
"phone": "+44 7700 900147"
}
],
"notes": "Sous insuline. Consigne personnelle : risque d'hypoglyc\u00e9mie : en cas ..."
},
"labels": {
"title": "En cas d'urgence - informations m\u00e9dicales",
"allergies": "Allergies",
"medicines": "M\u00e9dicaments",
"conditions": "Pathologies",
"implants": "Implants",
"contacts": "Personnes \u00e0 contacter",
"blood_type": "Groupe sanguin",
"doctor": "M\u00e9decin",
"updated": "Mis \u00e0 jour le",
"no_known_allergies": "Aucune allergie connue",
"born": "Date de naissance",
"notes": "Remarques",
"school": "\u00c9cole"
},
"phrases": [
{
"text": "J'ai un diab\u00e8te de type 1 et je prends de l'insuline.",
"english": "I have type 1 diabetes and take insulin."
},
{
"text": "Si je suis confus ou somnolent, contr\u00f4lez ma glyc\u00e9mie.",
"english": "If I am confused or drowsy, check my blood glucose."
}
],
"findings": [],
"prescan": [],
"caveats": [
"\"Units\" is given as \"unit\u00e9s\" (insulin units); \"hypo\" is rendered as ..."
]
}
The insulin names, "22", the dates and both phone numbers come back exactly as on the sheet; the reasons, frequencies, relationships and the condition are in French ("diabète de type 1", "frère"), and the headings arrive in labels. The page's reply check finds nothing to note. findings and prescan are empty because the page reported only info findings.
Build the input yourself
The checker and the reply checker are plain scripts that run under node (global.window = global; require("./icscan.js"); require("./icreply.js")), then:
const S = IcScan, R = IcReply;
const st = { notes: fs.readFileSync("card.txt", "utf8"), language: "French" };
const scan = S.scan(st); // fields, findings, counts, verdict, type, p0, stats
console.log(scan.type, scan.p0, scan.findings.map(f => f.id)); // card type, top-of-card lines, rule ids
const b = S.buildInput(st, "translate"); // or "structure"; b.ok is false (with b.note) when the notes cannot run
const wire = S.toWire(b.input); // task, notes, language, facts - all strings; POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "translate");
// R.check(reply, b) // {notes, warn}: anything the reply added, dropped or changed
// S.sheet(reply.card) // a structure reply's card as a sheet - the notes for a translate run
// R.md("Sam", b, reply) // the card and the page findings as Markdown
Get a token for the steps below from /tokens.html.
Step 1 - a tiny client helper
One function that sends JSON and unwraps the response. Paste a personal token from /tokens.html in place of YOUR_TOKEN, or leave it empty and mint a guest token in step 2.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN" # from https://ice-card.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from https://ice-card.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// ice-card.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from https://ice-card.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "YOUR_TOKEN" // from https://ice-card.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// IceCard.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class IceCard {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN"; // from https://ice-card.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = +"YOUR_TOKEN" # from https://ice-card.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, 443, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "YOUR_TOKEN"; // from https://ice-card.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "YOUR_TOKEN"; // from https://ice-card.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
Step 2 - a token
A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"ice-card"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "ice-card"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "ice-card" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "ice-card"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "ice-card"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "ice-card" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "ice-card"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "ice-card" }))!["token"]!.GetValue<string>();
Step 3 - GET /me
Who the token is (subject_type: user or guest) and the credit balance.
api GET /me | jq '.data | {subject_type, credits}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["credits"]}");
Step 4 - POST /estimate
Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra. Save the worked-sample input above as input.json (with task set to "structure" or "translate") to follow along.
# input.json: the run input object - {"task": "translate", "notes": "Name: ...\nAllergies:\n- ...\nMedicines:\n- ...\nContacts:\n- ...", "language": "French", "facts": "Name: ...\nRead by the page: ..."}
# - see the input table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "translate", "notes": "Name: ...", "language": "French", "facts": "Name: ...\nRead by the page: ..."}
# - see the input table
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "structure" or "translate", notes, language, facts} (see the input table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 - POST /run, then poll the job
Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses ice-card:<task>:<inputHash>:a<attempt> - the lane is part of the key, so a structure and a translation of the same notes are two runs - and its one automatic reformat retry (sent with a retry_note, which the hash leaves out) is a new attempt number, a2, named in the page as one extra run. Any stable hash of the input works for your own key. The code below parses the JSON object out of the output and prints the card - the title, name, birth date and blood type, the allergies (or no known allergies), the medicines and the contacts, under the translated headings when there are labels - then, for "translate", the phrases with their English meaning, and for "structure", the questions to confirm and what was left off.
KEY="ice-card:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
jq -r '(.labels // {}) as $l | .summary, "",
($l.title // "IN CASE OF EMERGENCY - MEDICAL INFORMATION"),
"\(.card.name) \(.card.born) \($l.blood_type // "Blood type"): \(.card.blood_type)",
"\($l.allergies // "Allergies"):",
(if .card.no_known_allergies then " \($l.no_known_allergies // "No known allergies")"
else (.card.allergies[]? | " ! \(.allergen): \(.reaction) (\(.severity))") end),
"\($l.medicines // "Medicines"):", (.card.medications[]? | " - \(.name) \(.dose) \(.frequency) (\(.reason))"),
"\($l.contacts // "Contacts"):", (.card.contacts[]? | " - \(.name) (\(.relation)): \(.phone)"),
(if .task == "translate" then (.phrases[]? | "> \(.text) [\(.english)]")
else (.questions[]? | "? \(.)"), (.left_out[]? | "left out: \(.item) - \(.why)") end)' reply.json
import hashlib, time
key = "ice-card:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
out = j["output"]["output"]
reply = json.loads(out[out.index("{"):out.rindex("}") + 1]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"))
card, lab = reply.get("card", {}), reply.get("labels") or {} # labels: translate only
print(reply.get("summary"), "\n")
print(lab.get("title") or "IN CASE OF EMERGENCY - MEDICAL INFORMATION")
print(card.get("name"), card.get("born"), (lab.get("blood_type") or "Blood type") + ":", card.get("blood_type"))
print((lab.get("allergies") or "Allergies") + ":")
if card.get("no_known_allergies"):
print(" " + (lab.get("no_known_allergies") or "No known allergies"))
for a in card.get("allergies", []):
print(f" ! {a['allergen']}: {a.get('reaction', '')} ({a.get('severity', '')})")
print((lab.get("medicines") or "Medicines") + ":")
for m in card.get("medications", []): # names and doses exactly as the person wrote them
print(f" - {m['name']} {m.get('dose', '')} {m.get('frequency', '')} ({m.get('reason', '')})")
print((lab.get("contacts") or "Contacts") + ":")
for c in card.get("contacts", []):
print(f" - {c['name']} ({c.get('relation', '')}): {c.get('phone', '')}")
if reply.get("task") == "translate":
for p in reply.get("phrases", []):
print(f"> {p['text']} [{p.get('english', '')}]")
else: # structure: confirm these before printing
for q in reply.get("questions", []):
print("?", q)
for x in reply.get("left_out", []):
print(f"left out: {x['item']} - {x.get('why', '')}")
import { createHash } from "node:crypto";
const key = `ice-card:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const out = j.output.output;
const reply = JSON.parse(out.slice(out.indexOf("{"), out.lastIndexOf("}") + 1));
console.log(j.charged_credits, j.truncated);
const card = reply.card || {}, lab = reply.labels || {}; // labels: translate only
console.log(reply.summary + "\n");
console.log(lab.title || "IN CASE OF EMERGENCY - MEDICAL INFORMATION");
console.log(card.name, card.born, (lab.blood_type || "Blood type") + ":", card.blood_type);
console.log((lab.allergies || "Allergies") + ":");
if (card.no_known_allergies) console.log(" " + (lab.no_known_allergies || "No known allergies"));
for (const a of card.allergies || []) console.log(` ! ${a.allergen}: ${a.reaction || ""} (${a.severity || ""})`);
console.log((lab.medicines || "Medicines") + ":");
for (const m of card.medications || []) console.log(` - ${m.name} ${m.dose || ""} ${m.frequency || ""} (${m.reason || ""})`);
console.log((lab.contacts || "Contacts") + ":");
for (const c of card.contacts || []) console.log(` - ${c.name} (${c.relation || ""}): ${c.phone || ""}`);
if (reply.task === "translate") {
for (const p of reply.phrases || []) console.log(`> ${p.text} [${p.english || ""}]`);
} else { // structure: confirm these before printing
for (const q of reply.questions || []) console.log("?", q);
for (const x of reply.left_out || []) console.log(`left out: ${x.item} - ${x.why || ""}`);
}
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "strings", "time" to the imports
key := "ice-card:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
out := j["output"].(map[string]any)["output"].(string)
var reply map[string]any
json.Unmarshal([]byte(out[strings.Index(out, "{"):strings.LastIndex(out, "}")+1]), &reply)
fmt.Println(j["charged_credits"], j["truncated"])
list := func(v any) []any { a, _ := v.([]any); return a }
obj := func(v any) map[string]any { o, _ := v.(map[string]any); if o == nil { o = map[string]any{} }; return o }
card, lab := obj(reply["card"]), obj(reply["labels"]) // labels: translate only
label := func(k, dflt string) string { if s, ok := lab[k].(string); ok && s != "" { return s }; return dflt }
fmt.Printf("%v\n\n%s\n", reply["summary"], label("title", "IN CASE OF EMERGENCY - MEDICAL INFORMATION"))
fmt.Printf("%v %v %s: %v\n", card["name"], card["born"], label("blood_type", "Blood type"), card["blood_type"])
fmt.Println(label("allergies", "Allergies") + ":")
if card["no_known_allergies"] == true { fmt.Println(" " + label("no_known_allergies", "No known allergies")) }
for _, x := range list(card["allergies"]) { a := obj(x); fmt.Printf(" ! %v: %v (%v)\n", a["allergen"], a["reaction"], a["severity"]) }
fmt.Println(label("medicines", "Medicines") + ":")
for _, x := range list(card["medications"]) { m := obj(x); fmt.Printf(" - %v %v %v (%v)\n", m["name"], m["dose"], m["frequency"], m["reason"]) }
fmt.Println(label("contacts", "Contacts") + ":")
for _, x := range list(card["contacts"]) { c := obj(x); fmt.Printf(" - %v (%v): %v\n", c["name"], c["relation"], c["phone"]) }
if reply["task"] == "translate" {
for _, x := range list(reply["phrases"]) { p := obj(x); fmt.Printf("> %v [%v]\n", p["text"], p["english"]) }
} else { // structure: confirm these before printing
for _, q := range list(reply["questions"]) { fmt.Println("?", q) }
for _, x := range list(reply["left_out"]) { o := obj(x); fmt.Printf("left out: %v - %v\n", o["item"], o["why"]) }
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "ice-card:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
String out = j.get("output").get("output").asText();
JsonNode reply = M.readTree(out.substring(out.indexOf('{'), out.lastIndexOf('}') + 1));
System.out.println(j.get("charged_credits") + " " + j.get("truncated"));
JsonNode card = reply.path("card"), lab = reply.path("labels"); // labels: translate only
java.util.function.BiFunction<String, String, String> label = (k, d) -> lab.path(k).asText("").isEmpty() ? d : lab.path(k).asText();
System.out.println(reply.path("summary").asText() + "\n");
System.out.println(label.apply("title", "IN CASE OF EMERGENCY - MEDICAL INFORMATION"));
System.out.println(card.path("name").asText() + " " + card.path("born").asText() + " " + label.apply("blood_type", "Blood type") + ": " + card.path("blood_type").asText());
System.out.println(label.apply("allergies", "Allergies") + ":");
if (card.path("no_known_allergies").asBoolean()) System.out.println(" " + label.apply("no_known_allergies", "No known allergies"));
for (JsonNode a : card.path("allergies")) System.out.println(" ! " + a.path("allergen").asText() + ": " + a.path("reaction").asText() + " (" + a.path("severity").asText() + ")");
System.out.println(label.apply("medicines", "Medicines") + ":");
for (JsonNode m : card.path("medications")) System.out.println(" - " + m.path("name").asText() + " " + m.path("dose").asText() + " " + m.path("frequency").asText() + " (" + m.path("reason").asText() + ")");
System.out.println(label.apply("contacts", "Contacts") + ":");
for (JsonNode c : card.path("contacts")) System.out.println(" - " + c.path("name").asText() + " (" + c.path("relation").asText() + "): " + c.path("phone").asText());
if ("translate".equals(reply.path("task").asText())) {
for (JsonNode p : reply.path("phrases")) System.out.println("> " + p.path("text").asText() + " [" + p.path("english").asText() + "]");
} else { // structure: confirm these before printing
for (JsonNode q : reply.path("questions")) System.out.println("? " + q.asText());
for (JsonNode x : reply.path("left_out")) System.out.println("left out: " + x.path("item").asText() + " - " + x.path("why").asText());
}
require "digest"
key = "ice-card:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
out = j["output"]["output"]
reply = JSON.parse(out[out.index("{")..out.rindex("}")])
puts [j["charged_credits"], j["truncated"]].inspect
card, lab = reply["card"] || {}, reply["labels"] || {} # labels: translate only
label = ->(k, d) { lab[k].to_s.empty? ? d : lab[k] }
puts reply["summary"], "", label.("title", "IN CASE OF EMERGENCY - MEDICAL INFORMATION")
puts "#{card["name"]} #{card["born"]} #{label.("blood_type", "Blood type")}: #{card["blood_type"]}"
puts "#{label.("allergies", "Allergies")}:"
puts " #{label.("no_known_allergies", "No known allergies")}" if card["no_known_allergies"]
(card["allergies"] || []).each { |a| puts " ! #{a["allergen"]}: #{a["reaction"]} (#{a["severity"]})" }
puts "#{label.("medicines", "Medicines")}:"
(card["medications"] || []).each { |m| puts " - #{m["name"]} #{m["dose"]} #{m["frequency"]} (#{m["reason"]})" }
puts "#{label.("contacts", "Contacts")}:"
(card["contacts"] || []).each { |c| puts " - #{c["name"]} (#{c["relation"]}): #{c["phone"]}" }
if reply["task"] == "translate"
(reply["phrases"] || []).each { |p| puts "> #{p["text"]} [#{p["english"]}]" }
else # structure: confirm these before printing
(reply["questions"] || []).each { |q| puts "? #{q}" }
(reply["left_out"] || []).each { |x| puts "left out: #{x["item"]} - #{x["why"]}" }
end
$key = "ice-card:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$out = $j["output"]["output"];
$from = strpos($out, "{");
$reply = json_decode(substr($out, $from, strrpos($out, "}") - $from + 1), true);
echo $j["charged_credits"], "\n";
$card = $reply["card"] ?? []; $lab = $reply["labels"] ?? []; // labels: translate only
$label = fn($k, $d) => ($lab[$k] ?? "") !== "" ? $lab[$k] : $d;
echo $reply["summary"] ?? "", "\n\n", $label("title", "IN CASE OF EMERGENCY - MEDICAL INFORMATION"), "\n";
echo $card["name"] ?? "", " ", $card["born"] ?? "", " ", $label("blood_type", "Blood type"), ": ", $card["blood_type"] ?? "", "\n";
echo $label("allergies", "Allergies"), ":\n";
if (!empty($card["no_known_allergies"])) echo " ", $label("no_known_allergies", "No known allergies"), "\n";
foreach ($card["allergies"] ?? [] as $a) echo " ! ", $a["allergen"], ": ", $a["reaction"] ?? "", " (", $a["severity"] ?? "", ")\n";
echo $label("medicines", "Medicines"), ":\n";
foreach ($card["medications"] ?? [] as $m) echo " - ", $m["name"], " ", $m["dose"] ?? "", " ", $m["frequency"] ?? "", " (", $m["reason"] ?? "", ")\n";
echo $label("contacts", "Contacts"), ":\n";
foreach ($card["contacts"] ?? [] as $c) echo " - ", $c["name"], " (", $c["relation"] ?? "", "): ", $c["phone"] ?? "", "\n";
if (($reply["task"] ?? "") === "translate") {
foreach ($reply["phrases"] ?? [] as $p) echo "> ", $p["text"], " [", $p["english"] ?? "", "]\n";
} else { // structure: confirm these before printing
foreach ($reply["questions"] ?? [] as $q) echo "? ", $q, "\n";
foreach ($reply["left_out"] ?? [] as $x) echo "left out: ", $x["item"], " - ", $x["why"] ?? "", "\n";
}
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"ice-card:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var output = j["output"]!["output"]!.GetValue<string>();
var from = output.IndexOf('{');
var reply = JsonNode.Parse(output.Substring(from, output.LastIndexOf('}') - from + 1))!;
Console.WriteLine($"{j["charged_credits"]} {j["truncated"]}");
JsonArray Arr(JsonNode? n) => n as JsonArray ?? new JsonArray();
var card = reply["card"] ?? new JsonObject(); var lab = reply["labels"]; // labels: translate only
string Label(string k, string d) => string.IsNullOrEmpty((string?)lab?[k]) ? d : (string)lab![k]!;
Console.WriteLine($"{reply["summary"]}\n\n{Label("title", "IN CASE OF EMERGENCY - MEDICAL INFORMATION")}");
Console.WriteLine($"{card["name"]} {card["born"]} {Label("blood_type", "Blood type")}: {card["blood_type"]}");
Console.WriteLine(Label("allergies", "Allergies") + ":");
if ((bool?)card["no_known_allergies"] == true) Console.WriteLine(" " + Label("no_known_allergies", "No known allergies"));
foreach (var a in Arr(card["allergies"])) Console.WriteLine($" ! {a!["allergen"]}: {a["reaction"]} ({a["severity"]})");
Console.WriteLine(Label("medicines", "Medicines") + ":");
foreach (var m in Arr(card["medications"])) Console.WriteLine($" - {m!["name"]} {m["dose"]} {m["frequency"]} ({m["reason"]})");
Console.WriteLine(Label("contacts", "Contacts") + ":");
foreach (var c in Arr(card["contacts"])) Console.WriteLine($" - {c!["name"]} ({c["relation"]}): {c["phone"]}");
if ((string?)reply["task"] == "translate") {
foreach (var p in Arr(reply["phrases"])) Console.WriteLine($"> {p!["text"]} [{p["english"]}]");
} else { // structure: confirm these before printing
foreach (var q in Arr(reply["questions"])) Console.WriteLine($"? {q}");
foreach (var x in Arr(reply["left_out"])) Console.WriteLine($"left out: {x!["item"]} - {x["why"]}");
}
Step 6 - POST /run-stream
The same run as an SSE stream, with the same input.json and the same kind of key. Browsers receive progress ticks rather than text deltas; the final done message carries the job, whose output.output is the reply.
# SSE: tick/delta messages while the job runs, then a final message with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: ice-card:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
rev = json.load(open("input.json")) # the same run input as input.json (see the input table)
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "ice-card:" + rev["task"] + ":" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `ice-card:${rev.task}:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "ice-card:"+input["task"].(string)+":"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 1<<20), 1<<24) // the final message carries the whole reply
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token)
.header("Idempotency-Key", "ice-card:" + M.readTree(rev).path("task").asText() + ":" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "ice-card:#{JSON.parse(rev)["task"]}:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, 443, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$task = json_decode($rev, true)["task"];
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: ice-card:$task:" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"ice-card:{JsonNode.Parse(rev)!["task"]}:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);