Every route lives under squad-affiliate/v1, across twenty controller files covering affiliates, groups, referrals, payouts, withdrawals, creatives, commission rules, settings, migration, reports, the leaderboard, referring domains and requests, and the logged-out auth screens. There isn’t one permission rule behind all of it — there are three, and which one applies depends on who the route is for.
Three permission patterns#
- Admin routes — referral actions, payouts, settings, commission rules, groups, migration, sample data — require the admin capability, which defaults to
manage_optionsand is filterable throughsquad_affiliate_admin_capability. If you run a role structure where someone other than an administrator should manage the programme — a marketing manager who isn’t a WordPress admin — that filter is how you grant them the API without granting themmanage_optionssite-wide. - Affiliate-facing routes — an affiliate’s own dashboard, their links, the leaderboard, withdrawals, requests for a new referring domain — just require
is_user_logged_in(). Any signed-in user can call their own data; the route itself scopes what comes back to the caller rather than gating on a capability, since an affiliate is not necessarily anything more privileged than a subscriber. - Logged-out auth routes —
/auth/login,/auth/register— require the opposite:! is_user_logged_in(). Somebody already signed in has no business hitting a login endpoint, and these are CSRF-protected with a REST nonce like everything else on the API.
There is no fourth pattern and no endpoint that skips a check because it was convenient — a route either asks for the admin capability, asks that somebody is logged in, or asks that nobody is.
Recording a referral manually#
POST /squad-affiliate/v1/referrals lets an admin create a referral by hand — useful for a checkout the plugin has no integration for, a phone order, or correcting the record after the fact. It’s gated by the admin capability, validates that the amount is greater than zero and that the affiliate exists, and runs the same fail-open RiskAssessor every other referral goes through (see Fraud scoring, and why it fails open) — a manually-posted referral gets scored exactly like a WooCommerce one.
What it does not do is enforce the self-referral block or the one-referral-per-order guard. Those live only in ReferralCreator, the internal class the nine platform integrations call from their purchase hooks — this REST endpoint is a separate, admin-gated path for manual entry, not a second door into the same guarded flow. If you’re wiring up a bespoke checkout and want those specific guards, the honest options are: build against a supported integration’s pattern and call into ReferralCreator the way they do, or accept that a manually-posted referral is exactly as careful as the admin posting it — which is a reasonable trade for a one-off correction, and a worse one for a checkout that runs every day.
Abilities#
Four abilities are registered through the WordPress Abilities API, all in includes/Intelligence/Abilities.php: squad-affiliate/affiliates-query, squad-affiliate/affiliate-get, squad-affiliate/referrals-query and squad-affiliate/program-stats. Every one of them is read-only — each is explicitly marked readonly: true, destructive: false, idempotent: true — and every one is gated by manage_options, hardcoded rather than routed through the filterable admin capability the REST routes use. There is no ability that writes, approves or suspends anything in this build; an agent can look at your programme, not change it.
Each ability carries its own input and output schema and an honest annotation, which is what makes it safe to hand to a model in the first place: a client calling program-stats gets a typed shape back, not a REST response it has to guess the structure of. On a WordPress install older than 6.9, wp_register_ability() doesn’t exist yet, so registration is skipped entirely and silently — no abilities, no error, nothing in a log to say why an agent connected to an older site sees none of this.
MCP#
Squad Affiliate does not run its own MCP server. It registers with a separate plugin — the WordPress MCP Adapter — and checks for it with class_exists('WP\MCP\Core\McpAdapter') before doing anything. If that plugin isn’t installed and active, registration silently no-ops: no abilities are exposed over MCP, and nothing errors, because there is nothing to error — the registration call simply never happens.
Install the adapter, and the route is squad-affiliate/mcp/squad-affiliate over streamable HTTP, exposing exactly the four abilities above. An MCP tool here can’t drift from what the Abilities API already defines, because it’s the same registration reused — there’s no second, parallel tool definition to keep in sync with the first. Squad Affiliate also tightens the gate beyond the adapter’s own default: the adapter would otherwise allow any logged-in user to call a registered tool, but this plugin requires manage_options regardless of what the adapter itself permits, so installing the adapter never loosens who can ask your programme questions through it.
Application passwords#
The plugin doesn’t implement or document anything application-password-specific — there’s no per-integration provisioning screen, no dedicated settings page for it, and no code anywhere that mentions application passwords by name. What it does have is current_user_can() and is_user_logged_in() checks on every route, which is exactly what WordPress’s own Application Passwords feature authenticates against. So it works, transparently, the same way any other WordPress REST client authenticates against a capability check — it’s WordPress core doing the work, not something Squad Affiliate built on top of it. If you’re scripting against the API or connecting a third-party tool, generate an application password for a user with the access level that route needs (an administrator for the admin routes, any logged-in user for their own affiliate data) the same way you would for any other WordPress site.
Where this fits with the rest of the plugin#
The REST API, the abilities and MCP are three views onto the same admin screens and the same data — none of them is a shortcut around a check the admin UI itself makes. A commission rule created over the API goes through the same CommissionRuleRepository validation the settings screen uses; a referral queried through an ability reads from the same ReferralRepository the admin table renders from. If you’re deciding which surface to use for something — scripting a bulk import, wiring an external dashboard, letting an AI agent answer “how did last week go” — the answer is usually whichever one already has the shape you need: the REST API for anything that writes, an ability (through MCP or directly) for anything that only needs to read and summarise.