← Five Bar Gate
API
Tokens

Drive Five Bar Gate from your own code

The one metered step - the tidy pass over one bird walk's notes - is available over HTTP. A natural use is a field-notes app, a bird club's shared sightings sheet or a voice-memo transcriber that already holds what someone saw: your code sends the lines and the effort, and gets back a call for every line, ID checks for the shaky ones, a verdict, comments for the checklist and the steps before submitting.

The split first: the page reads and counts; the model judges; the page checks and re-tallies. Reading each line into a count, marks and a bird, the tally by family, the totals and the flags come from gatekit.js (with birdlist.js), which run in node as well as in the browser. Its check function then applies the reply: every call must name a real line; a rename of a bird the person named, a name on an unsure line and a merge of two different birds are refused; a merge counts the larger number once; the model never writes a count; species and bird totals after tidying are recomputed by the page, and numbers in the drafted comments that are not in the notes are flagged.

Base URL and the envelope

Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }

The token is minted for this app (the guest call takes {"slug":"five-bar-gate"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare: never wrap it as {"input": {…}}.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or task is missing.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: the tidy pass is metered and needs a personal token.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field comes first

Every run sends task. There is one task, tidy. It returns title, verdict, summary; lines [{entry, call, as, with, why}], one per input line in order; id_checks [{entry, versus, look_for}], up to four; comments (one to three sentences for the checklist's comments field); before_you_submit (three to five steps); caveats.

verdictMeans
readyEvery line is named to species or honestly as a group, counted, and the effort is complete.
tidyA handful of calls - an ambiguous name, a missing count, a possible double count, one shaky ID - fix it.
reworkMost lines are groups or uncounted, or the effort is missing; the list cannot stand as a complete checklist yet.
callMeans
keepThe line stands as written.
nameSettle an ambiguous word, a nickname or an unknown name to one species (as), by what is written and the region. Never on an unsure line, never over a species the person named.
spuhReport it at the level the notes support - as is a "sp." or slash name.
countA count or a careful estimate is needed; why says how. The model never writes the number.
documentRare, late, or unsure with some support: describe it or attach the photo before submitting.
mergeProbably the same birds as the earlier line with; the page counts the larger number once.

Ids are the page's: lines S1…S80, in the order they were written (a line with several birds separated by commas or semicolons becomes several ids).

The declared input schema carries scalars only, so object-valued fields travel JSON-encoded as strings (GateKit.toWire does this). Every field except notes and retry_note is required by the schema; a missing one comes back as a warning from /estimate.

Input fields

FieldTypeNotes
taskstringAlways tidy.
todaystringYYYY-MM-DD.
outingstring (JSON){place, region, date, start, protocol, minutes, party, party_assumed, complete, distance_km?, distance_text?}. protocol is traveling, stationary or incidental; missing text values read (not given).
entriesstring (JSON)[{id, text, name, kind, family, count, where?, approx?, plus?, heard?, flyover?, photo?, uncertain?, could_be?}], up to 80. kind is species, group or unknown; count is a number or null; could_be lists the species an ambiguous word may be.
totalsstring (JSON){species, other_taxa, birds, lines, families} as the page counted them.
page_readsstring (JSON)[{level, code, text, ids[]}], level count, name, effort or info.
page_verdictstringThe page's own reading: ready, tidy or rework.
notesstringOptional, the observer's comments; past 600 characters the middle is cut around a marker.
retry_notestringOnly on a reformat retry.

The reply

The job's output.output is a string holding one JSON object. Parse it with GateKit.parseReply, then normalizeTidy, then apply and check it with check(reply, state), which returns {calls, after, before, notes, id_checks}: each line's call and whether the page applied it, the re-tallied rows, families and totals after tidying, the totals as written, and the notes (ok, info, warn, bad).

Worked examples

Replies produced against the app's prompt for the sample walks (made-up walks and counts; lists shortened; the full replies ship in example-result.js). The page replays each of them free from its sample buttons. Inputs are shown with the long fields folded.

task: tidy - Backyard feeder count - ready

Input:

{
 "task": "tidy",
 "today": "2026-10-19",
 "outing": "{\"place\":\"Our backyard feeders\",\"region\":\"Southern Michigan\",\"date\":\"2026-10-11\",\"start\":\"08:00\",\"protocol\":\"stationary\",\"minutes\":45,\"party\":1,\"party_assumed\":false,\"complete\":true}",
 "entries": "[{\"id\":\"S1\",\"text\":\"Dark-eyed Junco 9\",\"name\":\"Dark-eyed Junco\",\"kind\":\"species\",\"family\":\"New World sparrows\",\"count\":9,\"where\":\"Feeders\"},{\"id\":\"S2\",\"text\":\"White-throated Sparrow 4\",\"name\":\"White-throated Sparrow\",\"kind\":\"species\",\"family\":\"New World sparrows\",\"count\":4,\"where\":\"Feeders\"},{\"id\":\"S3\",\"text\":\"Black-capped Chickadee 3\",\"name\":\"Black-capped Chickadee\",\"kind\":\"species\",\"family\":\"Chickadees and titmice\",\"count\":3,\"where\":\"Feeders\"},\"... 13 more\"]",
 "totals": "{\"species\":16,\"other_taxa\":0,\"birds\":54,\"lines\":16,\"families\":12}",
 "page_reads": "[]",
 "page_verdict": "ready",
 "notes": "Two tube feeders and a platform feeder; watched from the kitchen window and the back step."
}

Reply (lines that are not a plain keep shown first):

{
 "task": "tidy",
 "title": "Southern Michigan Backyard Feeder List",
 "verdict": "ready",
 "summary": "This backyard feeder watch is ready to submit: all sixteen lines, from the nine Dark-eyed Juncos to the single Carolina Wren heard singing, are already named to full species with real counts and no page reads to fix. The only thing worth a second look before you submit is the busier feeder counts, like the nine juncos and six goldfinches. After these calls the list still holds sixteen species at species level, matching the page's own count for a complete 45-minute stationary watch.",
 "lines": [
  {
   "entry": "S1",
   "call": "keep",
   "as": "Dark-eyed Junco",
   "with": "",
   "why": ""
  },
  {
   "entry": "S2",
   "call": "keep",
   "as": "White-throated Sparrow",
   "with": "",
   "why": ""
  },
  {
   "entry": "S3",
   "call": "keep",
   "as": "Black-capped Chickadee",
   "with": "",
   "why": ""
  },
  {
   "entry": "S4",
   "call": "keep",
   "as": "Tufted Titmouse",
   "with": "",
   "why": ""
  },
  "... 12 more lines"
 ],
 "id_checks": [],
 "comments": "I watched two tube feeders and a platform feeder from the kitchen window and the back step for a stationary 45-minute count in Southern Michigan.",
 "before_you_submit": [
  "Double-check the busiest counts, like the nine juncos and six goldfinches, before you submit.",
  "Confirm Carolina Wren and Ruby-crowned Kinglet look right for your yard this time of year on your regional checklist.",
  "Submit as a complete stationary count - the 45 minutes and 08:00 start are already filled in.",
  "..."
 ],
 "caveats": [
  "This list only covers what's in your notes; any feeder visit you didn't jot down isn't part of the count.",
  "A reviewer may still ask for extra detail on any line, even a complete one like this."
 ]
}

task: tidy - Lakeshore park loop - tidy, with name, merge, sp., count and document calls

Input:

{
 "task": "tidy",
 "today": "2026-10-19",
 "outing": "{\"place\":\"Lakeshore park loop\",\"region\":\"Southeast Michigan, Lake Erie shore\",\"date\":\"2026-10-10\",\"start\":\"07:40\",\"protocol\":\"traveling\",\"minutes\":95,\"party\":2,\"party_assumed\":false,\"complete\":true,\"distance_km\":3.38,\"distance_text\":\"2.1 mi\"}",
 "entries": "[{\"id\":\"S1\",\"text\":\"Canada Goose flock of 60 flyover\",\"name\":\"Canada Goose\",\"kind\":\"species\",\"family\":\"Ducks, geese and swans\",\"count\":60,\"where\":\"Parking lot\",\"flyover\":true},{\"id\":\"S2\",\"text\":\"12 Ring-billed Gulls\",\"name\":\"Ring-billed Gull\",\"kind\":\"species\",\"family\":\"Gulls and terns\",\"count\":12,\"where\":\"Parking lot\"},{\"id\":\"S3\",\"text\":\"~40 grackles flyover\",\"name\":\"grackle sp.\",\"kind\":\"group\",\"family\":\"Blackbirds and orioles\",\"count\":40,\"where\":\"Parking lot\",\"approx\":true,\"flyover\":true,\"could_be\":[\"Common Grackle\",\"Great-tailed Grackle\"]},\"... 15 more\"]",
 "totals": "{\"species\":12,\"other_taxa\":5,\"birds\":206,\"lines\":18,\"families\":14}",
 "page_reads": "[{\"level\":\"count\",\"code\":\"no-count\",\"text\":\"3 lines have no number (\\\"kinglets - lots\\\", \\\"small hawk zipped through\\\", \\\"Belted Kingfisher heard\\\"). A count, or a careful estimate, is worth more than an X.\",\"ids\":[\"S8\",\"S11\",\"S17\"]},{\"level\":\"name\",\"code\":\"ambiguous\",\"text\":\"\\\"~40 grackles flyover\\\" could be Common Grackle or Great-tailed Grackle - your region and what you saw decide it.\",\"ids\":[\"S3\"]},{\"level\":\"name\",\"code\":\"ambiguous\",\"text\":\"\\\"grackles 30 flyover, same flock circling back\\\" could be Common Grackle or Great-tailed Grackle - your region and what you saw decide it.\",\"ids\":[\"S5\"]},\"... 6 more\"]",
 "page_verdict": "tidy",
 "notes": "Big Day morning. Light north wind, lots moving early over the parking lot."
}

Reply (lines that are not a plain keep shown first):

{
 "task": "tidy",
 "title": "Lakeshore Park Loop Fall Checklist",
 "verdict": "tidy",
 "summary": "This list is tidy: most lines, like 'Blue Jay 6' and 'Swamp Sparrow 3', are already full species with real counts, and the region settles the two grackle flocks ('~40 grackles flyover' and 'grackles 30 flyover, same flock circling back') as Common Grackle and 'chickadees x5' as Black-capped Chickadee. What is left is adding counts for 'kinglets - lots,' the 'small hawk zipped through,' and the heard-only Belted Kingfisher, plus a short description to back up the photographed Connecticut Warbler. After these calls the list holds 14 species at species level.",
 "lines": [
  {
   "entry": "S3",
   "call": "name",
   "as": "Common Grackle",
   "with": "",
   "why": "Common Grackle is the grackle expected in Michigan; Great-tailed Grackle would be unusual this far east."
  },
  {
   "entry": "S5",
   "call": "merge",
   "as": "Common Grackle",
   "with": "S3",
   "why": "Same flock circling back as S3, not a second group of birds."
  },
  {
   "entry": "S6",
   "call": "name",
   "as": "Black-capped Chickadee",
   "with": "",
   "why": "Black-capped Chickadee is the expected chickadee in Michigan."
  },
  {
   "entry": "S8",
   "call": "spuh",
   "as": "kinglet sp.",
   "with": "",
   "why": "Ruby-crowned and Golden-crowned Kinglets move together in fall and weren't separated here; also no count was given for 'lots'."
  },
  {
   "entry": "S11",
   "call": "spuh",
   "as": "Accipiter sp.",
   "with": "",
   "why": "Sharp-shinned and Cooper's Hawks both migrate through and look alike on a quick flyby; also no count was given."
  },
  "... 13 more lines"
 ],
 "id_checks": [
  {
   "entry": "S8",
   "versus": "Ruby-crowned Kinglet vs. Golden-crowned Kinglet",
   "look_for": "Golden-crowned shows a bold black-and-white face stripe with an orange or yellow crown patch, while Ruby-crowned has a plain face with a bold white eye-ring and a chattering call."
  },
  {
   "entry": "S11",
   "versus": "Sharp-shinned Hawk vs. Cooper's Hawk",
   "look_for": "Cooper's Hawk has a larger, more rounded head on a thicker neck and a longer, rounded tail tip, while Sharp-shinned Hawk's head looks barely wider than its neck with a squared-off tail."
  },
  "..."
 ],
 "comments": "Big Day morning with a light north wind; lots of birds were moving over the parking lot early, where I had the Canada Geese and the grackles. The two grackle counts (40, then 30) were the same flock circling back, so I'm counting them as one. The kinglets, the small hawk, and the maybe-vireo are reported at group level.",
 "before_you_submit": [
  "Add a count for the kinglets at S8 ('lots') - even a careful estimate beats leaving it blank.",
  "Give a number for the small hawk at S11, or '1' if that's what you saw.",
  "Say whether the Belted Kingfisher was one bird calling or more than one.",
  "..."
 ],
 "caveats": [
  "The grackle and chickadee calls (Common Grackle, Black-capped Chickadee) are regional estimates, not a confirmed ID from field marks.",
  "This page matches lines to a short list of common North American birds, so an unusual bird might not be reflected correctly.",
  "..."
 ]
}

task: tidy - Rushed phone notes - rework

Input:

{
 "task": "tidy",
 "today": "2026-10-19",
 "outing": "{\"place\":\"River trail\",\"region\":\"Central Michigan\",\"date\":\"2026-10-17\",\"start\":\"(not given)\",\"protocol\":\"traveling\",\"minutes\":null,\"party\":1,\"party_assumed\":true,\"complete\":true}",
 "entries": "[{\"id\":\"S1\",\"text\":\"ducks lots\",\"name\":\"duck sp.\",\"kind\":\"group\",\"family\":\"Ducks, geese and swans\",\"count\":null},{\"id\":\"S2\",\"text\":\"geese\",\"name\":\"goose sp.\",\"kind\":\"group\",\"family\":\"Ducks, geese and swans\",\"count\":null},{\"id\":\"S3\",\"text\":\"some hawks circling high\",\"name\":\"hawk sp.\",\"kind\":\"group\",\"family\":\"Osprey, hawks and eagles\",\"count\":null},\"... 10 more\"]",
 "totals": "{\"species\":2,\"other_taxa\":11,\"birds\":3,\"lines\":13,\"families\":11}",
 "page_reads": "[{\"level\":\"count\",\"code\":\"no-count\",\"text\":\"11 lines have no number (\\\"ducks lots\\\", \\\"geese\\\", \\\"some hawks circling high\\\" and 8 more). A count, or a careful estimate, is worth more than an X.\",\"ids\":[\"S1\",\"S2\",\"S3\",\"S4\",\"S5\",\"S6\",\"S8\",\"S10\",\"S11\",\"S12\",\"S13\"]},{\"level\":\"name\",\"code\":\"ambiguous\",\"text\":\"\\\"chickadees\\\" could be Black-capped Chickadee or Carolina Chickadee or Mountain Chickadee or Chestnut-backed Chickadee - your region and what you saw decide it.\",\"ids\":[\"S8\"]},{\"level\":\"name\",\"code\":\"ambiguous\",\"text\":\"\\\"crows\\\" could be American Crow or Fish Crow - your region and what you saw decide it.\",\"ids\":[\"S11\"]},\"... 6 more\"]",
 "page_verdict": "rework",
 "notes": "Typed on my phone while walking the dog, didn't count much."
}

Reply (lines that are not a plain keep shown first):

{
 "task": "tidy",
 "title": "River Trail Bird Walk Checklist",
 "verdict": "rework",
 "summary": "This list needs rework: effort is incomplete (no duration or distance given for a traveling count) and ten of the thirteen lines - \"ducks lots\", \"geese\", \"some hawks circling high\", and more - carry no number. The most important fix is adding how long and how far you walked, since the list is marked complete without that effort data. After these calls the list holds four confirmed species (Northern Cardinal, American Robin, Black-capped Chickadee, American Crow), with the Kirtland's Warbler pending a description and eight lines still reported at group level.",
 "lines": [
  {
   "entry": "S1",
   "call": "count",
   "as": "duck sp.",
   "with": "",
   "why": "No number for \"ducks lots\" - count a block and step it across the group, or give your best estimate."
  },
  {
   "entry": "S2",
   "call": "count",
   "as": "goose sp.",
   "with": "",
   "why": "No number for the geese - give a count or a careful estimate."
  },
  {
   "entry": "S3",
   "call": "count",
   "as": "hawk sp.",
   "with": "",
   "why": "No number for \"some hawks\" - give a count or a careful estimate."
  },
  {
   "entry": "S4",
   "call": "count",
   "as": "gull sp.",
   "with": "",
   "why": "No number for the seagulls - give a count or a careful estimate."
  },
  {
   "entry": "S5",
   "call": "count",
   "as": "woodpecker sp.",
   "with": "",
   "why": "No number for the woodpecker - add a count, even if it's just one."
  },
  "... 8 more lines"
 ],
 "id_checks": [
  {
   "entry": "S8",
   "versus": "Carolina Chickadee",
   "look_for": "Black-capped Chickadee has a cleaner white edge on the wing feathers and a slower, huskier \"chick-a-dee-dee\" call, while Carolina Chickadee's call is faster and higher-pitched, though the two can look nearly identical."
  },
  {
   "entry": "S9",
   "versus": "Palm Warbler",
   "look_for": "Palm Warbler also pumps its tail and shows yellowish underparts in fall but has a rufous-tinged cap and is far more expected in Michigan in October, while Kirtland's Warbler is larger and grayer above with bold white eye-arcs and heavier streaking on the flanks."
  },
  "..."
 ],
 "comments": "I typed these notes on my phone while walking the dog and didn't count much. Several lines are reported at group level, and the chickadee and crow are given as the species expected for Central Michigan rather than confirmed in the field. I wasn't sure about the Kirtland's Warbler, so that one needs a closer look before it goes on record.",
 "before_you_submit": [
  "Add how long you were out - the list says complete but no duration is given.",
  "Add how far you walked on this traveling count, or switch to stationary if you stayed in one spot.",
  "Give a count or a careful estimate for the still-grouped lines - ducks, geese, hawks, gulls, the woodpecker, the small brown birds, the blackbirds, and the warbler - rather than leaving them as \"lots\", \"some\", or \"a bunch\".",
  "..."
 ],
 "caveats": [
  "The chickadee, crow, and Kirtland's Warbler calls use general range and season knowledge, not certainty.",
  "Your regional records committee may still want more detail on the Kirtland's Warbler, even with a description."
 ]
}

Build the input yourself

Load the scripts in node (global.window = {}; require("./birdlist.js"); require("./gatekit.js")), then:

const K = GateKit;
const st = K.state({
  place: "Lakeshore park loop", region: "Southeast Michigan", date: "2026-10-10", start: "7:40",
  minutes: "95", protocol: "traveling", distance: "2.1", dist_unit: "mi", party: "2", complete: "yes",
  notes: "",
  sightings: "Parking lot:\n~40 grackles flyover\nBlue Jay 6\n\nWoods trail:\nchickadees x5\nkinglets - lots"
});
const { input } = K.buildInput(st, { today: "2026-10-19" });
const wire = K.toWire(input);           // POST this to /estimate and /run
// later: const reply = K.normalizeTidy(K.parseReply(job.output.output));
// const { calls, after, before, notes } = K.check(reply, st);

Step 1 — a tiny client helper

One function that sends JSON and unwraps the envelope. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here"   # from https://five-bar-gate.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
  curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
    ${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}

Step 2 — a token

A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.

# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"five-bar-gate"}' | jq -r '.data.token')

Step 3 — GET /me

Who the token is (subject_type: user or guest) and the credit balance.

api GET /me | jq '.data | {subject_type, credits}' 

Step 4 — POST /estimate

Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.

# input.json: the run input object - {"task": "tidy", "today": "2026-10-19", "outing": "{...}", "entries": "[...]", "totals": "{...}", "page_reads": "[...]", "page_verdict": "tidy"}
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses five-bar-gate:tidy:<inputHash>:a<attempt>, and its one reformat retry (with retry_note added) is attempt 2, a new key and a second run. The job's output.output is the reply text; truncated: true means the balance cut it short.

KEY="five-bar-gate:tidy:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
  J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
  case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json   # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}' 

Step 6 — POST /run-stream

The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.

# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: five-bar-gate:tidy:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json