Run Turnout Desk from your own code
The one metered task, plan, is available over HTTP and returns one JSON object. The division of labour: the page works out the show-up figure with its source, the registration ceiling with its arithmetic, the occupancy gate, the waitlist rule and drain date, the day-of lane and the catering number - all free, in turnkit.js. The run writes the plan around them; the page then holds every one of those numbers to its own (turnreply.js).
Base URL and the response shape
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response has one shape:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }
The token is minted for this app (the guest call takes {"slug":"turnout-desk"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required key (task, event, numbers, facts) is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: the plan is metered and needs a personal sign-in. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field
One task, plan. A missing or different task is answered as plan.
The input
| Field | Required | What |
|---|---|---|
task | yes | always plan. If it is missing or different, still write the plan and set "task": "plan". |
event | yes | the event, its format and price posture, the dates (event, headcount lock, today), whether it recurs, the venue's walk-up population, the channel to the waitlist, whether someone can sort the waitlist, and whether the form is open. |
numbers | yes | the page's five-line ceiling arithmetic, today's registrations and waitlist, the occupancy gate at the top of the band, the catering number and buffer, the waitlist rule with its drain date and claim window, the day-of lane with its expected walk-in volume, last edition's count and comped seats. |
facts | yes | the ceiling method and every finding tagged [fix], [check] or [info]. |
retry_note | no | optional; if present, your previous reply was unusable - follow it. |
The rules the model applies
- The occupancy limit (fire code, venue contract, the insurance written against them) is a gate, not a lever. Model expected attendance against it, state the arrival assumption and what happens if it does not hold; standing room or overflow counts only with the venue's written approval obtained before registrations are accepted against it. If a ratio would put expected attendance over the limit, the ratio moves, the limit does not.
- Free registration: the sourced 2x ceiling is the default - the MLH hackathon organizer guide says about 50% of registrants for a free event show up (a trendline across 323 events), so aim for double the registrations and do not go over double. The ceiling is the load-bearing half. Leaving registration open with no ceiling is the one posture the guide forbids: past the ceiling you guarantee registered people are turned away.
- An own-ratio ceiling (last edition's registered-versus-attended for the same event and format) is used only for a recurring free event whose cost locks against headcount before doors, and is held under the sourced double. One edition is a sample of one.
- Paid or nominal-fee event: no overbooking - accept up to capacity and close. An empty chair costs one cover; turning away someone who paid costs a refund and the public account of it. Paid events convert 90-97% of tickets (3-10% no-shows); free events see 40-60% no-shows (Event Tech Live). Never apply one band to the other, and never blend figures - the ~50% trendline, the 30-50% drop-off range and the signup curve answer different questions.
- Waitlist: signup order (FIFO) drained in one batch on a named date is the default; proximity to the venue only when someone can sort and check the list and registrations run far enough past the ceiling that order changes who gets in - it is an allocation on location data, must be published when the list opens and disadvantages people who travel. Publish the ordering rule, the drain date, the claim window and who decides at the moment the list opens. If the headcount guarantee locks before the drain date, the waitlist fills chairs but no longer moves the catering number. Never promote continuously one seat at a time.
- Day-of lane: a walk-in lane by default - a separate late-registration form, opened only after the pre-registered check-in window closes, with everyone completing the full form. A waitlist-fed lane (the waitlist told the night before that unclaimed seats open at a named time) only when the venue has no walk-up population and a channel reaches the waitlist within a day. Never open the door to unregistered arrivals with no form.
- Catering gets the expected-show-up number, never the registration count, with the buffer named separately.
- Deposits: do not recommend one. No established practice stands behind a refundable deposit as an attendance mechanic for developer events; name its three costs (refund handling for every attendee, the accounting and tax obligation of moving money, a barrier that lands hardest on the people a scholarship seat exists for) and route the concern to the ticket-price posture, where the nominal fee is the lever with practice behind it.
- Comped and scholarship seats behave as free seats until measured; count them separately. A low show-up rate can be demand, price, audience or reminders - not an operations failure by default. Reminder copy, check-in staffing and pricing belong to other owners; hand them numbers, do not do their jobs.
- Count what happens on the day - registered, attended, walk-ins, waitlist promotions and how many arrived - and keep it as next edition's input.
House rules
- Use only the input and the rules above. The page's numbers, rule and lane picks are exact - copy them; never recompute them differently. Every number in a sentence must appear in the input.
- Label every figure you state as sourced (and from where) or self-set.
- Never invent a venue, a person's name, a date, a price or an attendance figure. Where a person must be named (who closes the form, who decides promotions), say which role it should be.
- Never use backticks, never refer to this input's fields by their key names in a sentence, and never write reminder email copy.
- Reply with ONLY one JSON object - no prose before or after, no code fences.
The reply
summary- one sentence, at most 35 words: the ceiling, the catering number and the one thing to act on first.ceiling-registrations(the page's ceiling, a number),arithmetic(the five lines, each at most 30 words, in order: capacity, aimed attendance, show-up rate with its source, the ceiling, the catering number with its buffer),closes(one sentence: which role closes the form - the event organizer unless the input names another - and when; when a[fix]finding says the form is past the ceiling, say it closes now and carry that finding's stated cost).gate- one or two sentences, at most 50 words: expected attendance against the occupancy limit, the arrival assumption, and what happens at the top of the band.waitlist-rule(fifo,proximityornone, the page's pick),drain_date(the page's date, or "none"),claim_window(the page's window),publish(one sentence, at most 40 words: what is published when the list opens).walk_in-lane(walkin,fedornone, the page's pick),volume(the page's expected walk-ins, a number),opens(one sentence: when it opens and, for a walk-in lane, that the full form is still completed).catering-number(the page's catering number),buffer(the page's buffer),note(one sentence).deposit- one or two sentences, at most 50 words: no deposit recommended, its three costs, the price posture as the route.handoffs- 3 to 5 entries:to(a role: catering, check-in desk, reminder-email owner, venue contract signer, ticket pricing),what(one sentence with the number they need).measure- 3 to 5 short strings: what to count on the day.caveats- at most 4 short strings, carrying the stated cost of any[fix]finding not already in the plan: anything the organizer should know before trusting the plan (a borrowed ratio, an unapproved overflow, a lock date that has passed).[]when none.
{
"task": "plan",
"summary": "one sentence",
"ceiling": { "registrations": 400, "arithmetic": [ "line 1", "line 2", "line 3", "line 4", "line 5" ], "closes": "one sentence" },
"gate": "one or two sentences",
"waitlist": { "rule": "fifo", "drain_date": "YYYY-MM-DD", "claim_window": "48 hours (self-set)", "publish": "one sentence" },
"walk_in": { "lane": "walkin", "volume": 30, "opens": "one sentence" },
"catering": { "number": 200, "buffer": 200, "note": "one sentence" },
"deposit": "one or two sentences",
"handoffs": [ { "to": "catering", "what": "one sentence" } ],
"measure": [ "what to count" ],
"caveats": [ "a caveat" ]
}
The page holds it to its own numbers: the ceiling, the catering number and its buffer, the waitlist rule and drain date, the day-of lane and its volume must be the page's; the deposit answer must decline the mechanic; no number may appear that the input does not hold.
Worked examples - every sample
These are the page's three free samples. Each input is exactly what the page sends; each reply is the saved plan the sample replays, shipped as the validating model wrote it; the last line is the page's own check of it.
A free meetup, 250 seats, 410 signed up
Ceiling 500 by the sourced double; catering 250.
Input:
{
"task": "plan",
"event": "Event: Rust Meetup - winter edition - Meetup; Free registration\nEvent date: 2026-11-19; headcount lock (catering or venue guarantee): 2026-11-12; today: 2026-11-02\nRecurring: no; walk-up population at the venue: yes; channel that reaches the waitlist within a day: yes; someone can sort and check the waitlist: no\nRegistration form: open",
"numbers": "1. Hard capacity: 250 (the occupancy limit)\n2. Aimed attendance: 250\n3. Expected show-up rate: 50% - MLH hackathon organizer guide: about 50% of people who register for a free event show up (trendline across 323 events) - aim for double, and do not go over double; free band Event Tech Live, free events see 40-60% no-shows\n4. Registration ceiling: 250 / 50% = 500 registrations, and not one more\n5. Expected show-ups handed to catering at the ceiling: 250, with the 250-registration buffer named separately\nRegistrations today: 410; waitlist (including anyone past the ceiling): 0\nExpected show-ups on today's registrations: 205\nOccupancy gate: at 60% show-up the ceiling brings 300 against a limit of 250 (50 over); chance of exceeding the limit at exactly 50%: 48.2%\nCatering number: 250 expected show-ups at the ceiling (buffer 250 registrations) - never the registration count\nWaitlist rule: fifo - Signup order (FIFO), drained in one batch on a named date; drain date: 2026-11-12 - Drain on or before 2026-11-12, the headcount lock, so promotions still move the catering number.; claim window: 48 hours (self-set)\nDay-of lane: walkin - Walk-in lane (separate late form, opens after pre-registered check-in closes, full form for everyone); expected walk-in volume (empty places at the expected show-up): 45",
"facts": "Ceiling method: sourced 2x ceiling\nFindings (4):\n[info] no_prior (ratio): No prior registered-versus-attended count, so the ratio is borrowed. Count registered, attended, walk-ins and promoted arrivals this time - that count is what turns a borrowed ratio into your own.\n[check] gate_exposure (gate): At the top of the band (60% show up) the ceiling of 500 brings 300 people to a limit of 250 - 50 over. The walk-in lane closes and registered people are turned away; name this exposure to whoever signs the venue contract before setting the number. Even at exactly 50%, chance alone puts attendance over the limit 48.2% of the time.\n[info] ceiling_room (ceiling): 90 registrations left under the ceiling. Write the ceiling into the form's configuration and name who closes it.\n[info] format (ratio): The 2x ceiling was written for free hackathons; for a meetup treat it as an upper bound, not a target."
}
Reply:
{
"task": "plan",
"summary": "The sourced ceiling is 500 registrations with 250 expected show-ups handed to catering; act first by flagging the 50-over exposure at the band's top to whoever signs the venue contract before finalizing the number.",
"ceiling": {
"registrations": 500,
"arithmetic": [
"Hard capacity is 250, the occupancy limit (sourced).",
"Aimed attendance is 250 (sourced).",
"Expected show-up rate is 50%, sourced from the MLH hackathon organizer guide's trendline across 323 free events.",
"Registration ceiling is 250 divided by 50%, equaling 500 registrations, and not one more (sourced).",
"Catering gets 250 expected show-ups at the ceiling, with a 250-registration buffer named separately (sourced)."
],
"closes": "The event organizer closes the registration form at the 500-registration ceiling (sourced), no later than the 2026-11-12 headcount lock (sourced)."
},
"gate": "Expected attendance at the 50% show-up rate (sourced) sits at 250, exactly at the 250-person limit. At the band's 60% top (sourced) it reaches 300, 50 over, closing the walk-in lane and turning away registered people, with a 48.2% chance of exceeding the limit even at 50% (sourced).",
"waitlist": {
"rule": "fifo",
"drain_date": "2026-11-12",
"claim_window": "48 hours (self-set)",
"publish": "When the list opens, publish the FIFO order, the 2026-11-12 drain date (sourced), the 48-hour claim window (self-set), and that the event organizer decides promotions."
},
"walk_in": {
"lane": "walkin",
"volume": 45,
"opens": "The walk-in lane opens once the pre-registered check-in window closes, and everyone completes the full late-registration form (sourced)."
},
"catering": {
"number": 250,
"buffer": 250,
"note": "250 expected show-ups at the ceiling, never the registration count, with the 250-registration buffer named separately (sourced)."
},
"deposit": "No deposit is recommended; its costs are refund handling for every attendee, the accounting and tax burden of moving money, and a barrier that falls hardest on scholarship seats. Route this concern instead to the ticket-price posture, which here is free registration (sourced).",
"handoffs": [
{
"to": "catering",
"what": "Catering gets 250 expected show-ups at the ceiling, with a 250-registration buffer (sourced)."
},
{
"to": "check-in desk",
"what": "The check-in desk should expect up to 45 walk-ins once the pre-registered window closes (sourced)."
},
{
"to": "venue contract signer",
"what": "The venue contract signer should see that the band's 60% top brings 300 against the 250 limit, 50 over, before the ceiling is finalized (sourced)."
},
{
"to": "reminder-email owner",
"what": "The reminder-email owner gets today's 410 registrations against the 500 ceiling, 90 remaining, to target reminders (sourced)."
}
],
"measure": [
"registered count",
"attended count",
"walk-ins",
"waitlist promotions",
"how many promoted arrived"
],
"caveats": [
"The 50% show-up ratio is borrowed - no prior registered-vs-attended count for this event (sourced).",
"The sourced 2x ceiling was written for free hackathons; here it is an upper bound, not a target.",
"At the band's 60% top the ceiling brings 50 more attendees than the 250 limit allows (sourced).",
"Only 90 registrations remain before the 500 ceiling closes the form (sourced)."
]
}
The page's check: kept - nothing to flag
A free hackathon already past double
Ceiling 600 by the own-ratio method; catering 275.
Input:
{
"task": "plan",
"event": "Event: Fernhollow Hack 2026 - Hackathon; Free registration\nEvent date: 2026-11-28; headcount lock (catering or venue guarantee): 2026-11-14; today: 2026-11-02\nRecurring: yes; walk-up population at the venue: no; channel that reaches the waitlist within a day: yes; someone can sort and check the waitlist: yes\nRegistration form: open",
"numbers": "1. Hard capacity: 300 (the occupancy limit)\n2. Aimed attendance: 300\n3. Expected show-up rate: 45.9% - your own last edition: 280 of 610 registered attended (45.9%; 95% interval 42%-49.9%)\n4. Registration ceiling: 300 / 45.9% = 653, held at the sourced double: 600\n5. Expected show-ups handed to catering at the ceiling: 275, with the 325-registration buffer named separately\nRegistrations today: 720; waitlist (including anyone past the ceiling): 180\nExpected show-ups on today's registrations: 275\nOccupancy gate: at 49.9% show-up the ceiling brings 299 against a limit of 300 (under); chance of exceeding the limit at exactly 45.9%: 2%\nCatering number: 275 expected show-ups at the ceiling (buffer 325 registrations) - never the registration count\nWaitlist rule: proximity - Proximity to the venue, published when the list opens; drain date: 2026-11-14 - Drain on or before 2026-11-14, the headcount lock, so promotions still move the catering number.; claim window: 48 hours (self-set)\nDay-of lane: fed - Waitlist-fed lane (the same lane, the waitlist told the night before); expected walk-in volume (empty places at the expected show-up): 25\nLast edition: 280 attended of 610 registered = 45.9% (95% interval 42%-49.9%)\nComped or scholarship seats: 25",
"facts": "Ceiling method: own-ratio ceiling (its promotion condition holds)\nFindings (4):\n[check] overflow_unapproved (gate): The 40 standing or overflow places carry no written venue approval, so they are not counted - get approval from whoever the venue names before registrations are accepted against them, never on the day at the door.\n[fix] over_ceiling (ceiling): 720 registrations are past the ceiling of 600 by 120 - past double is the one posture the guide forbids; close the form and move everyone past the line to the waitlist.\n[info] proximity (waitlist): Proximity ordering is on the table: someone can sort and check the list and 180 wait past a ceiling of 600 (more than a quarter of it, a self-set line). Publish the rule when the list opens; it is an allocation on registrants' location data and disadvantages people who travel.\n[info] comped (ratio): 25 comped or scholarship seats: plan them at the free-seat rate and count them separately so next edition has their own rate."
}
Reply:
{
"task": "plan",
"summary": "The ceiling is 600 registrations (sourced) with 275 expected show-ups to catering (sourced); act first by closing the form now, since 720 registrations already sit 120 past the ceiling.",
"ceiling": {
"registrations": 600,
"arithmetic": [
"Hard capacity is 300, the occupancy limit (sourced).",
"Aimed attendance is 300 (sourced).",
"Expected show-up rate is 45.9%, sourced from last edition's own ratio: 280 of 610 registered attended (95% interval 42%-49.9%).",
"Registration ceiling is 300 divided by 45.9%, equaling 653, held at the sourced double of 600 (sourced).",
"Catering gets 275 expected show-ups at the ceiling, with a 325-registration buffer named separately (sourced)."
],
"closes": "The event organizer closes the registration form now, since 720 registrations already exceed the 600 ceiling by 120 (sourced), moving everyone past the line to the waitlist."
},
"gate": "At the sourced top of the interval (49.9%), the 600 ceiling brings 299 against the 300 limit - under it, with a 2% chance of exceeding it at the 45.9% rate (sourced). The 40 standing or overflow places lack written venue approval and do not count until approved (sourced).",
"waitlist": {
"rule": "proximity",
"drain_date": "2026-11-14",
"claim_window": "48 hours (self-set)",
"publish": "When the list opens, publish the proximity-to-venue order, the 2026-11-14 drain date (sourced), the 48-hour claim window (self-set), that the event organizer decides promotions, and that the ordering disadvantages people who travel."
},
"walk_in": {
"lane": "fed",
"volume": 25,
"opens": "The waitlist-fed lane opens when waitlisted registrants are told the night before that their unclaimed seats are available, using the same lane they already registered through (sourced)."
},
"catering": {
"number": 275,
"buffer": 325,
"note": "275 expected show-ups at the ceiling, never the registration count, with the 325-registration buffer named separately (sourced)."
},
"deposit": "No deposit is recommended; its costs are refund handling for every attendee, the accounting and tax burden of moving money, and a barrier that falls hardest on comped and scholarship seats. Route this concern instead to the ticket-price posture, which here is free registration (sourced).",
"handoffs": [
{
"to": "catering",
"what": "Catering gets 275 expected show-ups at the ceiling, with a 325-registration buffer (sourced)."
},
{
"to": "venue contract signer",
"what": "The venue contract signer needs written approval before the 40 standing or overflow places count toward capacity (sourced)."
},
{
"to": "reminder-email owner",
"what": "The reminder-email owner gets the 180 waitlisted registrants so reminders are not sent as if they are confirmed attendees (sourced)."
},
{
"to": "check-in desk",
"what": "The check-in desk should expect about 25 fed-lane arrivals from the waitlist the night before (sourced)."
}
],
"measure": [
"registered count",
"attended count",
"waitlist promotions and arrivals",
"fed-lane arrivals from unclaimed seats",
"comped seat attendance, counted separately"
],
"caveats": [
"720 registrations already sit 120 past the 600 ceiling (sourced) - close the form now.",
"The 40 standing or overflow places have no written venue approval and do not count (sourced).",
"The 45.9% own-ratio comes from a single prior edition (sourced) - one edition is a sample of one.",
"Proximity ordering is an allocation on location data and disadvantages people who travel (sourced)."
]
}
The page's check: kept - nothing to flag
A paid workshop sold past its room
Ceiling 80 by the no-overbooking rule; catering 75.
Input:
{
"task": "plan",
"event": "Event: Kubernetes in a Day (paid workshop) - Workshop day; Paid ticket\nEvent date: 2026-11-10; headcount lock (catering or venue guarantee): 2026-11-05; today: 2026-11-02\nRecurring: no; walk-up population at the venue: yes; channel that reaches the waitlist within a day: yes; someone can sort and check the waitlist: no\nRegistration form: open",
"numbers": "1. Hard capacity: 80 (the occupancy limit)\n2. Aimed attendance: 80\n3. Expected show-up rate: 93.5% - Event Tech Live: paid events convert 90-97% of tickets to attendance (3-10% no-shows)\n4. Registration ceiling: 80 - no overbooking on a paid event\n5. Expected show-ups handed to catering at the ceiling: 75, with the 5-registration buffer named separately\nRegistrations today: 86; waitlist (including anyone past the ceiling): 20\nExpected show-ups on today's registrations: 75\nOccupancy gate: at 97% show-up the ceiling brings 78 against a limit of 80 (under); chance of exceeding the limit at exactly 93.5%: 0%\nCatering number: 75 expected show-ups at the ceiling (buffer 5 registrations) - never the registration count\nWaitlist rule: fifo - Signup order (FIFO), drained in one batch on a named date; drain date: 2026-11-05 - Drain on or before 2026-11-05, the headcount lock, so promotions still move the catering number.; claim window: 48 hours (self-set)\nDay-of lane: walkin - Walk-in lane (separate late form, opens after pre-registered check-in closes, full form for everyone); expected walk-in volume (empty places at the expected show-up): 5",
"facts": "Ceiling method: no overbooking (paid or nominal fee)\nFindings (3):\n[info] no_prior (ratio): No prior registered-versus-attended count, so the ratio is borrowed. Count registered, attended, walk-ins and promoted arrivals this time - that count is what turns a borrowed ratio into your own.\n[fix] over_ceiling (ceiling): 86 registrations are past the ceiling of 80 by 6 - a paid event sold past the room: stop sales; refunds and the public account of turning away someone who paid are the cost.\n[check] lock_soon (waitlist): The headcount guarantee locks in 3 days - drain the waitlist before then or it stops moving the catering number."
}
Reply:
{
"task": "plan",
"summary": "The ceiling is 80 registrations (sourced) with 75 expected show-ups to catering (sourced); act first by stopping ticket sales now, since 86 registrations already exceed the ceiling by 6.",
"ceiling": {
"registrations": 80,
"arithmetic": [
"Hard capacity is 80, the occupancy limit (sourced).",
"Aimed attendance is 80 (sourced).",
"Expected show-up rate is 93.5%, sourced from Event Tech Live: paid events convert 90-97% of tickets (3-10% no-shows).",
"Registration ceiling is 80, with no overbooking on this paid event (sourced).",
"Catering gets 75 expected show-ups at the ceiling, with a 5-registration buffer named separately (sourced)."
],
"closes": "The event organizer stops ticket sales now, since 86 registrations already exceed the 80 ceiling by 6 (sourced)."
},
"gate": "At the sourced 93.5% show-up rate, the 80-seat ceiling brings about 75 expected attendees, well under the 80 limit, with 0% chance of exceeding it even at the band's 97% top (sourced), since this paid ceiling is capped at capacity with no overbooking.",
"waitlist": {
"rule": "fifo",
"drain_date": "2026-11-05",
"claim_window": "48 hours (self-set)",
"publish": "When the list opens, publish the FIFO order, the 2026-11-05 drain date (sourced), the 48-hour claim window (self-set), and that the event organizer decides promotions."
},
"walk_in": {
"lane": "walkin",
"volume": 5,
"opens": "The walk-in lane opens once the pre-registered check-in window closes, and everyone completes the full late-registration form (sourced)."
},
"catering": {
"number": 75,
"buffer": 5,
"note": "75 expected show-ups at the ceiling, never the registration count, with the 5-registration buffer named separately (sourced)."
},
"deposit": "No deposit is recommended; its costs are refund handling for every attendee, the accounting and tax burden of moving money, and a barrier that falls hardest on scholarship seats. This event already uses the ticket-price posture as its lever, since it is a paid ticket (sourced).",
"handoffs": [
{
"to": "catering",
"what": "Catering gets 75 expected show-ups at the ceiling, with a 5-registration buffer (sourced)."
},
{
"to": "check-in desk",
"what": "The check-in desk should expect up to 5 walk-ins once the pre-registered window closes (sourced)."
},
{
"to": "ticket pricing",
"what": "Ticket pricing stops sales now, since 86 registrations already exceed the 80 ceiling by 6 (sourced)."
},
{
"to": "reminder-email owner",
"what": "The reminder-email owner gets the 6-oversold count so no confirmation goes to registrants past the ceiling (sourced)."
}
],
"measure": [
"registered count",
"attended count",
"walk-ins",
"waitlist promotions and arrivals"
],
"caveats": [
"86 registrations already exceed the 80 ceiling by 6 (sourced) - a paid event sold past the room; stop sales now.",
"The headcount lock is 3 days away (2026-11-05, sourced) - drain the waitlist before then or it stops moving the catering number.",
"The 93.5% show-up rate is borrowed from Event Tech Live's general paid-event band, not this event's own history (sourced)."
]
}
The page's check: kept - nothing to flag
The numbers CSV for the first sample
figure,value,source
occupancy limit,250,venue
aimed attendance,250,organizer
show-up rate,50%,"MLH hackathon organizer guide: about 50% of people who register for a free event show up (trendline across 323 events) - aim for double, and do not go over double; free band Event Tech Live, free events see 40-60% no-shows"
registration ceiling,500,sourced
expected show-ups (catering),250,ceiling x rate
buffer,250,ceiling - catering
attendance at top of band,300,60%
waitlist rule,fifo,page
drain date,2026-11-12,"Drain on or before 2026-11-12, the headcount lock, so promotions still move the catering number."
day-of lane,walkin,page
expected walk-ins,45,limit - expected show-ups
Build the input yourself
Load the engine in node (global.window = global; require("./turnkit.js"); require("./turnreply.js")), then:
const K = TurnKit, R = TurnReply;
const f = K.form({ name: "Rust Meetup", format: "meetup", posture: "free", capacity: "250", regs: "410", waitlist: "0", status: "open",
today: "2026-11-02", eventDate: "2026-11-19", lockDate: "2026-11-12", recurring: "no", walkup: "yes", channel: "yes", sorter: "no" });
// f.ceiling, f.catering, f.buffer, f.limit, f.top (attendance at the top of the band), f.tail (chance over the limit at the planned rate)
// f.method: sourced | own | none f.wlRule: fifo | proximity f.drain (ms) f.lane: walkin | fed | none f.walkins f.findings
K.csv(f) // figure, value, source
const b = K.buildInput(f); // b.ok is false, with b.note, until the capacity is given
const wire = K.toWire(b.input); // POST this to /estimate and /run
// then: const plan = R.normalize(R.parseReply(job.output.output));
// R.check(plan, b) // {status: kept or flagged, notes}
// R.md("Rust Meetup", b, plan) // the plan as Markdown
Get a token for the steps below from /tokens.html.
Step 1 — a tiny client helper
One function that sends JSON and unwraps the response. Paste a personal token from /tokens.html, 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://turnout-desk.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://turnout-desk.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// turnout-desk.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://turnout-desk.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "paste-your-token-here" // from https://turnout-desk.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// TurnoutDesk.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class TurnoutDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://turnout-desk.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://turnout-desk.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "paste-your-token-here"; // from https://turnout-desk.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "paste-your-token-here"; // from https://turnout-desk.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
Step 2 — a token
A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"turnout-desk"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "turnout-desk"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "turnout-desk" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "turnout-desk"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "turnout-desk"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "turnout-desk" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "turnout-desk"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "turnout-desk" }))!["token"]!.GetValue<string>();
Step 3 — GET /me
Who the token is (subject_type: user or guest) and the credit balance.
api GET /me | jq '.data | {subject_type, credits}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["credits"]}");
Step 4 — POST /estimate
Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.
# input.json: the run input object - {"task": "plan", "event": "Event: ... - Meetup; Free registration ...", "numbers": "1. Hard capacity: 250 ...", "facts": "Ceiling method: sourced 2x ceiling ..."}
# - see the input table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "plan", "event": "Event: ... - Meetup; Free registration ...", "numbers": "1. Hard capacity: 250 ...", "facts": "Ceiling method: sourced 2x ceiling ..."}
# - see the input table
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "plan", event, numbers, facts} (see the input table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the task and the input: a retry with the same key never bills twice. The page uses turnout-desk:plan:<inputHash>:a<attempt>, and its one automatic reformat retry is a new attempt number, named in the page as one extra run. The plan carries summary, ceiling, gate, waitlist, walk_in, catering, deposit and handoffs; the code below prints the headline parts.
KEY="turnout-desk:plan:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
jq -r '.summary // .format, .gate // .deposit, ((.handoffs // .handoffs)[] | "\(.to // .to): \(.status // .what)")' reply.json
jq -r '.ceiling.registrations, .catering.number' reply.json # anything to know before trusting the plan
import hashlib, time
key = "turnout-desk:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["what"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("summary"))
print(reply.get("gate") or reply.get("deposit", ""))
for s in reply.get("handoffs") or reply.get("handoffs") or []:
print(s.get("to", s.get("to")), "-", s.get("status", s.get("what")))
if reply.get("caveats"):
print("caveats:", reply["caveats"])
import { createHash } from "node:crypto";
const key = `turnout-desk:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.summary);
console.log(reply.gate ?? reply.deposit);
for (const s of reply.handoffs || reply.handoffs || []) console.log(s.to ?? s.to, "-", s.what ?? s.what);
if (reply.caveats) console.log("caveats:", reply.caveats);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "turnout-desk:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["what"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
var reply map[string]any
json.Unmarshal([]byte(j["output"].(map[string]any)["output"].(string)), &reply)
items, _ := reply["handoffs"].([]any) // each hand-off is {to, what}
fmt.Println(j["charged_credits"], j["truncated"], reply["summary"])
fmt.Println(reply["gate"], reply["deposit"])
for _, x := range items {
c := x.(map[string]any)
fmt.Println(c["to"], "-", c["what"])
}
if fix, ok := reply["caveats"].(string); ok && fix != "" {
fmt.Println("caveats:", fix)
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "turnout-desk:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("what").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply.get("summary"));
System.out.println(reply.path("gate").asText(reply.path("deposit").asText()));
for (JsonNode s : reply.has("handoffs") ? reply.path("handoffs") : reply.path("handoffs")) System.out.println(s.path("to").asText(s.path("to").asText()) + " - " + s.path("what").asText(s.path("what").asText()));
if (!reply.path("caveats").asText().isEmpty()) System.out.println("caveats: " + reply.get("caveats").asText());
require "digest"
key = "turnout-desk:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["what"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["summary"]].inspect
puts reply["gate"] || reply["deposit"]
(reply["handoffs"] || reply["handoffs"] || []).each { |s| puts "#{s["to"] || s["to"]} - #{s["what"] || s["what"]}" }
puts "caveats: #{reply["caveats"]}" unless reply["caveats"].to_s.empty?
$key = "turnout-desk:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["what"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", ($reply["summary"] ?? ""), "\n";
echo $reply["gate"] ?? $reply["deposit"] ?? "", "\n";
foreach ($reply["handoffs"] ?? $reply["handoffs"] ?? [] as $s) echo $s["to"] ?? $s["to"], " - ", $s["what"] ?? $s["what"], "\n";
if (!empty($reply["caveats"])) echo "caveats: ", $reply["caveats"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"turnout-desk:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["what"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply!["summary"]}");
Console.WriteLine($"{reply["gate"] ?? reply["deposit"]}");
foreach (var s in (reply["handoffs"] ?? reply["handoffs"]) as JsonArray ?? new JsonArray()) Console.WriteLine($"{s!["to"] ?? s["to"]} - {s["what"] ?? s["what"]}");
if (reply["caveats"] is JsonNode fix && fix.GetValue<string>() != "") Console.WriteLine("caveats: " + fix);
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: turnout-desk:plan:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
rev = json.load(open("input.json")) # the same run input as input.json: {"task": "plan", "event": "Event: ... - Meetup; Free registration ...", "numbers": "1. Hard capacity: 250 ...", "facts": "Ceiling method: sourced 2x ceiling ..."}
# - see the input table
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "turnout-desk:plan:" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `turnout-desk:plan:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "turnout-desk:plan:"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "turnout-desk:plan:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "turnout-desk:plan:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: turnout-desk:plan:" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"turnout-desk:plan:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);