← Sequon Desk / API
Tokens

Drive Sequon Desk from your own code

Everything the web page does is available over HTTP: post a protein sequence in, get the same structured document back. Three lanes run over one sequence set — the glycosylation and liability review, the variant design that removes what the review found, and the characterisation panel that would confirm it — and they are selected by one task field.

The natural uses are a batch job that screens a whole variant library for sequons and liability motifs before anyone orders DNA, a construct-registration hook that refuses a sequence carrying an unpaired cysteine or an internal stop codon, and a nightly pass that re-reviews every candidate in a programme and reports which ones moved.

This is a protein-engineering tool. It is not medical, clinical, diagnostic or regulatory advice, and it is not a substitute for measurement.

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": "...", "message": "...", "status": 402, "details": { ... } } }

Where the slug goes — and where it does not

There is no X-App-Slug header. This is worth stating plainly because other apps' documentation on this platform says otherwise. The vendored SDK sends exactly two headers of its own — Content-Type and Authorization — and the slug appears in exactly one place in the whole API: the body of POST /guest, as {"slug": "sequon-desk"}. A bogus slug header is accepted and ignored, so a request built around one still returns 200 and tells you nothing.

Your token, on the other hand, is required on everything except /guest: Authorization: Bearer ….

Error codes

codestatuswhat it meanswhat to do
UNAUTHORIZED401no token, or a token that has expiredmint a guest token, or sign in for a personal one
FORBIDDEN403the token belongs to a different apptokens are scoped per app; get one for this slug
INSUFFICIENT_CREDITS402the balance cannot cover the holdtop up, or check /estimate first — /estimate is free
VALIDATION_ERROR400the body is not a usable input objectcheck you did not wrap it in an "input" key
RATE_LIMITED429too many requestsback off and retry; never tight-loop
NOT_FOUND404no such job idjob ids are scoped to the token that created them
INTERNAL500the platform failedretry with the SAME Idempotency-Key

The input object

The request body IS the input object. It is never wrapped in an input key. This matters more than it looks: a wrapped body returns 200 with a plausible-looking hold, and the model then never sees your task field at all — so a real, billed run executes against a payload the prompt cannot read, and there is no error to catch. Comparing holds does not reliably detect it either; the two shapes have measured identical holds on other apps. The check that works is in step 3 below.

fieldtyperequiredwhat it is
taskstringyesthe lane: glyco, design or assay. Documented first because it selects everything else. If it is missing or unrecognised the model picks the closest lane, sets lane to its choice and says so in the first sentence of summary — it does not blend two contracts.
fastastringyesthe sequence. Multi-record FASTA, or a bare single-letter paste. Headers, ; comments, embedded numbering, whitespace and case are all handled; gap characters are stripped and counted; a trailing * is the translated stop and is removed, while a * in the middle is reported as a truncated construct. Clipped at 30,000 characters from the middle, with the cut announced in band.
fasta_clippednumbernohow many characters the client cut. Send 0 if you did not clip.
formatstringyesantibody-igg, bispecific, fab-scfv, vhh-nanobody, fusion-protein, enzyme, peptide or other. Changes the grading: a C-terminal lysine on an IgG is expected heterogeneity, not a defect.
hoststringyescho, hek, ecoli, yeast, insect, cell-free or unknown. An N-glycosylation sequon in ecoli or cell-free is graded info, because those systems have no N-linked machinery.
routestringyesiv, sc, im, inhaled, topical or research-only. A pI inside 6–8 escalates for sc, where the dose has to be soluble at high concentration.
stagestringyesdiscovery, lead-opt or cmc. Deamidation, isomerisation and oxidation escalate at cmc.
goalstringnowhat you want out of this lane, in a sentence or two.
notesstringnotarget, measured data, constraints, anything you refuse to change. Measured numbers here are what the assay lane builds its acceptance criteria against.
upstreamstringnothe previous lane's artifact, carried across. The glyco document feeds design; the design document feeds assay. Omit it and the lane works from prescan alone and says so.
prescanobjectstrongly recommendedthe computed facts. In the browser this comes from the free in-browser scanner; from your own code you may send your own object of the same shape, or omit it entirely — the model then has only the raw sequence and will say its confidence is lower. The shape is described below.

The prescan object

This is what makes the model accountable rather than merely fluent. Every number in it is treated as authoritative — the model is instructed not to recompute it and not to contradict it — and every entry in prescan.flags must come back with a matching coverage_check entry. The important keys:

keywhat it carries
declared_contextthe four context fields, normalised.
chain_count / chains_sent / chains_omittedhow many chains exist and how many are in this payload. When chains are dropped they are drawn with a golden-ratio Kronecker sequence, never an every-nth stride — a stride shares a factor with any periodic column and can hand the model one phase of the data while looking like a sample.
chains[]per chain: id, length, sequence (possibly clipped, with each removed stretch marked in band as ...[N residues omitted: 120-460]...), mass_average_da, mass_monoisotopic_da, theoretical_pi, net_charge_ph7, charge_per_100_residues, gravy, aliphatic_index, aromaticity, cys_count, cys_parity, met_count, trp_count, ext_coeff_reduced, ext_coeff_cystines, nglyc_sequons[], proline_blocked_near_sequons[], noncanonical_nxc[], oglyco_hotspots[], hydrophobic_patches[], positive_charge_patches[], negative_charge_patches[] and parse_notes.
flags[] / flag_count / flag_total / flags_omittedflag_count describes the array immediately beside it, never the full list; flag_total and flags_omitted describe the difference. Each flag carries id, chain, kind, severity, title, detail and severity_reason — the mitigating fact already applied.
method_noteshow each number was computed, so the model can say what it does and does not mean: pI over the Bjellqvist pKa set, extinction by Gill & von Hippel, hydrophobic patches as a seven-residue window at mean Kyte-Doolittle 2.5 with no charged residue, and the O-glycosylation list explicitly a heuristic rather than a predictor.

Two rules the prescan itself obeys, because they are the errors that matter most here: the canonical sequon is N-X-S/T with X not proline, so NPS and NPT are listed separately as near-sequons and are NOT glycosylation sites; and an even cysteine count is described as consistent with complete pairing rather than as evidence of it.

The output contract

One JSON object, one envelope in every lane, so a client needs a single parser. The model is instructed to return the object and nothing else — no prose, no code fence.

fieldtypewhat it is
lanestringthe lane that answered. Compare it with your task.
titlestringa document title naming the molecule.
verdictstringone of the lane's four allowed values (per lane, below).
headlinestringone sentence.
summarystringtwo to four sentences: what was found, what it means, and what was not knowable.
checks[]array6–12 of {name, value, verdict, note} where verdict is good, weak, missing, risky or not-applicable.
findings[]arrayworst first: {id, severity, target, quote, why, so_what} where severity is critical, high, medium, low or info.
rows[]arraythe lane's table, at most 60: {key, label, a, b, c, d, note}. Every cell is a string. The column meanings are per lane, below.
artifactstringthe document itself, as Markdown. This is what a user exports.
artifact_jsonobjectthe same document as structured data; shape is per lane.
coverage_check[]arrayone entry per flag in prescan.flags: {flag_id, status, note} with status confirmed, cleared or not-applicable. The web page shows any flag with no entry as unanswered — a silence does not pass for agreement.
questions[]arrayat most six things that would have to be measured.
confidencestringhigh, medium or low.

1. Get a token

A guest token can call /me and /estimate. Running a lane is metered and needs a personal token, which comes from signing in — the token page is the shortest path to either.

# The shortest path is the token page. It shows the token this browser already
# holds and hands you a ready-made shell export:
#
#   https://sequon-desk.skillsafe.ai/tokens.html
#   export SKILLSAFE_TOKEN="..."
#
# To mint a GUEST token from the command line instead. A guest token is enough for
# /me and /estimate; running a lane is metered and needs a personal token from
# signing in. Note where the slug goes: in the BODY of this one call, and nowhere
# else in the whole API. There is no X-App-Slug header.
curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/guest" \
  -H "Content-Type: application/json" \
  -d '{"slug":"sequon-desk"}'
# {"ok":true,"data":{"token":"sk_guest_...","guest_id":"gst_...","subject_type":"guest"}}

2. Check who you are and what you can afford

/me is free. Compare credits against the hold from step 3 before you submit, rather than discovering a 402 afterwards.

# Who am I, and can I afford a run? A guest gets a subject_type of "guest" and
# usually no credits; a signed-in user gets "user" and a balance.
curl -sS "https://api.skillsafe.ai/v1/app-api/me" -H "Authorization: Bearer $SKILLSAFE_TOKEN"
# {"ok":true,"data":{"subject_id":"usr_...","subject_type":"user","credits":184320}}

3. Price the run for free

/estimate creates no job and costs nothing. It is also the only reliable way to confirm your payload shape is right: send a bare {"task": "glyco"} with no facts and confirm the hold drops materially. If a payload with a full prescan prices the same as a bare one, your facts are not reaching the prompt. Comparing two candidate shapes' holds against each other does not work — identical holds have been measured for wrapped and unwrapped bodies.

# /estimate is FREE and creates no job. It is also the fastest way to confirm the
# app is wired to the model you think it is: check model, model_alias and
# markup_bps in the reply.
#
# Note the body: it IS the input object. There is no "input" wrapper, and no slug
# header. Wrapping the object returns 200 with a plausible-looking hold, and the
# model then never sees your `task` field - so a real run bills against a payload
# the prompt cannot read, with no error to catch.
curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/estimate" \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"task":"glyco","fasta":">HC\nQVQLVQSGAEVKKPGASVKVSCKASGYTFTNYGMNWVRQAPGQGLEWMGWINTYTGEPTYAADFKR\nRVTMTRDTSISTAYMELSRLRSDDTAVYYCARDNGTYFDYWGQGTLVTVSS","format":"antibody-igg","host":"cho","route":"iv","stage":"lead-opt","goal":"Tell me whether the CDR-adjacent sequon has to go."}'
# {"ok":true,"data":{"model":"gpt-5.6-terra","model_alias":"gpt-terra",
#   "markup_bps":1000,"hold_credits":3084,"min_credits":420,"sponsor_enabled":false}}

4. Run it, and poll

Idempotency-Key is a header and must be derived from the input and the lane: two lanes over the same sequence are two distinct runs and must not collide on one key, while a retry of the same run must reuse its key or it double-bills. If the reply comes back with truncated: true the balance could not cover the full output cap — the sections that arrived are real, and topping up lifts the cap on the next run.

# A metered run. Idempotency-Key is a HEADER, and it must be derived from the
# input so a retry after a network blip cannot double-bill. Include the lane in
# it: two lanes over the same sequence are two distinct runs.
curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/run" \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: sequon-desk:glyco:$(printf %s '{"task":"glyco","fasta":">HC\nQVQLVQSGAEVKKPGASVKVSCKASGYTFTNYGMNWVRQAPGQGLEWMGWINTYTGEPTYAADFKR\nRVTMTRDTSISTAYMELSRLRSDDTAVYYCARDNGTYFDYWGQGTLVTVSS","format":"antibody-igg","host":"cho","route":"iv","stage":"lead-opt","goal":"Tell me whether the CDR-adjacent sequon has to go."}' | shasum -a 256 | cut -c1-16):1" \
  -d '{"task":"glyco","fasta":">HC\nQVQLVQSGAEVKKPGASVKVSCKASGYTFTNYGMNWVRQAPGQGLEWMGWINTYTGEPTYAADFKR\nRVTMTRDTSISTAYMELSRLRSDDTAVYYCARDNGTYFDYWGQGTLVTVSS","format":"antibody-igg","host":"cho","route":"iv","stage":"lead-opt","goal":"Tell me whether the CDR-adjacent sequon has to go."}'
# {"ok":true,"data":{"job_id":"job_...","status":"queued"}}

# Then poll to a terminal state.
curl -sS "https://api.skillsafe.ai/v1/app-api/jobs/job_..." -H "Authorization: Bearer $SKILLSAFE_TOKEN"
# {"ok":true,"data":{"status":"succeeded","charged_credits":1902,"truncated":false,
#   "output":"{\"lane\":\"glyco\", ...}"}}

5. Or stream it

Same body, same key rule. delta frames carry the output as it generates; the done frame carries the authoritative output and charged_credits. The web page uses this one, and advances its progress card on section headings appearing in the stream.

# The same run as an SSE stream. Same body, same Idempotency-Key rule; the reply
# is text/event-stream with `delta` events and a final `done`.
curl -sSN -X POST "https://api.skillsafe.ai/v1/app-api/run-stream" \
  -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -H "Idempotency-Key: sequon-desk:glyco:abc123def456:1" \
  -d '{"task":"glyco","fasta":">HC\nQVQLVQSGAEVKKPGASVKVSCKASGYTFTNYGMNWVRQAPGQGLEWMGWINTYTGEPTYAADFKR\nRVTMTRDTSISTAYMELSRLRSDDTAVYYCARDNGTYFDYWGQGTLVTVSS","format":"antibody-igg","host":"cho","route":"iv","stage":"lead-opt","goal":"Tell me whether the CDR-adjacent sequon has to go."}'
# event: job
# data: {"job_id":"job_..."}
# event: delta
# data: {"text":"{\"lane\":\"glyco\",\"title\":\"..."}
# event: done
# data: {"job_id":"job_...","status":"succeeded","charged_credits":1902,"output":"{...}"}

The three lanes, one worked example each

Same endpoint, same body shape, same output envelope. Only task and the meaning of rows change.

task: "glyco" — Glyco & liabilities

Source skill: @k-dense-ai/glycoengineering. Allowed verdict: clean, engineerable, liability-heavy, not-developable.

rows is one row per SITE:

not-a-site is allowed only for a proline-blocked near-sequon or a non-canonical N-X-C. Nothing else may carry it.

# the request body, in full
{
  "task": "glyco",
  "fasta": ">HC\nQVQLVQSGAEVKKPGASVKVSCKASGYTFTNYGMNWVRQAPGQGLEWMGWINTYTGEPTYAADFKR\nRVTMTRDTSISTAYMELSRLRSDDTAVYYCARDNGTYFDYWGQGTLVTVSS",
  "format": "antibody-igg",
  "host": "cho",
  "route": "iv",
  "stage": "lead-opt",
  "goal": "Tell me whether the CDR-adjacent sequon has to go."
}

task: "design" — Variant design

Source skill: @k-dense-ai/esm. Allowed verdict: ready-to-order, needs-modelling, high-risk, no-safe-variant.

rows is one row per PROPOSED MUTATION:

Every mutation must remove or reduce a liability that appears in the prescan or in upstream. A mutation with no target is a contract violation.

# the request body, in full
{
  "task": "design",
  "fasta": ">HC\nQVQLVQSGAEVKKPGASVKVSCKASGYTFTNYGMNWVRQAPGQGLEWMGWINTYTGEPTYAADFKR\nRVTMTRDTSISTAYMELSRLRSDDTAVYYCARDNGTYFDYWGQGTLVTVSS",
  "format": "antibody-igg",
  "host": "cho",
  "route": "iv",
  "stage": "lead-opt",
  "goal": "Tell me whether the CDR-adjacent sequon has to go.",
  "upstream": "# Liability review\\n\\n(the previous lane's artifact, verbatim)"
}

task: "assay" — Assay panel

Source skill: @k-dense-ai/adaptyv. Allowed verdict: panel-ready, gaps-remain, over-specified, not-testable.

rows is one row per ASSAY, in run order:

Order is part of the contract: nothing may depend on material an earlier row has not produced. A liability with no assay in the panel is a finding, not a silence.

# the request body, in full
{
  "task": "assay",
  "fasta": ">HC\nQVQLVQSGAEVKKPGASVKVSCKASGYTFTNYGMNWVRQAPGQGLEWMGWINTYTGEPTYAADFKR\nRVTMTRDTSISTAYMELSRLRSDDTAVYYCARDNGTYFDYWGQGTLVTVSS",
  "format": "antibody-igg",
  "host": "cho",
  "route": "iv",
  "stage": "lead-opt",
  "goal": "Tell me whether the CDR-adjacent sequon has to go.",
  "upstream": "# Liability review\\n\\n(the previous lane's artifact, verbatim)"
}

Notes that will save you a round trip