Drive scRNA Desk from your own code
Everything the web page does is available over HTTP: post one per-cell QC metrics table, name a
lane in task, and get the same structured worksheet back as one JSON object. The
natural uses are the ones a browser tab is bad at — re-running the QC read over every new
library as it comes off the sequencer, holding a nightly check that no new sample has drifted, or
wiring the read into the step where a metrics export is first written, so the thresholds get
argued about before anyone filters anything.
Say the important thing first: scRNA Desk has your metrics, not your genes. It never sees an expression matrix, it cannot tell you a cell type is present or rare, and it is not a diagnostic tool. It documents the consequences of the thresholds you send on the table you send, and names what contradicts itself. If a required column is missing, the reply says so rather than inventing a distribution to fill the hole. Build on that boundary rather than against it.
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api and every response
uses the same envelope, so one helper covers the whole API:
{"ok": true, "data": { ... }}
{"ok": false, "error": {"code": "VALIDATION_ERROR", "message": "human-readable", "details": { ... }}}
The slug identifies the app. It travels in the POST /guest body when you mint a
token, and after that the token itself carries it — there is no per-request app header to
set.
Error codes
| code | HTTP | what it means | what to do |
|---|---|---|---|
UNAUTHORIZED | 401 | No token, a malformed token, or one issued for another app. | Mint a fresh token. A guest token is enough for /me and /estimate. |
PAYMENT_REQUIRED | 402 | The balance cannot cover this run's minimum. | Top up. /estimate is free, so check it before every run and you will not meet this. |
VALIDATION_ERROR | 400 | The input object is not shaped as the app expects. | Read error.details. Usually a missing metrics string or a task that is not a lane id. |
NOT_FOUND | 404 | Unknown job id, or an endpoint this app does not expose. | Check the id you are polling came from this app's /run. |
RATE_LIMITED | 429 | Too many requests from this token or IP. | Back off and retry. Never tight-loop a poll; a second between polls is plenty. |
INTERNAL | 500 | The platform failed, not your call. | Retry once with the same Idempotency-Key so a partially-charged run is not billed twice. |
The input object
The run body is the input object itself — the fields below sit at the top
level of the JSON you post, not nested under an input key.
1. task — the lane, and the first field to get right
task selects which worksheet you get. It is a plain string and there are exactly
two values. Every other field is shared between them, because both lanes read the same work
object: one pasted metrics table.
task | lane | what comes back | source skill |
|---|---|---|---|
qc | QC read | Per-sample verdicts, the threshold call, what the cut costs in cells, what could be lost. Seven named checks. | @anthropics/single-cell-rna-qc |
integrate | Integration setup | The scvi-tools model, the covariates, the training budget, the falsifiable post-integration checks. Seven named checks. | @anthropics/scvi-tools |
An absent or unrecognised task does not error: the model picks the closest lane and
names the lane it produced in the reply's lane field. Read lane rather
than assuming you got what you asked for.
2. Everything else
| field | type | required | meaning |
|---|---|---|---|
metrics | string | yes | The per-cell QC metrics table, as text. adata.obs written out with to_csv, or a Seurat meta.data export. Comma, tab, pipe, semicolon and markdown tables all parse; # comment lines are ignored. |
species | string | no | human (default), mouse or other. Decides the mitochondrial and ribosomal gene prefixes quoted back to you. |
tissue | string | no | Free text: tissue, preparation, chemistry. This is what lets the reply say whether a high mitochondrial fraction is expected. |
modality | string | no | rna (default), rna_cross_tech, citeseq, atac, multiome, spatial_ref. Routes the scvi-tools model. |
labels | string | no | none (default), partial, full. Anything but none routes scVI to scANVI. |
mad_counts, mad_genes, mad_mt | string or number | no | MAD multipliers. Defaults 5, 5, 3 — the scverse best-practice values. |
mt_hard | string or number | no | Hard mitochondrial percentage cutoff. Default 8. |
min_genes | string or number | no | Minimum genes per cell. Default 100. |
doublet_thresh | string or number | no | Doublet-score cutoff. Default 0.2. |
context | string | no | Design, downstream goal, what has already been done to the matrix. Up to 4,000 characters. |
handoff | object | no | {from_lane, verdict, notes[]}. Present when this run follows the other lane in the same sitting. |
engine_facts | object | no, but send it | The free in-browser read. See below — without it the reply has only the raw table to reason from, and the reconciliation the app is built around cannot happen. |
3. engine_facts, and why you should compute it
The web page never asks the model for arithmetic. It computes the distributions, the MAD bounds,
the per-sample retention, the condition-versus-sample cross-tabulation and a routed scvi-tools
model in the browser, sends all of it as engine_facts, and then checks the reply
against it: every flag must be addressed exactly once, and a flag the reply ignored is shown
to the user as ignored.
From code you have two honest options. Send engine_facts yourself — the shape
is documented by the factsFor function in
/cellscan.js, which is plain, dependency-free JavaScript you
can read or reuse — or omit it and accept that the reply is working from the table alone,
with no ground truth to be held to. Do not send a hand-written engine_facts whose
numbers you have not actually computed: the whole point of the field is that it is not the model's
arithmetic, and a fabricated one is worse than none.
The output contract
One JSON object per run, the same envelope in both lanes. This is taken from the parsing code in
/app.js — parseResult strips any code fence
and takes the outermost {...}, then normalize coerces every field, so a
missing optional field degrades rather than throwing.
{
"lane": "qc", // "qc" | "integrate" - read this, do not assume
"dataset_name": "string",
"posture": "clear | caution | blocked",
"verdict": "one sentence",
"exec_summary": "one paragraph",
"samples": [ // one entry per engine sample ref, S1..Sn
{
"ref": "S1", "sample": "donor1",
"headline": "string", "detail": "string",
"severity": "none | low | medium | high | critical",
// qc lane keys: metrics_read, threshold_call, retention_note, watch_for[]
// integrate lane keys: batch_role, model_note, covariates[], risk
}
],
"findings": [{"id": "F-001", "severity": "critical|high|medium|low",
"title": "", "detail": "", "samples": ["S1"], "mitigation": ""}],
"checks": [{"check": "the fixed name, verbatim", "status": "pass|fail|partial|unknown",
"evidence": "", "requirement": ""}],
"steps": [{"id": "P-001", "stage": "inspect|correct|filter|model|verify",
"action": "", "rationale": "", "evidence": ""}],
"parameters": [{"name": "", "value": "", "unit": "", "rationale": ""}],
"coverage_check": [{"key": "MT-HIGH:S3", "addressed": true, "note": ""}],
"assumptions": ["..."], "open_questions": ["..."], "evidence_gaps": ["..."],
"summary": "closing paragraph"
}
Two contract details that matter if you are parsing this yourself. The seven check names
are fixed per lane and appear in a fixed order — the app compares them
case-insensitively against its own list and reports any that are off-contract, so you can key on
them. And coverage_check keys are the engine's flag keys
(MT-HIGH:S3, BATCH-CONFOUND:stimulated, OVERFILTER), one
entry per flag; a key that is not one of yours means the reply invented it.
The seven named checks, per lane
| # | task: "qc" | task: "integrate" |
|---|---|---|
| 1 | Required metric columns present | Model matches the modality and label situation |
| 2 | Counts are raw integers | Raw count layer named in setup_anndata |
| 3 | Mitochondrial scale and cutoff suit the tissue | Batch key names the technical unit |
| 4 | MAD thresholds reproduce the engine retention | Condition is not corrected away |
| 5 | Ambient RNA handled before filtering | Highly variable gene selection is batch-aware |
| 6 | Doublets handled by a doublet caller, not by MAD | Training budget fits the cell count |
| 7 | Rare populations survive the cut | Post-integration checks are falsifiable |
Step 1 — get a token
A guest token is free to mint and is enough for /me and
/estimate. A metered run needs a personal token, which comes from
signing in. The shortest path to the token this browser already holds, with a ready-made shell
export, is the token page.
# Mint a guest token. Free, and enough for /me and /estimate.
# The reply carries {token, expires_at}; keep the token and send it as a bearer.
# For a personal token, sign in at https://scrna-desk.skillsafe.ai/tokens.html
# and press Copy shell export.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/guest" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"slug": "scrna-desk"}'
# Mint a guest token. Free, and enough for /me and /estimate.
# The reply carries {token, expires_at}; keep the token and send it as a bearer.
# For a personal token, sign in at https://scrna-desk.skillsafe.ai/tokens.html
# and press Copy shell export.
import json, os, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = os.environ.get("SKILLSAFE_TOKEN", "YOUR_TOKEN")
def call(path, payload=None):
data = json.dumps(payload).encode() if payload 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")
with urllib.request.urlopen(req) as r:
envelope = json.load(r)
if not envelope.get("ok"):
raise RuntimeError(envelope["error"]["code"] + ": " + envelope["error"]["message"])
return envelope["data"]
payload = {
"slug": "scrna-desk"
}
print(call("/guest", payload))
// Mint a guest token. Free, and enough for /me and /estimate.
// The reply carries {token, expires_at}; keep the token and send it as a bearer.
// For a personal token, sign in at https://scrna-desk.skillsafe.ai/tokens.html
// and press Copy shell export.
const BASE = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // see tokens.html
async function call(path, payload) {
const res = await fetch(BASE + path, {
method: payload ? "POST" : "GET",
headers: {
Authorization: "Bearer " + TOKEN,
...(payload ? { "Content-Type": "application/json" } : {}),
},
body: payload ? JSON.stringify(payload) : undefined,
});
const envelope = await res.json();
if (!envelope.ok) throw new Error(envelope.error.code + ": " + envelope.error.message);
return envelope.data;
}
const payload = {
"slug": "scrna-desk"
};
console.log(await call("/guest", payload));
// Mint a guest token. Free, and enough for /me and /estimate.
// The reply carries {token, expires_at}; keep the token and send it as a bearer.
// For a personal token, sign in at https://scrna-desk.skillsafe.ai/tokens.html
// and press Copy shell export.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
const base = "https://api.skillsafe.ai/v1/app-api"
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, payload any) (json.RawMessage, error) {
method := http.MethodGet
var rdr io.Reader
if payload != nil {
b, _ := json.Marshal(payload)
rdr = bytes.NewReader(b)
method = http.MethodPost
}
req, _ := http.NewRequest(method, base+path, rdr)
req.Header.Set("Authorization", "Bearer "+os.Getenv("SKILLSAFE_TOKEN"))
if payload != nil {
req.Header.Set("Content-Type", "application/json")
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var e envelope
if err := json.NewDecoder(res.Body).Decode(&e); err != nil {
return nil, err
}
if !e.OK {
return nil, fmt.Errorf("%s: %s", e.Error.Code, e.Error.Message)
}
return e.Data, nil
}
func main() {
var payload map[string]any
json.Unmarshal([]byte(`{"slug": "scrna-desk"}`), &payload)
data, err := call("/guest", payload)
if err != nil {
panic(err)
}
fmt.Println(string(data))
}
// Mint a guest token. Free, and enough for /me and /estimate.
// The reply carries {token, expires_at}; keep the token and send it as a bearer.
// For a personal token, sign in at https://scrna-desk.skillsafe.ai/tokens.html
// and press Copy shell export.
import java.net.URI;
import java.net.http.*;
import java.util.Optional;
public class ScrnaDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = System.getenv("SKILLSAFE_TOKEN"); // or "YOUR_TOKEN"
static String call(String path, String payload) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN);
if (payload == null) {
b = b.GET();
} else {
b = b.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload));
}
HttpResponse<String> res = HttpClient.newHttpClient()
.send(b.build(), HttpResponse.BodyHandlers.ofString());
// The envelope is {"ok":bool,"data":...,"error":{"code","message"}}
return res.body();
}
public static void main(String[] args) throws Exception {
String payload = """
{
"slug": "scrna-desk"
}
""";
System.out.println(call("/guest", payload));
}
}
# Mint a guest token. Free, and enough for /me and /estimate.
# The reply carries {token, expires_at}; keep the token and send it as a bearer.
# For a personal token, sign in at https://scrna-desk.skillsafe.ai/tokens.html
# and press Copy shell export.
require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = ENV.fetch("SKILLSAFE_TOKEN", "YOUR_TOKEN")
def call(path, payload = nil)
uri = URI(BASE + path)
req = payload ? Net::HTTP::Post.new(uri) : Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
if payload
req["Content-Type"] = "application/json"
req.body = JSON.dump(payload)
end
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
envelope = JSON.parse(res.body)
raise "#{envelope["error"]["code"]}: #{envelope["error"]["message"]}" unless envelope["ok"]
envelope["data"]
end
payload = JSON.parse(<<~JSON)
{
"slug": "scrna-desk"
}
JSON
pp call("/guest", payload)
// Mint a guest token. Free, and enough for /me and /estimate.
// The reply carries {token, expires_at}; keep the token and send it as a bearer.
// For a personal token, sign in at https://scrna-desk.skillsafe.ai/tokens.html
// and press Copy shell export.
<?php
$base = "https://api.skillsafe.ai/v1/app-api";
$token = getenv("SKILLSAFE_TOKEN") ?: "YOUR_TOKEN";
function call(string $path, ?array $payload = null) {
global $base, $token;
$headers = ["Authorization: Bearer $token"];
$opts = ["http" => ["method" => "GET", "header" => ""]];
if ($payload !== null) {
$headers[] = "Content-Type: application/json";
$opts["http"]["method"] = "POST";
$opts["http"]["content"] = json_encode($payload);
}
$opts["http"]["header"] = implode("\r\n", $headers);
$raw = file_get_contents($base . $path, false, stream_context_create($opts));
$envelope = json_decode($raw, true);
if (!$envelope["ok"]) {
throw new RuntimeException($envelope["error"]["code"] . ": " . $envelope["error"]["message"]);
}
return $envelope["data"];
}
$payload = json_decode(<<<'JSON'
{
"slug": "scrna-desk"
}
JSON, true);
print_r(call("/guest", $payload));
// Mint a guest token. Free, and enough for /me and /estimate.
// The reply carries {token, expires_at}; keep the token and send it as a bearer.
// For a personal token, sign in at https://scrna-desk.skillsafe.ai/tokens.html
// and press Copy shell export.
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
class ScrnaDesk
{
const string Base = "https://api.skillsafe.ai/v1/app-api";
static readonly string Token =
Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN";
static async Task<string> Call(string path, string payload = null)
{
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", Token);
HttpResponseMessage res;
if (payload == null)
{
res = await http.GetAsync(Base + path);
}
else
{
var content = new StringContent(payload, Encoding.UTF8, "application/json");
res = await http.PostAsync(Base + path, content);
}
// The envelope is {"ok":bool,"data":...,"error":{"code","message"}}
return await res.Content.ReadAsStringAsync();
}
static async Task Main()
{
var payload = """
{
"slug": "scrna-desk"
}
""";
Console.WriteLine(await Call("/guest", payload));
}
}
Step 2 — GET /me
Confirms the token works, tells you whether you are a guest or a signed-in user, and gives the
credit balance you should compare against /estimate before running anything.
# Who am I, and what is my balance?
# subject_type is "user" for a personal token, "guest" for a guest one.
# credits is in platform credits; 10,000 credits is $1.00.
curl -s "https://api.skillsafe.ai/v1/app-api/me" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN"
# Who am I, and what is my balance?
# subject_type is "user" for a personal token, "guest" for a guest one.
# credits is in platform credits; 10,000 credits is $1.00.
import json, os, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = os.environ.get("SKILLSAFE_TOKEN", "YOUR_TOKEN")
def call(path, payload=None):
data = json.dumps(payload).encode() if payload 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")
with urllib.request.urlopen(req) as r:
envelope = json.load(r)
if not envelope.get("ok"):
raise RuntimeError(envelope["error"]["code"] + ": " + envelope["error"]["message"])
return envelope["data"]
print(call("/me"))
// Who am I, and what is my balance?
// subject_type is "user" for a personal token, "guest" for a guest one.
// credits is in platform credits; 10,000 credits is $1.00.
const BASE = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // see tokens.html
async function call(path, payload) {
const res = await fetch(BASE + path, {
method: payload ? "POST" : "GET",
headers: {
Authorization: "Bearer " + TOKEN,
...(payload ? { "Content-Type": "application/json" } : {}),
},
body: payload ? JSON.stringify(payload) : undefined,
});
const envelope = await res.json();
if (!envelope.ok) throw new Error(envelope.error.code + ": " + envelope.error.message);
return envelope.data;
}
console.log(await call("/me"));
// Who am I, and what is my balance?
// subject_type is "user" for a personal token, "guest" for a guest one.
// credits is in platform credits; 10,000 credits is $1.00.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
const base = "https://api.skillsafe.ai/v1/app-api"
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, payload any) (json.RawMessage, error) {
method := http.MethodGet
var rdr io.Reader
if payload != nil {
b, _ := json.Marshal(payload)
rdr = bytes.NewReader(b)
method = http.MethodPost
}
req, _ := http.NewRequest(method, base+path, rdr)
req.Header.Set("Authorization", "Bearer "+os.Getenv("SKILLSAFE_TOKEN"))
if payload != nil {
req.Header.Set("Content-Type", "application/json")
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var e envelope
if err := json.NewDecoder(res.Body).Decode(&e); err != nil {
return nil, err
}
if !e.OK {
return nil, fmt.Errorf("%s: %s", e.Error.Code, e.Error.Message)
}
return e.Data, nil
}
func main() {
data, err := call("/me", nil)
if err != nil {
panic(err)
}
fmt.Println(string(data))
}
// Who am I, and what is my balance?
// subject_type is "user" for a personal token, "guest" for a guest one.
// credits is in platform credits; 10,000 credits is $1.00.
import java.net.URI;
import java.net.http.*;
import java.util.Optional;
public class ScrnaDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = System.getenv("SKILLSAFE_TOKEN"); // or "YOUR_TOKEN"
static String call(String path, String payload) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN);
if (payload == null) {
b = b.GET();
} else {
b = b.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload));
}
HttpResponse<String> res = HttpClient.newHttpClient()
.send(b.build(), HttpResponse.BodyHandlers.ofString());
// The envelope is {"ok":bool,"data":...,"error":{"code","message"}}
return res.body();
}
public static void main(String[] args) throws Exception {
System.out.println(call("/me", null));
}
}
# Who am I, and what is my balance?
# subject_type is "user" for a personal token, "guest" for a guest one.
# credits is in platform credits; 10,000 credits is $1.00.
require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = ENV.fetch("SKILLSAFE_TOKEN", "YOUR_TOKEN")
def call(path, payload = nil)
uri = URI(BASE + path)
req = payload ? Net::HTTP::Post.new(uri) : Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
if payload
req["Content-Type"] = "application/json"
req.body = JSON.dump(payload)
end
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
envelope = JSON.parse(res.body)
raise "#{envelope["error"]["code"]}: #{envelope["error"]["message"]}" unless envelope["ok"]
envelope["data"]
end
pp call("/me")
// Who am I, and what is my balance?
// subject_type is "user" for a personal token, "guest" for a guest one.
// credits is in platform credits; 10,000 credits is $1.00.
<?php
$base = "https://api.skillsafe.ai/v1/app-api";
$token = getenv("SKILLSAFE_TOKEN") ?: "YOUR_TOKEN";
function call(string $path, ?array $payload = null) {
global $base, $token;
$headers = ["Authorization: Bearer $token"];
$opts = ["http" => ["method" => "GET", "header" => ""]];
if ($payload !== null) {
$headers[] = "Content-Type: application/json";
$opts["http"]["method"] = "POST";
$opts["http"]["content"] = json_encode($payload);
}
$opts["http"]["header"] = implode("\r\n", $headers);
$raw = file_get_contents($base . $path, false, stream_context_create($opts));
$envelope = json_decode($raw, true);
if (!$envelope["ok"]) {
throw new RuntimeException($envelope["error"]["code"] . ": " . $envelope["error"]["message"]);
}
return $envelope["data"];
}
print_r(call("/me"));
// Who am I, and what is my balance?
// subject_type is "user" for a personal token, "guest" for a guest one.
// credits is in platform credits; 10,000 credits is $1.00.
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
class ScrnaDesk
{
const string Base = "https://api.skillsafe.ai/v1/app-api";
static readonly string Token =
Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN";
static async Task<string> Call(string path, string payload = null)
{
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", Token);
HttpResponseMessage res;
if (payload == null)
{
res = await http.GetAsync(Base + path);
}
else
{
var content = new StringContent(payload, Encoding.UTF8, "application/json");
res = await http.PostAsync(Base + path, content);
}
// The envelope is {"ok":bool,"data":...,"error":{"code","message"}}
return await res.Content.ReadAsStringAsync();
}
static async Task Main()
{
Console.WriteLine(await Call("/me"));
}
}
Step 3 — POST /estimate (free)
Nothing is billed and no job is created. The reply carries model,
model_alias, markup_bps, hold_credits and
min_credits. hold_credits is a reservation sized for the full
output cap, not the price — the actual charge is usually far lower. Estimate per lane: the two
lanes have different prompt sections and different output caps, so their holds differ.
# Free: prices the run without creating a job or billing anything.
# Send the SAME object you would send to /run, including task -
# a different payload prices a different run.
# Assert model_alias == "gpt-terra" and markup_bps == 1000 if you
# want to be sure you are talking to the app you think you are.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/estimate" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"task": "qc", "metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437", "species": "human", "tissue": "PBMC, fresh, 10x 3' v3.1", "modality": "rna", "labels": "none", "mad_counts": "5", "mad_genes": "5", "mad_mt": "3", "mt_hard": "8", "min_genes": "100", "doublet_thresh": "0.2", "context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day."}'
# Free: prices the run without creating a job or billing anything.
# Send the SAME object you would send to /run, including task -
# a different payload prices a different run.
# Assert model_alias == "gpt-terra" and markup_bps == 1000 if you
# want to be sure you are talking to the app you think you are.
import json, os, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = os.environ.get("SKILLSAFE_TOKEN", "YOUR_TOKEN")
def call(path, payload=None):
data = json.dumps(payload).encode() if payload 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")
with urllib.request.urlopen(req) as r:
envelope = json.load(r)
if not envelope.get("ok"):
raise RuntimeError(envelope["error"]["code"] + ": " + envelope["error"]["message"])
return envelope["data"]
payload = {
"task": "qc",
"metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437",
"species": "human",
"tissue": "PBMC, fresh, 10x 3' v3.1",
"modality": "rna",
"labels": "none",
"mad_counts": "5",
"mad_genes": "5",
"mad_mt": "3",
"mt_hard": "8",
"min_genes": "100",
"doublet_thresh": "0.2",
"context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day."
}
print(call("/estimate", payload))
// Free: prices the run without creating a job or billing anything.
// Send the SAME object you would send to /run, including task -
// a different payload prices a different run.
// Assert model_alias == "gpt-terra" and markup_bps == 1000 if you
// want to be sure you are talking to the app you think you are.
const BASE = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // see tokens.html
async function call(path, payload) {
const res = await fetch(BASE + path, {
method: payload ? "POST" : "GET",
headers: {
Authorization: "Bearer " + TOKEN,
...(payload ? { "Content-Type": "application/json" } : {}),
},
body: payload ? JSON.stringify(payload) : undefined,
});
const envelope = await res.json();
if (!envelope.ok) throw new Error(envelope.error.code + ": " + envelope.error.message);
return envelope.data;
}
const payload = {
"task": "qc",
"metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437",
"species": "human",
"tissue": "PBMC, fresh, 10x 3' v3.1",
"modality": "rna",
"labels": "none",
"mad_counts": "5",
"mad_genes": "5",
"mad_mt": "3",
"mt_hard": "8",
"min_genes": "100",
"doublet_thresh": "0.2",
"context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day."
};
console.log(await call("/estimate", payload));
// Free: prices the run without creating a job or billing anything.
// Send the SAME object you would send to /run, including task -
// a different payload prices a different run.
// Assert model_alias == "gpt-terra" and markup_bps == 1000 if you
// want to be sure you are talking to the app you think you are.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
const base = "https://api.skillsafe.ai/v1/app-api"
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, payload any) (json.RawMessage, error) {
method := http.MethodGet
var rdr io.Reader
if payload != nil {
b, _ := json.Marshal(payload)
rdr = bytes.NewReader(b)
method = http.MethodPost
}
req, _ := http.NewRequest(method, base+path, rdr)
req.Header.Set("Authorization", "Bearer "+os.Getenv("SKILLSAFE_TOKEN"))
if payload != nil {
req.Header.Set("Content-Type", "application/json")
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var e envelope
if err := json.NewDecoder(res.Body).Decode(&e); err != nil {
return nil, err
}
if !e.OK {
return nil, fmt.Errorf("%s: %s", e.Error.Code, e.Error.Message)
}
return e.Data, nil
}
func main() {
var payload map[string]any
json.Unmarshal([]byte(`{"task": "qc", "metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437", "species": "human", "tissue": "PBMC, fresh, 10x 3' v3.1", "modality": "rna", "labels": "none", "mad_counts": "5", "mad_genes": "5", "mad_mt": "3", "mt_hard": "8", "min_genes": "100", "doublet_thresh": "0.2", "context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day."}`), &payload)
data, err := call("/estimate", payload)
if err != nil {
panic(err)
}
fmt.Println(string(data))
}
// Free: prices the run without creating a job or billing anything.
// Send the SAME object you would send to /run, including task -
// a different payload prices a different run.
// Assert model_alias == "gpt-terra" and markup_bps == 1000 if you
// want to be sure you are talking to the app you think you are.
import java.net.URI;
import java.net.http.*;
import java.util.Optional;
public class ScrnaDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = System.getenv("SKILLSAFE_TOKEN"); // or "YOUR_TOKEN"
static String call(String path, String payload) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN);
if (payload == null) {
b = b.GET();
} else {
b = b.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload));
}
HttpResponse<String> res = HttpClient.newHttpClient()
.send(b.build(), HttpResponse.BodyHandlers.ofString());
// The envelope is {"ok":bool,"data":...,"error":{"code","message"}}
return res.body();
}
public static void main(String[] args) throws Exception {
String payload = """
{
"task": "qc",
"metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437",
"species": "human",
"tissue": "PBMC, fresh, 10x 3' v3.1",
"modality": "rna",
"labels": "none",
"mad_counts": "5",
"mad_genes": "5",
"mad_mt": "3",
"mt_hard": "8",
"min_genes": "100",
"doublet_thresh": "0.2",
"context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day."
}
""";
System.out.println(call("/estimate", payload));
}
}
# Free: prices the run without creating a job or billing anything.
# Send the SAME object you would send to /run, including task -
# a different payload prices a different run.
# Assert model_alias == "gpt-terra" and markup_bps == 1000 if you
# want to be sure you are talking to the app you think you are.
require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = ENV.fetch("SKILLSAFE_TOKEN", "YOUR_TOKEN")
def call(path, payload = nil)
uri = URI(BASE + path)
req = payload ? Net::HTTP::Post.new(uri) : Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
if payload
req["Content-Type"] = "application/json"
req.body = JSON.dump(payload)
end
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
envelope = JSON.parse(res.body)
raise "#{envelope["error"]["code"]}: #{envelope["error"]["message"]}" unless envelope["ok"]
envelope["data"]
end
payload = JSON.parse(<<~JSON)
{
"task": "qc",
"metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437",
"species": "human",
"tissue": "PBMC, fresh, 10x 3' v3.1",
"modality": "rna",
"labels": "none",
"mad_counts": "5",
"mad_genes": "5",
"mad_mt": "3",
"mt_hard": "8",
"min_genes": "100",
"doublet_thresh": "0.2",
"context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day."
}
JSON
pp call("/estimate", payload)
// Free: prices the run without creating a job or billing anything.
// Send the SAME object you would send to /run, including task -
// a different payload prices a different run.
// Assert model_alias == "gpt-terra" and markup_bps == 1000 if you
// want to be sure you are talking to the app you think you are.
<?php
$base = "https://api.skillsafe.ai/v1/app-api";
$token = getenv("SKILLSAFE_TOKEN") ?: "YOUR_TOKEN";
function call(string $path, ?array $payload = null) {
global $base, $token;
$headers = ["Authorization: Bearer $token"];
$opts = ["http" => ["method" => "GET", "header" => ""]];
if ($payload !== null) {
$headers[] = "Content-Type: application/json";
$opts["http"]["method"] = "POST";
$opts["http"]["content"] = json_encode($payload);
}
$opts["http"]["header"] = implode("\r\n", $headers);
$raw = file_get_contents($base . $path, false, stream_context_create($opts));
$envelope = json_decode($raw, true);
if (!$envelope["ok"]) {
throw new RuntimeException($envelope["error"]["code"] . ": " . $envelope["error"]["message"]);
}
return $envelope["data"];
}
$payload = json_decode(<<<'JSON'
{
"task": "qc",
"metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437",
"species": "human",
"tissue": "PBMC, fresh, 10x 3' v3.1",
"modality": "rna",
"labels": "none",
"mad_counts": "5",
"mad_genes": "5",
"mad_mt": "3",
"mt_hard": "8",
"min_genes": "100",
"doublet_thresh": "0.2",
"context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day."
}
JSON, true);
print_r(call("/estimate", $payload));
// Free: prices the run without creating a job or billing anything.
// Send the SAME object you would send to /run, including task -
// a different payload prices a different run.
// Assert model_alias == "gpt-terra" and markup_bps == 1000 if you
// want to be sure you are talking to the app you think you are.
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
class ScrnaDesk
{
const string Base = "https://api.skillsafe.ai/v1/app-api";
static readonly string Token =
Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN";
static async Task<string> Call(string path, string payload = null)
{
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", Token);
HttpResponseMessage res;
if (payload == null)
{
res = await http.GetAsync(Base + path);
}
else
{
var content = new StringContent(payload, Encoding.UTF8, "application/json");
res = await http.PostAsync(Base + path, content);
}
// The envelope is {"ok":bool,"data":...,"error":{"code","message"}}
return await res.Content.ReadAsStringAsync();
}
static async Task Main()
{
var payload = """
{
"task": "qc",
"metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437",
"species": "human",
"tissue": "PBMC, fresh, 10x 3' v3.1",
"modality": "rna",
"labels": "none",
"mad_counts": "5",
"mad_genes": "5",
"mad_mt": "3",
"mt_hard": "8",
"min_genes": "100",
"doublet_thresh": "0.2",
"context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day."
}
""";
Console.WriteLine(await Call("/estimate", payload));
}
}
Step 4 — POST /run, then poll
/run returns a job_id immediately. Poll
GET /jobs/{job_id} until status is terminal
(succeeded or failed); the worksheet text is at
data.output.output. Send an Idempotency-Key header on every run: it is
what stops a network blip from billing the same worksheet twice. Derive it from the lane plus a hash
of the input, so two lanes over one table are two distinct keys.
# Metered. Needs a personal token.
# Send an Idempotency-Key header derived from (task, input, attempt):
# Idempotency-Key: scrna-desk:qc:<hash-of-input>:a1
# The reply is {job_id}; poll GET /jobs/{job_id} about once a second
# until status is succeeded or failed, then read data.output.output.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/run" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"task": "qc", "metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437", "species": "human", "tissue": "PBMC, fresh, 10x 3' v3.1", "modality": "rna", "labels": "none", "mad_counts": "5", "mad_genes": "5", "mad_mt": "3", "mt_hard": "8", "min_genes": "100", "doublet_thresh": "0.2", "context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day."}'
# Metered. Needs a personal token.
# Send an Idempotency-Key header derived from (task, input, attempt):
# Idempotency-Key: scrna-desk:qc:<hash-of-input>:a1
# The reply is {job_id}; poll GET /jobs/{job_id} about once a second
# until status is succeeded or failed, then read data.output.output.
import json, os, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = os.environ.get("SKILLSAFE_TOKEN", "YOUR_TOKEN")
def call(path, payload=None):
data = json.dumps(payload).encode() if payload 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")
with urllib.request.urlopen(req) as r:
envelope = json.load(r)
if not envelope.get("ok"):
raise RuntimeError(envelope["error"]["code"] + ": " + envelope["error"]["message"])
return envelope["data"]
payload = {
"task": "qc",
"metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437",
"species": "human",
"tissue": "PBMC, fresh, 10x 3' v3.1",
"modality": "rna",
"labels": "none",
"mad_counts": "5",
"mad_genes": "5",
"mad_mt": "3",
"mt_hard": "8",
"min_genes": "100",
"doublet_thresh": "0.2",
"context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day."
}
print(call("/run", payload))
// Metered. Needs a personal token.
// Send an Idempotency-Key header derived from (task, input, attempt):
// Idempotency-Key: scrna-desk:qc:<hash-of-input>:a1
// The reply is {job_id}; poll GET /jobs/{job_id} about once a second
// until status is succeeded or failed, then read data.output.output.
const BASE = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // see tokens.html
async function call(path, payload) {
const res = await fetch(BASE + path, {
method: payload ? "POST" : "GET",
headers: {
Authorization: "Bearer " + TOKEN,
...(payload ? { "Content-Type": "application/json" } : {}),
},
body: payload ? JSON.stringify(payload) : undefined,
});
const envelope = await res.json();
if (!envelope.ok) throw new Error(envelope.error.code + ": " + envelope.error.message);
return envelope.data;
}
const payload = {
"task": "qc",
"metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437",
"species": "human",
"tissue": "PBMC, fresh, 10x 3' v3.1",
"modality": "rna",
"labels": "none",
"mad_counts": "5",
"mad_genes": "5",
"mad_mt": "3",
"mt_hard": "8",
"min_genes": "100",
"doublet_thresh": "0.2",
"context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day."
};
console.log(await call("/run", payload));
// Metered. Needs a personal token.
// Send an Idempotency-Key header derived from (task, input, attempt):
// Idempotency-Key: scrna-desk:qc:<hash-of-input>:a1
// The reply is {job_id}; poll GET /jobs/{job_id} about once a second
// until status is succeeded or failed, then read data.output.output.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
const base = "https://api.skillsafe.ai/v1/app-api"
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, payload any) (json.RawMessage, error) {
method := http.MethodGet
var rdr io.Reader
if payload != nil {
b, _ := json.Marshal(payload)
rdr = bytes.NewReader(b)
method = http.MethodPost
}
req, _ := http.NewRequest(method, base+path, rdr)
req.Header.Set("Authorization", "Bearer "+os.Getenv("SKILLSAFE_TOKEN"))
if payload != nil {
req.Header.Set("Content-Type", "application/json")
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var e envelope
if err := json.NewDecoder(res.Body).Decode(&e); err != nil {
return nil, err
}
if !e.OK {
return nil, fmt.Errorf("%s: %s", e.Error.Code, e.Error.Message)
}
return e.Data, nil
}
func main() {
var payload map[string]any
json.Unmarshal([]byte(`{"task": "qc", "metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437", "species": "human", "tissue": "PBMC, fresh, 10x 3' v3.1", "modality": "rna", "labels": "none", "mad_counts": "5", "mad_genes": "5", "mad_mt": "3", "mt_hard": "8", "min_genes": "100", "doublet_thresh": "0.2", "context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day."}`), &payload)
data, err := call("/run", payload)
if err != nil {
panic(err)
}
fmt.Println(string(data))
}
// Metered. Needs a personal token.
// Send an Idempotency-Key header derived from (task, input, attempt):
// Idempotency-Key: scrna-desk:qc:<hash-of-input>:a1
// The reply is {job_id}; poll GET /jobs/{job_id} about once a second
// until status is succeeded or failed, then read data.output.output.
import java.net.URI;
import java.net.http.*;
import java.util.Optional;
public class ScrnaDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = System.getenv("SKILLSAFE_TOKEN"); // or "YOUR_TOKEN"
static String call(String path, String payload) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN);
if (payload == null) {
b = b.GET();
} else {
b = b.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload));
}
HttpResponse<String> res = HttpClient.newHttpClient()
.send(b.build(), HttpResponse.BodyHandlers.ofString());
// The envelope is {"ok":bool,"data":...,"error":{"code","message"}}
return res.body();
}
public static void main(String[] args) throws Exception {
String payload = """
{
"task": "qc",
"metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437",
"species": "human",
"tissue": "PBMC, fresh, 10x 3' v3.1",
"modality": "rna",
"labels": "none",
"mad_counts": "5",
"mad_genes": "5",
"mad_mt": "3",
"mt_hard": "8",
"min_genes": "100",
"doublet_thresh": "0.2",
"context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day."
}
""";
System.out.println(call("/run", payload));
}
}
# Metered. Needs a personal token.
# Send an Idempotency-Key header derived from (task, input, attempt):
# Idempotency-Key: scrna-desk:qc:<hash-of-input>:a1
# The reply is {job_id}; poll GET /jobs/{job_id} about once a second
# until status is succeeded or failed, then read data.output.output.
require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = ENV.fetch("SKILLSAFE_TOKEN", "YOUR_TOKEN")
def call(path, payload = nil)
uri = URI(BASE + path)
req = payload ? Net::HTTP::Post.new(uri) : Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
if payload
req["Content-Type"] = "application/json"
req.body = JSON.dump(payload)
end
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
envelope = JSON.parse(res.body)
raise "#{envelope["error"]["code"]}: #{envelope["error"]["message"]}" unless envelope["ok"]
envelope["data"]
end
payload = JSON.parse(<<~JSON)
{
"task": "qc",
"metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437",
"species": "human",
"tissue": "PBMC, fresh, 10x 3' v3.1",
"modality": "rna",
"labels": "none",
"mad_counts": "5",
"mad_genes": "5",
"mad_mt": "3",
"mt_hard": "8",
"min_genes": "100",
"doublet_thresh": "0.2",
"context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day."
}
JSON
pp call("/run", payload)
// Metered. Needs a personal token.
// Send an Idempotency-Key header derived from (task, input, attempt):
// Idempotency-Key: scrna-desk:qc:<hash-of-input>:a1
// The reply is {job_id}; poll GET /jobs/{job_id} about once a second
// until status is succeeded or failed, then read data.output.output.
<?php
$base = "https://api.skillsafe.ai/v1/app-api";
$token = getenv("SKILLSAFE_TOKEN") ?: "YOUR_TOKEN";
function call(string $path, ?array $payload = null) {
global $base, $token;
$headers = ["Authorization: Bearer $token"];
$opts = ["http" => ["method" => "GET", "header" => ""]];
if ($payload !== null) {
$headers[] = "Content-Type: application/json";
$opts["http"]["method"] = "POST";
$opts["http"]["content"] = json_encode($payload);
}
$opts["http"]["header"] = implode("\r\n", $headers);
$raw = file_get_contents($base . $path, false, stream_context_create($opts));
$envelope = json_decode($raw, true);
if (!$envelope["ok"]) {
throw new RuntimeException($envelope["error"]["code"] . ": " . $envelope["error"]["message"]);
}
return $envelope["data"];
}
$payload = json_decode(<<<'JSON'
{
"task": "qc",
"metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437",
"species": "human",
"tissue": "PBMC, fresh, 10x 3' v3.1",
"modality": "rna",
"labels": "none",
"mad_counts": "5",
"mad_genes": "5",
"mad_mt": "3",
"mt_hard": "8",
"min_genes": "100",
"doublet_thresh": "0.2",
"context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day."
}
JSON, true);
print_r(call("/run", $payload));
// Metered. Needs a personal token.
// Send an Idempotency-Key header derived from (task, input, attempt):
// Idempotency-Key: scrna-desk:qc:<hash-of-input>:a1
// The reply is {job_id}; poll GET /jobs/{job_id} about once a second
// until status is succeeded or failed, then read data.output.output.
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
class ScrnaDesk
{
const string Base = "https://api.skillsafe.ai/v1/app-api";
static readonly string Token =
Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN";
static async Task<string> Call(string path, string payload = null)
{
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", Token);
HttpResponseMessage res;
if (payload == null)
{
res = await http.GetAsync(Base + path);
}
else
{
var content = new StringContent(payload, Encoding.UTF8, "application/json");
res = await http.PostAsync(Base + path, content);
}
// The envelope is {"ok":bool,"data":...,"error":{"code","message"}}
return await res.Content.ReadAsStringAsync();
}
static async Task Main()
{
var payload = """
{
"task": "qc",
"metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437",
"species": "human",
"tissue": "PBMC, fresh, 10x 3' v3.1",
"modality": "rna",
"labels": "none",
"mad_counts": "5",
"mad_genes": "5",
"mad_mt": "3",
"mt_hard": "8",
"min_genes": "100",
"doublet_thresh": "0.2",
"context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day."
}
""";
Console.WriteLine(await Call("/run", payload));
}
}
Step 5 — POST /run-stream (SSE)
Same billing, same input, but the reply is a text/event-stream of deltas and the
page can show progress. The web app uses this one. The six section keys arrive in a fixed order
— verdict, samples, findings, checks,
parameters, coverage_check — which is exactly how the progress card
knows what stage it is at, and how a stream that dies early can still be parsed for the sections
that arrived.
# Metered, streamed as SSE. Note task: "integrate" and the handoff object,
# which is how the integration lane runs on the QC read's conclusion.
# Accept: text/event-stream, and send the same Idempotency-Key discipline.
# Concatenate the delta payloads; the result is the JSON object above.
# If the stream dies, parse what arrived - the section order is fixed.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/run-stream" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"task": "integrate", "metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437", "species": "human", "tissue": "PBMC, fresh, 10x 3' v3.1", "modality": "rna", "labels": "none", "mad_counts": "5", "mad_genes": "5", "mad_mt": "3", "mt_hard": "8", "min_genes": "100", "doublet_thresh": "0.2", "context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day.", "handoff": {"from_lane": "qc", "verdict": "The default 8% mitochondrial cutoff removes donor3 entirely, and donor3 is the only sample carrying the stimulated condition.", "notes": ["mt_hard = 12 (donor3 median is 11.21%)", "critical: condition confounded with sample"]}}'
# Metered, streamed as SSE. Note task: "integrate" and the handoff object,
# which is how the integration lane runs on the QC read's conclusion.
# Accept: text/event-stream, and send the same Idempotency-Key discipline.
# Concatenate the delta payloads; the result is the JSON object above.
# If the stream dies, parse what arrived - the section order is fixed.
import json, os, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = os.environ.get("SKILLSAFE_TOKEN", "YOUR_TOKEN")
def call(path, payload=None):
data = json.dumps(payload).encode() if payload 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")
with urllib.request.urlopen(req) as r:
envelope = json.load(r)
if not envelope.get("ok"):
raise RuntimeError(envelope["error"]["code"] + ": " + envelope["error"]["message"])
return envelope["data"]
payload = {
"task": "integrate",
"metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437",
"species": "human",
"tissue": "PBMC, fresh, 10x 3' v3.1",
"modality": "rna",
"labels": "none",
"mad_counts": "5",
"mad_genes": "5",
"mad_mt": "3",
"mt_hard": "8",
"min_genes": "100",
"doublet_thresh": "0.2",
"context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day.",
"handoff": {
"from_lane": "qc",
"verdict": "The default 8% mitochondrial cutoff removes donor3 entirely, and donor3 is the only sample carrying the stimulated condition.",
"notes": [
"mt_hard = 12 (donor3 median is 11.21%)",
"critical: condition confounded with sample"
]
}
}
print(call("/run-stream", payload))
// Metered, streamed as SSE. Note task: "integrate" and the handoff object,
// which is how the integration lane runs on the QC read's conclusion.
// Accept: text/event-stream, and send the same Idempotency-Key discipline.
// Concatenate the delta payloads; the result is the JSON object above.
// If the stream dies, parse what arrived - the section order is fixed.
const BASE = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // see tokens.html
async function call(path, payload) {
const res = await fetch(BASE + path, {
method: payload ? "POST" : "GET",
headers: {
Authorization: "Bearer " + TOKEN,
...(payload ? { "Content-Type": "application/json" } : {}),
},
body: payload ? JSON.stringify(payload) : undefined,
});
const envelope = await res.json();
if (!envelope.ok) throw new Error(envelope.error.code + ": " + envelope.error.message);
return envelope.data;
}
const payload = {
"task": "integrate",
"metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437",
"species": "human",
"tissue": "PBMC, fresh, 10x 3' v3.1",
"modality": "rna",
"labels": "none",
"mad_counts": "5",
"mad_genes": "5",
"mad_mt": "3",
"mt_hard": "8",
"min_genes": "100",
"doublet_thresh": "0.2",
"context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day.",
"handoff": {
"from_lane": "qc",
"verdict": "The default 8% mitochondrial cutoff removes donor3 entirely, and donor3 is the only sample carrying the stimulated condition.",
"notes": [
"mt_hard = 12 (donor3 median is 11.21%)",
"critical: condition confounded with sample"
]
}
};
console.log(await call("/run-stream", payload));
// Metered, streamed as SSE. Note task: "integrate" and the handoff object,
// which is how the integration lane runs on the QC read's conclusion.
// Accept: text/event-stream, and send the same Idempotency-Key discipline.
// Concatenate the delta payloads; the result is the JSON object above.
// If the stream dies, parse what arrived - the section order is fixed.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
const base = "https://api.skillsafe.ai/v1/app-api"
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, payload any) (json.RawMessage, error) {
method := http.MethodGet
var rdr io.Reader
if payload != nil {
b, _ := json.Marshal(payload)
rdr = bytes.NewReader(b)
method = http.MethodPost
}
req, _ := http.NewRequest(method, base+path, rdr)
req.Header.Set("Authorization", "Bearer "+os.Getenv("SKILLSAFE_TOKEN"))
if payload != nil {
req.Header.Set("Content-Type", "application/json")
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var e envelope
if err := json.NewDecoder(res.Body).Decode(&e); err != nil {
return nil, err
}
if !e.OK {
return nil, fmt.Errorf("%s: %s", e.Error.Code, e.Error.Message)
}
return e.Data, nil
}
func main() {
var payload map[string]any
json.Unmarshal([]byte(`{"task": "integrate", "metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437", "species": "human", "tissue": "PBMC, fresh, 10x 3' v3.1", "modality": "rna", "labels": "none", "mad_counts": "5", "mad_genes": "5", "mad_mt": "3", "mt_hard": "8", "min_genes": "100", "doublet_thresh": "0.2", "context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day.", "handoff": {"from_lane": "qc", "verdict": "The default 8% mitochondrial cutoff removes donor3 entirely, and donor3 is the only sample carrying the stimulated condition.", "notes": ["mt_hard = 12 (donor3 median is 11.21%)", "critical: condition confounded with sample"]}}`), &payload)
data, err := call("/run-stream", payload)
if err != nil {
panic(err)
}
fmt.Println(string(data))
}
// Metered, streamed as SSE. Note task: "integrate" and the handoff object,
// which is how the integration lane runs on the QC read's conclusion.
// Accept: text/event-stream, and send the same Idempotency-Key discipline.
// Concatenate the delta payloads; the result is the JSON object above.
// If the stream dies, parse what arrived - the section order is fixed.
import java.net.URI;
import java.net.http.*;
import java.util.Optional;
public class ScrnaDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = System.getenv("SKILLSAFE_TOKEN"); // or "YOUR_TOKEN"
static String call(String path, String payload) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN);
if (payload == null) {
b = b.GET();
} else {
b = b.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload));
}
HttpResponse<String> res = HttpClient.newHttpClient()
.send(b.build(), HttpResponse.BodyHandlers.ofString());
// The envelope is {"ok":bool,"data":...,"error":{"code","message"}}
return res.body();
}
public static void main(String[] args) throws Exception {
String payload = """
{
"task": "integrate",
"metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437",
"species": "human",
"tissue": "PBMC, fresh, 10x 3' v3.1",
"modality": "rna",
"labels": "none",
"mad_counts": "5",
"mad_genes": "5",
"mad_mt": "3",
"mt_hard": "8",
"min_genes": "100",
"doublet_thresh": "0.2",
"context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day.",
"handoff": {
"from_lane": "qc",
"verdict": "The default 8% mitochondrial cutoff removes donor3 entirely, and donor3 is the only sample carrying the stimulated condition.",
"notes": [
"mt_hard = 12 (donor3 median is 11.21%)",
"critical: condition confounded with sample"
]
}
}
""";
System.out.println(call("/run-stream", payload));
}
}
# Metered, streamed as SSE. Note task: "integrate" and the handoff object,
# which is how the integration lane runs on the QC read's conclusion.
# Accept: text/event-stream, and send the same Idempotency-Key discipline.
# Concatenate the delta payloads; the result is the JSON object above.
# If the stream dies, parse what arrived - the section order is fixed.
require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = ENV.fetch("SKILLSAFE_TOKEN", "YOUR_TOKEN")
def call(path, payload = nil)
uri = URI(BASE + path)
req = payload ? Net::HTTP::Post.new(uri) : Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
if payload
req["Content-Type"] = "application/json"
req.body = JSON.dump(payload)
end
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
envelope = JSON.parse(res.body)
raise "#{envelope["error"]["code"]}: #{envelope["error"]["message"]}" unless envelope["ok"]
envelope["data"]
end
payload = JSON.parse(<<~JSON)
{
"task": "integrate",
"metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437",
"species": "human",
"tissue": "PBMC, fresh, 10x 3' v3.1",
"modality": "rna",
"labels": "none",
"mad_counts": "5",
"mad_genes": "5",
"mad_mt": "3",
"mt_hard": "8",
"min_genes": "100",
"doublet_thresh": "0.2",
"context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day.",
"handoff": {
"from_lane": "qc",
"verdict": "The default 8% mitochondrial cutoff removes donor3 entirely, and donor3 is the only sample carrying the stimulated condition.",
"notes": [
"mt_hard = 12 (donor3 median is 11.21%)",
"critical: condition confounded with sample"
]
}
}
JSON
pp call("/run-stream", payload)
// Metered, streamed as SSE. Note task: "integrate" and the handoff object,
// which is how the integration lane runs on the QC read's conclusion.
// Accept: text/event-stream, and send the same Idempotency-Key discipline.
// Concatenate the delta payloads; the result is the JSON object above.
// If the stream dies, parse what arrived - the section order is fixed.
<?php
$base = "https://api.skillsafe.ai/v1/app-api";
$token = getenv("SKILLSAFE_TOKEN") ?: "YOUR_TOKEN";
function call(string $path, ?array $payload = null) {
global $base, $token;
$headers = ["Authorization: Bearer $token"];
$opts = ["http" => ["method" => "GET", "header" => ""]];
if ($payload !== null) {
$headers[] = "Content-Type: application/json";
$opts["http"]["method"] = "POST";
$opts["http"]["content"] = json_encode($payload);
}
$opts["http"]["header"] = implode("\r\n", $headers);
$raw = file_get_contents($base . $path, false, stream_context_create($opts));
$envelope = json_decode($raw, true);
if (!$envelope["ok"]) {
throw new RuntimeException($envelope["error"]["code"] . ": " . $envelope["error"]["message"]);
}
return $envelope["data"];
}
$payload = json_decode(<<<'JSON'
{
"task": "integrate",
"metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437",
"species": "human",
"tissue": "PBMC, fresh, 10x 3' v3.1",
"modality": "rna",
"labels": "none",
"mad_counts": "5",
"mad_genes": "5",
"mad_mt": "3",
"mt_hard": "8",
"min_genes": "100",
"doublet_thresh": "0.2",
"context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day.",
"handoff": {
"from_lane": "qc",
"verdict": "The default 8% mitochondrial cutoff removes donor3 entirely, and donor3 is the only sample carrying the stimulated condition.",
"notes": [
"mt_hard = 12 (donor3 median is 11.21%)",
"critical: condition confounded with sample"
]
}
}
JSON, true);
print_r(call("/run-stream", $payload));
// Metered, streamed as SSE. Note task: "integrate" and the handoff object,
// which is how the integration lane runs on the QC read's conclusion.
// Accept: text/event-stream, and send the same Idempotency-Key discipline.
// Concatenate the delta payloads; the result is the JSON object above.
// If the stream dies, parse what arrived - the section order is fixed.
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
class ScrnaDesk
{
const string Base = "https://api.skillsafe.ai/v1/app-api";
static readonly string Token =
Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN";
static async Task<string> Call(string path, string payload = null)
{
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", Token);
HttpResponseMessage res;
if (payload == null)
{
res = await http.GetAsync(Base + path);
}
else
{
var content = new StringContent(payload, Encoding.UTF8, "application/json");
res = await http.PostAsync(Base + path, content);
}
// The envelope is {"ok":bool,"data":...,"error":{"code","message"}}
return await res.Content.ReadAsStringAsync();
}
static async Task Main()
{
var payload = """
{
"task": "integrate",
"metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437",
"species": "human",
"tissue": "PBMC, fresh, 10x 3' v3.1",
"modality": "rna",
"labels": "none",
"mad_counts": "5",
"mad_genes": "5",
"mad_mt": "3",
"mt_hard": "8",
"min_genes": "100",
"doublet_thresh": "0.2",
"context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day.",
"handoff": {
"from_lane": "qc",
"verdict": "The default 8% mitochondrial cutoff removes donor3 entirely, and donor3 is the only sample carrying the stimulated condition.",
"notes": [
"mt_hard = 12 (donor3 median is 11.21%)",
"critical: condition confounded with sample"
]
}
}
""";
Console.WriteLine(await Call("/run-stream", payload));
}
}
A worked example per lane, end to end
Both examples use the same seven-row table so the difference between them is only the lane. The
table is deliberately small and deliberately awkward: donor2 carries an ambient-looking droplet at
301 UMIs, donor3 sits above the 8% mitochondrial cutoff, donor3 is the only sample with the
stimulated condition, and one cell scores 0.437 on the doublet score. The full 180-cell
version, with a saved reply for each lane, is on the app's front page behind
Load the example metrics table — that path is free and needs no account.
task: "qc"
What comes back: three samples entries carrying metrics_read,
threshold_call, retention_note and watch_for; the seven qc
checks; steps with the ambient-correction step at stage: "correct" ahead
of anything at stage: "filter"; and parameters naming the MAD multipliers,
the hard cutoff, the minimum genes and the doublet cutoff it recommends.
# The qc lane, priced for free. Swap /estimate for /run to actually run it.
# Expect posture "caution" or "blocked" on this table: the 8% cutoff removes
# donor3 entirely, which is a decision rather than a default.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/estimate" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"task": "qc", "metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437", "species": "human", "tissue": "PBMC, fresh, 10x 3' v3.1", "modality": "rna", "labels": "none", "mad_counts": "5", "mad_genes": "5", "mad_mt": "3", "mt_hard": "8", "min_genes": "100", "doublet_thresh": "0.2", "context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day."}'
# The qc lane, priced for free. Swap /estimate for /run to actually run it.
# Expect posture "caution" or "blocked" on this table: the 8% cutoff removes
# donor3 entirely, which is a decision rather than a default.
import json, os, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = os.environ.get("SKILLSAFE_TOKEN", "YOUR_TOKEN")
def call(path, payload=None):
data = json.dumps(payload).encode() if payload 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")
with urllib.request.urlopen(req) as r:
envelope = json.load(r)
if not envelope.get("ok"):
raise RuntimeError(envelope["error"]["code"] + ": " + envelope["error"]["message"])
return envelope["data"]
payload = {
"task": "qc",
"metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437",
"species": "human",
"tissue": "PBMC, fresh, 10x 3' v3.1",
"modality": "rna",
"labels": "none",
"mad_counts": "5",
"mad_genes": "5",
"mad_mt": "3",
"mt_hard": "8",
"min_genes": "100",
"doublet_thresh": "0.2",
"context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day."
}
print(call("/estimate", payload))
// The qc lane, priced for free. Swap /estimate for /run to actually run it.
// Expect posture "caution" or "blocked" on this table: the 8% cutoff removes
// donor3 entirely, which is a decision rather than a default.
const BASE = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // see tokens.html
async function call(path, payload) {
const res = await fetch(BASE + path, {
method: payload ? "POST" : "GET",
headers: {
Authorization: "Bearer " + TOKEN,
...(payload ? { "Content-Type": "application/json" } : {}),
},
body: payload ? JSON.stringify(payload) : undefined,
});
const envelope = await res.json();
if (!envelope.ok) throw new Error(envelope.error.code + ": " + envelope.error.message);
return envelope.data;
}
const payload = {
"task": "qc",
"metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437",
"species": "human",
"tissue": "PBMC, fresh, 10x 3' v3.1",
"modality": "rna",
"labels": "none",
"mad_counts": "5",
"mad_genes": "5",
"mad_mt": "3",
"mt_hard": "8",
"min_genes": "100",
"doublet_thresh": "0.2",
"context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day."
};
console.log(await call("/estimate", payload));
// The qc lane, priced for free. Swap /estimate for /run to actually run it.
// Expect posture "caution" or "blocked" on this table: the 8% cutoff removes
// donor3 entirely, which is a decision rather than a default.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
const base = "https://api.skillsafe.ai/v1/app-api"
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, payload any) (json.RawMessage, error) {
method := http.MethodGet
var rdr io.Reader
if payload != nil {
b, _ := json.Marshal(payload)
rdr = bytes.NewReader(b)
method = http.MethodPost
}
req, _ := http.NewRequest(method, base+path, rdr)
req.Header.Set("Authorization", "Bearer "+os.Getenv("SKILLSAFE_TOKEN"))
if payload != nil {
req.Header.Set("Content-Type", "application/json")
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var e envelope
if err := json.NewDecoder(res.Body).Decode(&e); err != nil {
return nil, err
}
if !e.OK {
return nil, fmt.Errorf("%s: %s", e.Error.Code, e.Error.Message)
}
return e.Data, nil
}
func main() {
var payload map[string]any
json.Unmarshal([]byte(`{"task": "qc", "metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437", "species": "human", "tissue": "PBMC, fresh, 10x 3' v3.1", "modality": "rna", "labels": "none", "mad_counts": "5", "mad_genes": "5", "mad_mt": "3", "mt_hard": "8", "min_genes": "100", "doublet_thresh": "0.2", "context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day."}`), &payload)
data, err := call("/estimate", payload)
if err != nil {
panic(err)
}
fmt.Println(string(data))
}
// The qc lane, priced for free. Swap /estimate for /run to actually run it.
// Expect posture "caution" or "blocked" on this table: the 8% cutoff removes
// donor3 entirely, which is a decision rather than a default.
import java.net.URI;
import java.net.http.*;
import java.util.Optional;
public class ScrnaDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = System.getenv("SKILLSAFE_TOKEN"); // or "YOUR_TOKEN"
static String call(String path, String payload) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN);
if (payload == null) {
b = b.GET();
} else {
b = b.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload));
}
HttpResponse<String> res = HttpClient.newHttpClient()
.send(b.build(), HttpResponse.BodyHandlers.ofString());
// The envelope is {"ok":bool,"data":...,"error":{"code","message"}}
return res.body();
}
public static void main(String[] args) throws Exception {
String payload = """
{
"task": "qc",
"metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437",
"species": "human",
"tissue": "PBMC, fresh, 10x 3' v3.1",
"modality": "rna",
"labels": "none",
"mad_counts": "5",
"mad_genes": "5",
"mad_mt": "3",
"mt_hard": "8",
"min_genes": "100",
"doublet_thresh": "0.2",
"context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day."
}
""";
System.out.println(call("/estimate", payload));
}
}
# The qc lane, priced for free. Swap /estimate for /run to actually run it.
# Expect posture "caution" or "blocked" on this table: the 8% cutoff removes
# donor3 entirely, which is a decision rather than a default.
require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = ENV.fetch("SKILLSAFE_TOKEN", "YOUR_TOKEN")
def call(path, payload = nil)
uri = URI(BASE + path)
req = payload ? Net::HTTP::Post.new(uri) : Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
if payload
req["Content-Type"] = "application/json"
req.body = JSON.dump(payload)
end
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
envelope = JSON.parse(res.body)
raise "#{envelope["error"]["code"]}: #{envelope["error"]["message"]}" unless envelope["ok"]
envelope["data"]
end
payload = JSON.parse(<<~JSON)
{
"task": "qc",
"metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437",
"species": "human",
"tissue": "PBMC, fresh, 10x 3' v3.1",
"modality": "rna",
"labels": "none",
"mad_counts": "5",
"mad_genes": "5",
"mad_mt": "3",
"mt_hard": "8",
"min_genes": "100",
"doublet_thresh": "0.2",
"context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day."
}
JSON
pp call("/estimate", payload)
// The qc lane, priced for free. Swap /estimate for /run to actually run it.
// Expect posture "caution" or "blocked" on this table: the 8% cutoff removes
// donor3 entirely, which is a decision rather than a default.
<?php
$base = "https://api.skillsafe.ai/v1/app-api";
$token = getenv("SKILLSAFE_TOKEN") ?: "YOUR_TOKEN";
function call(string $path, ?array $payload = null) {
global $base, $token;
$headers = ["Authorization: Bearer $token"];
$opts = ["http" => ["method" => "GET", "header" => ""]];
if ($payload !== null) {
$headers[] = "Content-Type: application/json";
$opts["http"]["method"] = "POST";
$opts["http"]["content"] = json_encode($payload);
}
$opts["http"]["header"] = implode("\r\n", $headers);
$raw = file_get_contents($base . $path, false, stream_context_create($opts));
$envelope = json_decode($raw, true);
if (!$envelope["ok"]) {
throw new RuntimeException($envelope["error"]["code"] . ": " . $envelope["error"]["message"]);
}
return $envelope["data"];
}
$payload = json_decode(<<<'JSON'
{
"task": "qc",
"metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437",
"species": "human",
"tissue": "PBMC, fresh, 10x 3' v3.1",
"modality": "rna",
"labels": "none",
"mad_counts": "5",
"mad_genes": "5",
"mad_mt": "3",
"mt_hard": "8",
"min_genes": "100",
"doublet_thresh": "0.2",
"context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day."
}
JSON, true);
print_r(call("/estimate", $payload));
// The qc lane, priced for free. Swap /estimate for /run to actually run it.
// Expect posture "caution" or "blocked" on this table: the 8% cutoff removes
// donor3 entirely, which is a decision rather than a default.
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
class ScrnaDesk
{
const string Base = "https://api.skillsafe.ai/v1/app-api";
static readonly string Token =
Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN";
static async Task<string> Call(string path, string payload = null)
{
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", Token);
HttpResponseMessage res;
if (payload == null)
{
res = await http.GetAsync(Base + path);
}
else
{
var content = new StringContent(payload, Encoding.UTF8, "application/json");
res = await http.PostAsync(Base + path, content);
}
// The envelope is {"ok":bool,"data":...,"error":{"code","message"}}
return await res.Content.ReadAsStringAsync();
}
static async Task Main()
{
var payload = """
{
"task": "qc",
"metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437",
"species": "human",
"tissue": "PBMC, fresh, 10x 3' v3.1",
"modality": "rna",
"labels": "none",
"mad_counts": "5",
"mad_genes": "5",
"mad_mt": "3",
"mt_hard": "8",
"min_genes": "100",
"doublet_thresh": "0.2",
"context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day."
}
""";
Console.WriteLine(await Call("/estimate", payload));
}
}
task: "integrate"
What comes back: three samples entries carrying batch_role,
model_note, covariates and risk; the seven integrate checks;
and parameters naming the model class, the counts layer, the batch key, the
highly-variable-gene count, max_epochs and each covariate. On this table the reply has
to say plainly that no integration method separates stimulated from donor3 — that
is a design fact, not a parameter.
# The integrate lane, priced for free, carrying the qc lane's verdict
# in handoff - which is exactly what the app's handoff button sends.
# Swap /estimate for /run-stream to run it with progress.
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/estimate" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"task": "integrate", "metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437", "species": "human", "tissue": "PBMC, fresh, 10x 3' v3.1", "modality": "rna", "labels": "none", "mad_counts": "5", "mad_genes": "5", "mad_mt": "3", "mt_hard": "8", "min_genes": "100", "doublet_thresh": "0.2", "context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day.", "handoff": {"from_lane": "qc", "verdict": "The default 8% mitochondrial cutoff removes donor3 entirely, and donor3 is the only sample carrying the stimulated condition.", "notes": ["mt_hard = 12 (donor3 median is 11.21%)", "critical: condition confounded with sample"]}}'
# The integrate lane, priced for free, carrying the qc lane's verdict
# in handoff - which is exactly what the app's handoff button sends.
# Swap /estimate for /run-stream to run it with progress.
import json, os, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = os.environ.get("SKILLSAFE_TOKEN", "YOUR_TOKEN")
def call(path, payload=None):
data = json.dumps(payload).encode() if payload 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")
with urllib.request.urlopen(req) as r:
envelope = json.load(r)
if not envelope.get("ok"):
raise RuntimeError(envelope["error"]["code"] + ": " + envelope["error"]["message"])
return envelope["data"]
payload = {
"task": "integrate",
"metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437",
"species": "human",
"tissue": "PBMC, fresh, 10x 3' v3.1",
"modality": "rna",
"labels": "none",
"mad_counts": "5",
"mad_genes": "5",
"mad_mt": "3",
"mt_hard": "8",
"min_genes": "100",
"doublet_thresh": "0.2",
"context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day.",
"handoff": {
"from_lane": "qc",
"verdict": "The default 8% mitochondrial cutoff removes donor3 entirely, and donor3 is the only sample carrying the stimulated condition.",
"notes": [
"mt_hard = 12 (donor3 median is 11.21%)",
"critical: condition confounded with sample"
]
}
}
print(call("/estimate", payload))
// The integrate lane, priced for free, carrying the qc lane's verdict
// in handoff - which is exactly what the app's handoff button sends.
// Swap /estimate for /run-stream to run it with progress.
const BASE = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // see tokens.html
async function call(path, payload) {
const res = await fetch(BASE + path, {
method: payload ? "POST" : "GET",
headers: {
Authorization: "Bearer " + TOKEN,
...(payload ? { "Content-Type": "application/json" } : {}),
},
body: payload ? JSON.stringify(payload) : undefined,
});
const envelope = await res.json();
if (!envelope.ok) throw new Error(envelope.error.code + ": " + envelope.error.message);
return envelope.data;
}
const payload = {
"task": "integrate",
"metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437",
"species": "human",
"tissue": "PBMC, fresh, 10x 3' v3.1",
"modality": "rna",
"labels": "none",
"mad_counts": "5",
"mad_genes": "5",
"mad_mt": "3",
"mt_hard": "8",
"min_genes": "100",
"doublet_thresh": "0.2",
"context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day.",
"handoff": {
"from_lane": "qc",
"verdict": "The default 8% mitochondrial cutoff removes donor3 entirely, and donor3 is the only sample carrying the stimulated condition.",
"notes": [
"mt_hard = 12 (donor3 median is 11.21%)",
"critical: condition confounded with sample"
]
}
};
console.log(await call("/estimate", payload));
// The integrate lane, priced for free, carrying the qc lane's verdict
// in handoff - which is exactly what the app's handoff button sends.
// Swap /estimate for /run-stream to run it with progress.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
const base = "https://api.skillsafe.ai/v1/app-api"
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, payload any) (json.RawMessage, error) {
method := http.MethodGet
var rdr io.Reader
if payload != nil {
b, _ := json.Marshal(payload)
rdr = bytes.NewReader(b)
method = http.MethodPost
}
req, _ := http.NewRequest(method, base+path, rdr)
req.Header.Set("Authorization", "Bearer "+os.Getenv("SKILLSAFE_TOKEN"))
if payload != nil {
req.Header.Set("Content-Type", "application/json")
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var e envelope
if err := json.NewDecoder(res.Body).Decode(&e); err != nil {
return nil, err
}
if !e.OK {
return nil, fmt.Errorf("%s: %s", e.Error.Code, e.Error.Message)
}
return e.Data, nil
}
func main() {
var payload map[string]any
json.Unmarshal([]byte(`{"task": "integrate", "metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437", "species": "human", "tissue": "PBMC, fresh, 10x 3' v3.1", "modality": "rna", "labels": "none", "mad_counts": "5", "mad_genes": "5", "mad_mt": "3", "mt_hard": "8", "min_genes": "100", "doublet_thresh": "0.2", "context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day.", "handoff": {"from_lane": "qc", "verdict": "The default 8% mitochondrial cutoff removes donor3 entirely, and donor3 is the only sample carrying the stimulated condition.", "notes": ["mt_hard = 12 (donor3 median is 11.21%)", "critical: condition confounded with sample"]}}`), &payload)
data, err := call("/estimate", payload)
if err != nil {
panic(err)
}
fmt.Println(string(data))
}
// The integrate lane, priced for free, carrying the qc lane's verdict
// in handoff - which is exactly what the app's handoff button sends.
// Swap /estimate for /run-stream to run it with progress.
import java.net.URI;
import java.net.http.*;
import java.util.Optional;
public class ScrnaDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = System.getenv("SKILLSAFE_TOKEN"); // or "YOUR_TOKEN"
static String call(String path, String payload) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN);
if (payload == null) {
b = b.GET();
} else {
b = b.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload));
}
HttpResponse<String> res = HttpClient.newHttpClient()
.send(b.build(), HttpResponse.BodyHandlers.ofString());
// The envelope is {"ok":bool,"data":...,"error":{"code","message"}}
return res.body();
}
public static void main(String[] args) throws Exception {
String payload = """
{
"task": "integrate",
"metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437",
"species": "human",
"tissue": "PBMC, fresh, 10x 3' v3.1",
"modality": "rna",
"labels": "none",
"mad_counts": "5",
"mad_genes": "5",
"mad_mt": "3",
"mt_hard": "8",
"min_genes": "100",
"doublet_thresh": "0.2",
"context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day.",
"handoff": {
"from_lane": "qc",
"verdict": "The default 8% mitochondrial cutoff removes donor3 entirely, and donor3 is the only sample carrying the stimulated condition.",
"notes": [
"mt_hard = 12 (donor3 median is 11.21%)",
"critical: condition confounded with sample"
]
}
}
""";
System.out.println(call("/estimate", payload));
}
}
# The integrate lane, priced for free, carrying the qc lane's verdict
# in handoff - which is exactly what the app's handoff button sends.
# Swap /estimate for /run-stream to run it with progress.
require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = ENV.fetch("SKILLSAFE_TOKEN", "YOUR_TOKEN")
def call(path, payload = nil)
uri = URI(BASE + path)
req = payload ? Net::HTTP::Post.new(uri) : Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
if payload
req["Content-Type"] = "application/json"
req.body = JSON.dump(payload)
end
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
envelope = JSON.parse(res.body)
raise "#{envelope["error"]["code"]}: #{envelope["error"]["message"]}" unless envelope["ok"]
envelope["data"]
end
payload = JSON.parse(<<~JSON)
{
"task": "integrate",
"metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437",
"species": "human",
"tissue": "PBMC, fresh, 10x 3' v3.1",
"modality": "rna",
"labels": "none",
"mad_counts": "5",
"mad_genes": "5",
"mad_mt": "3",
"mt_hard": "8",
"min_genes": "100",
"doublet_thresh": "0.2",
"context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day.",
"handoff": {
"from_lane": "qc",
"verdict": "The default 8% mitochondrial cutoff removes donor3 entirely, and donor3 is the only sample carrying the stimulated condition.",
"notes": [
"mt_hard = 12 (donor3 median is 11.21%)",
"critical: condition confounded with sample"
]
}
}
JSON
pp call("/estimate", payload)
// The integrate lane, priced for free, carrying the qc lane's verdict
// in handoff - which is exactly what the app's handoff button sends.
// Swap /estimate for /run-stream to run it with progress.
<?php
$base = "https://api.skillsafe.ai/v1/app-api";
$token = getenv("SKILLSAFE_TOKEN") ?: "YOUR_TOKEN";
function call(string $path, ?array $payload = null) {
global $base, $token;
$headers = ["Authorization: Bearer $token"];
$opts = ["http" => ["method" => "GET", "header" => ""]];
if ($payload !== null) {
$headers[] = "Content-Type: application/json";
$opts["http"]["method"] = "POST";
$opts["http"]["content"] = json_encode($payload);
}
$opts["http"]["header"] = implode("\r\n", $headers);
$raw = file_get_contents($base . $path, false, stream_context_create($opts));
$envelope = json_decode($raw, true);
if (!$envelope["ok"]) {
throw new RuntimeException($envelope["error"]["code"] . ": " . $envelope["error"]["message"]);
}
return $envelope["data"];
}
$payload = json_decode(<<<'JSON'
{
"task": "integrate",
"metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437",
"species": "human",
"tissue": "PBMC, fresh, 10x 3' v3.1",
"modality": "rna",
"labels": "none",
"mad_counts": "5",
"mad_genes": "5",
"mad_mt": "3",
"mt_hard": "8",
"min_genes": "100",
"doublet_thresh": "0.2",
"context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day.",
"handoff": {
"from_lane": "qc",
"verdict": "The default 8% mitochondrial cutoff removes donor3 entirely, and donor3 is the only sample carrying the stimulated condition.",
"notes": [
"mt_hard = 12 (donor3 median is 11.21%)",
"critical: condition confounded with sample"
]
}
}
JSON, true);
print_r(call("/estimate", $payload));
// The integrate lane, priced for free, carrying the qc lane's verdict
// in handoff - which is exactly what the app's handoff button sends.
// Swap /estimate for /run-stream to run it with progress.
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
class ScrnaDesk
{
const string Base = "https://api.skillsafe.ai/v1/app-api";
static readonly string Token =
Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN";
static async Task<string> Call(string path, string payload = null)
{
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", Token);
HttpResponseMessage res;
if (payload == null)
{
res = await http.GetAsync(Base + path);
}
else
{
var content = new StringContent(payload, Encoding.UTF8, "application/json");
res = await http.PostAsync(Base + path, content);
}
// The envelope is {"ok":bool,"data":...,"error":{"code","message"}}
return await res.Content.ReadAsStringAsync();
}
static async Task Main()
{
var payload = """
{
"task": "integrate",
"metrics": "barcode,sample,condition,n_genes_by_counts,total_counts,pct_counts_mt,doublet_score\ndonor1_AAACCCAAGCGT-1,donor1,control,2214,8931,4.82,0.041\ndonor1_AAACGCTTCAGC-1,donor1,control,2402,9518,3.97,0.052\ndonor2_AACCATGCAAGT-1,donor2,control,1908,7382,5.35,0.061\ndonor2_AAGGTTCCTCCA-1,donor2,control,187,301,9.14,0.048\ndonor3_ACAGCTAGGTCA-1,donor3,stimulated,1121,3090,11.21,0.058\ndonor3_ACGATGTCAGGT-1,donor3,stimulated,1043,2874,12.60,0.437",
"species": "human",
"tissue": "PBMC, fresh, 10x 3' v3.1",
"modality": "rna",
"labels": "none",
"mad_counts": "5",
"mad_genes": "5",
"mad_mt": "3",
"mt_hard": "8",
"min_genes": "100",
"doublet_thresh": "0.2",
"context": "Three-donor pilot; donor3 is the stimulated arm and ran on a different day.",
"handoff": {
"from_lane": "qc",
"verdict": "The default 8% mitochondrial cutoff removes donor3 entirely, and donor3 is the only sample carrying the stimulated condition.",
"notes": [
"mt_hard = 12 (donor3 median is 11.21%)",
"critical: condition confounded with sample"
]
}
}
""";
Console.WriteLine(await Call("/estimate", payload));
}
}
Rate limits, idempotency and good manners
- Always
/estimatefirst. It is free, it validates your input shape, and it tells you whether the balance covers the run. A 402 after submit is a bug in your client, not a surprise from the platform. - One
Idempotency-Keyper (lane, input, attempt). Include the lane: two lanes over one table are two different runs and must not collide on one key. Reuse the key when retrying the same run so a blip cannot double-bill. - Poll no faster than once a second, and back off on a 429.
- Read
lanefrom the reply before routing it into a parser: an absent or unrecognisedtaskgets the closest lane, named honestly. - Handle
truncated: true. If the balance sits betweenmin_creditsandhold_creditsthe run still executes with a reduced output cap. Surface that rather than presenting a clipped worksheet as complete.
What this app will not do
- It will not read
.h5ad,.h5or any matrix format. Metrics tables only. - It will not claim a cell type is present, rare or absent. It has no gene-level information.
- It will not invent a distribution for a column you did not send. A missing required column comes back as a named check that failed.
- It will not tell you that a method "handles" a condition confounded with its batch. That is a design fact and the reply says so.