Run Partner Onramp from your own code
Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page ranks the four onboarding rungs, audits the named journey for the skill's failure modes and reads the four funnel checkpoints against its thresholds, then hands it all to the run as facts. The run writes the onboarding design or the fixes; the page then holds it to its own facts (poreply.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":"partner-onramp"}), so no slug header is needed afterwards. Send it as Authorization: Bearer YOUR_TOKEN. 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, program, journey, facts) is missing. The page always sends all five of task, program, journey, carry, facts; carry is an empty string for design. |
| 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: both lanes are metered and need 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 comes first
Two lanes, chosen by task, in the order the work runs - the journey partners walk, then where they stall today:
design- the onboarding design: named stages with gates, owners and metrics, the four rungs and their promotions, when commercial decisions land, the four funnel checkpoints and the disclosure line.fixes- the fixes: every issue the page found, in its order, each with the specific change, an owner and a metric.
A missing or unknown task is answered as design. Both lanes send the same five 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 design's decisions into carry for the fixes lane.
The input
| Field | Required | What |
|---|---|---|
task | yes | design or fixes. If it is missing or neither, do design and set "task": "design". |
program | yes | the platform, what partners build, the program today, time to a first app, data sensitivity, partners, the interview's yes/no answers, support capacity, deadline, mandate and effort; "not given" means not given. |
journey | yes | the stages in order with their gate, owner and metric, the post-launch thresholds and whether each is disclosed, the funnel this quarter, and the page's issues in fix order. |
carry | send it (empty for design) | fixes task only: the design's decisions, when the design task ran first; may be empty. |
facts | yes | the page's checks, one line each as area | id | status | evidence, the verdict and the ranked rungs. A line starting "must:" applies to your reply. |
retry_note | no | optional; if present, your previous reply was unusable - follow it. |
The rules the model applies
From the source skill, @samber/partner-app-onboarding - the only standard the run applies:
- Name the journey's stages before designing any one of them - an unnamed stage never gets an owner or a metric. Decouple "start building" from "get approved": accounts, sandboxes and credentials provision instantly and free; human review concentrates at the app-submission gate.
- Entry gate: self-service signup is the default - a form and a program-agreement checkbox, no human in the loop. Agreement-split entry when commercial sellers transact through the platform: building stays ungated, a verified seller track (agreement, KYC/KYB) gates selling. Application-reviewed registration is the starved option, promoted only when a first working app takes weeks to months or the data is sensitive enough that you must know who builds. Don't invent a graded entry ladder - entry tiers gate nothing at most programs.
- Provisioning at signup, never after review: instant dedicated dev tenancy by default (free, no time limit, no payment info); the partner's own account as the sandbox when actions are safe and reversible - deleted, not demoted, the moment apps touch money, irreversible actions or other tenants' data; production-mirror sandboxes, rationed, only when apps cannot be validated without production-realistic behavior.
- Education: an optional parallel track by default - courses and docs from day one, required by nothing; a credentialed education loop when partner credibility is a buyer-facing signal, still non-gating; mandatory pre-submission certification only when first reviews still fail above roughly 30-40% (self-set) with a validator in place and the failures trace to knowledge gaps. Disclose every post-launch threshold - install counts, tenure - inside the onboarding docs, never at the moment the partner hits it.
- Support rungs stack, never replace: docs plus a community forum; real-time community chat once repeated questions cluster; office hours when a recurring hard step needs a platform engineer; named partner managers for commercial ISVs only, never for every individual developer.
- Commercial decisions late: the monetization model is the last gate before listing, never at registration; the seller or distribution agreement at first listing, revenue reporting at first sale.
- Funnel, instrumented from day one at four checkpoints: signup to provisioned, provisioned to first API call, first API call to first app submitted, submission to approval. The headline metric, time-to-first-submitted-app, is self-defined - flag it so. Over ten minutes to a first call: fix onboarding friction before anything else. First-review fail rate above roughly 30-40%: build a pre-submission validator before any mandatory education. Time to first submission far exceeding review time: the bottleneck is docs and enablement, not the review team. Publish the review checklist inside the onboarding docs.
House rules
- Use only the input. Never invent a partner, a person, a figure, a date or a vendor fact.
- The page's checks, rungs and issue order are settled: never move a rung, reorder the issues or call a failed gate fine.
- No URL at all.
- Write in the language of the input; keep the JSON keys, rung ids, checkpoint ids, issue ids and stage names as given.
- Never use backticks, never write the input's field names as labels in your text, and never copy this prompt's wording into a reply.
- Reply with ONLY one JSON object - no prose before or after, no code fences.
The replies
design
Write the onboarding design - the named journey, the four rungs, when commercial decisions land and what the funnel measures. A target or a threshold the input does not give is a proposal: start it with "Proposed: ".
summary- one sentence, at most 40 words: the entry gate, when sandboxes arrive, and the first thing to change.stages- at least four objects in journey order:stage(a name; keep the program's stage names where they work),what(one sentence),gate(none,agreement,review,certification,pricing,paymentorkyc),owner(the person the input names, else a role the input mentions, else "unassigned - needs an owner"),metric. Credentials are provisioned in a stage before any app review; certification never gates submission; pricing lands at launch.rungs-entry(self,splitorreviewed),provisioning(dedicated,ownormirror),education(optional,credentialedormandatory) - each exactly as the facts rank it - andsupport(the stacked ladder offorum,chat,office,managers, exactly as the facts stack it).promotions- objects:rung(a rung id),condition(one sentence: what would promote or delete it).commercial-monetization(the stage where the monetization model is asked),agreements(one sentence: when the seller agreement and revenue reporting land).funnel-checkpoints(four objects in order:checkpoint-signup-provisioned,provisioned-first-call,first-call-submitted,submitted-approved- andtarget, one short string),headline(one sentence naming time-to-first-submitted-app as self-defined).disclosure- one sentence: every post-launch threshold disclosed in the onboarding docs, naming the ones the input lists.caveats- at most 4 short strings;[]when there is nothing to say.
{
"task": "design",
"summary": "one sentence",
"stages": [ { "stage": "Sign up", "what": "one sentence", "gate": "agreement", "owner": "a role", "metric": "a short string" } ],
"rungs": { "entry": "self", "provisioning": "dedicated", "education": "optional", "support": [ "forum" ] },
"promotions": [ { "rung": "chat", "condition": "one sentence" } ],
"commercial": { "monetization": "Launch", "agreements": "one sentence" },
"funnel": { "checkpoints": [ { "checkpoint": "signup-provisioned", "target": "a short string" } ], "headline": "one sentence" },
"disclosure": "one sentence",
"caveats": [ ]
}
The page holds it to its own facts (check() in poreply.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; the page's four rungs and its support ladder; at least four stages, each with a gate, an owner and a metric; credentials before any app review; no pricing gate in the first two stages; no certification gating submission; at least two promotion conditions; monetization at launch and when agreements land; the four checkpoints in order with targets; a self-defined headline metric; thresholds disclosed in the onboarding docs.
fixes
Write the fixes for the issues the page found, in its order - the biggest leak first.
summary- one sentence, at most 40 words.fixes- one object per issue, in the facts' order:issue(friction,sandbox,validator,checklist,pricing,certification,walls,owners,instrumentorenablement, exactly as the facts list it),fix(one or two sentences: the specific change - friction speeds up the first API call, sandbox moves credentials to signup, validator builds the pre-submission validator, checklist publishes the review checklist, pricing moves monetization to launch, certification becomes non-gating with quality enforced at review, walls are disclosed in the onboarding docs, owners names an owner and a metric per stage, instrument counts the four checkpoints, enablement fixes docs and enablement rather than the review team),owner(the person the input names, else a role the input mentions, else "unassigned - needs an owner"),metric(the number that shows it worked).caveats- at most 4 short strings;[]when there is nothing to say.
{
"task": "fixes",
"summary": "one sentence",
"fixes": [ { "issue": "friction", "fix": "one or two sentences", "owner": "a role", "metric": "a short string" } ],
"caveats": [ ]
}
The page requires: the reply answers the task it was sent; the same text rules; exactly the page's issues in its order; a fix, an owner and a metric each; and the specific change each issue needs - the first call, the sandbox at signup, the validator, the checklist, pricing at launch, non-gating certification, disclosure, owners, the four checkpoints, docs and enablement.
Worked examples
Three runs from the page's free samples (invented programs): the design for a CRM marketplace where partners stall, the fixes for the same program carrying that design, and the design for a banking-data API with a reviewed entry. Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped unedited - but the facts and long strings 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.
design - A CRM marketplace where partners stall
The page read it as: partners stall before they ship; rungs split, dedicated, optional, forum+chat+managers; the biggest leak first-call-submitted; 1 of 5 gates; failing checks: decouple, commercial, disclosed, named, certification, friction, validator.
Input (journey is 2212 characters; the first 1393 are shown; facts is 3115 characters; the first 1388 are shown):
{
"task": "design",
"program": "Platform: FieldCRM\nWhat partners build and the program today: A field-sales CRM with a marketplace; ISVs and agencies build apps, and partners sign up but few ever submit.\nBuilds: Marketplace apps; existing: Signup exists, the journey after it is ad-hoc; time to a first working app: Days; data sensitivity: Medium; partners: A mix\nApps touch money, irreversible actions or other tenants' data: no; commercial sellers transact through us: yes; credibility is a buyer-facing signal: no; apps need production-realistic validation: no\nA pre-submission validator: no; the review checklist is published in onboarding: no; repeated questions cluster: yes; a recurring hard step needs a platform engineer: no; support capacity: One partner manager, two docs writers, a forum moderated weekly\nDeadline: no; a compounding ecosystem; effort ceiling: medium; as-of: 2026-11-01",
"journey": "Journey stages in order (stage | what happens | gate | owner | metric):\n- Register | partner form, choose Free or Paid pricing for the future app | a pricing or monetization choice | Nadia Pellworth | signups\n- Application | partnerships team reviews the company | a human review | Nadia Pellworth | approval time\n- Build | dev sandbox and API credentials issued | no gate | NO OWNER | NO METRIC\n- Learn | FieldCRM Builder course | a certification | Tobin Arquell | completions\n- Submit | app submitted for marketplace review | a human review | Tobin Arquell | first-review pass rate\nPost-launch thresholds (threshold | disclosed in onboarding):\n- Marketplace review starts at 5 active installs | NO\n- Featured placement needs 6 months listed | NO\n- Partner tier at 50 installs | yes\nFunnel this quarter: signup to sandbox and credentials 45% (180 of 400); provisioned to first API call 50% (90 of 180); first API call to first app submitted 33.3% (30 of 90); submission to approval 40% (12 of 30); minutes to first call 25; first-review fail rate 45%; days to first submission 60; review days 6\nIssues in fix order: friction - 25 minutes to a first API call - over ten, the skill fixes onboarding friction before anything else (developer-tool funnels lose most partners before the first call); sandbox - credentials arrive at 'Build', after the review at 'Application' - partners approved to build cannot test;\n\n[... 819 more characters cut from this display ...]",
"carry": "",
"facts": "The page's checks (7 fail, 2 warn, 1 pass, 4 n/a); verdict: partners stall before they ship:\n- The four rung menus | entry | na | Agreement-split entry - commercial sellers transact through the platform - building stays ungated, a verified seller track gates selling\n- The four rung menus | provisioning | na | Instant dedicated dev tenancy - the default: a free dev tenancy at signup, no time limit, no payment info\n- The four rung menus | education | na | Optional parallel track - the default: courses and docs from day one, required by nothing\n- The four rung menus | support | na | Docs plus a community forum + Real-time community chat + Named partner managers (commercial partners only) - repeated questions cluster - real-time chat with periodic townhalls; commercial ISVs - named partner managers for that segment only\n- The journey | decouple | fail | credentials arrive at 'Build', after the review at 'Application' - partners approved to build cannot test\n- The journey | commercial | fail | pricing is asked at 'Register', before the app exists - it belongs at launch, the last gate before listing\n- The journey | disclosed | fail | Marketplace review starts at 5 active installs; Featured placement needs 6 months listed - not in the onboarding docs; partners meet it after they think they are done\n- The journey | named | fail | Build (no owner) (no metric)\n- The journey |\n\n[... 1727 more characters cut from this display ...]"
}
Reply:
{
"task": "design",
"summary": "Split entry keeps building ungated behind a form and program agreement, a dedicated dev tenancy and credentials arrive at signup, and the first change is cutting the 25 minutes to a first API call.",
"stages": [
{
"stage": "Register",
"what": "Partners fill in a form and accept the program agreement; nothing about money is asked and no human reviews them.",
"gate": "agreement",
"owner": "Nadia Pellworth",
"metric": "signups"
},
{
"stage": "Build",
"what": "A free dedicated dev tenancy and API credentials are issued instantly at signup, with no time limit and no payment info.",
"gate": "none",
"owner": "Developer relations lead",
"metric": "minutes to first API call"
},
{
"stage": "Learn",
"what": "The FieldCRM Builder course runs as an optional parallel track from day one and gates nothing.",
"gate": "none",
"owner": "Tobin Arquell",
"metric": "completions"
},
{
"stage": "Submit",
"what": "The app is submitted for marketplace review against the review checklist published in the onboarding docs.",
"gate": "review",
"owner": "Tobin Arquell",
"metric": "first-review pass rate"
},
{
"stage": "Seller track",
"what": "Partners who will sell complete the verified seller track, with agreement and KYC/KYB, before they transact.",
"gate": "kyc",
"owner": "Nadia Pellworth",
"metric": "verified sellers"
},
{
"stage": "Launch",
"what": "The partner chooses Free or Paid pricing as the last gate before the listing goes live.",
"gate": "pricing",
"owner": "Nadia Pellworth",
"metric": "apps listed"
}
],
"rungs": {
"entry": "split",
"provisioning": "dedicated",
"education": "optional",
"support": [
"forum",
"chat",
"managers"
]
},
"promotions": [
{
"rung": "chat",
"condition": "Real-time community chat stays because repeated questions cluster, and would drop back to the forum alone if they stop clustering."
},
{
"rung": "managers",
"condition": "Named partner managers serve commercial ISVs only and would never extend to every individual developer or agency."
},
{
"rung": "education",
"condition": "Mandatory certification would be considered only if first reviews still fail above roughly 30-40% after the pre-submission validator is in place and the failures trace to knowledge gaps."
}
],
"commercial": {
"monetization": "Launch",
"agreements": "The seller agreement lands at first listing through the verified seller track, and revenue reporting starts at the first sale."
},
"funnel": {
"checkpoints": [
{
"checkpoint": "signup-provisioned",
"target": "Proposed: near 100%, provisioned instantly at signup"
},
{
"checkpoint": "provisioned-first-call",
"target": "Proposed: first call under 10 minutes"
},
{
"checkpoint": "first-call-submitted",
"target": "Proposed: above 33.3% this quarter"
},
{
"checkpoint": "submitted-approved",
"target": "Proposed: first-review fail rate under 30%"
}
],
"headline": "Time-to-first-submitted-app, now 60 days against 6 days of review, is the headline metric and is self-defined by FieldCRM."
},
"disclosure": "The onboarding docs disclose every post-launch threshold up front: marketplace review starts at 5 active installs, featured placement needs 6 months listed, and partner tier comes at 50 installs.",
"caveats": [
"The Application review is removed so credentials no longer wait on it.",
"A pre-submission validator comes before any mandatory education, given the 45% first-review fail rate.",
"The Build owner is a proposed role; no person is named for it."
]
}
What step 5 prints for it: Register agreement | Build none | Learn none | Submit review | Seller track kyc | Launch pricing
The page's check: kept - nothing to flag
fixes - A CRM marketplace where partners stall
The input carries the saved design's decisions (R.laneForm fills carry from the design reply), as the page's handoff does. The issues in fix order: friction, sandbox, validator, checklist, pricing, certification, walls, owners, enablement.
Input (journey is 2212 characters; the first 1393 are shown; facts is 2909 characters; the first 1388 are shown):
{
"task": "fixes",
"program": "Platform: FieldCRM\nWhat partners build and the program today: A field-sales CRM with a marketplace; ISVs and agencies build apps, and partners sign up but few ever submit.\nBuilds: Marketplace apps; existing: Signup exists, the journey after it is ad-hoc; time to a first working app: Days; data sensitivity: Medium; partners: A mix\nApps touch money, irreversible actions or other tenants' data: no; commercial sellers transact through us: yes; credibility is a buyer-facing signal: no; apps need production-realistic validation: no\nA pre-submission validator: no; the review checklist is published in onboarding: no; repeated questions cluster: yes; a recurring hard step needs a platform engineer: no; support capacity: One partner manager, two docs writers, a forum moderated weekly\nDeadline: no; a compounding ecosystem; effort ceiling: medium; as-of: 2026-11-01",
"journey": "Journey stages in order (stage | what happens | gate | owner | metric):\n- Register | partner form, choose Free or Paid pricing for the future app | a pricing or monetization choice | Nadia Pellworth | signups\n- Application | partnerships team reviews the company | a human review | Nadia Pellworth | approval time\n- Build | dev sandbox and API credentials issued | no gate | NO OWNER | NO METRIC\n- Learn | FieldCRM Builder course | a certification | Tobin Arquell | completions\n- Submit | app submitted for marketplace review | a human review | Tobin Arquell | first-review pass rate\nPost-launch thresholds (threshold | disclosed in onboarding):\n- Marketplace review starts at 5 active installs | NO\n- Featured placement needs 6 months listed | NO\n- Partner tier at 50 installs | yes\nFunnel this quarter: signup to sandbox and credentials 45% (180 of 400); provisioned to first API call 50% (90 of 180); first API call to first app submitted 33.3% (30 of 90); submission to approval 40% (12 of 30); minutes to first call 25; first-review fail rate 45%; days to first submission 60; review days 6\nIssues in fix order: friction - 25 minutes to a first API call - over ten, the skill fixes onboarding friction before anything else (developer-tool funnels lose most partners before the first call); sandbox - credentials arrive at 'Build', after the review at 'Application' - partners approved to build cannot test;\n\n[... 819 more characters cut from this display ...]",
"carry": "Rungs: entry split, provisioning dedicated, education optional, support forum + chat + managers\nStages: Register (agreement, Nadia Pellworth); Build (none, Developer relations lead); Learn (none, Tobin Arquell); Submit (review, Tobin Arquell); Seller track (kyc, Nadia Pellworth); Launch (pricing, Nadia Pellworth)\nMonetization: Launch\nCheckpoints: signup-provisioned Proposed: near 100%, provisioned instantly at signup; provisioned-first-call Proposed: first call under 10 minutes; first-call-submitted Proposed: above 33.3% this quarter; submitted-approved Proposed: first-review fail rate under 30%",
"facts": "The page's checks (7 fail, 2 warn, 1 pass, 4 n/a); verdict: partners stall before they ship:\n- The four rung menus | entry | na | Agreement-split entry - commercial sellers transact through the platform - building stays ungated, a verified seller track gates selling\n- The four rung menus | provisioning | na | Instant dedicated dev tenancy - the default: a free dev tenancy at signup, no time limit, no payment info\n- The four rung menus | education | na | Optional parallel track - the default: courses and docs from day one, required by nothing\n- The four rung menus | support | na | Docs plus a community forum + Real-time community chat + Named partner managers (commercial partners only) - repeated questions cluster - real-time chat with periodic townhalls; commercial ISVs - named partner managers for that segment only\n- The journey | decouple | fail | credentials arrive at 'Build', after the review at 'Application' - partners approved to build cannot test\n- The journey | commercial | fail | pricing is asked at 'Register', before the app exists - it belongs at launch, the last gate before listing\n- The journey | disclosed | fail | Marketplace review starts at 5 active installs; Featured placement needs 6 months listed - not in the onboarding docs; partners meet it after they think they are done\n- The journey | named | fail | Build (no owner) (no metric)\n- The journey |\n\n[... 1521 more characters cut from this display ...]"
}
Reply:
{
"task": "fixes",
"summary": "Cut the 25-minute first call first, then issue sandbox credentials at signup, add a validator for the 45% first-review fail rate, publish the checklist, move pricing to Launch and disclose the install and tenure walls.",
"fixes": [
{
"issue": "friction",
"fix": "Strip every step between signup and a first API call - prefilled credentials, a copy-paste quickstart against the dev sandbox - so the call that takes 25 minutes today lands in under ten.",
"owner": "Developer relations lead",
"metric": "Minutes to first call from 25 to under 10 (Proposed: under 10); provisioned to first API call above 50%"
},
{
"issue": "sandbox",
"fix": "Issue the dev sandbox and API credentials at Register instead of at Build after the Application review, and keep the partnerships review only on the seller track that gates selling.",
"owner": "Nadia Pellworth",
"metric": "Signup to sandbox and credentials from 45% (180 of 400) to Proposed: near 100%, provisioned instantly at signup"
},
{
"issue": "validator",
"fix": "Build a pre-submission validator that runs the marketplace review's automatable checks before Submit, since 45% of first reviews fail and none exists today.",
"owner": "Tobin Arquell",
"metric": "First-review fail rate from 45% to Proposed: under 30%"
},
{
"issue": "checklist",
"fix": "Publish the marketplace review checklist inside the onboarding docs so partners meet the requirements before Submit, not at their first rejection.",
"owner": "Tobin Arquell",
"metric": "Submission to approval above 40% (12 of 30) (Proposed: rising each quarter)"
},
{
"issue": "pricing",
"fix": "Remove the Free or Paid choice from Register and ask for the monetization model at Launch, the last gate before listing.",
"owner": "Nadia Pellworth",
"metric": "Zero pricing questions before Launch; Proposed: signup completion tracked before and after"
},
{
"issue": "certification",
"fix": "Make the FieldCRM Builder course at Learn an optional parallel track that no longer gates submission, and enforce quality at the marketplace review with the validator.",
"owner": "Tobin Arquell",
"metric": "Share of submissions blocked by certification at 0%; first API call to first app submitted above 33.3% (Proposed)"
},
{
"issue": "walls",
"fix": "Disclose in the onboarding docs that marketplace review starts at 5 active installs and that featured placement needs 6 months listed, alongside the already-disclosed 50-install partner tier.",
"owner": "Nadia Pellworth",
"metric": "Post-launch thresholds disclosed in onboarding: 3 of 3, up from 1 of 3"
},
{
"issue": "owners",
"fix": "Give Build an owner and a metric: the developer relations lead owns it and measures time from provisioning to first API call.",
"owner": "Developer relations lead",
"metric": "Stages with an owner and a metric: 5 of 5, up from 4 of 5"
},
{
"issue": "enablement",
"fix": "Put the effort into docs, samples and the forum for the stretch from first call to submission rather than the review team, since 60 days to a first submission dwarfs 6 days of review.",
"owner": "Two docs writers",
"metric": "Days to first submission from 60 to Proposed: under 30"
}
],
"caveats": [
"Time-to-first-submitted-app is a self-defined headline metric.",
"Targets marked Proposed are not given by the program.",
"The one partner manager is for commercial ISVs only, not every developer."
]
}
What step 5 prints for it: friction Developer relations lead | sandbox Nadia Pellworth | validator Tobin Arquell | checklist Tobin Arquell | pricing Nadia Pellworth | certification Tobin Arquell | walls Nadia Pellworth | owners Developer relations lead | enablement Two docs writers
The page's check: kept - nothing to flag
design - A banking-data API with a reviewed entry
The page read it as: partners stall before they ship; rungs reviewed, mirror, credentialed, forum+chat+office+managers; the biggest leak submitted-approved; 4 of 5 gates; failing checks: instrumented.
Input (facts is 2516 characters; the first 1397 are shown):
{
"task": "design",
"program": "Platform: Corvalith\nWhat partners build and the program today: An open-banking data API; fintech ISVs build account-aggregation apps against customer financial data.\nBuilds: Both; existing: A full journey live, being revisited; time to a first working app: Weeks to months; data sensitivity: High; partners: Mostly commercial ISVs\nApps touch money, irreversible actions or other tenants' data: yes; commercial sellers transact through us: yes; credibility is a buyer-facing signal: yes; apps need production-realistic validation: yes\nA pre-submission validator: yes; the review checklist is published in onboarding: yes; repeated questions cluster: yes; a recurring hard step needs a platform engineer: yes; support capacity: Three partner engineers, a solutions architect per top-tier ISV\nDeadline: no; a compounding ecosystem; effort ceiling: high; as-of: 2026-11-01",
"journey": "Journey stages in order (stage | what happens | gate | owner | metric):\n- Apply | company application and KYB | a human review | Lena Vashcombe | days to admission\n- Plan | solution design session with a partner engineer | no gate | Lena Vashcombe | sessions booked\n- Build | production-mirror sandbox and test credentials | no gate | Omar Quisset | first sandbox call\n- Certify | security self-assessment and Corvalith certificate | no gate | Omar Quisset | certificates\n- Launch | app submitted for security review, then pricing set | a human review | Lena Vashcombe | first-review pass rate\nPost-launch thresholds (threshold | disclosed in onboarding):\n- Production keys after a passed security review | yes\nFunnel this quarter: signup to sandbox and credentials not counted; provisioned to first API call not counted; first API call to first app submitted not counted; submission to approval 66.7% (6 of 9); minutes to first call not given; first-review fail rate 22%; days to first submission 70; review days 21\nIssues in fix order: instrument - the four checkpoints are not all counted - instrument them before the next stall; enablement - 70 days to a first submission against 21 days of review - the bottleneck is docs and enablement, not the review team",
"carry": "",
"facts": "The page's checks (1 fail, 1 warn, 4 pass, 4 n/a); verdict: partners stall before they ship:\n- The four rung menus | entry | na | Application-reviewed registration - weeks to a first working app - unguided partners fail, so a reviewed entry earns its overhead; the seller track (agreement, KYC/KYB) still gates selling at first listing\n- The four rung menus | provisioning | na | Production-mirror sandboxes - apps touch money, irreversible actions or other tenants' data - own-account-as-sandbox is deleted, not demoted; apps cannot be validated without production-realistic behavior - production-mirror sandboxes, rationed\n- The four rung menus | education | na | Credentialed education loop - partner credibility is a buyer-facing signal - named, displayable certificates, still non-gating\n- The four rung menus | support | na | Docs plus a community forum + Real-time community chat + Office hours + Named partner managers (commercial partners only) - repeated questions cluster - real-time chat with periodic townhalls; a recurring hard step needs a platform engineer, not a peer - office hours; commercial ISVs - named partner managers for that segment only\n- The journey | decouple | pass | a reviewed entry by design - access follows admission\n- The journey | commercial | pass | no pricing asked before launch\n- The journey | disclosed | pass | every threshold is in the onboarding docs\n-\n\n[... 1119 more characters cut from this display ...]"
}
Reply:
{
"task": "design",
"summary": "Entry stays application-reviewed with KYB, rationed production-mirror sandboxes and test credentials arrive right after admission, and the first change is counting all four funnel checkpoints.",
"stages": [
{
"stage": "Admission",
"what": "Each company and its KYB are reviewed before admission, because a first working build takes weeks to months and the data is highly sensitive.",
"gate": "review",
"owner": "Lena Vashcombe",
"metric": "days to admission"
},
{
"stage": "Build",
"what": "A rationed production-mirror sandbox and test credentials are issued at admission, before any app review.",
"gate": "none",
"owner": "Omar Quisset",
"metric": "first sandbox call"
},
{
"stage": "Plan",
"what": "A solution design session with a partner engineer helps partners through the recurring hard step.",
"gate": "none",
"owner": "Lena Vashcombe",
"metric": "sessions booked"
},
{
"stage": "Certify",
"what": "The security self-assessment and Corvalith certificate run as a credentialed, displayable track that never gates submission.",
"gate": "none",
"owner": "Omar Quisset",
"metric": "certificates"
},
{
"stage": "Submit",
"what": "The app is submitted for security review against the published checklist and the pre-submission validator.",
"gate": "review",
"owner": "Lena Vashcombe",
"metric": "first-review pass rate"
},
{
"stage": "Launch",
"what": "Pricing is set as the last gate before listing, after the security review passes.",
"gate": "pricing",
"owner": "Lena Vashcombe",
"metric": "apps listed"
}
],
"rungs": {
"entry": "reviewed",
"provisioning": "mirror",
"education": "credentialed",
"support": [
"forum",
"chat",
"office",
"managers"
]
},
"promotions": [
{
"rung": "own",
"condition": "The partner's own account as sandbox stays deleted, not demoted, because apps touch money, irreversible actions or other tenants' data."
},
{
"rung": "reviewed",
"condition": "Reviewed entry would step down to agreement-split entry if a first working app came to take days rather than weeks and the data were less sensitive."
},
{
"rung": "office",
"condition": "Office hours stay while a recurring hard step still needs a platform engineer rather than a peer."
},
{
"rung": "managers",
"condition": "Named partner managers stay limited to commercial ISVs and never extend to every individual developer."
}
],
"commercial": {
"monetization": "Launch",
"agreements": "The seller agreement lands at first listing and revenue reporting starts at the first sale."
},
"funnel": {
"checkpoints": [
{
"checkpoint": "signup-provisioned",
"target": "Proposed: count admissions provisioned within a day"
},
{
"checkpoint": "provisioned-first-call",
"target": "Proposed: first sandbox call under 10 minutes"
},
{
"checkpoint": "first-call-submitted",
"target": "Proposed: count it this quarter, then set a target"
},
{
"checkpoint": "submitted-approved",
"target": "Proposed: hold at or above 66.7%"
}
],
"headline": "Time-to-first-submitted-app, now 70 days against 21 days of review, is the headline metric and is self-defined by Corvalith."
},
"disclosure": "Every post-launch threshold stays disclosed in the onboarding docs, including production keys only after a passed security review.",
"caveats": [
"The Apply stage is renamed Admission so the entry review reads apart from the security review.",
"Only submission to approval is counted today; the other three checkpoints need instrumenting first.",
"70 days to a first submission against 21 days of review points to docs and enablement, not the review team."
]
}
What step 5 prints for it: Admission review | Build none | Plan none | Certify none | Submit review | Launch pricing
The page's check: kept - nothing to flag
Build the input yourself
Load the engine in node (global.window = global; require("./pokit.js"); require("./poreply.js")), then:
const K = OnrampKit, R = OnrampReply;
const f = K.form(Object.assign(K.blank(), { name: "Acme", sellers: "yes", mincall: "25",
stages: "Apply | partner form | review | Ann | approvals\nBuild | sandbox and credentials | none | Bo | first calls\nSubmit | app submitted | review | Ann | pass rate" }));
// f.entry, f.provision, f.education (the ranked rungs), f.support.ladder, f.stageRows, f.funnel, f.issues (in fix order), f.gate
// K.journeyMd(f), K.funnelCsv(f) - the journey table and the funnel
const b = K.buildInput(f, "design"); // or "fixes" (R.laneForm(form, "fixes", design) fills carry)
const wire = K.toWire(b.input); // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "design");
// R.check(reply, b) // {status: kept or flagged, notes}
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://partner-onramp.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 = "YOUR_TOKEN" # from https://partner-onramp.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"]
// partner-onramp.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from https://partner-onramp.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 = "YOUR_TOKEN" // from https://partner-onramp.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
}
// Partner Onramp.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 Partner Onramp {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN"; // from https://partner-onramp.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 = "YOUR_TOKEN" # from https://partner-onramp.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 = "YOUR_TOKEN"; // from https://partner-onramp.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 = "YOUR_TOKEN"; // from https://partner-onramp.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":"partner-onramp"}' | 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": "partner-onramp"})["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: "partner-onramp" })).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": "partner-onramp"}, 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", "partner-onramp"), 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: "partner-onramp" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "partner-onramp"])["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 = "partner-onramp" }))!["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": "design", "program": "Platform: FieldCRM
What partners build and the program today: ...", "journey": "Journey stages in order (stage | what happens | gate | owner | metric):
- Register | ... | a pricing or monetization choice | Nadia Pellworth | signups ...", "carry": "", "facts": "The page's checks (...); verdict: partners stall before they ship:
- The four rung menus | entry | na | ..."}
# - 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": "design", "program": "Platform: FieldCRM
What partners build and the program today: ...", "journey": "Journey stages in order (stage | what happens | gate | owner | metric):
- Register | ... | a pricing or monetization choice | Nadia Pellworth | signups ...", "carry": "", "facts": "The page's checks (...); verdict: partners stall before they ship:
- The four rung menus | entry | na | ..."}
# - 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: "design" or "fixes", program, journey, carry, 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 lane and the input: a retry with the same key never bills twice. The page uses partner-onramp:<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. A design reply carries summary, stages, rungs, promotions, commercial, funnel, disclosure and caveats; a fixes reply carries summary, fixes and caveats. The code below prints the gist of either: each stage and its gate, or each issue and its owner.
KEY="partner-onramp:$(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 == "fixes" then (.fixes[] | "\(.issue): \(.owner)")
else (.stages[] | "\(.stage): \(.gate)") end' reply.json
import hashlib, time
key = "partner-onramp:" + 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 reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply["task"])
if reply["task"] == "fixes":
for x in reply["fixes"]:
print(x["issue"], x["owner"])
else:
for x in reply["stages"]:
print(x["stage"], x["gate"])
import { createHash } from "node:crypto";
const key = `partner-onramp:${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.task);
if (reply.task === "fixes") { for (const x of reply.fixes) console.log(x.issue, x.owner); }
else { for (const x of reply.stages) console.log(x.stage, x.gate); }
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "partner-onramp:" + 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 }
}
var reply map[string]any
json.Unmarshal([]byte(j["output"].(map[string]any)["output"].(string)), &reply)
fmt.Println(j["charged_credits"], j["truncated"], reply["task"])
key, name, field := "stages", "stage", "gate"
if reply["task"] == "fixes" {
key, name, field = "fixes", "issue", "owner"
}
ls, _ := reply[key].([]any)
for _, x := range ls {
xx := x.(map[string]any)
fmt.Println(xx[name], xx[field])
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "partner-onramp:" + 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.path("task").asText());
if (reply.path("task").asText().equals("fixes")) {
for (JsonNode x : reply.path("fixes")) System.out.println(x.path("issue").asText() + " " + x.path("owner").asText());
} else {
for (JsonNode x : reply.path("stages")) System.out.println(x.path("stage").asText() + " " + x.path("gate").asText());
}
require "digest"
key = "partner-onramp:#{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["task"]].inspect
if reply["task"] == "fixes"
reply["fixes"].each { |x| puts "#{x["issue"]} #{x["owner"]}" }
else
reply["stages"].each { |x| puts "#{x["stage"]} #{x["gate"]}" }
end
$key = "partner-onramp:" . $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["task"], "\n";
if ($reply["task"] === "fixes") { foreach ($reply["fixes"] as $x) echo $x["issue"], " ", $x["owner"], "\n"; }
else { foreach ($reply["stages"] as $x) echo $x["stage"], " ", $x["gate"], "\n"; }
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"partner-onramp:{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>())!;
var task = (string?)reply["task"];
Console.WriteLine($"{j["charged_credits"]} {task}");
if (task == "fixes") { foreach (var x in reply["fixes"]!.AsArray()) Console.WriteLine($"{x!["issue"]} {x["owner"]}"); }
else { foreach (var x in reply["stages"]!.AsArray()) Console.WriteLine($"{x!["stage"]} {x["gate"]}"); }
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: partner-onramp:$(jq -r .task input.json):$(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": "design", "program": "Platform: FieldCRM
What partners build and the program today: ...", "journey": "Journey stages in order (stage | what happens | gate | owner | metric):
- Register | ... | a pricing or monetization choice | Nadia Pellworth | signups ...", "carry": "", "facts": "The page's checks (...); verdict: partners stall before they ship:
- The four rung menus | entry | na | ..."}
# - 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": "partner-onramp:" + rev["task"] + ":" + 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": `partner-onramp:${rev.task}:${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", "partner-onramp:"+input["task"].(string)+":"+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", "partner-onramp:" + input.get("task") + ":" + 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" => "partner-onramp:#{JSON.parse(rev)["task"]}:#{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: partner-onramp:" . json_decode($rev, true)["task"] . ":" . 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", $"partner-onramp:{input["task"]}:{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);