Drive Event Desk from your own code
The page does its free work in the browser: it reads what your company does and your notes on one event (labelled
lines or prose), adds up every cost line, counts your buyers in the room, works out the qualified meetings (your
stated target, or for a booth the skill's directional 10-20%), the cost per qualified meeting, the ratio to what a
meeting costs you elsewhere and the days to go, with flags EV-01..EV-12 (the 3x pass line, attendee counts without
composition, vanity metrics, no follow-up plan, soft pricing inside 8 weeks, over budget). Its two paid lanes are
available over HTTP. You send one JSON object with task set to "decide" or
"plan", and the model replies with one JSON object. The easiest way to build the facts
string is the page itself:
EventKit.buildInput(lane, EventKit.analyze(you, event, {company, role, outcome, question}), {decideHandoff})
in eventkit.js, which also runs under Node.
Base URL: https://api.skillsafe.ai/v1/app-api. Every call sends
Content-Type: application/json and, once you have a token,
Authorization: Bearer <token>. Both lanes run on gpt-terra with a 10%
publisher markup. A run needs a signed-in (user) token and is metered: the platform holds
the estimate's hold_credits while the run is in flight and then charges only
what it used (charged_credits). /estimate is free. A plain-text summary is at
/llms.txt.
The response envelope
Every response is {"data": ..., "error": null} on success or {"data": null, "error": {"code", "message"}} on failure.
Error codes
| HTTP | code | meaning |
|---|---|---|
| 400 | VALIDATION_ERROR | The body is not a JSON object, or a field is the wrong type. Every field of the run input is a string. |
| 401 | UNAUTHORIZED | No token, or an expired one: get a new one (step 2). |
| 402 | INSUFFICIENT_CREDITS | The balance is below the run's minimum. Top up at skillsafe.ai/account/billing/. |
| 403 | FORBIDDEN | A guest token tried a metered run: sign in for a personal token. |
| 404 | NOT_FOUND | Unknown job id. |
| 429 | RATE_LIMITED | Back off and retry with the same Idempotency-Key. |
The task field: two lanes
| task | what the model does |
|---|---|
"decide" | The should-we-sponsor evaluation: a verdict of go, go with changes or pass (pass is required when a meeting costs 3 times your other channels even at the best end and no strategic multiplier is shown), the role to take, the outcome, a reading of the page's math, the in-person test, the three strategic multipliers, the prospectus red_flags quoted, the counterfactual, the negotiate asks in the skill's order, an alternative play, conditions and what to find_out. |
"plan" | The before, during and after: a goal (metric, target, owner), the target_list, a 4-8 step timeline ordered before, during, after, the meeting-booking outreach email, the qualifying_question, what to capture, three follow_up tiers, content and metrics. |
Input fields (all strings)
| field | when | content |
|---|---|---|
task | required | decide or plan |
company | required | Your company's name (may be empty; the page then reads it from the first line of you). |
role | required | host, sponsor (sponsoring or exhibiting), speak or attend. |
outcome | optional | The one outcome that matters: pipeline, authority, community or content. |
you | required | What your company does: the buyer, the deal size, the sales cycle, what a qualified meeting costs you from outbound or ads, who can work the event. Past 5,000 characters the page cuts the middle on line boundaries and leaves an [event-desk: ...] marker. |
event | required | Your notes on the event: the prospectus, each cost and its amount, the attendees and how many are your buyers, the date, a budget, a meetings target. Cut the same way past 6,000 characters. |
facts | required | A JSON string: company, role, outcome, today, currency, event{name, format, date, attendees, icp, icp_share, icp_basis, costs[{label, amount}], cost_total, budget, meetings_stated, team, followup_stated, recording_stated}, you{icp_stated, icp_quote, acv, cycle_months}, benchmark, math{meetings_low, meetings_high, meetings_basis, cost_per_meeting_low, cost_per_meeting_high, cost_per_icp, ratio_low, ratio_high, days_until, over_budget}, flags[{id, severity, text}], notes[]. |
question | optional | What you want to know. |
decide_handoff | plan only | A JSON string of the decision the plan follows: verdict, role, outcome, conditions[], negotiate[], alternative. |
retry_note | optional | Only on a reformat retry: what was wrong with the previous reply. |
The reply contracts
Both lanes share an envelope: task, headline, questions[], summary.
task: "decide"
verdict: go|go with changes|pass, reason, role: host|sponsor|speak|attend, outcome: pipeline|authority|community|content, math_reading, in_person{needed: yes|no|unclear, why, evidence[]}, multipliers[{multiplier, present: yes|no|unknown, why, evidence[]}] (exactly three, in order: exact buyers concentrated nowhere else, category-defining event, access you cannot buy elsewhere), red_flags[{flag, evidence[]}], counterfactual, negotiate[{ask, why}] (for a sponsorship that goes ahead, 2 to 5 in order: speaking or panel slot, side-event rights, attendee list reality check, placement and timing, price), alternative{play: side-event dinner|attend and pre-book meetings|host a webinar|speak|none, why}, conditions[], find_out[].
task: "plan"
source (decide handoff|from the input), role, outcome, goal{metric, target, owner}, target_list{who, how_many, sources[]}, timeline[{stage: before|during|after, when, owner, what}] (4 to 8, in stage order), outreach{subject, body} (at most 100 words), qualifying_question, capture, follow_up[{tier: hot|warm|scan-only, when, action}] (exactly three, in that order), content[] (1 to 4), metrics[{metric, tier: real|decisive}] (3 to 6; real: qualified conversations, meetings booked, opportunities created, pipeline influenced; decisive: cost per qualified meeting, cost per opportunity, closed-won influenced).
Worked examples
These are the page's own bundled examples (invented companies, people and numbers), with the input as app.js builds it and the saved model reply the page replays for free. One worked example per verdict and per lane, and the decide-to-plan handoff.
Decide: a $28,000 trade show booth with 350 of the buyers among 6,000 attendees (go with changes)
Input (description, notes and facts shortened here):
{
"task": "decide",
"company": "Paywistle",
"role": "sponsor",
"you": "Paywistle - accounts payable automation for mid-market finance teams (200-2,000 employees).\nOur buyer is the controller or AP manager; the CFO signs.\nACV is about $24,000 and our sales cycle runs 4-6 months.\nAn outbound meeting costs us about $450 (SDR time plus data), and a paid-search demo about $600.\nWe have two AEs and one SDR who can travel.\n",
"event": "Remitfold Expo 2026 - trade show, Chicago, 17-19 November 2026\nAttendees: 6,000 (organiser's number)\nICP attendees: about 350 controllers and AP managers, per last year's attendee breakdown\nBooth package (10x10, Silver): $28,000\nTravel and hotels for three people: $7,500\nBooth build and shipping: $6,000\nSwag: $1,500\nBudget: $45,000\nThe prospectus headline is \"40,000 impressions across the expo app\".\nLast year two of our... (536 characters in all)",
"facts": "(a JSON string; decoded: company Paywistle, role sponsor, outcome pipeline, event Remitfold Expo 2026 on 2026-11-17, 6000 attendees, 350 of your buyers, 4 cost lines totalling 43000, benchmark 450, meetings 35-70, cost per meeting 614-1229, ratio 1.4-2.7, 47 days to go; flags EV-06, EV-07, EV-08, EV-10)",
"outcome": "pipeline",
"question": "Should we take the booth?"
}
Reply:
{
"task": "decide",
"verdict": "go with changes",
"reason": "The meetings math clears the skill's 3 times pass line and the room holds about 350 of your buyers, but the prospectus sells impressions and badge scans, so it only pays if you pre-book meetings and negotiate.",
"role": "sponsor",
"outcome": "pipeline",
"math_reading": "At 35 to 70 meetings (the directional 10-20% of the 350 buyers in the room), a meeting costs $614 to $1,229, 1.4 to 2.7 times your $450 outbound meeting. That is under the 3 times pass line, and the $43,000 total sits inside the $45,000 budget, but the top end is close to the line if the booth is worked poorly.",
"in_person": {
"needed": "yes",
"why": "A multi-stakeholder deal where the CFO signs, at about $24,000 ACV on a cycle of up to 6 months, is the kind the skill says earns in-person cost.",
"evidence": [
"Our buyer is the controller or AP manager; the CFO signs.",
"our sales cycle runs 4-6 months"
]
},
"multipliers": [
{
"multiplier": "exact buyers concentrated nowhere else",
"present": "unknown",
"why": "About 350 controllers and AP managers is a real concentration, but the notes do not say they cannot be reached elsewhere.",
"evidence": []
},
{
"multiplier": "category-defining event",
"present": "unknown",
"why": "Nothing in the notes says Remitfold Expo 2026 sets the agenda for AP automation buyers.",
"evidence": []
},
{
"multiplier": "access you cannot buy elsewhere",
"present": "unknown",
"why": "The Silver package lists a booth and badge scans, not speaking, a buyer lounge or hosted meetings.",
"evidence": []
}
],
"red_flags": [
{
"flag": "impressions as the headline metric",
"evidence": [
"40,000 impressions across the expo app"
]
},
{
"fla
... (shortened here; the page shows it whole)
Plan: the same booth, carrying the decision
Input (with the decision handed over):
{
"task": "plan",
"company": "Paywistle",
"role": "sponsor",
"you": "Paywistle - accounts payable automation for mid-market finance teams (200-2,000 employees).\nOur buyer is the controller or AP manager; the CFO signs.\nACV is about $24,000 and our sales cycle runs 4-6 months.\nAn outbound meeting costs us about $450 (SDR time plus data), and a paid-search demo about $600.\nWe have two AEs and one SDR who can travel.\n",
"event": "Remitfold Expo 2026 - trade show, Chicago, 17-19 November 2026\nAttendees: 6,000 (organiser's number)\nICP attendees: about 350 controllers and AP managers, per last year's attendee breakdown\nBooth package (10x10, Silver): $28,000\nTravel and hotels for three people: $7,500\nBooth build and shipping: $6,000\nSwag: $1,500\nBudget: $45,000\nThe prospectus headline is \"40,000 impressions across the expo app\".\nLast year two of our... (536 characters in all)",
"facts": "(a JSON string; decoded: company Paywistle, role sponsor, outcome pipeline, event Remitfold Expo 2026 on 2026-11-17, 6000 attendees, 350 of your buyers, 4 cost lines totalling 43000, benchmark 450, meetings 35-70, cost per meeting 614-1229, ratio 1.4-2.7, 47 days to go; flags EV-06, EV-07, EV-08, EV-10)",
"outcome": "pipeline",
"question": "Should we take the booth?",
"decide_handoff": "{\"verdict\":\"go with changes\",\"role\":\"sponsor\",\"outcome\":\"pipeline\",\"conditions\":[\"Get this year's count of controllers and AP managers registered, in writing, before signing.\",\"Negotiate the price or add stage time or side-event rights, since pricing is soft inside 8 weeks.\",\"Pre-book meetings with named controllers 1-..."
}
Reply:
{
"task": "plan",
"source": "decide handoff",
"role": "sponsor",
"outcome": "pipeline",
"goal": {
"metric": "meetings booked",
"target": "[target]",
"owner": "[owner]"
},
"target_list": {
"who": "Controllers and AP managers at mid-market finance teams (200-2,000 employees) attending Remitfold Expo 2026, plus their CFOs where they come.",
"how_many": "15-30 named people",
"sources": [
"This year's attendee breakdown by role, requested in writing from the organiser",
"Your open pipeline and closed-lost accounts with a controller or AP manager likely in Chicago",
"The Remitfold Expo 2026 speaker and exhibitor lists, for finance teams sending people"
]
},
"timeline": [
{
"stage": "before",
"when": "[date]",
"owner": "[owner]",
"what": "Get this year's count of controllers and AP managers registered, in writing, before signing (the attendee list reality check)."
},
{
"stage": "before",
"when": "[date]",
"owner": "[owner]",
"what": "Before signing, ask for a speaking or panel slot and side-event rights, then negotiate price, since pricing is soft inside 8 weeks."
},
{
"stage": "before",
"when": "[date]",
"owner": "SDR",
"what": "Build the list of 15-30 named controllers and AP managers to meet in Chicago."
},
{
"stage": "before",
"when": "2-4 weeks out",
"owner": "AEs",
"what": "If side-event rights are won, invite 8-14 named controllers and CFOs to a dinner during the expo."
},
{
"stage": "before",
"when": "1-3 weeks ahead",
"owner": "SDR",
"what": "Book 15-minute booth meetings with the named list, with a specific reason to come, so the booth is not relying on walk-bys."
},
{
"stage": "during",
"when": "17-19 November 2026",
"owner": "AEs",
"what": "Work t
... (shortened here; the page shows it whole)
Decide: a $60,000 gold sponsorship that fails the 3x line (pass)
Input:
{
"task": "decide",
"company": "Portalwren",
"role": "sponsor",
"you": "Portalwren - internal developer portal for platform engineering teams at companies with 500+ engineers.\nOur buyer is the head of platform engineering; deals are around $60,000 ACV with a 6-month sales cycle.\nA qualified meeting from outbound costs us about $700, all in.\n",
"event": "Stackmoot Global 2026 - developer conference, Las Vegas, March 9-11, 2027\n12,000 attendees expected, \"the largest developer audience of the year\".\nThe organiser could not say how many platform engineering leads attend; our guess from last year's speaker list is that around 150 of them are our buyers.\nGold sponsorship (booth, logo on lanyards, 2 passes): $60,000\nExtra passes for two more people: $4,000\nFlights and hote... (576 characters in all)",
"facts": "(a JSON string; decoded: company Portalwren, role sponsor, outcome pipeline, event Stackmoot Global 2026 on 2027-03-09, 12000 attendees, 150 of your buyers, 4 cost lines totalling 85000, benchmark 700, meetings 15-30, cost per meeting 2833-5667, ratio 4-8.1, 159 days to go; flags EV-01, EV-06, EV-08)",
"outcome": "pipeline"
}
Reply:
{
"task": "decide",
"verdict": "pass",
"reason": "Even at the best end a meeting costs 4 times your outbound cost, and nothing in the notes shows a strategic multiplier that would justify it.",
"role": "sponsor",
"outcome": "pipeline",
"math_reading": "At 15 to 30 meetings (the directional 10-20% of a guessed 150 buyers), a meeting costs $2,833 to $5,667, 4 to 8.1 times your $700 outbound meeting. That is past the skill's 3 times pass line at both ends, on a total of $85,000.",
"in_person": {
"needed": "yes",
"why": "A $60,000 ACV deal on a 6-month cycle is the kind of sale the skill says earns in-person cost, so the question is which in-person play, not whether.",
"evidence": [
"deals are around $60,000 ACV with a 6-month sales cycle"
]
},
"multipliers": [
{
"multiplier": "exact buyers concentrated nowhere else",
"present": "unknown",
"why": "The 150 buyers are your guess from a speaker list, and the notes do not show they cannot be reached elsewhere.",
"evidence": []
},
{
"multiplier": "category-defining event",
"present": "unknown",
"why": "The organiser's audience-size claim is about developers in general, not about platform engineering leaders.",
"evidence": []
},
{
"multiplier": "access you cannot buy elsewhere",
"present": "unknown",
"why": "Gold buys a booth, lanyard logos and passes, none of which is access to heads of platform engineering.",
"evidence": []
}
],
"red_flags": [
{
"flag": "attendee counts without composition",
"evidence": [
"12,000 attendees expected",
"The organiser could not say how many platform engineering leads attend"
]
},
{
"flag": "impressions as the headline metric",
"evidence": [
"Headline metric in the prospectus",
"500,000 impres
... (shortened here; the page shows it whole)
Decide: two people at a 180-person regional forum (go)
Input:
{
"task": "decide",
"company": "Scaffwick",
"role": "attend",
"you": "Scaffwick - safety-compliance software for regional construction contractors (50-500 field staff).\nWe sell to the operations director or safety manager. ACV is around $9,000.\nOur cost per meeting from LinkedIn ads is about $380.\n",
"event": "Midwest Builders Safety Forum - regional conference, Columbus, 2-3 December 2026\nAttendees: about 180, mostly safety managers and operations directors from contractors in Ohio and Indiana\nICP attendees: about 120 (organiser's attendee breakdown by role)\nTwo tickets: $1,200\nTravel (driving) and one hotel night for two: $900\nPlan: attend, book meetings with 20 named contractors before we go, and host a breakfast for 10... (582 characters in all)",
"facts": "(a JSON string; decoded: company Scaffwick, role attend, outcome pipeline, event Midwest Builders Safety Forum on 2026-12-02, 180 attendees, 120 of your buyers, 3 cost lines totalling 2800, benchmark 380, meetings 12-12, cost per meeting 233-233, ratio 0.6-0.6, 62 days to go; flags none)",
"outcome": "pipeline",
"question": "Is this worth two people for two days?"
}
Reply:
{
"task": "decide",
"verdict": "go",
"reason": "About 120 of the 180 attendees are your buyers, the stated 12 meetings cost $233 each against $380 from LinkedIn ads, and the plan is already pre-booked meetings plus a breakfast.",
"role": "attend",
"outcome": "pipeline",
"math_reading": "At the 12 meetings you target, a meeting costs $233 on a $2,800 total, 0.6 times the $380 a LinkedIn ads meeting costs you. A niche regional event of about 180 people is the kind the skill says often wins on cost per qualified meeting.",
"in_person": {
"needed": "yes",
"why": "Safety compliance for regional construction contractors is a conservative, compliance-driven buyer, where the skill says face time earns its cost.",
"evidence": [
"safety-compliance software for regional construction contractors"
]
},
"multipliers": [
{
"multiplier": "exact buyers concentrated nowhere else",
"present": "yes",
"why": "The room is mostly your two buyer roles from a two-state regional market, a mix you rarely get in one place.",
"evidence": [
"mostly safety managers and operations directors from contractors in Ohio and Indiana"
]
},
{
"multiplier": "category-defining event",
"present": "unknown",
"why": "The notes do not say whether this forum sets the agenda for contractor safety in the region.",
"evidence": []
},
{
"multiplier": "access you cannot buy elsewhere",
"present": "unknown",
"why": "The breakfast creates access you control, but the notes do not show access only the organiser can give.",
"evidence": []
}
],
"red_flags": [],
"counterfactual": "The same $2,800 in LinkedIn ads buys meetings at about $380 each, more than the $233 a meeting here if the 12 land.",
"negotiate": [],
"alternative": {
"play": "none",
... (shortened here; the page shows it whole)
Plan: a 12-seat dinner for target accounts, from the input
Input:
{
"task": "plan",
"company": "Tidewick Analytics",
"role": "host",
"you": "Tidewick Analytics - revenue forecasting for B2B SaaS finance teams.\nOur buyer is the VP Finance or CFO at SaaS companies with $20M-$200M ARR.\nWe have 14 target accounts whose finance leaders will be at SaaSFin Summit in Boston.\nOur cost per meeting from outbound is about $520.\n",
"event": "Side dinner during SaaSFin Summit - a dinner we host, Boston, 21 October 2026\nFormat: private dinner, 12 seats, at a restaurant two blocks from the venue\nGuests: 10 finance leaders from our target accounts plus 2 respected CFOs as magnetic guests\nRestaurant (private room, set menu for 14 incl. our two hosts): $3,400\nInvitations: personal emails from our CEO\nMeetings target: 8\nOwner: Tamsin Brockhollow (head of demand gen)\n",
"facts": "(a JSON string; decoded: company Tidewick Analytics, role host, outcome pipeline, event Side dinner during SaaSFin Summit on 2026-10-21, None attendees, 10 of your buyers, 1 cost lines totalling 3400, benchmark 520, meetings 8-8, cost per meeting 425-425, ratio 0.8-0.8, 20 days to go; flags EV-07)",
"outcome": "pipeline"
}
Reply:
{
"task": "plan",
"source": "from the input",
"role": "host",
"outcome": "pipeline",
"goal": {
"metric": "meetings booked",
"target": "8",
"owner": "Tamsin Brockhollow (head of demand gen)"
},
"target_list": {
"who": "The VP Finance or CFO at each of your 14 target accounts at SaaSFin Summit, plus 2 respected CFOs as magnetic guests.",
"how_many": "14",
"sources": [
"Your list of 14 target accounts whose finance leaders will be at SaaSFin Summit",
"The SaaSFin Summit speaker and attendee lists, filtered to SaaS companies with $20M-$200M ARR",
"Your CEO's network, for the 2 magnetic CFOs"
]
},
"timeline": [
{
"stage": "before",
"when": "[date]",
"owner": "Tamsin Brockhollow (head of demand gen)",
"what": "Name the VP Finance or CFO at each of the 14 target accounts and confirm the 2 magnetic CFOs before invitations go out."
},
{
"stage": "before",
"when": "2-4 weeks out",
"owner": "CEO",
"what": "Send the personal dinner invitations now, since the dinner is 20 days away and the window is closing."
},
{
"stage": "before",
"when": "1-3 weeks ahead",
"owner": "Tamsin Brockhollow (head of demand gen)",
"what": "Book 15-minute meetings at SaaSFin Summit with target-account leaders who cannot make the dinner."
},
{
"stage": "before",
"when": "[date]",
"owner": "[owner]",
"what": "Tell the target accounts you will be at SaaSFin Summit, with the 2 CFO guests as the reason to come."
},
{
"stage": "during",
"when": "21 October 2026",
"owner": "CEO",
"what": "Host the 12-seat dinner, seat a host near each cluster of target-account guests, and ask each the qualifying question."
},
{
"stage": "during",
"when": "21 October 2026",
"owner": "Tamsin Brockhollow (head of demand gen)",
"what":
... (shortened here; the page shows it whole)
The page re-checks the reply; the API does not
The page holds every reply to account (recon.js): for a decision, pass when a meeting costs 3 times your other channels even at the best end (EV-01) and no strategic multiplier is shown; go only when your buyers in the room and the cost are both known and nothing is over budget; conditions for a go with changes; the outcome you chose; the three multipliers in order, quoted when shown; only the four prospectus red flags, quoted; for a sponsorship that goes ahead, 2 to 5 negotiation asks in the skill's order; an alternative play. For a plan, the role and outcome you chose; a goal metric from the real or decisive tier, never vanity; 4 to 8 timeline steps ordered before, during, after; a meeting-booking email of at most 100 words; follow-up in the tiers hot, warm, scan-only, with no sales sequence for scans; 3 to 6 metrics with the right tier. In both, quotes are checked word for word and no cost, count, rate, date or duration may be invented. Over the API you get the model's reply as it is; run the same checks yourself with Recon.reconcile in recon.js, or paste the same input into the page.
Step 1 — a tiny client helper
One function that sends JSON and unwraps the envelope.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://event-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://event-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"]
// event-desk.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://event-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://event-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
}
// EventDesk.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 EventDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://event-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://event-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://event-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://event-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":"event-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": "event-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: "event-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": "event-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", "event-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: "event-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" => "event-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 = "event-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 (not wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model reads gpt-5.6-terra and model_alias gpt-terra.
# input.json: the run input object - {"task": "decide", "company": "...", "role": "sponsor", "you": "...", "event": "...", "facts": "..."}
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": "decide", "company": "...", "role": "sponsor", "you": "...", "event": "...", "facts": "...", ...}
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, company, role, outcome, you, event, question, facts, ...}
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses event-desk:<lane>:<inputHash>:a<attempt>. The job's output.output is the reply text; truncated: true means the balance cut it short.
KEY="event-desk:decide:$(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 lane's contract
echo "$J" | jq '.data | {charged_credits, truncated}'
import hashlib, time
key = "event-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["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the lane's JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"))
import { createHash } from "node:crypto";
const key = `event-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.verdict);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "event-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["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
fmt.Println(j["charged_credits"], j["truncated"], j["output"].(map[string]any)["output"])
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "event-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("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply);
require "digest"
key = "event-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["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["verdict"]].inspect
$key = "event-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["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["verdict"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"event-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["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply}");
Step 6 — POST /run-stream (plan)
The same run as Server-Sent Events. plan.json is a plan-lane input, with decide_handoff when it follows a decision.
# 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: event-desk:plan:$(shasum -a 256 plan.json | cut -c1-16):a1" --data @plan.json
rev = json.load(open("plan.json")) # {"task": "plan", "company": "...", "role": "...", "you": "...", "event": "...", "facts": "...", "decide_handoff": "..."}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "event-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("plan.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": `event-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("plan.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", "event-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("plan.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", "event-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("plan.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "event-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("plan.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: event-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("plan.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", $"event-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);