← AppSource Ready
API
Tokens

Run AppSource Ready from your own code

Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page reads the Teams manifest, parses the plans, builds the locked-choice register, runs every check, compares the plans with the plans as published, re-ranks the path ladder, sets the co-sell target and lays out the timeline from the target date, then hands it all to the run as facts. The run plans the listing or answers a validation report; the page then holds it to its own facts (msreply.js). AppSource Ready is not affiliated with or endorsed by Microsoft.

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":"appsource-ready"}), 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, product, offer, setup, facts) is missing. The page always sends all eight of task, product, manifest, offer, setup, report, carry, facts; report and carry are empty strings for plan, and manifest is empty when no manifest was pasted.
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 listing, then a validation report:

A missing or unknown task is answered as plan. Both lanes send the same eight 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 plan's path, register, fixes and submission into carry for the fixes lane.

The input

FieldRequiredWhat
taskyesplan or fixes. If it is missing or neither, do plan and set "task": "plan".
productyeswhat the product does and the interview answers: the Microsoft 365 surface, the hosting and the Microsoft integration, the offer type and the MACC buyers, the stage and whether the offer is published, the target date with its days from today, Publisher Verification and Attestation, Frontier Accelerate, the threshold, the architecture diagram, the test accounts, regulated buyers and a planned renewal private offer.
manifestsend it (empty when no manifest was pasted)the Teams manifest.json as the page read it, field by field (links described, never copied), or empty when none was pasted.
offeryesthe offer and its plans as the page parsed them (id, name, visibility, pricing, seats, price, dimensions), then the plans as published when the offer is published.
setupyesthe landing page and the webhook described (given, https, contains #), the Entra app, the permissions requested at activation and the notes for certification as pasted.
reportsend it (the validation report for fixes, empty for plan)fixes task only: the validation report text as pasted.
carrysend it (empty for plan)fixes task only: the listing plan's path, register, fixes and submission when the plan ran first, and the previous resubmission's points once one is logged; may be empty.
factsyesthe page's checks, one line each with the rule, the id, the status and the evidence separated by vertical bars; then the locked-choice register, the comparison with the plans as published, the path ladder, the starved option, the co-sell target and its gap, the timeline, the submission packet, the stage, the figures and the footnote. A line starting "must:" applies to your reply.
retry_notenooptional; if present, your previous reply was unusable - follow it and reply with only the JSON object.

The rules the model applies

From the source skill, @samber/microsoft-appsource-playbook - the only standard the run applies:

House rules

The replies

plan

Plan the listing: the path, the locked-choice register, one fix per failing check, the submission, the co-sell target and the timeline.

  1. summary - one sentence, at most 40 words: what stands between the offer and its first Marketplace transaction today and what the plan changes.
  2. path - an object: rung (exactly the facts' top rung: transact_private, teams_linked, ip_cosell, teams_only or contact_me), why (one or two sentences naming the interview answer behind it and what moved the ladder).
  3. locked - exactly one object per row of the facts' register, in its order: choice (exactly the row's choice: pricing model, plan id, seat limits, dimensions, contract or plan visibility), decision (the decision as the input gives it, naming the plan id or the dimension id of that row, or "undecided"), owner (exactly "unassigned" unless the input names a role for it), by (a date only when the input gives one; exactly "already published" when the offer is published; otherwise exactly "before first publish"). No other row.
  4. fixes - one object per check the facts mark fail, and any warn worth fixing: check (the page's check id), change (one concrete sentence: what to change in the manifest, the plans, Partner Center, the setup or the notes for certification). Every fail check appears. When a fix touches a locked choice of a published offer, say it needs a new offer or plan.
  5. submission - the submission plan, one object per item: item (one sentence), object (teams_app, saas_offer or cosell), done (true or false, as the facts' packet and checks say).
  6. cosell - an object: status (exactly the facts' co-sell target: not_targeted, cosell_ready or ip_cosell_eligible), gap (one sentence: what is missing, from the facts' gap, or "none").
  7. timeline - the steps to the first Marketplace transaction and beyond, each an object: step (one sentence), wait (the wait, with † on every Microsoft figure); one failed validation cycle included.
  8. footnote - exactly the skill's footnote.
  9. caveats - at most 4 short strings: what the team must confirm; [] when there is nothing to say.
{
  "task": "plan",
  "summary": "one sentence",
  "path": { "rung": "transact_private", "why": "one or two sentences" },
  "locked": [ { "choice": "pricing model", "decision": "per user", "owner": "unassigned", "by": "before first publish" } ],
  "fixes": [ { "check": "teams_pricing", "change": "one sentence" } ],
  "submission": [ { "item": "one sentence", "object": "saas_offer", "done": false } ],
  "cosell": { "status": "not_targeted", "gap": "one sentence" },
  "timeline": [ { "step": "one sentence", "wait": "three to six weeks†" } ],
  "footnote": "† verify in Partner Center before quoting it, since Microsoft renames programs and moves criteria yearly.",
  "caveats": [ ]
}

The page holds it to its own facts (check() in msreply.js; any warning makes the reply flagged): the reply answers the task it was sent; no URL with a scheme; no backticks; no implied Microsoft endorsement; no price the input does not give; a summary of at most 40 words; every Microsoft figure with its dagger and the exact footnote; the page's top rung and co-sell target (a different one is put back and flagged); exactly as many locked rows of each choice as the register has, none invented, every plan ID and dimension ID named, the pricing model as the input gives it, an owner of "unassigned" unless the input names that role and a by of "before first publish" ("already published" for a published offer) unless the input gives the date; a fix for every fail check and only the page's check ids; submission items with an object of teams_app, saas_offer or cosell and a done that does not contradict the page's packet; a timeline.

fixes

Answer the validation report: one fix per item the report raises, the re-validation effect, then the reply to the reviewer.

  1. summary - one sentence, at most 40 words: what the report returned and what the resubmission changes.
  2. points - one object per item the report raises, in its order: n (1, 2, 3...), report (a short quote or paraphrase of the item, from the report), severity (exactly the report's label for that item: must_fix, good_to_fix, blocker or query; unlabelled when the report gives none), fix (the concrete change, one sentence; for a Query, the answer you give), check (the page's check id the item matches, or ""). When the carried plan has a fix for that check, use it.
  3. resubmit - an object: object (teams_app, saas_offer or cosell - the object the report reviewed), effect (one sentence on re-validation: any manifest, screenshot or Partner Center change re-enters validation; web-service-only changes do not - say which applies to these fixes), when (one sentence: reply first, wait for confirmation, then resubmit once with every fix in one pass).
  4. reply - the reply to the reviewer: thanks them, then each point on its own line with its fix, numbered in the report's order, then a closing line. Separate the lines with a newline, written \n inside the JSON string - for example "Thank you for the review.\n1. Publisher Verification is now complete.\n2. The name no longer uses Teams.\nWe will resubmit once you confirm." Polite, never argues, never asks to expedite, never claims endorsement, no URL.
  5. footnote - exactly the skill's footnote.
  6. caveats - at most 4 short strings: what to confirm before resubmitting; [] when there is nothing to say.
{
  "task": "fixes",
  "summary": "one sentence",
  "points": [ { "n": 1, "report": "short quote or paraphrase", "severity": "must_fix", "fix": "one sentence", "check": "publisher_verification" } ],
  "resubmit": { "object": "teams_app", "effect": "one sentence", "when": "one sentence" },
  "reply": "Thank you for the review.\n1. first point - its fix\n2. second point - its fix\nWe will resubmit once you confirm.",
  "footnote": "† verify in Partner Center before quoting it, since Microsoft renames programs and moves criteria yearly.",
  "caveats": [ ]
}

The page requires: the reply answers the task it was sent; the same text rules; at least as many points as the report numbers or labels, each item's severity exactly as the report labels it, only the page's check ids, a fix on each; object teams_app, saas_offer or cosell; an effect that says the resubmission re-enters validation whenever a fix touches the manifest, the screenshots or Partner Center, and never claims a web-service-only change re-enters it; a when; a reply that lists every point - numbered points count on their own lines or inline - never asks to expedite and never argues.

Worked examples

Three runs from the page's free samples (invented products, hosts on the reserved .example domain): the listing plan for a Teams-linked offer priced flat, the fixes for its validation report carrying that plan, and the plan for an offer already published on another cloud. 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.

plan - A Teams-linked offer priced flat

The page read it as: returned as it stands; stage in validation; top rung transact_private; co-sell not_targeted; register rows pricing model 1, plan id 2, seat limits 0, dimensions 3, contract 1, plan visibility 2; fail checks: teams_pricing, landing_hash, entra_multitenant, cert_notes, test_accounts_mfa, publisher_verification, app_name, developer_urls; against the plans as published: not published.

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

{
 "task": "plan",
 "product": "What the product does: Tallyboard turns a team's tasks and channel activity into a weekly scorecard tab inside Teams and posts a Friday summary to the channel. Teams buy it in the Teams store; scorecard reports and exports are metered.\nMicrosoft 365 surface (answer 1): a Teams app\nHosting (answer 2): primarily Azure; integrates with a Microsoft product: yes\nPricing and buyers (answer 3): offer type transact; buyers hold Azure commitments (MACC): no\nStage (answer 4): in validation; offer already published: no\nTarget date for a first Marketplace transaction (answer 5): 2026-12-02 (30 days from today, 2026-11-02); a partner manager or sales lead owns Microsoft seller outreach: no\nPublisher Verification done: no; Publisher Attestation: not yet; Frontier Accelerate enrolled: no\nAzure consumed or billed through the offer in the trailing 12 months† reaches the $100,000† threshold: unknown; reference architecture diagram ready: no\nCertification test accounts without MFA: no; regulated buyers' security reviews stall deals: no; renewal, migrated or upsell private offer planned: no",
 "manifest": "manifest.json, the fields the review reads:\nmanifestVersion 1.16; $schema missing; id given; version 1.0.0\nname.short: Tallyboard for Teams; name.full: Tallyboard for Teams - weekly team scorecards\ndeveloper: name Tallyboard Inc.; websiteUrl https; privacyUrl https; termsOfUseUrl missing; mpnId not given\ndescription.short: A weekly scorecard of your team's work, in a channel tab.\ncapabilities: 1 bot, 0 static tabs, 1 configurable tab, 0 message extensions, 0 declarative or custom agents; channel-enabled\nvalidDomains: *.tallyboard.example\nwebApplicationInfo: none\nRSC permissions: ChannelMessage.Read.Group (Application)\nsubscriptionOffer: tallyboardinc.tallyboard-teams",
 "offer": "The offer as pasted: offer tallyboard-teams; pricing model flat rate; contract standard contract\nPlans (id | name | visibility | pricing | seats | price | dimensions):\n- starter | Starter | public | flat rate | no seat limits | price 99 USD per month | dimensions reports=0.05\n- business | Business | public | flat rate | no seat limits | price 499 USD per month | dimensions reports=0.03, exports=0.10\nThe offer is not published yet.",
 "setup": "Landing page: given - https, contains #\nWebhook: given - https\nEntra app multitenant: no\nPermissions requested at activation: User.Read, Group.Read.All\nNotes for certification:\nnone given",
 "report": "",
 "carry": "",
 "facts": "The page's checks (8 fail, 6 warn, 8 pass, 3 info, 12 n/a); verdict: returned as it stands; rule | id | status | evidence:\n- teams | teams_pricing | fail | the offer is flat rate but the manifest declares subscriptionOffer - the pricing model is fixed per offer, and a Teams link needs per user: a second offer, review and MACC enrollment; per-user plans carry no metered dimensions\n- teams | link_live | warn | subscriptionOffer is declared while the offer is in validation (answer 4) - linking needs a live SaaS offer; add it once live, and that update re-enters validation\n- teams | subscription_offer_id | pass | \"tallyboardinc.tallyboard-teams\" names the offer tallyboard-teams\n- locked | pricing_mixed | pass | every plan is flat rate\n- locked | metering_per_user | pass | metered dimensions only on flat-rate plans; metering is sent hourly† per resource and dimension, and events older than 24 hours† are rejected\n- locked | locked_pricing | na | the offer is not published yet (interview) - locked once it publishes\n- locked | locked_plan_id | na | the offer is not published yet (interview) - locked once it publishes\n- locked | locked_seats | na | the offer is not published yet (interview) - locked once it publishes\n- locked | locked_dimensions | na | the offer is not published yet (interview) - locked once it publishes\n- locked | locked_contract | na | the offer is not published yet\n\n[... 8938 more characters cut from this display ...]"
}

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

{
 "task": "plan",
 "summary": "A flat-rate offer behind a Teams link, a landing page with #, a single-tenant Entra app, no certification notes and no Publisher Verification block the listing; the plan moves to per user before the first publish and fixes the rest.",
 "path": {
  "rung": "transact_private",
  "why": "A transactable SaaS offer on Azure with private offers comes first; the target date in about a month promotes the Teams app and private offers, so the first deal goes through a private offer while the Teams app finishes validation."
 },
 "locked": [
  {
   "choice": "pricing model",
   "decision": "flat rate today, which a Teams link cannot keep, so decide per user for every plan before the first publish",
   "owner": "unassigned",
   "by": "before first publish"
  },
  {
   "choice": "plan id",
   "decision": "starter (Starter)",
   "owner": "unassigned",
   "by": "before first publish"
  },
  {
   "choice": "plan id",
   "decision": "business (Business)",
   "owner": "unassigned",
   "by": "before first publish"
  },
  {
   "choice": "dimensions",
   "decision": "reports = 0.05 on starter, which per-user plans cannot keep",
   "owner": "unassigned",
   "by": "before first publish"
  },
  {
   "choice": "dimensions",
   "decision": "reports = 0.03 on business, which per-user plans cannot keep",
   "owner": "unassigned",
   "by": "before first publish"
  }
 ],
 "fixes": [
  {
   "check": "teams_pricing",
   "change": "Set every plan to per user before the first publish, drop the reports and exports dimensions, and set a seat minimum and maximum per plan, because a Teams-linked offer must be per user and metering exists only on flat-rate plans."
  },
  {
   "check": "landing_hash",
   "change": "Serve activation from a landing page path with no # in the URL, keep it always up, and enter that address in Technical configuration."
  },
  {
   "check": "entra_multitenant",
   "change": "Register a multitenant Entra app that accepts work and personal Microsoft accounts and mint the API tokens with the tenant and app IDs entered in Technical configuration."
  },
  {
   "check": "activation_permissions",
   "change": "Request only User.Read during activation and ask for Group.Read.All after the subscription is active."
  },
  {
   "check": "cert_notes",
   "change": "Write the notes for certification with two test accounts and the purchase-to-activation steps from the Teams store to the landing page."
  }
 ],
 "submission": [
  {
   "item": "Publisher Verification for the Partner Center account.",
   "object": "teams_app",
   "done": false
  },
  {
   "item": "The renamed package validated with the Teams developer tools and tested on desktop, web and mobile.",
   "object": "teams_app",
   "done": false
  },
  {
   "item": "The terms of use link added and identical in the manifest.json and Partner Center.",
   "object": "teams_app",
   "done": false
  },
  {
   "item": "Publisher Attestation on the day the app goes live.",
   "object": "teams_app",
   "done": false
  },
  {
   "item": "A multitenant Entra app requesting only User.Read at activation.",
   "object": "saas_offer",
   "done": false
  }
 ],
 "cosell": {
  "status": "not_targeted",
  "gap": "none: no MACC buyers, no co-sell submission and nobody owning Microsoft seller outreach, so co-sell waits until one of those changes."
 },
 "timeline": [
  {
   "step": "Complete Publisher Verification and fix the name, the terms of use link, validDomains and the schema version in the package.",
   "wait": "before resubmitting"
  },
  {
   "step": "Resubmit the Teams app; the validation report targets 24 working hours† and the whole round, with one failed cycle, runs long.",
   "wait": "three to six weeks†"
  },
  {
   "step": "Rebuild the plans per user, fix the landing page and the Entra app, then publish the SaaS offer to a preview audience and Go live.",
   "wait": "engineering weeks, not stated in the input"
  },
  {
   "step": "Take the first deal as a private offer by the target date, without waiting for the Teams link.",
   "wait": "by the target date"
  },
  {
   "step": "Add subscriptionOffer once the offer is live and complete Publisher Attestation the day the app goes live.",
   "wait": "after the offer is live, then one more validation"
  }
 ],
 "footnote": "† verify in Partner Center before quoting it, since Microsoft renames programs and moves criteria yearly.",
 "caveats": [
  "Confirm with the product owner that the metered reports and exports can become part of a per-user price.",
  "The new app name is a suggestion; choose it before the next submission."
 ]
}

Cut from this display only (the saved reply and the check use the full text): locked (9 items, 5 shown); fixes (13 items, 5 shown); submission (9 items, 5 shown).

What step 5 prints for it: teams_pricing | landing_hash | entra_multitenant | activation_permissions | cert_notes | test_accounts_mfa | publisher_verification | app_name | developer_urls | manifest_schema | valid_domains | sso_app | link_live

The page's check: kept - nothing to flag

fixes - A Teams-linked offer priced flat

The input carries the saved plan (R.laneForm fills carry from the plan reply), as the page's handoff does, and the validation report, whose 4 items are labelled must_fix, must_fix, blocker, must_fix.

Input (carry is 3679 characters; the first 1393 are shown; facts is 10247 characters; the first 1398 are shown):

{
 "task": "fixes",
 "product": "What the product does: Tallyboard turns a team's tasks and channel activity into a weekly scorecard tab inside Teams and posts a Friday summary to the channel. Teams buy it in the Teams store; scorecard reports and exports are metered.\nMicrosoft 365 surface (answer 1): a Teams app\nHosting (answer 2): primarily Azure; integrates with a Microsoft product: yes\nPricing and buyers (answer 3): offer type transact; buyers hold Azure commitments (MACC): no\nStage (answer 4): in validation; offer already published: no\nTarget date for a first Marketplace transaction (answer 5): 2026-12-02 (30 days from today, 2026-11-02); a partner manager or sales lead owns Microsoft seller outreach: no\nPublisher Verification done: no; Publisher Attestation: not yet; Frontier Accelerate enrolled: no\nAzure consumed or billed through the offer in the trailing 12 months† reaches the $100,000† threshold: unknown; reference architecture diagram ready: no\nCertification test accounts without MFA: no; regulated buyers' security reviews stall deals: no; renewal, migrated or upsell private offer planned: no",
 "manifest": "manifest.json, the fields the review reads:\nmanifestVersion 1.16; $schema missing; id given; version 1.0.0\nname.short: Tallyboard for Teams; name.full: Tallyboard for Teams - weekly team scorecards\ndeveloper: name Tallyboard Inc.; websiteUrl https; privacyUrl https; termsOfUseUrl missing; mpnId not given\ndescription.short: A weekly scorecard of your team's work, in a channel tab.\ncapabilities: 1 bot, 0 static tabs, 1 configurable tab, 0 message extensions, 0 declarative or custom agents; channel-enabled\nvalidDomains: *.tallyboard.example\nwebApplicationInfo: none\nRSC permissions: ChannelMessage.Read.Group (Application)\nsubscriptionOffer: tallyboardinc.tallyboard-teams",
 "offer": "The offer as pasted: offer tallyboard-teams; pricing model flat rate; contract standard contract\nPlans (id | name | visibility | pricing | seats | price | dimensions):\n- starter | Starter | public | flat rate | no seat limits | price 99 USD per month | dimensions reports=0.05\n- business | Business | public | flat rate | no seat limits | price 499 USD per month | dimensions reports=0.03, exports=0.10\nThe offer is not published yet.",
 "setup": "Landing page: given - https, contains #\nWebhook: given - https\nEntra app multitenant: no\nPermissions requested at activation: User.Read, Group.Read.All\nNotes for certification:\nnone given",
 "report": "The validation report:\nTeams app validation report - Tallyboard for Teams 1.0.0\n\nHello,\n\nWe tested your submission and found the following issues:\n\n1. [Must fix] Publisher Verification is not complete for this Partner Center account.\n2. [Must fix] The app name contains \"Teams\". Remove it from the short and full names in the manifest and in Partner Center.\n3. [Blocker] We could not test the app: the notes for certification give no test accounts.\n4. [Must fix] The manifest has no terms of use link.\n\nPlease fix these issues and reply to this email before you resubmit.\n\nValidation team",
 "carry": "The listing plan (path transact_private, co-sell not_targeted), the locked-choice register:\n- pricing model: flat rate today, which a Teams link cannot keep, so decide per user for every plan before the first publish (unassigned, before first publish)\n- plan id: starter (Starter) (unassigned, before first publish)\n- plan id: business (Business) (unassigned, before first publish)\n- dimensions: reports = 0.05 on starter, which per-user plans cannot keep (unassigned, before first publish)\n- dimensions: reports = 0.03 on business, which per-user plans cannot keep (unassigned, before first publish)\n- dimensions: exports = 0.10 on business, which per-user plans cannot keep (unassigned, before first publish)\n- plan visibility: starter public; going private later is locked (unassigned, before first publish)\n- plan visibility: business public; going private later is locked (unassigned, before first publish)\n- contract: standard contract (unassigned, before first publish)\nThe plan's fixes:\n- teams_pricing: Set every plan to per user before the first publish, drop the reports and exports dimensions, and set a seat minimum and maximum per plan, because a Teams-linked offer must be per user and metering exists only on flat-rate plans.\n- landing_hash: Serve activation from a landing page path with no # in the URL, keep it always up, and enter that address in Technical configuration.\n-\n\n[... 2286 more characters cut from this display ...]",
 "facts": "The page's checks (8 fail, 6 warn, 8 pass, 3 info, 12 n/a); verdict: returned as it stands; rule | id | status | evidence:\n- teams | teams_pricing | fail | the offer is flat rate but the manifest declares subscriptionOffer - the pricing model is fixed per offer, and a Teams link needs per user: a second offer, review and MACC enrollment; per-user plans carry no metered dimensions\n- teams | link_live | warn | subscriptionOffer is declared while the offer is in validation (answer 4) - linking needs a live SaaS offer; add it once live, and that update re-enters validation\n- teams | subscription_offer_id | pass | \"tallyboardinc.tallyboard-teams\" names the offer tallyboard-teams\n- locked | pricing_mixed | pass | every plan is flat rate\n- locked | metering_per_user | pass | metered dimensions only on flat-rate plans; metering is sent hourly† per resource and dimension, and events older than 24 hours† are rejected\n- locked | locked_pricing | na | the offer is not published yet (interview) - locked once it publishes\n- locked | locked_plan_id | na | the offer is not published yet (interview) - locked once it publishes\n- locked | locked_seats | na | the offer is not published yet (interview) - locked once it publishes\n- locked | locked_dimensions | na | the offer is not published yet (interview) - locked once it publishes\n- locked | locked_contract | na | the offer is not published yet\n\n[... 8849 more characters cut from this display ...]"
}

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

{
 "task": "fixes",
 "summary": "The report returned Publisher Verification, the Teams name, missing test accounts and the terms of use link; the resubmission fixes all four in one pass and re-enters validation.",
 "points": [
  {
   "n": 1,
   "report": "Publisher Verification is not complete for this Partner Center account.",
   "severity": "must_fix",
   "fix": "Complete Publisher Verification for the Partner Center account before resubmitting.",
   "check": "publisher_verification"
  },
  {
   "n": 2,
   "report": "The app name contains Teams; remove it from the short and full names in the manifest and in Partner Center.",
   "severity": "must_fix",
   "fix": "Rename the app Tallyboard Scorecards in the manifest's short and full names and use the same name in Partner Center.",
   "check": "app_name"
  },
  {
   "n": 3,
   "report": "The app could not be tested: the notes for certification give no test accounts.",
   "severity": "blocker",
   "fix": "Add two test accounts without MFA and the purchase-to-activation steps to the notes for certification.",
   "check": "cert_notes"
  },
  {
   "n": 4,
   "report": "The manifest has no terms of use link.",
   "severity": "must_fix",
   "fix": "Add the terms of use link to the developer block of the manifest and match it in Partner Center.",
   "check": "developer_urls"
  }
 ],
 "resubmit": {
  "object": "teams_app",
  "effect": "The fixes change the manifest and Partner Center, so this resubmission re-enters validation; a web-service-only change would not.",
  "when": "Reply to the report first, wait for confirmation, then resubmit once with all four fixes in one pass."
 },
 "reply": "Hello,\n\nThank you for the review. We have fixed every item:\n1. Publisher Verification is now complete for our Partner Center account.\n2. The app is renamed Tallyboard Scorecards in the manifest's short and full names and in Partner Center.\n3. The notes for [... 242 more characters cut from this display]",
 "footnote": "† verify in Partner Center before quoting it, since Microsoft renames programs and moves criteria yearly.",
 "caveats": [
  "The report did not raise the flat-rate pricing, but the Teams link still needs per-user plans before the first publish.",
  "Take subscriptionOffer out of this resubmission until the offer is live, as the plan says."
 ]
}

Cut from this display only (the saved reply and the check use the full text): reply (498 characters, 256 shown).

What step 5 prints for it: 1 must_fix publisher_verification | 2 must_fix app_name | 3 blocker cert_notes | 4 must_fix developer_urls

The page's check: kept - nothing to flag

plan - A SaaS on another cloud, already published

The page read it as: returned as it stands; stage live; top rung transact_private; co-sell cosell_ready; register rows pricing model 1, plan id 2, seat limits 2, dimensions 0, contract 1, plan visibility 2; fail checks: locked_pricing, macc_azure, ip_cosell; against the plans as published: pricing model changed (flat rate -> per user).

Input (facts is 9418 characters; the first 1390 are shown):

{
 "task": "plan",
 "product": "What the product does: Routekeep indexes the bills of lading and delivery notes a logistics team stores and answers questions about them as a Copilot agent. The service runs on another cloud and integrates with Microsoft 365 through the agent and Entra single sign-on. A large customer wants to renew its off-marketplace contract through a private offer.\nMicrosoft 365 surface (answer 1): a Copilot agent\nHosting (answer 2): another cloud; integrates with a Microsoft product: yes\nPricing and buyers (answer 3): offer type transact; buyers hold Azure commitments (MACC): yes\nStage (answer 4): live; offer already published: yes\nTarget date for a first Marketplace transaction (answer 5): not given; a partner manager or sales lead owns Microsoft seller outreach: yes\nPublisher Verification done: yes; Publisher Attestation: due before an update; Frontier Accelerate enrolled: yes\nAzure consumed or billed through the offer in the trailing 12 months† reaches the $100,000† threshold: no; reference architecture diagram ready: no\nCertification test accounts without MFA: yes; regulated buyers' security reviews stall deals: yes; renewal, migrated or upsell private offer planned: yes",
 "manifest": "manifest.json, the fields the review reads:\nmanifestVersion 1.19; $schema missing; id given; version 3.0.0\nname.short: Routekeep; name.full: Routekeep shipment documents\ndeveloper: name Routekeep; websiteUrl https; privacyUrl https; termsOfUseUrl https; mpnId not given\ndescription.short: Find any bill of lading or delivery note by asking.\ncapabilities: 0 bots, 0 static tabs, 0 configurable tabs, 0 message extensions, 1 declarative or custom agent\nvalidDomains: routekeep.example\nwebApplicationInfo: id given, resource given\nRSC permissions: none\nsubscriptionOffer: none",
 "offer": "The offer as pasted: offer routekeep; pricing model per user; contract standard contract\nPlans (id | name | visibility | pricing | seats | price | dimensions):\n- crew | Crew | public | per user | seats 1-1000 | price 9 USD per user per month | no dimensions\n- fleet | Fleet | private | per user | seats 50-10000 | price 7 USD per user per month | no dimensions\nThe plans as published: offer routekeep; pricing model flat rate; contract standard contract\nPlans (id | name | visibility | pricing | seats | price | dimensions):\n- crew | Crew | public | flat rate | no seat limits | price 900 USD per month | no dimensions\n- fleet | Fleet | private | flat rate | no seat limits | price 6000 USD per month | no dimensions",
 "setup": "Landing page: given - https, no #\nWebhook: given - https\nEntra app multitenant: yes\nPermissions requested at activation: User.Read\nNotes for certification:\nTest accounts: two users in our test tenant without MFA; no one from our team needs to be online.\nPurchase to activation: buy the Crew plan, open Set up now, sign in on the landing page with the first test account and the subscription activates in under a minute.",
 "report": "",
 "carry": "",
 "facts": "The page's checks (3 fail, 2 warn, 22 pass, 3 info, 7 n/a); verdict: returned as it stands; rule | id | status | evidence:\n- teams | teams_pricing | na | no Teams link: the manifest has no subscriptionOffer and answer 1 is not a Teams app on a transact offer\n- teams | link_live | na | no subscriptionOffer in the manifest\n- teams | subscription_offer_id | na | no subscriptionOffer in the manifest\n- locked | pricing_mixed | pass | every plan is per user\n- locked | metering_per_user | na | no metered dimension\n- locked | locked_pricing | fail | the offer: flat rate -> per user - locked once published: a new offer or plan\n- locked | locked_plan_id | pass | unchanged against the plans as published\n- locked | locked_seats | pass | unchanged against the plans as published\n- locked | locked_dimensions | pass | unchanged against the plans as published\n- locked | locked_contract | pass | unchanged against the plans as published\n- locked | locked_visibility | pass | unchanged against the plans as published\n- listing | listing_eligibility | warn | another cloud with a Microsoft integration (answer 2) - say how it integrates in the description, accept Entra SSO; it is not purchasable in the Azure management console\n- listing | offer_type | pass | transact (answer 3) - MACC, IP co-sell and private offers stay open\n- saas | landing_hash | pass | no # in the landing page URL\n- saas |\n\n[... 8028 more characters cut from this display ...]"
}

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

{
 "task": "plan",
 "summary": "The pasted per-user plans contradict the flat-rate offer as published, and MACC buyers cannot draw down on a service on another cloud; the plan keeps the published offer, adds per user as a new offer and stops at co-sell ready.",
 "path": {
  "rung": "transact_private",
  "why": "A transactable SaaS offer with private offers stays first; another cloud deletes IP co-sell, and a Copilot agent follows its own Microsoft 365 submission guide, so the Teams link drops out and the agent alone comes next."
 },
 "locked": [
  {
   "choice": "pricing model",
   "decision": "per user as pasted, but the offer was published flat rate: per user needs a new offer",
   "owner": "unassigned",
   "by": "already published"
  },
  {
   "choice": "plan id",
   "decision": "crew (Crew)",
   "owner": "unassigned",
   "by": "already published"
  },
  {
   "choice": "plan id",
   "decision": "fleet (Fleet)",
   "owner": "unassigned",
   "by": "already published"
  },
  {
   "choice": "seat limits",
   "decision": "crew: minimum 1, maximum 1000, only on a new per-user offer",
   "owner": "unassigned",
   "by": "already published"
  },
  {
   "choice": "seat limits",
   "decision": "fleet: minimum 50, maximum 10000, only on a new per-user offer",
   "owner": "unassigned",
   "by": "already published"
  }
 ],
 "fixes": [
  {
   "check": "locked_pricing",
   "change": "Leave the published flat-rate plans as they are and create a new offer, priced per user with the crew and fleet seat limits, since a published pricing model needs a new offer or plan."
  },
  {
   "check": "macc_azure",
   "change": "Tell the MACC buyers that purchases of this offer do not draw down their Azure commitments, because MACC counts only for usage running in Azure."
  },
  {
   "check": "ip_cosell",
   "change": "Drop IP co-sell from the plan while the service runs on another cloud, unless an Azure migration is already decided, and stop at co-sell ready."
  },
  {
   "check": "listing_eligibility",
   "change": "Say in the description how Routekeep integrates with Microsoft 365 through the agent and Entra single sign-on, and that it is not purchasable in the Azure management console."
  },
  {
   "check": "attestation",
   "change": "Complete Publisher Attestation before submitting the next update of the agent."
  },
  {
   "check": "renewal_attestation",
   "change": "Create the renewal private offer for the large customer with the renewal attestation ticked at creation, so the fee halves† for the whole term."
  }
 ],
 "submission": [
  {
   "item": "Publisher Attestation before the next update.",
   "object": "teams_app",
   "done": false
  },
  {
   "item": "The agent package validated under its own Microsoft 365 submission guide.",
   "object": "teams_app",
   "done": false
  },
  {
   "item": "A new per-user offer with the crew and fleet plans, next to the published flat-rate offer.",
   "object": "saas_offer",
   "done": false
  },
  {
   "item": "The renewal private offer for the large customer, with the renewal attestation ticked when it is created.",
   "object": "saas_offer",
   "done": false
  },
  {
   "item": "The one-pager, pitch deck and a sales contact per geography for co-sell ready.",
   "object": "cosell",
   "done": false
  },
  {
   "item": "Frontier Accelerate for Marketplace enrollment.",
   "object": "cosell",
   "done": true
  }
 ],
 "cosell": {
  "status": "cosell_ready",
  "gap": "The one-pager, pitch deck and a sales contact per geography; IP co-sell stays out of reach while the service runs on another cloud."
 },
 "timeline": [
  {
   "step": "Complete Publisher Attestation, then submit the agent update.",
   "wait": "before the update"
  },
  {
   "step": "Create the new per-user offer and the renewal private offer with the renewal attestation ticked at creation.",
   "wait": "engineering weeks, not stated in the input"
  },
  {
   "step": "Submit the co-sell solution with its collateral.",
   "wait": "a few weeks of review"
  },
  {
   "step": "Start Microsoft 365 Certification, since regulated buyers' security reviews stall deals.",
   "wait": "a yearly audit, duration not stated in the input"
  }
 ],
 "footnote": "† verify in Partner Center before quoting it, since Microsoft renames programs and moves criteria yearly.",
 "caveats": [
  "An offer created before October 2024† never qualifies for the renewal attestation; confirm when the customer's current contract was signed.",
  "Confirm with Partner Center support how existing flat-rate customers move to the new per-user offer."
 ]
}

Cut from this display only (the saved reply and the check use the full text): locked (8 items, 5 shown).

What step 5 prints for it: locked_pricing | macc_azure | ip_cosell | listing_eligibility | attestation | renewal_attestation

The page's check: kept - nothing to flag

Build the input yourself

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

const K = MsKit, R = MsReply;
const f = K.form(Object.assign(K.blank(), { surface: "teams", stage: "building", offertype: "transact",
  plans: "offer: notes | pricing: flat | contract: standard\nplan: base | Base | public | price 50 USD per month | dimension: calls=0.01",
  landing: "https://app.notes.example/#/start", what: "Notes for every team, in a Teams tab." }));
// f.parsed (error with its line and column), f.m (the manifest), f.offer (the plans), f.register (the locked choices),
// f.rows (the checks), f.locked (against the plans as published), f.ladder, f.cosell, f.timeline, f.plan (route, packet, report items)
// K.registerCsv(f), K.checksCsv(f), K.board(f), K.applyPlan(f, K.board(f), planReply), K.applyPlans(f) - the exports
const b = K.buildInput(f, "plan");                           // or "fixes" (needs report; R.laneForm(form, "fixes", planReply) fills carry)
const wire = K.toWire(b.input);                              // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "plan");
// 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://appsource-ready.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":"appsource-ready"}' | 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": "plan", "product": "What the product does: Tallyboard turns a team's tasks and channel activity into a weekly scorecard tab inside Teams and posts a Friday summary to the channel. Teams buy it in the Teams store; scorecard reports and exports are metered.
...", "manifest": "manifest.json, the fields the review reads:
...", "offer": "The offer as pasted: offer tallyboard-teams; pricing model flat rate; contract standard contract
...", "setup": "Landing page: given - https, contains #
...", "report": "", "carry": "", "facts": "The page's checks (...); verdict: returned as it stands; rule | id | status | evidence:
- teams | teams_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 appsource-ready:<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 plan reply carries summary, path, locked, fixes, submission, cosell, timeline, footnote and caveats; a fixes-lane reply carries summary, points, resubmit, reply, footnote and caveats. The code below prints the gist of either: each fix of the plan, or each report point with its severity and fix.

KEY="appsource-ready:$(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 (.points[] | "\(.n). [\(.severity)] \(.report) -> \(.fix)") 
       else (.fixes[] | "\(.check) \(.change)") 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: appsource-ready:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json