← WP.org Ready
API
Tokens

Run WP.org Ready from your own code

Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page parses the readme and the header, scans the code for a fixed set of signal kinds, runs every check, finds every external host, ranks where paid value lives and lays out the stage's plan, then hands it all to the run as facts. The run plans the submission or answers the reviewer; the page then holds it to its own facts (wpreply.js). WP.org Ready is not affiliated with WordPress.org or the WordPress Foundation.

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":"wporg-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, plugin, readme, header, facts) is missing. The page always sends all eight of task, plugin, readme, header, code, review, carry, facts; review and carry are empty strings for plan.
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 submission, then the review email:

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 fixes into carry for the reply lane. A theme is out of scope and the page never sends it.

The input

FieldRequiredWhat
taskyesplan or reply. If it is missing or neither, do plan and set "task": "plan".
pluginyesthe plugin's name and slug, your brand and whether the brand in the name is yours, the submitting account and email domain, the stage, where paid features are enforced, whether paid features must run locally, active installs and whether launch is within 30 days; "not given" means not given.
readmeyesthe readme.txt as pasted (the page names any cut).
headeryesthe main plugin file's header comment as pasted.
codesend it (may be empty)the PHP or JS that calls the service, as pasted; may be empty.
reviewsend it (the email for reply, empty for plan)reply task only: the reviewer's email as pasted.
carrysend it (empty for plan)reply task only: the submission plan's fixes, when the plan ran first; may be empty.
factsyesthe page's checks, one line each as rule | id | status | evidence, then the detected hosts, the signals, the paid-value rungs, 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.

The rules the model applies

From the source skill, @samber/wordpress-plugin-directory-playbook - the only standard the run applies:

House rules

The replies

plan

Plan the submission: where paid value lives, one fix per failing check, the External services entries, the packet and the timeline.

  1. summary - one sentence, at most 40 words: what blocks review today and what the plan changes.
  2. verdict - an object: rung (exactly the facts' top rung: service, addon or selfhosted), why (one or two sentences from the interview and the checks).
  3. fixes - one object per check the facts mark fail, and any warn worth fixing: check (the page's check id), rule (the check's rule, as the facts give it), change (one concrete sentence: what to change in the plugin, the readme, the header or the account), where (readme, header, code, account or service). Every fail check appears.
  4. services - exactly one object per host the facts list as detected, the host spelled exactly as detected: host, what (the service it is), data (the data sent), when (when it is sent). Write "not stated in the input" for anything the input does not say. No other host.
  5. packet - the submission packet items still open or worth confirming, each an object: item (one sentence), owner (a role, not a name), done (true or false).
  6. timeline - the steps from here to listed, each an object: step (one sentence), wait (the wait, with † on any figure from the skill).
  7. footnote - exactly the skill's footnote.
  8. caveats - at most 4 short strings: what the team must confirm; [] when there is nothing to say.
{
  "task": "plan",
  "summary": "one sentence",
  "verdict": { "rung": "service", "why": "one or two sentences" },
  "fixes": [ { "check": "stable_tag", "rule": "release", "change": "one sentence", "where": "readme" } ],
  "services": [ { "host": "api.service.example", "what": "one phrase", "data": "one phrase", "when": "one phrase" } ],
  "packet": [ { "item": "one sentence", "owner": "Engineering", "done": false } ],
  "timeline": [ { "step": "one sentence", "wait": "one phrase" } ],
  "footnote": "† verify against the current developer.wordpress.org handbook and make.wordpress.org/plugins; the release process changes often.",
  "caveats": [ ]
}

The page holds it to its own facts (check() in wpreply.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; every figure from the skill with its dagger and the exact footnote; no host the input does not have; the page's top rung (a different rung is put back and flagged); a fix for every fail check, only the page's check ids, a where of readme, header, code, account or service and a change on each; exactly one services entry per detected host with what, data and when; a packet when the plugin is not yet submitted, an owner on every item; a timeline.

reply

Answer the reviewer: one fix per point the email raises, then the reply in the same thread.

  1. summary - one sentence, at most 40 words: what the reviewer asked and what the upload changes.
  2. points - one object per distinct point the email raises, in the email's order: n (1, 2, 3...), reviewer (a short quote or paraphrase of the point, from the email), fix (the change made, one sentence), check (the page's check id the point matches, or ""). When the carried plan has a fix for that check, use it.
  3. email - an object: subject ("Re: " and the email's subject when it has one), body (the reply text: thanks the reviewer, says the fixed version is uploaded, lists every point as a numbered line with its fix, says every fix is in this one upload; polite, same thread, never argues, never asks to speed up or jump the queue, no URL).
  4. footnote - exactly the skill's footnote.
  5. caveats - at most 4 short strings: what to confirm before sending; [] when there is nothing to say.
{
  "task": "reply",
  "summary": "one sentence",
  "points": [ { "n": 1, "reviewer": "short quote or paraphrase", "fix": "one sentence", "check": "trialware" } ],
  "email": { "subject": "Re: the email's subject", "body": "the reply text, one numbered line per point" },
  "footnote": "† verify against the current developer.wordpress.org handbook and make.wordpress.org/plugins; the release process changes often.",
  "caveats": [ ]
}

The page requires: the reply answers the task it was sent; the same text rules; at least as many points as the email numbers, only the page's check ids, a fix on each; the subject "Re: " and the email's subject; a body that lists every point as a numbered line, says the fixes are in one upload, never asks to expedite or jump the queue and never argues.

Worked examples

Three runs from the page's free samples (invented plugins, hosts on the reserved .example domain): the submission plan for a CRM forms connector with a locked Pro tab, the reply to its reviewer carrying that plan, and the plan for a payments sync named after another company's brand. 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 CRM forms connector with a locked Pro tab

The page read it as: fails review as it stands; stage in review; top rung service; detected hosts api.leadwren.example, app.leadwren.example, cdn.jsdelivr.net; fail checks: trialware, services_documented, consent, iframe_admin, remote_assets, account, text_domain, stable_tag.

Input (facts is 5052 characters; the first 1399 are shown):

{
 "task": "plan",
 "plugin": "Plugin: Leadwren Forms Sync (slug: leadwren-forms-sync)\nOur brand: Leadwren; the brand in the name is ours: yes\nSubmitting account: a personal or agency account; email domain: gmail.com\nStage: in review; paid features enforced: in the plugin; paid features that must run locally in WordPress: no; keep an iframed admin app or own updater as core: no\nActive installs: not live; launch date within 30 days: yes",
 "readme": "readme.txt:\n=== Leadwren Forms Sync ===\nContributors: pixelbrambleagency\nTags: crm, forms, leads, contact form, lead capture, sync, marketing, sales\nRequires at least: 6.2\nTested up to: 6.6\nStable tag: trunk\nLicense: GPLv2 or later\nLicense URI: https://www.gnu.org/licenses/gpl-2.0.html\n\nSend every form entry to your Leadwren CRM pipeline and see your lead dashboard right inside your admin.\n\n== Description ==\n\nLeadwren Forms Sync connects your site's forms to Leadwren CRM. Every entry becomes a lead in your pipeline.\n\nThe free version syncs one form. Upgrade to Pro to unlock unlimited forms, field mapping and the lead dashboard.\n\n== Installation ==\n\n1. Upload the plugin and activate it.\n2. Open Leadwren > Settings and paste your Leadwren API key.\n\n== Changelog ==\n\n= 1.0.0 =\n* First release.",
 "header": "Main plugin file header:\n<?php\n/**\n * Plugin Name: Leadwren Forms Sync\n * Plugin URI: https://leadwren.example/wordpress\n * Description: Sends form entries to Leadwren CRM.\n * Version: 1.0.0\n * Author: Pixelbramble Agency\n * Author URI: https://pixelbramble.example\n * License: GPLv2 or later\n * Text Domain: leadwren\n * Requires at least: 6.2\n * Requires PHP: 7.4\n */",
 "code": "Code excerpts:\nregister_activation_hook( __FILE__, 'lwfs_activate' );\nfunction lwfs_activate() {\n  wp_remote_post( 'https://api.leadwren.example/v1/installs', array(\n    'body' => array( 'site' => home_url(), 'wp' => get_bloginfo( 'version' ) ),\n  ) );\n}\n\nadd_action( 'admin_menu', function () {\n  add_menu_page( 'Leadwren', 'Leadwren', 'manage_options', 'leadwren', 'lwfs_dashboard_page' );\n} );\n\nfunction lwfs_dashboard_page() {\n  echo '<iframe src=\"https://app.leadwren.example/embed/dashboard\" width=\"100%\" height=\"800\"></iframe>';\n}\n\nadd_action( 'admin_enqueue_scripts', function () {\n  wp_enqueue_script( 'lwfs-chart', 'https://cdn.jsdelivr.net/npm/chart.js', array(), null, true );\n} );\n\nfunction lwfs_settings_tabs() {\n  $is_pro = lwfs_is_pro();\n  echo '<a class=\"nav-tab' . ( $is_pro ? '' : ' pro-locked' ) . '\" href=\"#mapping\">Field mapping (Pro)</a>';\n  if ( ! $is_pro ) {\n    echo '<p class=\"description\">Upgrade to Pro to unlock field mapping.</p>';\n  }\n}\n\nfunction lwfs_is_pro() {\n  return 'valid' === get_option( 'lwfs_license_status' );\n}\n\nadd_action( 'lwfs_form_submitted', function ( $entry ) {\n  wp_remote_post( 'https://api.leadwren.example/v1/leads', array(\n    'headers' => array( 'Authorization' => 'Bearer ' . get_option( 'lwfs_api_key' ) ),\n    'body'    => wp_json_encode( $entry ),\n  ) );\n} );",
 "review": "",
 "carry": "",
 "facts": "The page's checks (8 fail, 2 warn, 12 pass, 2 n/a); verdict: fails review as it stands; rule | id | status | evidence:\n- 1 | trialware | fail | 6 locks in shipped code (code line 21, code line 22, code line 24, code line 28 and 2 more: $is_pro = lwfs_is_pro();) - a key that unlocks local code is trialware; enforce the plan on your service and show its answer; the interview says paid features are enforced in the plugin\n- 2 | services_documented | fail | no External services section in the readme - the code reaches api.leadwren.example, app.leadwren.example, cdn.jsdelivr.net; every service the plugin calls, your own included, is named there\n- 2 | services_links | na | no External services section yet\n- 3 | consent | fail | an activation hook calls home (the callback lwfs_activate(), code line 1) - with no opt-in: install analytics is not the service; count connections at OAuth or registration instead\n- 4 | iframe_admin | fail | an iframe in admin page code (code line 13) - build the settings screen locally and call your API\n- 4 | remote_assets | fail | enqueued from cdn.jsdelivr.net (code line 17) - your admin UI library is not the service and ships in the plugin\n- 4 | update_checker | pass | no custom update checker\n- 5 | name_brand | pass | 'Leadwren Forms Sync' is <Your brand> <job>\n- 5 | account | fail | a personal or agency account - a brand plugin from one gets flagged for\n\n[... 3653 more characters cut from this display ...]"
}

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

{
 "task": "plan",
 "summary": "A locked Pro tab, an activation ping, an iframed dashboard, a CDN script and an agency gmail account block review; the plan moves enforcement to the Leadwren service and fixes every point in one upload.",
 "verdict": {
  "rung": "service",
  "why": "Leadwren is a SaaS CRM and nothing must run locally, so the plan limits belong on the Leadwren service; the plugin only shows the service's answer. Paid features enforced in the plugin today are trialware on this rung."
 },
 "fixes": [
  {
   "check": "trialware",
   "rule": "1",
   "change": "Remove lwfs_is_pro(), the license status option and the pro-locked Field mapping tab; ship field mapping to every user and let the Leadwren service enforce the plan's form limit and return the message the plugin shows.",
   "where": "code"
  },
  {
   "check": "services_documented",
   "rule": "2",
   "change": "Add an External services section naming api.leadwren.example, the form fields it receives and when, and the terms and privacy links; app.leadwren.example and cdn.jsdelivr.net leave the plugin with the next two fixes.",
   "where": "readme"
  },
  {
   "check": "consent",
   "rule": "3",
   "change": "Delete the activation request to the installs endpoint in lwfs_activate(); count connected sites on the service when an administrator saves a valid API key.",
   "where": "code"
  },
  {
   "check": "iframe_admin",
   "rule": "4",
   "change": "Replace the iframed dashboard in lwfs_dashboard_page() with a settings screen built in the plugin that calls the Leadwren API.",
   "where": "code"
  },
  {
   "check": "remote_assets",
   "rule": "4",
   "change": "Bundle Chart.js inside the plugin and enqueue it from the plugin folder instead of cdn.jsdelivr.net.",
   "where": "code"
  }
 ],
 "services": [
  {
   "host": "api.leadwren.example",
   "what": "The Leadwren CRM API",
   "data": "The submitted form entry, with the API key; on activation today, the site URL and the WordPress version",
   "when": "When a form is submitted; the activation call is removed by the consent fix"
  },
  {
   "host": "app.leadwren.example",
   "what": "The Leadwren web app, embedded as the admin dashboard",
   "data": "not stated in the input",
   "when": "When an administrator opens the Leadwren admin page; removed by the iframe fix"
  },
  {
   "host": "cdn.jsdelivr.net",
   "what": "A public CDN serving Chart.js to admin pages",
   "data": "not stated in the input",
   "when": "On admin page loads; removed by bundling Chart.js"
  }
 ],
 "packet": [
  {
   "item": "Plugin Check passes with no error in the Plugin Repo category before the fixed upload.",
   "owner": "Engineering",
   "done": false
  },
  {
   "item": "The zip stays under 10 MB† with Chart.js bundled, and carries no node_modules, tests or logs.",
   "owner": "Engineering",
   "done": false
  },
  {
   "item": "Terms and privacy pages are live for the External services section to link.",
   "owner": "Product and legal",
   "done": false
  },
  {
   "item": "A Leadwren company account with a company-domain email and 2FA on owns the plugin.",
   "owner": "Account owner",
   "done": false
  },
  {
   "item": "Plugin URI and Author URI are live before the reply.",
   "owner": "Web team",
   "done": false
  }
 ],
 "timeline": [
  {
   "step": "Upload the fixed zip and reply in the same review thread with every point answered.",
   "wait": "about 10 business days† for the reviewer's reply"
  },
  {
   "step": "Answer any further round the same way, all fixes in one upload.",
   "wait": "about 10 business days† per round; a review still incomplete after 3 months† is rejected"
  },
  {
   "step": "After approval, commit the code to SVN before launch.",
   "wait": "an approved plugin with no code for 6 months† can be closed"
  },
  {
   "step": "The plugin appears in directory search.",
   "wait": "6 to 14 days† after the first commit"
  }
 ],
 "footnote": "† verify against the current developer.wordpress.org handbook and make.wordpress.org/plugins; the release process changes often.",
 "caveats": [
  "The launch inside 30 days is at risk: there is no queue jumping, so announce the plugin as coming to WordPress.org if approval misses it.",
  "Confirm the company account exists before replying - the reviewer changes the owner only on request."
 ]
}

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

What step 5 prints for it: trialware (code) | services_documented (readme) | consent (code) | iframe_admin (code) | remote_assets (code) | account (account) | text_domain (header) | stable_tag (readme) | tags (readme)

The page's check: kept - nothing to flag

reply - A CRM forms connector with a locked Pro tab

The input carries the saved plan (R.laneForm fills carry from the plan reply), as the page's handoff does, and the reviewer's email, which numbers 4 points.

Input (carry is 1560 characters; the first 1397 are shown; facts is 5040 characters; the first 1399 are shown):

{
 "task": "reply",
 "plugin": "Plugin: Leadwren Forms Sync (slug: leadwren-forms-sync)\nOur brand: Leadwren; the brand in the name is ours: yes\nSubmitting account: a personal or agency account; email domain: gmail.com\nStage: in review; paid features enforced: in the plugin; paid features that must run locally in WordPress: no; keep an iframed admin app or own updater as core: no\nActive installs: not live; launch date within 30 days: yes",
 "readme": "readme.txt:\n=== Leadwren Forms Sync ===\nContributors: pixelbrambleagency\nTags: crm, forms, leads, contact form, lead capture, sync, marketing, sales\nRequires at least: 6.2\nTested up to: 6.6\nStable tag: trunk\nLicense: GPLv2 or later\nLicense URI: https://www.gnu.org/licenses/gpl-2.0.html\n\nSend every form entry to your Leadwren CRM pipeline and see your lead dashboard right inside your admin.\n\n== Description ==\n\nLeadwren Forms Sync connects your site's forms to Leadwren CRM. Every entry becomes a lead in your pipeline.\n\nThe free version syncs one form. Upgrade to Pro to unlock unlimited forms, field mapping and the lead dashboard.\n\n== Installation ==\n\n1. Upload the plugin and activate it.\n2. Open Leadwren > Settings and paste your Leadwren API key.\n\n== Changelog ==\n\n= 1.0.0 =\n* First release.",
 "header": "Main plugin file header:\n<?php\n/**\n * Plugin Name: Leadwren Forms Sync\n * Plugin URI: https://leadwren.example/wordpress\n * Description: Sends form entries to Leadwren CRM.\n * Version: 1.0.0\n * Author: Pixelbramble Agency\n * Author URI: https://pixelbramble.example\n * License: GPLv2 or later\n * Text Domain: leadwren\n * Requires at least: 6.2\n * Requires PHP: 7.4\n */",
 "code": "Code excerpts:\nregister_activation_hook( __FILE__, 'lwfs_activate' );\nfunction lwfs_activate() {\n  wp_remote_post( 'https://api.leadwren.example/v1/installs', array(\n    'body' => array( 'site' => home_url(), 'wp' => get_bloginfo( 'version' ) ),\n  ) );\n}\n\nadd_action( 'admin_menu', function () {\n  add_menu_page( 'Leadwren', 'Leadwren', 'manage_options', 'leadwren', 'lwfs_dashboard_page' );\n} );\n\nfunction lwfs_dashboard_page() {\n  echo '<iframe src=\"https://app.leadwren.example/embed/dashboard\" width=\"100%\" height=\"800\"></iframe>';\n}\n\nadd_action( 'admin_enqueue_scripts', function () {\n  wp_enqueue_script( 'lwfs-chart', 'https://cdn.jsdelivr.net/npm/chart.js', array(), null, true );\n} );\n\nfunction lwfs_settings_tabs() {\n  $is_pro = lwfs_is_pro();\n  echo '<a class=\"nav-tab' . ( $is_pro ? '' : ' pro-locked' ) . '\" href=\"#mapping\">Field mapping (Pro)</a>';\n  if ( ! $is_pro ) {\n    echo '<p class=\"description\">Upgrade to Pro to unlock field mapping.</p>';\n  }\n}\n\nfunction lwfs_is_pro() {\n  return 'valid' === get_option( 'lwfs_license_status' );\n}\n\nadd_action( 'lwfs_form_submitted', function ( $entry ) {\n  wp_remote_post( 'https://api.leadwren.example/v1/leads', array(\n    'headers' => array( 'Authorization' => 'Bearer ' . get_option( 'lwfs_api_key' ) ),\n    'body'    => wp_json_encode( $entry ),\n  ) );\n} );",
 "review": "The reviewer's email:\nSubject: [Plugin Directory] Review in Progress: Leadwren Forms Sync\n\nHello,\n\nThank you for submitting Leadwren Forms Sync. Before we can approve it, the following issues need to be fixed.\n\n1. Trialware: the Field mapping tab is visible but locked until a license is entered, and the readme says Pro unlocks unlimited forms. Plugins may not contain locked or restricted features.\n\n2. Calling home on activation: the plugin sends the site URL and the WordPress version to your server when it is activated, without the user's consent.\n\n3. Undocumented external services: the plugin sends data to api.leadwren.example and loads app.leadwren.example in an iframe, but the readme does not document these services, the data sent or your terms and privacy policy.\n\n4. Remote admin code: the admin dashboard is an iframe of your web app, and Chart.js is loaded from cdn.jsdelivr.net. Admin pages must be built in the plugin and scripts must ship with it.\n\nAlso note that the plugin was submitted from a personal account with a gmail.com address. As the plugin uses the Leadwren brand, it must be owned by an account belonging to Leadwren with an email on the company's domain. Reply to this email to change the owner; do not resubmit.\n\nPlease upload the corrected version on your submission page and reply to this email once every issue is fixed.\n\n--\nPlugin Review Team",
 "carry": "The submission plan (service), one fix per failing check:\n- trialware (rule 1, code): Remove lwfs_is_pro(), the license status option and the pro-locked Field mapping tab; ship field mapping to every user and let the Leadwren service enforce the plan's form limit and return the message the plugin shows.\n- services_documented (rule 2, readme): Add an External services section naming api.leadwren.example, the form fields it receives and when, and the terms and privacy links; app.leadwren.example and cdn.jsdelivr.net leave the plugin with the next two fixes.\n- consent (rule 3, code): Delete the activation request to the installs endpoint in lwfs_activate(); count connected sites on the service when an administrator saves a valid API key.\n- iframe_admin (rule 4, code): Replace the iframed dashboard in lwfs_dashboard_page() with a settings screen built in the plugin that calls the Leadwren API.\n- remote_assets (rule 4, code): Bundle Chart.js inside the plugin and enqueue it from the plugin folder instead of cdn.jsdelivr.net.\n- account (rule 5, account): Reply to the review email asking to move the plugin to a Leadwren company account with a company-domain email and 2FA on; do not resubmit.\n- text_domain (rule name, header): Set Text Domain to leadwren-forms-sync and name the main file after the slug.\n- stable_tag (rule release, readme): Set Stable tag to 1.0.0, the Version in the\n\n[... 163 more characters cut from this display ...]",
 "facts": "The page's checks (8 fail, 2 warn, 12 pass, 2 n/a); verdict: fails review as it stands; rule | id | status | evidence:\n- 1 | trialware | fail | 6 locks in shipped code (code line 21, code line 22, code line 24, code line 28 and 2 more: $is_pro = lwfs_is_pro();) - a key that unlocks local code is trialware; enforce the plan on your service and show its answer; the interview says paid features are enforced in the plugin\n- 2 | services_documented | fail | no External services section in the readme - the code reaches api.leadwren.example, app.leadwren.example, cdn.jsdelivr.net; every service the plugin calls, your own included, is named there\n- 2 | services_links | na | no External services section yet\n- 3 | consent | fail | an activation hook calls home (the callback lwfs_activate(), code line 1) - with no opt-in: install analytics is not the service; count connections at OAuth or registration instead\n- 4 | iframe_admin | fail | an iframe in admin page code (code line 13) - build the settings screen locally and call your API\n- 4 | remote_assets | fail | enqueued from cdn.jsdelivr.net (code line 17) - your admin UI library is not the service and ships in the plugin\n- 4 | update_checker | pass | no custom update checker\n- 5 | name_brand | pass | 'Leadwren Forms Sync' is <Your brand> <job>\n- 5 | account | fail | a personal or agency account - a brand plugin from one gets flagged for\n\n[... 3641 more characters cut from this display ...]"
}

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

{
 "task": "reply",
 "summary": "The reviewer raised trialware, an activation call, undocumented services, remote admin code and the agency account; the fixed upload removes the lock, the ping, the iframe and the CDN script and documents the API.",
 "points": [
  {
   "n": 1,
   "reviewer": "The Field mapping tab is visible but locked until a license is entered, and the readme says Pro unlocks unlimited forms.",
   "fix": "The license check and the locked tab are gone; field mapping works for every user and the Leadwren service enforces the plan's limits, which the plugin only shows.",
   "check": "trialware"
  },
  {
   "n": 2,
   "reviewer": "The plugin sends the site URL and the WordPress version on activation without consent.",
   "fix": "The activation request is removed; nothing is sent until an administrator connects a Leadwren account.",
   "check": "consent"
  },
  {
   "n": 3,
   "reviewer": "Data goes to api.leadwren.example and app.leadwren.example loads in an iframe, but the readme documents neither service, the data or the terms and privacy policy.",
   "fix": "The readme has an External services section naming api.leadwren.example, the form fields sent, when, and links to the terms and privacy pages; the iframe to app.leadwren.example is gone.",
   "check": "services_documented"
  },
  {
   "n": 4,
   "reviewer": "The admin dashboard is an iframe of the web app and Chart.js loads from cdn.jsdelivr.net.",
   "fix": "The dashboard is a settings screen built in the plugin that calls the Leadwren API, and Chart.js ships inside the plugin.",
   "check": "iframe_admin"
  },
  {
   "n": 5,
   "reviewer": "Submitted from a personal account with a gmail.com address while using the Leadwren brand; reply to change the owner, do not resubmit.",
   "fix": "A Leadwren company account with a company-domain email and two-factor authentication is ready to own the plugin.",
   "check": "account"
  }
 ],
 "email": {
  "subject": "Re: [Plugin Directory] Review in Progress: Leadwren Forms Sync",
  "body": "Hello,\n\nThank you for the review. The fixed version is uploaded on the submission page and addresses each point:\n\n1. Trialware: the license check and the locked Field mapping tab are removed. Field mapping works for every user, and plan limits are enforced by [... 904 more characters cut from this display]"
 },
 "footnote": "† verify against the current developer.wordpress.org handbook and make.wordpress.org/plugins; the release process changes often.",
 "caveats": [
  "Fill in the company account's username before sending.",
  "Run Plugin Check on the zip before the upload."
 ]
}

Cut from this display only (the saved reply and the check use the full text): email.body (1163 characters, 259 shown).

What step 5 prints for it: 1 trialware | 2 consent | 3 services_documented | 4 iframe_admin | 5 account

The page's check: kept - nothing to flag

plan - A payments sync named after another brand

The page read it as: fails review as it stands; stage release blocked; top rung addon; detected hosts fonts.googleapis.com, api.paylark.example; fail checks: services_documented, name_brand, tags, compliance_claim.

Input (facts is 4703 characters; the first 1399 are shown):

{
 "task": "plan",
 "plugin": "Plugin: Tillgate Payments Sync (slug: tillgate-payments-sync)\nOur brand: Paylark; the brand in the name is ours: no\nSubmitting account: the company account; email domain: paylark.example\nStage: release blocked; paid features enforced: an add-on sold on our site; paid features that must run locally in WordPress: yes; keep an iframed admin app or own updater as core: no\nActive installs: 10,000† or more; launch date within 30 days: no",
 "readme": "readme.txt:\n=== Tillgate Payments Sync ===\nContributors: paylark\nTags: tillgate, payments, checkout, pos, point of sale, sync, inventory, orders, refunds, receipts, ecommerce, store, gateway, tillgate alternative\nTested up to: 6.6\nStable tag: 2.4.0\nLicense: GPLv2 or later\nLicense URI: https://www.gnu.org/licenses/gpl-2.0.html\n\nSync Tillgate point-of-sale payments, orders and stock with your store. Makes your store PCI compliant and GDPR compliant out of the box.\n\n== Description ==\n\nTillgate Payments Sync keeps your Tillgate point-of-sale terminal and your online store in step: payments, refunds, orders and stock levels sync both ways through the Paylark service.\n\nPaylark Pro, sold separately on our site, adds offline receipt printing and multi-location stock rules that run inside WordPress.\n\n== External services ==\n\nThis plugin connects to the Paylark API (api.paylark.example) to sync payments, orders and stock with your Tillgate account.\nIt sends order totals, line items, stock levels and the site URL whenever an order changes, and only after an administrator connects a Paylark account.\nPaylark terms of use: https://paylark.example/terms\nPaylark privacy policy: https://paylark.example/privacy\n\n== Changelog ==\n\n= 2.4.0 =\n* Faster stock sync.",
 "header": "Main plugin file header:\n<?php\n/**\n * Plugin Name: Tillgate Payments Sync\n * Plugin URI: https://paylark.example/tillgate-sync\n * Description: Syncs Tillgate point-of-sale payments, orders and stock through Paylark.\n * Version: 2.4.0\n * Requires at least: 6.3\n * Requires PHP: 8.0\n * Author: Paylark\n * Author URI: https://paylark.example\n * License: GPLv2 or later\n * Text Domain: tillgate-payments-sync\n */",
 "code": "Code excerpts:\nadd_action( 'admin_enqueue_scripts', function () {\n  wp_enqueue_style( 'tgps-font', 'https://fonts.googleapis.com/css2?family=Inter:wght@400;600', array(), null );\n} );\n\nadd_action( 'woocommerce_order_status_changed', function ( $order_id ) {\n  $order = wc_get_order( $order_id );\n  wp_remote_post( 'https://api.paylark.example/v1/orders/sync', array(\n    'headers' => array( 'Authorization' => 'Bearer ' . get_option( 'tgps_api_key' ) ),\n    'body'    => wp_json_encode( array( 'id' => $order_id, 'total' => $order->get_total() ) ),\n  ) );\n} );\n\nfunction tgps_send_usage_stats() {\n  if ( 'yes' !== get_option( 'tgps_allow_tracking', 'no' ) ) {\n    return;\n  }\n  wp_remote_post( 'https://api.paylark.example/v1/telemetry', array( 'body' => array( 'site' => home_url() ) ) );\n}\n\nadd_action( 'admin_post_tgps_save_settings', function () {\n  update_option( 'tgps_api_key', sanitize_text_field( wp_unslash( $_POST['tgps_api_key'] ) ) );\n  wp_safe_redirect( admin_url( 'options-general.php?page=tillgate' ) );\n  exit;\n} );",
 "review": "",
 "carry": "",
 "facts": "The page's checks (4 fail, 2 warn, 17 pass, 1 n/a); verdict: fails review as it stands; rule | id | status | evidence:\n- 1 | trialware | pass | no license lock, trial or local quota in the code; paid features an add-on sold on our site\n- 2 | services_documented | fail | the section \"External services\" does not name: fonts.googleapis.com (fonts are excepted from the CDN rule, not from the disclosure - name the font service or ship the font)\n- 2 | services_links | pass | terms and privacy both named\n- 3 | consent | warn | telemetry at code line 13, code line 17 - an opt-in is present (code line 14); confirm it defaults off and nothing is sent before it is ticked\n- 4 | iframe_admin | pass | no iframe in admin code\n- 4 | remote_assets | pass | only fonts load remotely (fonts.googleapis.com) - fonts are excepted\n- 4 | update_checker | pass | no custom update checker\n- 5 | name_brand | fail | 'Tillgate Payments Sync' starts with another company's brand (interview) - name it '<Job> for <Their brand>'; a brand-first name belongs to the brand owner\n- 5 | account | pass | company account, email at paylark.example\n- name | slug_words | pass | 'tillgate-payments-sync'\n- name | text_domain | pass | Text Domain: tillgate-payments-sync\n- name | name_match | pass | both read 'Tillgate Payments Sync'\n- name | headers_complete | pass | Plugin URI, Author URI, License and License URI are all set\n\n[... 3304 more characters cut from this display ...]"
}

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

{
 "task": "plan",
 "summary": "Version 2.4.0 is blocked and the plugin leads with another company's brand, claims PCI and GDPR compliance and lists 14 tags; the plan fixes the handler and the readme and ships a new version.",
 "verdict": {
  "rung": "addon",
  "why": "Offline receipt printing and multi-location stock rules must run inside WordPress, so they belong in Paylark Pro sold on your site; the free plugin may link to it but never install it or hold its code."
 },
 "fixes": [
  {
   "check": "",
   "rule": "release",
   "change": "Add a nonce check and a manage_options capability check to the admin_post_tgps_save_settings handler, then ship a new version rather than appealing the block.",
   "where": "code"
  },
  {
   "check": "name_brand",
   "rule": "5",
   "change": "Rename the display name to Payments Sync for Tillgate in the main file header and the readme title; the slug and text domain stay tillgate-payments-sync.",
   "where": "header"
  },
  {
   "check": "compliance_claim",
   "rule": "7",
   "change": "Delete the sentence that makes the store PCI compliant and GDPR compliant; describe what the plugin syncs and what it sends instead.",
   "where": "readme"
  },
  {
   "check": "tags",
   "rule": "rank",
   "change": "Cut the tags to five: payments, point of sale, inventory, orders, refunds.",
   "where": "readme"
  },
  {
   "check": "tags_brands",
   "rule": "rank",
   "change": "Drop tillgate and tillgate alternative from the tags.",
   "where": "readme"
  }
 ],
 "services": [
  {
   "host": "fonts.googleapis.com",
   "what": "Google Fonts, serving the Inter font to admin pages",
   "data": "not stated in the input",
   "when": "On admin page loads"
  },
  {
   "host": "api.paylark.example",
   "what": "The Paylark API",
   "data": "Order totals, line items, stock levels and the site URL; the site URL for usage statistics once tracking is allowed",
   "when": "Whenever an order changes, after an administrator connects a Paylark account; usage statistics only after the opt-in is ticked"
  }
 ],
 "packet": [
  {
   "item": "Plugin Check passes, Security category included, before the new tag.",
   "owner": "Engineering",
   "done": false
  },
  {
   "item": "Version bumped in the main file and Stable tag set to the new tag.",
   "owner": "Engineering",
   "done": false
  },
  {
   "item": "Release Confirmation turned on for the account.",
   "owner": "Account owner",
   "done": false
  }
 ],
 "timeline": [
  {
   "step": "Fix the handler and the readme, then commit trunk and copy it to the new tag.",
   "wait": "a cooldown of 6 hours† while the automated security review runs"
  },
  {
   "step": "Sites move off the blocked 2.4.0.",
   "wait": "sites keep the previous version until a new release passes; the listing stays up"
  },
  {
   "step": "Reply to the email once the new version is out.",
   "wait": "an appeal by email is slower than a fixed release"
  },
  {
   "step": "For a risky release, hold automatic updates with Release Confirmation.",
   "wait": "a 24-hour† delay on automatic updates"
  }
 ],
 "footnote": "† verify against the current developer.wordpress.org handbook and make.wordpress.org/plugins; the release process changes often.",
 "caveats": [
  "Above 10,000† installs, closing or transferring the plugin takes a written request to the Plugins Team - never close it to pause.",
  "The slug cannot change after approval, so the new display name and the old slug will differ."
 ]
}

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

What step 5 prints for it: no page check (code) | name_brand (header) | compliance_claim (readme) | tags (readme) | tags_brands (readme) | services_documented (code) | consent (code)

The page's check: kept - nothing to flag

Build the input yourself

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

const K = WpKit, R = WpReply;
const f = K.form(Object.assign(K.blank(), { brand: "Acme", domain: "gmail.com", stage: "notsub", paid: "plugin",
  readme: "=== Acme Forms Sync ===\nStable tag: trunk\n\nSends form entries to Acme.",
  header: "/*\n * Plugin Name: Acme Forms Sync\n * Version: 1.0.0\n */",
  code: "wp_remote_post( 'https://api.acme.example/v1/entries', $args );" }));
// f.rows (the checks), f.signals, f.hosts, f.ladder (where paid value lives), f.plan (the stage's plan)
// K.checksCsv(f), K.readmeSkeleton(f), K.board(f) - the exports
const b = K.buildInput(f, "plan");                           // or "reply" (needs review; R.laneForm(form, "reply", 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://wporg-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":"wporg-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", "plugin": "Plugin: Leadwren Forms Sync (slug: leadwren-forms-sync)
Our brand: ...", "readme": "readme.txt:
=== Leadwren Forms Sync ===
...", "header": "Main plugin file header:
...", "code": "Code excerpts:
...", "review": "", "carry": "", "facts": "The page's checks (...); verdict: fails review as it stands; rule | id | status | evidence:
- 1 | trialware | 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 wporg-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, verdict, fixes, services, packet, timeline, footnote and caveats; a reply-lane reply carries summary, points, email, footnote and caveats. The code below prints the gist of either: each fix of the plan, or each reviewer point with its fix.

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