← Portal Path
API
Tokens

Run Portal Path from your own code

Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page classifies every page of the navigation, runs the checks, sets the time-to-first-call target, ranks the friction rungs and build vs buy, and groups the gaps into a Priority 1/2/3 roadmap, then hands it all to the run as facts. The run redesigns the navigation or plans the path; the page then holds it to its own facts (ppreply.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":"portal-path"}), so no slug header is needed afterwards. Send it as Authorization: Bearer YOUR_TOKEN. The run body is the input object, sent bare.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required key (task, portal, tree, steps, facts) is missing. The page always sends all six of task, portal, tree, steps, carry, facts; carry is an empty string for ia.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: both lanes are metered and need a personal sign-in.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field comes first

Two lanes, chosen by task, in the order the work runs - the navigation, then the path through it:

A missing or unknown task is answered as ia. Both lanes send the same six fields. The facts are the page's checks, one row per line, and a line starting "must:" is a rule the reply has to hold. The page's handoff carries the redesigned navigation into carry for the path lane. An internal service catalog is out of scope and the page never sends it.

The input

FieldRequiredWhat
taskyesia or path. If it is missing or neither, do ia and set "task": "ia".
portalyesthe portal's name, the API complexity class, the measured median time to first call when given, and the interview's answers; "not given" means not given.
treeyesthe navigation as it is today, one page per line as #id | page (indented two spaces per level) | kind | layer | audience | access.
stepsyesthe onboarding steps today, landing to first call, one per line as #n | step | minutes | stage | friction.
carrysend it (empty for ia)path task only: the navigation the ia task proposed, when it ran first; may be empty.
factsyesthe page's checks, one line each as area | id | status | evidence, then the time-to-first-call target, the rung order, the build-vs-buy ladder, the maturity placement and the priority grouping. A line starting "must:" applies to your reply.
retry_notenooptional; if present, your previous reply was unusable - follow it.

The rules the model applies

From the source skill, @samber/developer-portal-design - the only standard the run applies:

House rules

The replies

ia

Redesign the navigation so both audiences find what they need at the right depth and every self-service surface has a home.

  1. summary - one sentence, at most 40 words: what is wrong with the navigation today and what the redesign changes.
  2. options - two or three candidate architectures, each an object: id (layered, split or docs_first, each used once), name, tradeoff (one sentence), recommended (true or false). Exactly one is recommended, and it is the leaning the facts give.
  3. tree - the proposed navigation for the recommended option, in reading order, each node an object: label (the page name), depth (0 for a top-level item, 1 under it, and so on), layer (top, solution or bottom), audience (evaluator, engineer or both), gated (true only for pages that hold a developer's own data: keys, logs, usage, billing, team), kind (one of the kind ids the facts list), note (one short sentence: why it sits there, or empty). Pricing and every use-case page are never gated. Include at least one use-case page in the solution layer and a quickstart page.
  4. placement - exactly one object per surface the facts list as needing a home: kind (the surface's kind id), where (the label of the node in your proposed tree that holds it, spelled exactly as in the tree), entry (one sentence: how a developer reaches it).
  5. caveats - at most 4 short strings: what the team must confirm before building it; [] when there is nothing to say.
{
  "task": "ia",
  "summary": "one sentence",
  "options": [ { "id": "layered", "name": "A single portal layered by depth", "tradeoff": "one sentence", "recommended": true } ],
  "tree": [ { "label": "Pricing", "depth": 0, "layer": "top", "audience": "evaluator", "gated": false, "kind": "pricing", "note": "one short sentence" } ],
  "placement": [ { "kind": "keys", "where": "API keys", "entry": "one sentence" } ],
  "caveats": [ ]
}

The page holds it to its own facts (check() in ppreply.js; any warning makes the reply flagged): the reply answers the task it was sent; no URL; no backticks; a summary of at most 40 words; two or three options with ids from layered, split and docs_first, each once, exactly one recommended and it is the page's leaning; every tree node with a kind from the page's enum, a valid layer and audience; pricing and use cases never gated; a use-case page in the solution layer and a quickstart page; one placement per surface the page lists, each in a page the proposed tree has.

path

Plan the path from landing to the first successful call against the page's target, with the friction fixes in the page's order and the roadmap.

  1. summary - one sentence, at most 40 words: where the path loses developers today and what the plan changes.
  2. ttfc - an object: target_minutes (exactly the facts' target, a number), rationale (one or two sentences: the complexity class and why the target fits it; the 5-minute figure only as context).
  3. steps - the proposed path in order, each an object: n (1, 2, 3...), step (one sentence), minutes (a number), removes (the friction this step removes compared with today, or ""). It runs from landing to the first successful call: a docs or exploration step comes before any sign-up step, then sign-up, verification, credential, quickstart and the first call. The minutes total at most the target. When the carried navigation is given, name its pages.
  4. fixes - one object per open rung, in exactly the facts' rung order: rung (the rung id: ungated, collection, autokey or personalized), action (one sentence for this portal), why (one sentence from the input).
  5. roadmap - objects: priority (1, 2 or 3), area (discovery, decision, onboarding, golive, maintenance or community), item (one sentence), check (the check id it closes, or ""). Every check the facts mark fail appears as a priority 1 item carrying its check id; warnings are priority 2 or 3 as the facts group them.
  6. build - an object: choice (exactly the facts' top build rung: managed, assembled or custom), why (one or two sentences from the interview).
  7. caveats - at most 4 short strings: what the team must measure or confirm; [] when there is nothing to say.
{
  "task": "path",
  "summary": "one sentence",
  "ttfc": { "target_minutes": 15, "rationale": "one or two sentences" },
  "steps": [ { "n": 1, "step": "one sentence", "minutes": 2, "removes": "" } ],
  "fixes": [ { "rung": "ungated", "action": "one sentence", "why": "one sentence" } ],
  "roadmap": [ { "priority": 1, "area": "decision", "item": "one sentence", "check": "pricing" } ],
  "build": { "choice": "managed", "why": "one or two sentences" },
  "caveats": [ ]
}

The page requires: the reply answers the task it was sent; the same text rules; the page's target minutes (a different target is put back and flagged); every step timed, the total at or under the target, a docs step before any sign-up step and the path ending at the first call; the fixes in exactly the page's open rung order; every fail check as a Priority 1 roadmap item carrying its check id; only known check ids, the six area ids and priorities 1 to 3; the page's build-vs-buy choice.

Worked examples

Three runs from the page's free samples (invented portals): the navigation redesign for a banking API with pricing behind the login wall, the path for the same portal carrying that redesign, and the redesign for a partner portal with two auth models. Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped unedited - but the long fields, strings and lists are shortened in this display only, and every cut is listed under the example. The last line is the page's own check of the full reply.

ia - A banking API with pricing behind the login wall

The page read it as: decision information behind the login wall; maturity project; leaning layered; Priority 1: ungated_docs, pricing, solution, self_keys, signup_first, friction, ttfc_path, ttfc_measured.

Input (facts is 5528 characters; the first 1398 are shown):

{
 "task": "ia",
 "portal": "Portal: Corvantle Open Banking (an existing portal)\nAPI complexity class: Complex - OAuth plus webhooks; measured median TTFC today: 2880 minutes\nSelf-service keys: via a ticket or sales; audience: both engineers and evaluators; who: public developers; partners use a different auth model: no\nPlatform: an API gateway with a bundled portal: yes; a CMS its authors love: no; business-side authors need a real CMS: no; compliance or data residency no managed platform meets: no; a multi-gateway portfolio: no\nA named portal owner: yes; several roles or products one quickstart cannot serve: no; where the funnel stalls: signup to first call\nHard deadline: no; goal: a compounding asset; effort ceiling: medium",
 "tree": "Navigation tree (#id | page, indented two spaces per level | kind | layer | audience | access):\n#1 | Developers | home | top | both | open\n#2 | Products | catalog | top | both | open\n#3 |   Accounts API | catalog | top | both | open\n#4 |   Payments API | catalog | top | both | open\n#5 |   Consents API | catalog | top | both | open\n#6 | Pricing | pricing | top | evaluator | login\n#7 | Use cases | usecases | solution | evaluator | login\n#8 |   Open banking for lenders | usecases | solution | evaluator | login (inherited)\n#9 | Documentation | guides | bottom | engineer | login\n#10 |   Getting started | quickstart | bottom | engineer | login (inherited)\n#11 |   Authentication | guides | bottom | engineer | login (inherited)\n#12 |   API reference | reference | bottom | engineer | login (inherited)\n#13 |     Accounts | reference | bottom | engineer | login (inherited)\n#14 |     Payments | reference | bottom | engineer | login (inherited)\n#15 | My account | other | top | both | login\n#16 |   Applications | keys | bottom | engineer | login (inherited), ticket\n#17 |   Billing & plan | billing | bottom | both | login (inherited)\n#18 | Support | support | bottom | both | open",
 "steps": "Onboarding steps, landing to first call (#n | step | minutes | stage | friction):\n#1 | Land on the developer portal | 1 min | other\n#2 | Register an account | 5 min | sign up\n#3 | Verify your email address | 10 min | verification | friction: email or phone verification\n#4 | Wait for manual approval of the developer account | 2880 min | other | friction: manual approval\n#5 | Open a support ticket to request sandbox API keys | 1440 min | credential | friction: a support ticket\n#6 | Copy the client secret from the email into your config | 5 min | credential | friction: copying a key by hand\n#7 | Read the getting started guide | 15 min | quickstart / docs\n#8 | Install the SDK | 10 min | install | friction: an SDK install\n#9 | Make the first API call | 10 min | first call",
 "carry": "",
 "facts": "The page's checks (8 fail, 14 warn, 5 pass, 1 n/a); verdict: decision information behind the login wall:\n- Discovery | reference | pass | at 'API reference', 'Accounts', 'Payments'\n- Discovery | ungated_docs | fail | every docs page is behind sign-in - mandatory signup before any exploration\n- Discovery | layering | pass | top layer: 'Developers', 'Products', 'Pricing', 'Use cases'\n- Discovery | search | warn | no page carries it - classic full-text or semantic search over the whole portal\n- Discovery | assistant | warn | no page carries it - an expected component now - answers grounded in the docs, citing a linkable source, saying 'I don't know' when the docs don't cover it\n- Discovery | split | na | public developers only\n- Decision-making | pricing | fail | 'Pricing' is behind login - decision information behind a login wall is a sales-enablement failure, not a content failure\n- Decision-making | solution | fail | 'Use cases', 'Open banking for lenders' are behind login - a sales-enablement failure\n- Onboarding | self_keys | fail | keys come through 'Applications' [ticket] - key lifecycle actions that need a support ticket fail the self-service line\n- Onboarding | quickstart | warn | 'Getting started' is behind login\n- Onboarding | sandbox | warn | no sandbox or test-mode entry - sandbox-versus-production confusion is a common friction point\n- Onboarding | samples | warn |\n\n[... 4130 more characters cut from this display ...]"
}

Reply (shortened for display - see the cuts below):

{
 "task": "ia",
 "summary": "Pricing, use cases and every docs page sit behind the login wall and keys need a ticket; the redesign opens the top and solution layers and the docs, and gives every console surface a home.",
 "options": [
  {
   "id": "layered",
   "name": "A single portal layered by depth",
   "tradeoff": "One domain and one sign-in, with pricing, use cases and the docs opened at the top and solution layers and the console kept at the bottom.",
   "recommended": true
  },
  {
   "id": "docs_first",
   "name": "A docs-first minimal portal with credentials embedded",
   "tradeoff": "Quickest for engineers, but evaluators comparing open banking providers would still find no solution layer.",
   "recommended": false
  }
 ],
 "tree": [
  {
   "label": "Developers",
   "depth": 0,
   "layer": "top",
   "audience": "both",
   "gated": false,
   "kind": "home",
   "note": "The landing: what the APIs do and a call to read the docs, no sign-in."
  },
  {
   "label": "Products",
   "depth": 0,
   "layer": "top",
   "audience": "both",
   "gated": false,
   "kind": "catalog",
   "note": "Product summaries an evaluator can scan before any code."
  },
  {
   "label": "Accounts API",
   "depth": 1,
   "layer": "top",
   "audience": "both",
   "gated": false,
   "kind": "catalog",
   "note": "Product summary linking down to its reference."
  },
  {
   "label": "Payments API",
   "depth": 1,
   "layer": "top",
   "audience": "both",
   "gated": false,
   "kind": "catalog",
   "note": "Product summary linking down to its reference."
  },
  {
   "label": "Consents API",
   "depth": 1,
   "layer": "top",
   "audience": "both",
   "gated": false,
   "kind": "catalog",
   "note": "Product summary linking down to its reference."
  },
  {
   "label": "Pricing",
   "depth": 0,
   "layer": "top",
   "audience": "evaluator",
   "gated": false,
   "kind": "pricing",
   "note": "Moved out from behind the login wall: decision information is sales enablement."
  }
 ],
 "placement": [
  {
   "kind": "quickstart",
   "where": "Getting started",
   "entry": "Linked from the landing's main call to action and the top of Docs."
  },
  {
   "kind": "reference",
   "where": "API reference",
   "entry": "Under Docs and from each product summary."
  },
  {
   "kind": "guides",
   "where": "Docs",
   "entry": "In the top navigation, open to everyone."
  },
  {
   "kind": "keys",
   "where": "API keys",
   "entry": "First item in the console after sign-in."
  },
  {
   "kind": "sandbox",
   "where": "Sandbox",
   "entry": "In the console and from every try-it console, which targets the sandbox by default."
  },
  {
   "kind": "logs",
   "where": "Request logs",
   "entry": "In the console, linked from each failed try-it call."
  }
 ],
 "caveats": [
  "Confirm that pricing can be published openly before moving it out from behind the login wall.",
  "Confirm which sandbox data a key created at sign-in may reach before keys stop needing a ticket."
 ]
}

Cut from this display only (the saved reply and the check use the full text): tree (25 items, 6 shown); placement (14 items, 6 shown).

What step 5 prints for it: Developers (top) | Products (top) | Accounts API (top) | Payments API (top) | Consents API (top) | Pricing (top) | Use cases (solution) | Open banking for lenders (solution) | Docs (bottom) | Getting started (bottom) | Authentication (bottom) | API reference (bottom) | Error codes (bottom) | Postman collection (bottom) | Search (bottom) | Ask AI (bottom) | Changelog (bottom) | Status (bottom) | Support (bottom) | Console (bottom) | API keys (bottom) | Sandbox (bottom) | Request logs (bottom) | Usage (bottom) | Billing and plan (bottom)

The page's check: kept - nothing to flag

path - A banking API with pricing behind the login wall

The input carries the saved redesign (R.laneForm fills carry from the ia reply), as the page's handoff does. Target 60 minutes; the path today 4376 minutes; open rungs in order: autokey, ungated, collection, personalized; build: managed.

Input (facts is 5060 characters; the first 1398 are shown):

{
 "task": "path",
 "portal": "Portal: Corvantle Open Banking (an existing portal)\nAPI complexity class: Complex - OAuth plus webhooks; measured median TTFC today: 2880 minutes\nSelf-service keys: via a ticket or sales; audience: both engineers and evaluators; who: public developers; partners use a different auth model: no\nPlatform: an API gateway with a bundled portal: yes; a CMS its authors love: no; business-side authors need a real CMS: no; compliance or data residency no managed platform meets: no; a multi-gateway portfolio: no\nA named portal owner: yes; several roles or products one quickstart cannot serve: no; where the funnel stalls: signup to first call\nHard deadline: no; goal: a compounding asset; effort ceiling: medium",
 "tree": "Navigation tree (#id | page, indented two spaces per level | kind | layer | audience | access):\n#1 | Developers | home | top | both | open\n#2 | Products | catalog | top | both | open\n#3 |   Accounts API | catalog | top | both | open\n#4 |   Payments API | catalog | top | both | open\n#5 |   Consents API | catalog | top | both | open\n#6 | Pricing | pricing | top | evaluator | login\n#7 | Use cases | usecases | solution | evaluator | login\n#8 |   Open banking for lenders | usecases | solution | evaluator | login (inherited)\n#9 | Documentation | guides | bottom | engineer | login\n#10 |   Getting started | quickstart | bottom | engineer | login (inherited)\n#11 |   Authentication | guides | bottom | engineer | login (inherited)\n#12 |   API reference | reference | bottom | engineer | login (inherited)\n#13 |     Accounts | reference | bottom | engineer | login (inherited)\n#14 |     Payments | reference | bottom | engineer | login (inherited)\n#15 | My account | other | top | both | login\n#16 |   Applications | keys | bottom | engineer | login (inherited), ticket\n#17 |   Billing & plan | billing | bottom | both | login (inherited)\n#18 | Support | support | bottom | both | open",
 "steps": "Onboarding steps, landing to first call (#n | step | minutes | stage | friction):\n#1 | Land on the developer portal | 1 min | other\n#2 | Register an account | 5 min | sign up\n#3 | Verify your email address | 10 min | verification | friction: email or phone verification\n#4 | Wait for manual approval of the developer account | 2880 min | other | friction: manual approval\n#5 | Open a support ticket to request sandbox API keys | 1440 min | credential | friction: a support ticket\n#6 | Copy the client secret from the email into your config | 5 min | credential | friction: copying a key by hand\n#7 | Read the getting started guide | 15 min | quickstart / docs\n#8 | Install the SDK | 10 min | install | friction: an SDK install\n#9 | Make the first API call | 10 min | first call",
 "carry": "Proposed navigation (layered):\n- Developers (home, top, both)\n- Products (catalog, top, both)\n  - Accounts API (catalog, top, both)\n  - Payments API (catalog, top, both)\n  - Consents API (catalog, top, both)\n- Pricing (pricing, top, evaluator)\n- Use cases (usecases, solution, evaluator)\n  - Open banking for lenders (usecases, solution, evaluator)\n- Docs (guides, bottom, engineer)\n  - Getting started (quickstart, bottom, engineer)\n  - Authentication (guides, bottom, engineer)\n  - API reference (reference, bottom, engineer)\n  - Error codes (errors, bottom, engineer)\n  - Postman collection (samples, bottom, engineer)\n- Search (search, bottom, both)\n- Ask AI (assistant, bottom, both)\n- Changelog (changelog, bottom, both)\n- Status (status, bottom, both)\n- Support (support, bottom, both)\n- Console (other, bottom, engineer, behind login)\n  - API keys (keys, bottom, engineer, behind login)\n  - Sandbox (sandbox, bottom, engineer, behind login)\n  - Request logs (logs, bottom, engineer, behind login)\n  - Usage (usage, bottom, engineer, behind login)\n  - Billing and plan (billing, bottom, both, behind login)",
 "facts": "The page's checks (8 fail, 14 warn, 5 pass, 1 n/a); verdict: decision information behind the login wall:\n- Discovery | reference | pass | at 'API reference', 'Accounts', 'Payments'\n- Discovery | ungated_docs | fail | every docs page is behind sign-in - mandatory signup before any exploration\n- Discovery | layering | pass | top layer: 'Developers', 'Products', 'Pricing', 'Use cases'\n- Discovery | search | warn | no page carries it - classic full-text or semantic search over the whole portal\n- Discovery | assistant | warn | no page carries it - an expected component now - answers grounded in the docs, citing a linkable source, saying 'I don't know' when the docs don't cover it\n- Discovery | split | na | public developers only\n- Decision-making | pricing | fail | 'Pricing' is behind login - decision information behind a login wall is a sales-enablement failure, not a content failure\n- Decision-making | solution | fail | 'Use cases', 'Open banking for lenders' are behind login - a sales-enablement failure\n- Onboarding | self_keys | fail | keys come through 'Applications' [ticket] - key lifecycle actions that need a support ticket fail the self-service line\n- Onboarding | quickstart | warn | 'Getting started' is behind login\n- Onboarding | sandbox | warn | no sandbox or test-mode entry - sandbox-versus-production confusion is a common friction point\n- Onboarding | samples | warn |\n\n[... 3662 more characters cut from this display ...]"
}

Reply (shortened for display - see the cuts below):

{
 "task": "path",
 "summary": "Developers sign up before reading anything, then wait days for an approval and a ticket for keys; the plan opens the docs, issues a sandbox key at sign-up and brings the path inside the hour.",
 "ttfc": {
  "target_minutes": 60,
  "rationale": "An open banking API with OAuth and consent flows is a complex class, so under an hour is the anchor; the 5-minute figure is context, not a bar."
 },
 "steps": [
  {
   "n": 1,
   "step": "Read Getting started and the Authentication guide on the open Docs.",
   "minutes": 10,
   "removes": "mandatory signup before any exploration"
  },
  {
   "n": 2,
   "step": "Sign up for an account from the Getting started page.",
   "minutes": 3,
   "removes": "the wait for manual approval"
  },
  {
   "n": 3,
   "step": "Confirm the email address from the link, which returns to the quickstart.",
   "minutes": 3,
   "removes": ""
  },
  {
   "n": 4,
   "step": "Find a sandbox key already created in API keys and injected into the quickstart snippets.",
   "minutes": 2,
   "removes": "the support ticket and copying a secret by hand"
  },
  {
   "n": 5,
   "step": "Import the Postman collection with the sandbox key filled in.",
   "minutes": 5,
   "removes": "the SDK install before a first call"
  },
  {
   "n": 6,
   "step": "Work through the OAuth consent flow in the sandbox from the Authentication guide.",
   "minutes": 20,
   "removes": ""
  },
  {
   "n": 7,
   "step": "Make the first API call to the Accounts API in the sandbox and get a 200 back.",
   "minutes": 5,
   "removes": ""
  }
 ],
 "fixes": [
  {
   "rung": "autokey",
   "action": "Create a sandbox key at sign-up and inject it into every quickstart snippet and try-it call, retiring the key ticket.",
   "why": "The funnel stalls between signup and first call, so its fix goes first."
  },
  {
   "rung": "ungated",
   "action": "Open Pricing, Use cases and the Docs to everyone and move sign-up after the first docs step.",
   "why": "Every docs page is behind sign-in today, and that is mostly a policy flip."
  },
  {
   "rung": "collection",
   "action": "Publish a ready-to-run collection of sandbox calls for the Accounts, Payments and Consents APIs.",
   "why": "No samples exist today and the path makes developers install the SDK first."
  },
  {
   "rung": "personalized",
   "action": "Later, add a short intake that picks the quickstart by use case.",
   "why": "Highest value and highest effort, so it comes last here."
  }
 ],
 "roadmap": [
  {
   "priority": 1,
   "area": "decision",
   "item": "Publish pricing before login.",
   "check": "pricing"
  },
  {
   "priority": 1,
   "area": "decision",
   "item": "Open the use-case pages before login.",
   "check": "solution"
  },
  {
   "priority": 1,
   "area": "discovery",
   "item": "Open every docs page to anonymous readers.",
   "check": "ungated_docs"
  },
  {
   "priority": 1,
   "area": "onboarding",
   "item": "Make key creation, rotation and revocation self-service in API keys.",
   "check": "self_keys"
  },
  {
   "priority": 1,
   "area": "onboarding",
   "item": "Move sign-up after the first docs step.",
   "check": "signup_first"
  },
  {
   "priority": 1,
   "area": "onboarding",
   "item": "Remove the manual approval and the key ticket from the path.",
   "check": "friction"
  }
 ],
 "build": {
  "choice": "managed",
  "why": "The organization already runs a gateway with a bundled portal, which comes nearly free."
 },
 "caveats": [
  "Measure the consent step in the sandbox before promising the 20 minutes.",
  "Confirm the email confirmation can return to the quickstart."
 ]
}

Cut from this display only (the saved reply and the check use the full text): roadmap (14 items, 6 shown).

What step 5 prints for it: 1 10 min | 2 3 min | 3 3 min | 4 2 min | 5 5 min | 6 20 min | 7 5 min

The page's check: kept - nothing to flag

The page read it as: a portal with Priority 1 gaps; maturity project; leaning split; Priority 1: solution, friction, ttfc_path, owner.

Input (facts is 5465 characters; the first 1395 are shown):

{
 "task": "ia",
 "portal": "Portal: Brevoxa Logistics (an existing portal)\nAPI complexity class: Moderate - several products or SDK setup; measured median TTFC today: not given\nSelf-service keys: yes; audience: both engineers and evaluators; who: public developers and partners; partners use a different auth model: yes\nPlatform: an API gateway with a bundled portal: no; a CMS its authors love: yes; business-side authors need a real CMS: no; compliance or data residency no managed platform meets: no; a multi-gateway portfolio: no\nA named portal owner: no; several roles or products one quickstart cannot serve: yes; where the funnel stalls: unknown\nHard deadline: yes; goal: a compounding asset; effort ceiling: medium",
 "tree": "Navigation tree (#id | page, indented two spaces per level | kind | layer | audience | access):\n#1 | Developer portal | home | top | both | open\n#2 | Partner program | other | top | both | partner\n#3 |   Partner onboarding | other | top | both | partner\n#4 |   Partner API reference | reference | bottom | engineer | partner\n#5 | Public APIs | catalog | top | both | open\n#6 |   Tracking API | catalog | top | both | open\n#7 |   Rates API | catalog | top | both | open\n#8 | Docs | guides | bottom | engineer | open\n#9 |   Getting started | quickstart | bottom | engineer | open\n#10 |   API reference | reference | bottom | engineer | open\n#11 |   Guides | guides | bottom | engineer | open\n#12 | Pricing | pricing | top | evaluator | open\n#13 | Dashboard | other | top | both | login\n#14 |   API keys | keys | bottom | engineer | login (inherited)\n#15 |   Request logs | logs | bottom | engineer | login (inherited)\n#16 | Status | status | bottom | both | open",
 "steps": "Onboarding steps, landing to first call (#n | step | minutes | stage | friction):\n#1 | Read the getting started guide | 5 min | quickstart / docs\n#2 | Sign up for a public account | 3 min | sign up\n#3 | Apply to the partner program and wait for approval | 1440 min | other | friction: manual approval\n#4 | Create an API key in the dashboard | 2 min | credential\n#5 | Install the SDK for your language | 10 min | install | friction: an SDK install\n#6 | Make the first request to the Tracking API | 5 min | first call",
 "carry": "",
 "facts": "The page's checks (4 fail, 15 warn, 8 pass, 1 n/a); verdict: a portal with Priority 1 gaps:\n- Discovery | reference | pass | at 'Partner API reference', 'API reference'\n- Discovery | ungated_docs | warn | behind sign-in: 'Partner API reference'\n- Discovery | layering | pass | top layer: 'Developer portal', 'Public APIs', 'Pricing'\n- Discovery | search | warn | no page carries it - classic full-text or semantic search over the whole portal\n- Discovery | assistant | warn | no page carries it - an expected component now - answers grounded in the docs, citing a linkable source, saying 'I don't know' when the docs don't cover it\n- Discovery | split | warn | partners use a different auth model - consider separate public and partner portals off one API backend; one shape forced onto both is a failure mode\n- Decision-making | pricing | pass | at 'Pricing', open\n- Decision-making | solution | fail | no use-case or solutions page - endpoints exposed with no page connecting them to business problems\n- Onboarding | self_keys | pass | the key dashboard at 'API keys'\n- Onboarding | quickstart | pass | at 'Getting started'\n- Onboarding | sandbox | warn | no sandbox or test-mode entry - sandbox-versus-production confusion is a common friction point\n- Onboarding | samples | warn | no page carries it - an importable, forkable set of working example calls\n- Onboarding | sdks | warn | no page\n\n[... 4070 more characters cut from this display ...]"
}

Reply (shortened for display - see the cuts below):

{
 "task": "ia",
 "summary": "Public developers and partners share one shape despite different auth models, and nothing connects the APIs to business problems; the redesign splits the partner portal off one backend and adds a solution layer.",
 "options": [
  {
   "id": "split",
   "name": "Split public and partner portals",
   "tradeoff": "Each audience gets its own domain and auth model off one API backend, at the cost of two navigations to keep in step.",
   "recommended": true
  },
  {
   "id": "layered",
   "name": "A single portal layered by depth",
   "tradeoff": "One navigation to maintain, but it forces one shape onto two incompatible auth models.",
   "recommended": false
  }
 ],
 "tree": [
  {
   "label": "Developer portal",
   "depth": 0,
   "layer": "top",
   "audience": "both",
   "gated": false,
   "kind": "home",
   "note": "The public portal's landing, with a link across to the partner portal."
  },
  {
   "label": "Public APIs",
   "depth": 0,
   "layer": "top",
   "audience": "both",
   "gated": false,
   "kind": "catalog",
   "note": "Product summaries for public developers."
  },
  {
   "label": "Tracking API",
   "depth": 1,
   "layer": "top",
   "audience": "both",
   "gated": false,
   "kind": "catalog",
   "note": ""
  },
  {
   "label": "Rates API",
   "depth": 1,
   "layer": "top",
   "audience": "both",
   "gated": false,
   "kind": "catalog",
   "note": ""
  },
  {
   "label": "Pricing",
   "depth": 0,
   "layer": "top",
   "audience": "evaluator",
   "gated": false,
   "kind": "pricing",
   "note": "Open before login, as today."
  },
  {
   "label": "Use cases",
   "depth": 0,
   "layer": "solution",
   "audience": "evaluator",
   "gated": false,
   "kind": "usecases",
   "note": "New: the missing solution layer."
  }
 ],
 "placement": [
  {
   "kind": "quickstart",
   "where": "Getting started",
   "entry": "From the landing's call to action."
  },
  {
   "kind": "reference",
   "where": "API reference",
   "entry": "Under Docs and from each product summary."
  },
  {
   "kind": "guides",
   "where": "Guides",
   "entry": "Under Docs."
  },
  {
   "kind": "keys",
   "where": "API keys",
   "entry": "First item in the dashboard."
  },
  {
   "kind": "sandbox",
   "where": "Sandbox",
   "entry": "In the dashboard and from every try-it console."
  },
  {
   "kind": "logs",
   "where": "Request logs",
   "entry": "In the dashboard."
  }
 ],
 "caveats": [
  "Name a portal owner before building two portals.",
  "Confirm which retail use case the solution layer should lead with."
 ]
}

Cut from this display only (the saved reply and the check use the full text): tree (27 items, 6 shown); placement (15 items, 6 shown).

What step 5 prints for it: Developer portal (top) | Public APIs (top) | Tracking API (top) | Rates API (top) | Pricing (top) | Use cases (solution) | Shipment tracking for retailers (solution) | Docs (bottom) | Getting started (bottom) | Guides (bottom) | API reference (bottom) | Error codes (bottom) | Search (bottom) | Ask AI (bottom) | Changelog (bottom) | Status (bottom) | Support (bottom) | Dashboard (bottom) | API keys (bottom) | Sandbox (bottom) | Request logs (bottom) | Usage (bottom) | Billing and plan (bottom) | Team and roles (bottom) | Partner portal (top) | Partner onboarding (bottom) | Partner API reference (bottom)

The page's check: kept - nothing to flag

Build the input yourself

Load the engine in node (global.window = global; require("./ppkit.js"); require("./ppreply.js")), then:

const K = PortalKit, R = PortalReply;
const f = K.form(Object.assign(K.blank(), { name: "Acme API", cls: "simple",
  tree: "Pricing [login]\nDocs\n  Getting started\n  API reference\nDashboard [login]\n  API keys [ticket]",
  steps: "Sign up | 3\nVerify your email | 5\nRead the quickstart | 5\nMake the first API call | 2" }));
// f.nodes (kind, layer, audience, access), f.rows (the checks), f.target, f.rungs, f.build, f.maturity, f.roadmap
// K.treeMd(f), K.checksCsv(f), K.stepsCsv(f) - the checked tree and the exports
const b = K.buildInput(f, "ia");                            // or "path" (R.laneForm(form, "path", iaReply) fills carry)
const wire = K.toWire(b.input);                              // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "ia");
// R.check(reply, b)                                         // {status: kept or flagged, notes, fixed}

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="YOUR_TOKEN"   # from https://portal-path.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
  curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
    ${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}

Step 2 — a token

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

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

Step 3 — GET /me

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

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

Step 4 — POST /estimate

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

# input.json: the run input object - {"task": "ia", "portal": "Portal: Corvantle Open Banking (an existing portal)
API complexity class: ...", "tree": "Navigation tree (#id | page | kind | layer | audience | access):
#1 | Developers | home | top | both | open
...", "steps": "Onboarding steps, landing to first call (#n | step | minutes | stage | friction):
#1 | ...", "carry": "", "facts": "The page's checks (...); verdict: decision information behind the login wall:
- Decision-making | pricing | fail | ..."}
# - see the input table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses portal-path:<task>:<inputHash>:a<attempt> - the lane is part of the key, retry_note is left out of the hash - and its one automatic reformat retry is a new attempt number, named in the page as one extra run. An ia reply carries summary, options, tree, placement and caveats; a path reply carries summary, ttfc, steps, fixes, roadmap, build and caveats. The code below prints the gist of either: each page of the proposed tree, or each step of the proposed path.

KEY="portal-path:$(jq -r .task input.json):$(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 'if .task == "path" then (.steps[] | "\(.n). \(.step) (\(.minutes) min)") 
       else (.tree[] | "\(.label) [\(.layer), \(.kind)]") end' reply.json

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: portal-path:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json