API Documentation
Introduction
The Coupon Affiliates REST API gives external tools, scripts and AI agents structured JSON access to your affiliate program: affiliates, coupons, referred orders, commission, payouts, registrations, click tracking, a change-event feed and store-wide reports. It also delivers outbound webhooks (PRO), so other systems can react the moment something happens in your program. On the free version the same ground is covered by polling the change-event feed, which carries a cursor for exactly that.
Everything lives under one base URL on your own site:
https://example.com/wp-json/wcusage/v2
A first request, using a WordPress application password for an admin user:
curl https://example.com/wp-json/wcusage/v2/me \
-u "admin:abcd efgh ijkl mnop qrst uvwx"
{
"user": { "id": 1, "display_name": "Admin", "login": "admin", "email": "[email protected]" },
"is_admin": true,
"is_affiliate": false,
"auth": { "method": "wordpress", "key_id": null, "scopes": null },
"coupons": [],
"api_version": "8.2.0"
}
What you can do with it
- Report — pull program totals, per-affiliate stats, per-coupon sales and commission, and referred-order lists into a BI tool, spreadsheet or custom dashboard.
- Automate — approve or decline affiliate applications, create payout requests, and react to program events in real time.
- Integrate — connect Zapier/Make-style automation, a CRM, an accounting system, or an AI agent that answers questions about your affiliates.
- Build affiliate-facing tools — an affiliate can authenticate as themselves and read only their own data, so you can build a custom affiliate app without handing out admin access.
Who can use it
- Admins — any user who passes the plugin's admin access check (usually
manage_woocommerce, or whichever capability you configured) can read and manage everything. - Affiliés — can read their own coupons, stats, referred orders, click stats and payouts, and can request a payout for their own coupon. They cannot see other affiliates, store-wide reports or the events feed.
- Multi-level uplines (PRO) — can also read a downline affiliate's coupon, stats and referred orders.
- Nobody else. There are no anonymous endpoints other than the OpenAPI document.
Requirements
- Coupons affiliés 8.2 or later, with the API switched on (see the next page).
- The WordPress REST API enabled — it is by default.
- Pretty permalinks, if you want to use the
/wp-json/…URL form. On a site with plain permalinks, usehttps://example.com/?rest_route=/wcusage/v2/meinstead: the/wp-json/path is not a REST request at all on such a site, and API keys will not authenticate against it. - HTTPS. API keys are refused over plain HTTP outside local and development environments.
- Some endpoints depend on features that may not be present (payouts, registrations, clicks, activity log). Those answer
501rather than pretending to be empty — see Availability.
Enabling the API
The API ships switched off. A site never starts answering API requests just because the plugin was updated — an administrator has to turn it on deliberately.
- Aller à Coupon Affiliates → Admin Tools → API in wp-admin.
- Press Enable API at the top of the page.
- Create an API key, or use a WordPress application password.
The same screen shows your base URL, the OpenAPI URL, every registered endpoint with the access level it needs, your API keys, and — on PRO — your webhooks.
Turning individual endpoints on and off
Each endpoint has its own checkbox on the API screen. Unticking one removes it from the route table for every request — it answers 404 with the core code rest_no_route, exactly as though it had never been registered. This is the right tool for narrowing what any integration can reach at all, regardless of who authenticates.
The settings live in the wcusage_api_settings option. Only the exceptions are stored, so an endpoint added in a later release is on by default once the API itself is on.
What the master switch controls
| Behaviour | API on | API off |
|---|---|---|
wcusage/v2 routes | Registered | Not registered (404) |
| API key authentication | Works | Refused everywhere, v1 included |
| Webhook deliveries (PRO) | Queued and sent | Not queued; already-queued retries are dropped |
| Adding and testing webhooks in wp-admin (PRO) | Works | Works |
Legacy woo-coupon-usage/v1 routes | Available | Still available (application password or cookie auth only) |
Authentication
Every endpoint except /openapi requires an authenticated WordPress user. Three methods work; each resolves to a user, and that user's capabilities decide what the request may access.
Application passwords
Built into WordPress. Create one under Users → Profile → Application Passwords, then send it with HTTP Basic auth:
curl https://example.com/wp-json/wcusage/v2/affiliates \
-u "admin:abcd efgh ijkl mnop qrst uvwx"
An application password carries the user's full permissions, on this API and on every other WordPress endpoint. It is the quickest way to get started, and fine for a trusted server-to-server integration you control.
API keys
Plugin API keys are the better choice for anything you do not fully control, because they can be scoped, expired and revoked without touching the WordPress account. Create one under Coupon Affiliates → Admin Tools → API, or through the key management endpoints.
A key looks like wcus_9f2c1e4a… and is shown once, at creation — only a SHA-256 hash is stored. Send it as a bearer token:
curl https://example.com/wp-json/wcusage/v2/me \
-H "Authorization: Bearer wcus_9f2c1e4a7b3d5e6f8a9b0c1d2e3f4a5b6c7d8e9f"
If your client cannot set the Authorization header (some hosts strip it), an alternative header is accepted:
X-WCUsage-API-Key: wcus_9f2c1e4a7b3d5e6f8a9b0c1d2e3f4a5b6c7d8e9f
Every key:
- Acts as one WordPress user. A key created against an affiliate's account can only ever reach that affiliate's own data, whatever it asks for. A key against an admin account has admin access.
- Carries scopes that restrict it further, on top of that user's capabilities.
- Works only on this plugin's namespaces —
wcusage/v2etwoo-coupon-usage/v1. Presenting one to a core or WooCommerce endpoint fails with403 wcusage_api_key_wrong_namespace. An API key is not a general-purpose WordPress credential. - Can have an optional expiry date, and can be revoked at any moment. Revocation takes effect on the very next request.
- Records a last used timestamp, updated at most once every five minutes — a liveness signal, not an audit log.
Scopes
| Scope | Grants |
|---|---|
read | Every GET endpoint: affiliates, coupons, stats, referred orders, payouts, registrations, clicks, events, reports. |
write | Creating and changing data: payout requests, payout status changes, registration approval and decline, and refreshing stored coupon stats. |
manage | Managing the API itself: creating and revoking API keys, and — on PRO — creating, editing, testing and deleting webhooks. |
manage is not a peer of the other two. A key holding it can issue itself another key with any scope, and can point a webhook at a server of its choosing. Treat it as full access for the user behind the key, and give it only to integrations that genuinely administer the API.Scopes apply seulement to API keys. With an application password or a logged-in cookie, auth.scopes est null and access is governed purely by capabilities. A key missing a required scope receives 403 with the code wcusage_api_insufficient_scope.
On the legacy woo-coupon-usage/v1 namespace, which predates scopes, keys are held to a fail-closed default: GET, HEAD et OPTIONS need read, anything else needs write.
Cookie authentication
Same-site JavaScript can use the logged-in cookie with a REST nonce, exactly as with any WordPress REST endpoint:
fetch( '/wp-json/wcusage/v2/me', {
headers: { 'X-WP-Nonce': wpApiSettings.nonce },
credentials: 'same-origin'
} ).then( r => r.json() );
HTTPS
Bearer tokens are credentials, so they are refused over plain HTTP unless wp_get_environment_type() rapports local ou development. Such a request fails with 401 wcusage_api_https_required. Les wcusage_api_require_https filter can override it — do that only if you know exactly why.
Failed-attempt lockout
An address that presents 20 invalid keys within 15 minutes is refused further attempts with 429 wcusage_api_too_many_auth_failures until the window rolls over. Counts are kept per IP address, so one client cannot lock out another. Adjust with the wcusage_api_max_auth_failures filter.
Who am I?
Whatever method you use, GET /me reports exactly what the request is authenticated as, what access level it has, and which scopes are in force. Call it first when setting up any integration.
Requests & Responses
Conventions
- All responses are JSON.
GETparameters go in the query string;POSTetPATCHparameters go in a JSON body withContent-Type: application/json(WordPress also accepts form-encoded bodies). - Resources are addressed by numeric ID. Coupon codes are never identifiers: WooCommerce coupon codes are case-insensitive and are not guaranteed unique.
- Money values are plain JSON numbers in the store's currency — no symbol, no thousands separators.
GET /reports/summaryreports the currency code. - Collections are returned as a bare JSON array, with totals in response headers.
Pagination
Every collection endpoint takes the same two parameters:
| Param | Type | Description |
|---|---|---|
| page | integer | Page number, starting at 1. Default 1. |
| per_page | integer | Items per page, 1–100. Default 20. |
And returns the same two headers:
X-WP-Total— total matching items.X-WP-TotalPages— total pages at the currentper_page.
A page past the end of the result set returns an empty array with correct headers, without running the underlying query.
Dates
- Request parameters (
from,à,expires) useY-m-d, e.g.2026-08-01. Anything else is rejected with400 rest_invalid_param. - Response fields use ISO 8601 in the site's timezone, e.g.
2026-08-07T14:03:22. Empty dates arenull. - Ranges include both ends. When
fromis given withoutà,àdefaults to today.
Errors
Errors use the standard WordPress REST shape. Every code raised by this plugin is prefixed wcusage_api_:
{
"code": "wcusage_api_forbidden",
"message": "You do not have permission to access this resource.",
"data": { "status": 403 }
}
Rate-limit and throttle errors add retry_after (seconds) to data.
| Statut | Meaning |
|---|---|
| 400 | Invalid parameter, or the action is not possible right now (no unpaid balance, status already set, activity log disabled). |
| 401 | Not authenticated, or the API key is invalid, revoked, expired, or was sent over plain HTTP. |
| 403 | Authenticated but not permitted — wrong capability, missing scope, or a key used outside the plugin's namespaces. |
| 404 | Not found. Also returned for a resource that exists but belongs to somebody else, and for an endpoint switched off in the settings. |
| 409 | Conflict — another request is mid-flight, or the resource changed underneath this one. |
| 429 | Rate limited, throttled, or locked out after repeated authentication failures. |
| 500 | Server error, e.g. the site could not generate a secure token. |
| 501 | The feature this endpoint reads is not present on this install (payouts add-on inactive, table missing). |
404, not 403. That is deliberate: it stops an authenticated affiliate walking the ID space to learn which coupons, payouts or affiliates exist. So do not read a 404 as proof that an ID is unused.A full list of plugin error codes is in the Error reference.
Rate limiting
Requests to both plugin namespaces are counted per minute, per identity — the API key, the logged-in user, or a shared bucket for anonymous callers:
- Authenticated: 120 requests per minute
- Unauthenticated: 30 requests per minute
Over the limit, requests get 429 wcusage_api_rate_limited with retry_after: 60. Malformed requests are counted too, so a client stuck retrying a bad parameter is throttled like any other. Adjust the ceiling with the wcusage_api_rate_limit filter.
Performance & Caching
Most endpoints read cached figures and are cheap enough to poll. A few recalculate from the order history and are deliberately expensive; those are cached and throttled so one client cannot make the database do unbounded work.
What is cached
| Endpoint | Behaviour |
|---|---|
/coupons/{id}/stats (all-time) | Reads the stored snapshot — the same figures the affiliate dashboard shows. One meta read. Reports source: "cache". |
/coupons/{id}/stats (with from/àou refresh=true) | Recalculates from the orders. Each range is cached 60 seconds; an uncached calculation is limited to one per coupon per minute. |
/coupons/{id}/orders | The prepared list is cached whole for 60 seconds per coupon + range + status, so paginating through it costs nothing. An uncached combination is limited to one per coupon per minute. |
/affiliates/{id}/stats (with dates) | Cached 60 seconds per range; uncached calculations limited to one per affiliate per minute. |
/reports/summary | Cached 5 minutes per top value. refresh=true bypasses it. |
| Everything else | Answered live from indexed queries. |
Throttle responses
When an uncached recalculation is asked for inside the one-per-minute window, the response is 429 with code wcusage_api_throttled et retry_after: 60. The one exception is all-time coupon stats, which return the stored snapshot with source: "throttled" rather than failing.
429s.Working efficiently
- For "what changed?", poll
/events?after={id}— one cheap indexed query — or use webhooks and stop polling altogether. - For "how is the program doing?", one call to
/reports/summaryreplaces walking every affiliate. - Utilisation
per_page=100on collections instead of many small pages. - Prefer all-time stats (no
from/à) for frequent reads; ask for a range only when you need one.
Security & Disclaimers
The API hands external systems real access to your affiliate program — personal data, commission figures and, in some configurations, the ability to move money. Please read this page before building anything against it.
Your responsibilities
Enabling the API is an explicit decision, and everything done with the credentials you issue is done on your authority and under your account. In particular, you are responsible for:
- Every key you create. A key acts as the WordPress user you attached it to, with that user's capabilities. Creating a key against an administrator account and pasting it into a third-party service gives that service administrator-level reach over your affiliate data.
- Where the data goes. Once a response leaves your server, its handling is governed by whoever received it — the SaaS platform, the spreadsheet, the AI provider — not by this plugin.
- What your automations do. Approving applications, cancelling payouts and creating payout requests are all real, consequential actions. The API performs what it is asked to; it cannot know whether the request reflects what you intended.
- Storing credentials safely. Keys and webhook secrets belong in a secret store or environment variable, never in a repository, a support ticket, a screenshot or a shared document.
Personal data
Several endpoints return personal data about real people: affiliate names, logins and email addresses, registration profile fields such as phone numbers and websites, and any custom registration fields your store collects.
- Under the GDPR, the UK GDPR and comparable regimes, you are the data controller for that information. Sending it to a third-party tool generally makes that tool a processor, which usually means you need a lawful basis for the transfer and an appropriate data-processing agreement with the provider.
- Deliberate omissions in this API are safety features, not oversights: order rows carry no customer data, payout destination details are never returned, click IP addresses are never exposed, and logins and emails are withheld from callers who are neither an admin nor the person concerned. If you add fields back with the response filters, you take on responsibility for what you have exposed and to whom.
- Grant the narrowest access that does the job. A read-only key on a dedicated account, scoped to the endpoints you actually call, limits the blast radius of a leak far more effectively than anything you can add afterwards.
Money and financial records
Commission figures, payout amounts and report totals come from the plugin's own records and are provided for information. They are pas an accounting system, a tax record or a substitute for your payment provider's statements.
- Reconcile against your gateway and your books before paying anybody or filing anything. Figures can legitimately change — refunds, cancellations, manual adjustments and snapshot rebuilds all move them.
- All-time statistics are read from stored snapshots that can lag reality in either direction. Where the number matters, check
sourceetlast_refreshed, or request a fresh calculation. - The plugin's authors accept no responsibility for payments made, withheld or duplicated as a result of an integration you build.
Treat inbound webhooks as untrusted
A webhook receiver is a public HTTP endpoint that anybody on the internet can send a request to. Nothing about a payload proves it came from your store except a valid signature.
- Always verify the HMAC signature and reject stale timestamps before acting on a delivery. If your platform cannot verify signatures, do not let it take consequential action on a payload alone — read the object back from the API first.
- Deliveries are at-least-once and unordered. Build handlers that tolerate duplicates and out-of-sequence arrivals.
- Validate and escape payload values like any other external input before writing them into your own systems.
Third-party and AI services
Connecting an external platform or an AI assistant means transmitting affiliate data to that provider, where it may be logged, retained or used for their own purposes according to their terms — not yours. Check what you are agreeing to before you connect it.
AI agents carry a specific extra risk: an agent acts on text it reads, and some of that text can come from outside your control — an application's "how will you promote us" field, a campaign name, a website URL. An agent with write access can be steered by content like that into taking actions you never asked for. Give agents read-only keys unless you have a concrete reason not to, and never give one the manage scope.
Stability and support
- Endpoints, response fields and defaults may change between plugin versions. Fields may be added at any time, so write clients that ignore unfamiliar keys rather than failing on them.
- Do not depend on anything not documented here — internal option names, database tables, undocumented fields or the exact wording of a message. The
codein an error response is the stable part; themessageis not, and is translated. - Rate limits, cache windows and throttles exist to protect your site and may be adjusted in future releases. Handle
429gracefully rather than assuming a fixed budget. - Support covers the plugin and its own endpoints. Debugging custom integrations, third-party platforms and AI tooling is outside its scope, though the
/me,/openapiet/webhooksendpoints are usually enough to work out where a problem lies.
No warranty
Coupon Affiliates is free software, licensed under the GNU General Public License version 3. Sections 15 and 16 of that license disclaim all warranties and limit liability, and those terms apply to the API exactly as they apply to the rest of the plugin. The API, the code samples in these docs and the integration patterns they describe are therefore provided as is, without warranty of any kind.
The examples are illustrative starting points, not production-hardened code: they omit the logging, retry policy, input validation and secret management your own environment will need. Review, adapt and test anything you take from here before relying on it.
To the fullest extent permitted by law, the plugin's authors accept no liability for loss or damage arising from use of the API — including lost or exposed data, incorrect commission or payout amounts, missed or duplicated webhook deliveries, or the actions of any third-party service or automated agent you connect to it. Nothing here affects any statutory rights that cannot lawfully be excluded.
Licensing
The plugin, its API and its add-ons are distributed under the GPLv3; a copy ships as license.txt in the plugin folder. You are free to use, study, modify and redistribute the code on those terms.
- Nothing on this page restricts those rights. It is operational guidance and a disclaimer of warranty and liability — which the GPL expressly permits — not an additional condition on using the software.
- The code samples in these docs are yours to use. Copy, adapt and ship them in your own integrations, commercial or otherwise, with no attribution required. They are short illustrative snippets, published so that people can build against the API.
- Your integration is your own work. A client that talks to this API over HTTP is a separate program; calling a REST endpoint does not make your codebase a derivative of the plugin. If you instead modify or bundle the plugin's own PHP, the GPL applies to what you distribute in the ordinary way.
- The licence covers the software, not your obligations. It says nothing about data protection, consumer law or your duties to your own affiliates and customers — those apply regardless of how the plugin is licensed.
Endpoint Index
Every route in the API, with the access level and scope it needs. All paths are relative to https://example.com/wp-json/wcusage/v2.
| Method & path | Accès | Scope | Purpose |
|---|---|---|---|
GET /me | Any logged-in user | — | Identify the caller, its access level and scopes. |
GET /affiliates | Administrateur | read | List affiliates with coupons and balances. |
GET /affiliates/{id} | Admin or self | read | One affiliate, with profile fields and groups. |
GET /affiliates/{id}/stats | Admin or self | read | Totals across all of an affiliate's coupons. |
GET /coupons | Administrateur | read | List affiliate coupons. |
GET /coupons/{id} | Admin, owner, upline | read | One coupon, with commission rates and referral URL. |
GET /coupons/{id}/stats | Admin, owner, upline | read (+write to refresh) | Sales and commission, all-time or by date range. |
GET /coupons/{id}/orders | Admin, owner, upline | read | Orders referred by a coupon, with commission per order. |
GET /payouts | Any logged-in user | read | List payouts. Non-admins see only their own. |
POST /payouts | Admin or coupon owner | write | Request a payout for a coupon's unpaid balance. |
GET /payouts/{id} | Admin or owner | read | One payout. |
POST /payouts/{id}/status | Administrateur | write | Change a payout status. Bookkeeping only. |
GET /registrations | Administrateur | read | List affiliate applications. |
GET /registrations/{id} | Administrateur | read | One application. |
POST /registrations/{id}/status | Administrateur | write | Approve or decline an application. |
GET /clicks/stats | Admin, or owner with coupon_id | read | Clicks, conversions and conversion rate. |
GET /events | Administrateur | read | Change feed with a cursor for polling. |
GET /reports/summary | Administrateur | read | Store-wide totals and top affiliates. |
GET /keys | Administrateur | manage | List API keys. |
POST /keys | Administrateur | manage | Create an API key. |
DELETE /keys/{id} | Administrateur | manage | Revoke an API key. |
GET /webhooks | Administrateur | manage | PRO. List webhook endpoints. |
POST /webhooks | Administrateur | manage | PRO. Create a webhook endpoint. |
PATCH /webhooks/{id} | Administrateur | manage | PRO. Change status or subscribed events. |
DELETE /webhooks/{id} | Administrateur | manage | PRO. Delete a webhook endpoint. |
POST /webhooks/{id}/test | Administrateur | manage | PRO. Send a test delivery. |
GET /webhooks/events | Administrateur | read | PRO. The catalog of subscribable events. |
GET /openapi | Public by default | — | Machine-readable description of this API. |
"Owner" means the affiliate the coupon is assigned to; "upline" means their multi-level parent, which exists in PRO only. Payout and webhook routes are PRO only. Every endpoint can additionally be switched off per site — see Enabling the API.
Me
Identify the authenticated caller, its access level and its scopes. This is the ideal first call for any integration — an AI agent can use it to discover what it is allowed to do before attempting anything.
GET /wp-json/wcusage/v2/me
Permission: any authenticated user. No scope is needed for the identity fields; the coupons array requires read.
{
"user": { "id": 1456, "display_name": "Sarah J", "login": "sarahj", "email": "[email protected]" },
"is_admin": false,
"is_affiliate": true,
"auth": { "method": "api_key", "key_id": 3, "scopes": ["read"] },
"coupons": [
{
"id": 8338,
"code": "sarah10",
"user_id": 1456,
"date_created": "2023-05-02T10:11:12",
"unpaid_commission": 40.46,
"pending_order_commission": 0,
"pending_payout_commission": 0
}
],
"api_version": "8.2.0"
}
| Champ d'application | Type | Description |
|---|---|---|
| utilisateur | object | id et display_name always; login et courriel only when the caller is an admin or is that user. |
| is_admin | boolean | Whether the caller passes the plugin's admin access check. |
| is_affiliate | boolean | Whether the user has at least one affiliate coupon. |
| auth.method | string | api_key ou wordpress. |
| auth.key_id | integer | The API key's ID, or null. |
| auth.scopes | array | Scopes in force, or null for capability-based auth (full access for that user). |
| coupons | array | The caller's own affiliate coupons with balances. Empty for non-affiliates. |
| api_version | string | The installed Coupon Affiliates version. |
auth.scopes: null means the request is pas scope-limited — it authenticated with an application password or cookie, so the user's capabilities are the only limit.Affiliés
An affiliate is a WordPress user with at least one published coupon assigned to them. There is no separate affiliate table; these endpoints derive the entity the same way the admin list table does, and aggregate across all of an affiliate's coupons.
List affiliates
GET /wp-json/wcusage/v2/affiliates
Permission: admin, read scope.
| Param | Type | Description |
|---|---|---|
| recherche | string | Partial match against user login, email or display name. |
| page / per_page | integer | Standard pagination. |
[
{
"user": { "id": 1456, "display_name": "Sarah J", "login": "sarahj", "email": "[email protected]" },
"coupons": [
{ "id": 8338, "code": "sarah10", "user_id": 1456, "date_created": "2023-05-02T10:11:12",
"unpaid_commission": 40.46, "pending_order_commission": 0, "pending_payout_commission": 0 }
],
"unpaid_commission": 40.46,
"pending_payout_commission": 0
}
]
Results are ordered by user ID ascending. Only users holding at least one published assigned coupon appear.
Get one affiliate
GET /wp-json/wcusage/v2/affiliates/{user_id}
Permission: admin, or the affiliate themselves. read scope.
Returns the list shape plus detail fields, and each coupon carries its cached all-time stats block:
| Extra field | Type | Description |
|---|---|---|
| date_registered | string | When the WordPress account was created. |
| profile | object | Registration profile fields: phone, website, promote, referrer. |
| groups | array | Affiliate group roles the user holds. |
| mla_parents | object | PRO only. Multi-level upline chain, keyed by tier. Absent entirely in the free build. |
Affiliate stats
GET /wp-json/wcusage/v2/affiliates/{user_id}/stats
Permission: admin, or the affiliate themselves. read scope.
| Param | Type | Description |
|---|---|---|
| from | date | Optional start date (Y-m-d). When set, figures are recalculated from the orders for the range instead of read from the all-time cache. |
| à | date | Optional end date. Defaults to today when from is set. |
{
"user_id": 1456,
"from": null,
"to": null,
"totals": {
"orders_count": 19,
"total_sales": 1977.80,
"total_discount": 197.78,
"total_commission": 181.60,
"unpaid_commission": 40.46,
"pending_payout_commission": 0
},
"coupons": [
{ "id": 8338, "code": "sarah10", "orders_count": 19, "total_sales": 1977.80,
"total_discount": 197.78, "total_commission": 181.60 }
]
}
The two balance figures in totals are always current balances; they are not affected by from/à.
429 wcusage_api_throttled.Coupons
Affiliate coupons, with balances, stats and referred orders. Reads go through the same functions the affiliate dashboard uses, so caps, rounding and commission rules always match what affiliates see.
List coupons
GET /wp-json/wcusage/v2/coupons
Permission: admin, read scope.
| Param | Type | Description |
|---|---|---|
| user_id | integer | Only coupons assigned to this affiliate. Omit for every assigned affiliate coupon. |
| recherche | string | Match against the coupon code. |
| page / per_page | integer | Standard pagination. |
Returns published coupons that have an affiliate assigned, newest first. Coupons with no assigned affiliate are never listed.
Get one coupon
GET /wp-json/wcusage/v2/coupons/{id}
Permission: admin, the assigned affiliate, or their multi-level upline (PRO). read scope.
{
"id": 8338,
"code": "sarah10",
"user_id": 1456,
"user": { "id": 1456, "display_name": "Sarah J" },
"date_created": "2023-05-02T10:11:12",
"unpaid_commission": 40.46,
"pending_order_commission": 0,
"pending_payout_commission": 0,
"stats": {
"orders_count": 19, "total_sales": 1977.80, "total_discount": 197.78,
"total_shipping": 0, "total_commission": 181.60, "last_refreshed": "2026-08-01T02:00:00"
},
"commission": { "percent": 10, "percent_override": "", "fixed_per_order": "", "fixed_per_product": "" },
"referral_url": "https://example.com/affiliate-dashboard/?couponid=sarah10"
}
| Champ d'application | Description |
|---|---|
| commission_non_rémunérée | Commission earned, cleared, and not yet paid out or requested. |
| pending_order_commission | Commission on orders still inside the pending period (not yet payable). |
| pending_payout_commission | Commission tied up in payouts that have been requested but not paid. |
| commission.percent | The percentage rate resolved for this coupon: its own override if it has one, otherwise the store's default rate. |
| commission.percent_override | The per-coupon override itself. Empty when the coupon inherits the store default. |
| commission.fixed_per_order | Fixed amount per referred order, if configured. Empty when unused. |
| commission.fixed_per_product | Fixed amount per product, if configured. Empty when unused. |
| referral_url | The affiliate's referral URL for this coupon, pointing at the affiliate dashboard page. |
| stats.last_refreshed | When the stored snapshot was last rebuilt, or null if it never has been. |
Draft and private coupons are addressable — a coupon does not have to be published to have an affiliate and a balance. Trashed and auto-draft coupons answer 404.
code_ambiguous below.Coupon stats
GET /wp-json/wcusage/v2/coupons/{id}/stats
Permission: admin, owner, or MLA upline. read scope; refresh=true additionally needs write.
| Param | Type | Description |
|---|---|---|
| from / to | date | Optional range. Without dates, the stored all-time snapshot is returned (fast). With dates, the range is calculated from the orders. |
| refresh | boolean | Recalculate the all-time figures from the orders and save the result. Default faux. Ignored when a date range is given, since those are always calculated. |
{
"coupon_id": 8338,
"code": "sarah10",
"from": null,
"to": null,
"source": "cache",
"code_ambiguous": false,
"orders_count": 19,
"total_sales": 1977.80,
"total_discount": 197.78,
"total_shipping": 0,
"total_commission": 181.60,
"status_counts": { "Completed": 17, "Refunded": 2 },
"last_refreshed": "2026-08-01T02:00:00",
"unpaid_commission": 40.46,
"pending_order_commission": 0,
"pending_payout_commission": 0
}
| Champ d'application | Description |
|---|---|
| source | cache — the stored snapshot or a short-lived cached calculation. live — freshly calculated for this request. throttled — a rebuild was wanted but the per-coupon budget was spent, so the stored snapshot was returned instead. |
| code_ambiguous | vrai when another published coupon answers to the same code. The stats layer is keyed by code, so figures for this coupon are not reliably addressable and are never saved back to it. |
| status_counts | Order counts by display status name, e.g. {"Completed": 17}. An empty object when the figures came from the stored snapshot, which holds no breakdown. |
| last_refreshed | null for ranged requests — those are always calculated, so there is no "last refreshed" moment to report. |
The response shape is stable regardless of which path answered: status_counts et last_refreshed are always present, even when empty.
refresh=true is a write wearing a GET's clothes — it rescans the coupon's entire order history and saves the result. A read-only key gets 403 wcusage_api_insufficient_scope; it still receives figures from the stored snapshot on an ordinary request.Referred orders
GET /wp-json/wcusage/v2/coupons/{id}/orders
Permission: admin, owner, or MLA upline. read scope.
| Param | Type | Description |
|---|---|---|
| from / to | date | Optional date range. |
| status | string | Order status slug without the wc- prefix, e.g. achevé. |
| page / per_page | integer | Standard pagination. |
[
{
"order_id": 8355,
"date": "2023-07-28T12:46:58",
"status": "completed",
"total": 179.80,
"discount": 17.98,
"commission": 16.18
}
]
Newest first. Only orders that actually counted towards cette coupon are listed — an order that used the code but was later reassigned to another affiliate is excluded, which keeps this endpoint consistent with /stats.
One request considers at most 5,000 orders (the newest ones). When that cap is hit, the response carries the header X-WCUsage-Truncated: 1, so a client can tell a capped total from a real one. Raise it with the wcusage_api_max_order_rows filter if your program needs a deeper window.
wcusage_api_order_item filter.Paiements
Read payout history and create payout requests. Payouts are a PRO feature; on a build or install without them, these endpoints are absent or answer 501 wcusage_api_unavailable.
gateway_triggered.These are the highest-consequence endpoints in the API. Prove any automation on a staging store before pointing it at a live one, reconcile amounts against your payment provider rather than treating these figures as an accounting record, and keep the responsibility for verifying payments with a person. If you would rather nothing automated could ever create a payout, switch the /payouts endpoints off on the API screen — everything else keeps working.
List payouts
GET /wp-json/wcusage/v2/payouts
Permission: any authenticated user, read scope. Non-admins are always restricted to their own payouts, whatever user_id they send.
| Param | Type | Description |
|---|---|---|
| user_id | integer | Filter by affiliate. Admin only — overridden for everyone else. |
| coupon_id | integer | Filter by coupon. |
| status | string | One of pending, created, paid, cancel. |
| from / to | date | Filter by request date. |
| page / per_page | integer | Standard pagination. |
{
"id": 321,
"user": { "id": 1456, "display_name": "Sarah J" },
"coupon_id": 8338,
"coupon_code": "sarah10",
"amount": 40.46,
"method": "PayPal",
"method_type": "paypal",
"status": "pending",
"has_details": true,
"transaction_id": "",
"invoice_id": 0,
"date": "2026-08-07T09:00:00",
"date_paid": null
}
has_details tells you whether any are stored.Get one payout
GET /wp-json/wcusage/v2/payouts/{id}
Permission: admin or the payout's owner. read scope. Somebody else's payout answers 404.
Request a payout
POST /wp-json/wcusage/v2/payouts
Permission: admin (any coupon), or the coupon's affiliate (their own). write scope.
| Body param | Type | Description |
|---|---|---|
| coupon_id | integer | Required. The coupon's full unpaid balance is requested; there is no partial-amount parameter. |
curl -X POST https://example.com/wp-json/wcusage/v2/payouts \
-H "Authorization: Bearer wcus_..." \
-H "Content-Type: application/json" \
-d '{"coupon_id": 8338}'
Returns 201 with the created payout, plus two extra fields:
gateway_triggered—vraiwhen auto-accept moved the payout straight tocreatedoupaid, meaning a real gateway may have been called.gateway_notice— present only when a gateway handler produced a message worth passing on.
Idempotency
Only one open request can exist per coupon at a time. If one already exists (pending, created ou traitement), the existing payout is returned with 200 and the header X-WCUsage-Existing: 1 — so a retrying client can never create duplicates. Concurrent requests for the same coupon are serialised with a lock; the loser gets 409 wcusage_api_in_progress.
Rules that are enforced
Requests are held to the same rules as the affiliate dashboard, so the API cannot be used to route around a store's configuration:
| Error code | Condition |
|---|---|
payouts_disabled | Payouts are switched off in the plugin settings. |
requests_disabled | Affiliate self-service requests are switched off; only the store owner creates payouts. Not applied to admin callers. |
no_affiliate | The coupon has no affiliate assigned. |
no_balance | The unpaid balance is zero or less. |
below_threshold | The unpaid balance is under the store's minimum payout threshold. Enforced for admins too. |
no_payout_details | The affiliate has not saved payout details and the store requires them. |
method_disabled | The affiliate's saved payout method is no longer enabled on the store. Not applied to admin callers. |
invoice_required | The store requires an invoice upload for this payout method. Invoices cannot be uploaded through the API, so the request must be made from the affiliate dashboard. |
not_created | Everything checked out but the site refused the request — typically a custom wcusage_before_payout_submit filter, or a failed write. |
Update payout status
POST /wp-json/wcusage/v2/payouts/{id}/status
Permission: admin, write scope.
| Body param | Type | Description |
|---|---|---|
| status | string | Required. One of pending, created, paid, cancel. |
This runs the same status flow as the admin screen: balances move between the unpaid, pending-payout and paid pools, the activity log records the change, notification emails are sent and webhooks fire. No payment gateway is contacted.
Permitted transitions
| From | To |
|---|---|
pending | paid, cancel, created |
created | paid, cancel, pending |
paid | cancel, pending, created |
cancel | pending, created |
Anything else is refused with 400 wcusage_api_invalid_transition rather than leaving the balances inconsistent. Setting the status a payout already has returns 400 wcusage_api_no_change.
traitement et failed are deliberately not accepted. Nothing in the plugin writes them and the status arithmetic has no branch for them, so moving a payout through either one would strand its balance with no way back.Two further guards:
- Re-opening a cancelled payout takes its amount back out of the unpaid balance. If that money is no longer there — already paid out, or the balance was edited — the request is refused with
409 wcusage_api_insufficient_unpaidrather than letting the ledger drift. - Concurrent status changes are settled by a conditional update. Only the request that actually changes the row runs the balance arithmetic; the loser gets
409 wcusage_api_conflict.
Enregistrements
Read and moderate affiliate applications. These endpoints read the plugin's registration table, and approval goes through the same function the admin screen calls, so the full accept flow runs identically.
List registrations
GET /wp-json/wcusage/v2/registrations
Permission: admin, read scope.
| Param | Type | Description |
|---|---|---|
| status | string | pending, accepted ou declined. |
| user_id | integer | Filter by WordPress user. |
| page / per_page | integer | Standard pagination. |
{
"id": 512,
"user": { "id": 2201, "display_name": "New Affiliate", "login": "newaffiliate", "email": "[email protected]" },
"coupon_code": "newaff15",
"status": "pending",
"type": "",
"promote": "Instagram and my newsletter",
"referrer": "",
"website": "https://blog.example.net",
"custom_fields": { "Audience size": "12000" },
"date": "2026-08-05T18:22:41",
"date_accepted": null
}
custom_fields holds whatever extra registration fields the store has configured, as a flat object. It is an empty object when there are none.
Get one registration
GET /wp-json/wcusage/v2/registrations/{id}
Permission: admin, read scope.
Approve or decline
POST /wp-json/wcusage/v2/registrations/{id}/status
Permission: admin, write scope.
| Body param | Type | Description |
|---|---|---|
| status | string | Required. accepted ou declined. |
| message | string | Optional message included in the notification email. |
| send_email | boolean | Whether to send the notification email. Default vrai. |
Returns the updated registration.
| Error code | Condition |
|---|---|
no_change (400) | The registration already has that status. |
already_accepted (400) | The registration was already accepted. Accepting is not reversible through the API, because an accept → decline → accept cycle would create a second published coupon with the same code and its own separate balance. Reverse it from the admin screens if you really must. |
no_coupon_code (409) | The registration carries no coupon code, so its status cannot be changed. Add one from the admin screens first. |
not_updated (409) | Something on the site refused the change. The response reports what the row actually says rather than what was asked for. |
unavailable (501) | The registrations table does not exist on this install. |
Clicks
Aggregated referral-link click statistics, counted in SQL rather than loaded row by row.
GET /wp-json/wcusage/v2/clicks/stats
Permission: admin for store-wide figures; affiliates must pass a coupon_id they own. read scope.
| Param | Type | Description |
|---|---|---|
| coupon_id | integer | Limit to one coupon. Required for non-admin users — omitting it returns 400 wcusage_api_coupon_required. |
| campaign | string | Filter by campaign name (exact match). |
| from / to | date | Optional date range. |
{
"coupon_id": 8338,
"campaign": null,
"from": "2026-07-01",
"to": "2026-07-31",
"clicks": 412,
"conversions": 19,
"conversion_rate": 4.61
}
conversion_rate is a percentage rounded to two decimals, and is 0 when there were no clicks.
501 wcusage_api_unavailable.Events
A cursor-based change feed over the plugin's activity log. This is the polling counterpart to webhooks: ask "what happened since event X" and act on the answer. Webhooks are PRO, so on the free version this endpoint is how you keep an external system in step.
GET /wp-json/wcusage/v2/events
Permission: admin, read scope.
| Param | Type | Description |
|---|---|---|
| after | integer | Cursor. Returns only events with a higher ID, oldest first. Without it, newest first. |
| event | string | Filter by event type. |
| user_id | integer | Filter by the acting user. |
| page / per_page | integer | Standard pagination. page is ignored in cursor mode. |
[
{ "id": 9911, "event": "payout_paid", "event_id": 321, "user_id": 1, "info": "40.46", "date": "2026-08-07T10:15:00" },
{ "id": 9912, "event": "referral", "event_id": 8355, "user_id": 0, "info": "sarah10", "date": "2026-08-07T10:16:31" }
]
The response header X-WCUsage-Last-Event carries the highest event ID returned. Store it and pass it as after on the next poll:
GET /wp-json/wcusage/v2/events?after=9912&per_page=100
→ X-WCUsage-Last-Event: 9987
Event types
| Événement | event_id refers to |
|---|---|
referral | Order |
commission_added, commission_removed | Coupon |
mla_commission_added, mla_commission_removed | Coupon (PRO) |
registration, registration_accept | Registration |
payout_request, payout_paid, payout_reversed, payout_cancelled | Payout (PRO) |
reward_earned | Reward (PRO) |
new_campaign | Campaign (PRO) |
direct_link_domain | Direct link (PRO) |
mla_invite | Invite (PRO) |
lifetime_link_edited | Customer (PRO) |
api_key_created, api_key_revoked | API key |
user_id is the acting user, and is 0 for system and guest actions such as a referral from a logged-out shopper. info is free-form context whose meaning depends on the event type.
registration.declined webhook (PRO) if you need them.400 wcusage_api_log_disabled. Webhooks are unaffected — they fire from the same funnel, before the log setting is consulted.Rapports
A store-wide program summary, built for dashboards and AI assistants that need the whole picture in one call.
GET /wp-json/wcusage/v2/reports/summary
Permission: admin, read scope.
| Param | Type | Description |
|---|---|---|
| top | integer | How many top affiliates to include, by all-time commission. 0–50, default 10. |
| refresh | boolean | Bypass the 5-minute report cache. Default faux. |
{
"generated": "2026-08-07T14:05:00",
"currency": "USD",
"totals": {
"affiliates": 448,
"coupons": 512,
"orders_count": 10231,
"total_sales": 812337.20,
"total_discount": 79110.55,
"total_commission": 81233.71,
"unpaid_commission": 6120.44,
"pending_payout_commission": 1240.00
},
"payouts": { "pending_count": 12, "pending_amount": 1240.00, "paid_count": 981, "paid_amount": 73873.27 },
"pending_registrations": 7,
"top_affiliates": [
{ "user_id": 1456, "orders_count": 19, "total_sales": 1977.80, "total_commission": 181.60,
"user": { "id": 1456, "display_name": "Sarah J", "login": "sarahj", "email": "[email protected]" } }
],
"cached": false
}
cached: true means the response came from the 5-minute cache. The paiements block is present only when the payouts add-on is available — the free build omits it entirely rather than reporting four zeros as though the program had simply never paid anybody.
commission_non_rémunérée, pending_payout_commission) are always current.API Key Management
Keys can be managed in the admin UI (Coupon Affiliates → Admin Tools → API) or programmatically. Every route here requires an admin with the manage scope.
List keys
GET /wp-json/wcusage/v2/keys
Standard pagination. Returns metadata only — never tokens or hashes:
[
{
"id": 4,
"user_id": 1456,
"description": "Zapier integration",
"key_prefix": "wcus_9f2c1e",
"scopes": ["read"],
"status": "active",
"last_used": "2026-08-07T13:55:00",
"date_created": "2026-08-01T09:12:00",
"date_expires": null
}
]
key_prefix is the first 12 characters of the token — enough to recognise a key in your own logs, useless as a credential.
Create a key
POST /wp-json/wcusage/v2/keys
| Body param | Type | Description |
|---|---|---|
| user_id | integer | The user the key acts as. Defaults to the current user. Use an affiliate's user ID to create a key limited to their own data. |
| description | string | Label, e.g. "Zapier integration". Up to 200 characters. |
| scopes | array | Any of read, write, manage. Default ["read"]. |
| expires | date | Optional expiry (Y-m-d). The key stops working at the end of that day, in the site's timezone. |
{
"id": 4,
"token": "wcus_9f2c1e4a7b3d5e6f8a9b0c1d2e3f4a5b6c7d8e9f",
"notice": "Store this token now - it cannot be shown again.",
"user_id": 1456
}
201. Only a SHA-256 hash is stored on the server, so a lost token cannot be recovered — revoke it and create another.You may always create a key for yourself. Creating one for another user requires the capability to edit that user, so a lower-privileged manager cannot mint a key that acts as a full administrator. Otherwise the response is 403 wcusage_api_cannot_create_for_user.
Revoke a key
DELETE /wp-json/wcusage/v2/keys/{id}
{ "revoked": true }
Revocation is immediate: clients using the key receive 401 on their next request. It is idempotent — revoking an already-revoked key reports success. The same "could you edit that user" rule applies, so one plugin admin cannot destroy an administrator's integration credential.
Key creation and revocation are both recorded in the activity log (api_key_created, api_key_revoked), so they show up in /events.
Webhooks Overview
/webhooks* routes are not registered and the Webhooks card does not appear on the API screen. The free equivalent is to poll /events, which carries a cursor so you only ever fetch what is new.Webhooks push signed JSON notifications to a URL you choose, the moment something happens in your affiliate program — no polling required. Set them up under Coupon Affiliates → Admin Tools → API → Webhooks, or through the management API.
Every lifecycle event in the plugin already flows through one funnel, and the webhook dispatcher listens to it. Deliveries are queued (Action Scheduler when WooCommerce provides it, otherwise WP-Cron), so your endpoint never slows down a checkout or an admin action.
Available events
Every event below needs PRO, since webhooks themselves do. These first ones are raised by the core plugin, so they arrive on any PRO install:
| Événement | Fires when |
|---|---|
referral.created | A referred order is attributed to an affiliate. |
registration.created | A new affiliate registration is submitted. |
registration.accepted | A registration is approved. |
registration.declined | A registration is declined. |
commission.added | Commission is credited to an affiliate. |
commission.removed | Commission is removed (refund, cancellation, manual deduction). |
affiliate.created | An affiliate's coupon has been created and is ready to use. Fires after approval, and also when an admin creates an affiliate directly. |
These further events come from individual PRO add-ons, so they need that add-on to be active as well:
| Événement | Fires when | Add-on |
|---|---|---|
payout.requested | An affiliate requests a payout. | Paiements |
payout.paid | A payout is marked as paid. | Paiements |
payout.reversed | A payout is reversed. | Paiements |
payout.cancelled | A payout is cancelled and its amount returned to the unpaid balance. | Paiements |
affiliate.payout_details_updated | An affiliate changes their payout method or details. | Paiements |
reward.earned | An affiliate earns a reward or bonus. | Rewards |
campaign.created | An affiliate creates a campaign. | Campagnes |
directlink.created | An affiliate registers a direct-link domain. | Lien direct |
commission.mla_added | Multi-level commission is credited to an upline. | Multi-Level |
commission.mla_removed | Multi-level commission is removed from an upline. | Multi-Level |
mla.invite_created | A multi-level affiliate invite is created. | Multi-Level |
mla.sub_registered | Someone registers as a sub-affiliate under an existing affiliate. | Multi-Level |
Subscribe an endpoint to specific events, or to * for everything. The live catalog for votre install is always available at GET /wp-json/wcusage/v2/webhooks/events — it lists only events this build can actually raise, so you never subscribe to a notification that could never arrive.
affiliate.payout_details_updated arriving shortly before payout.requested is a well-known fraud pattern. It is worth watching even if you do nothing else with webhooks.Endpoint requirements
- The delivery URL must be HTTPS and publicly reachable. Loopback and private-network addresses are rejected at creation. Local development environments may use HTTP — see the
wcusage_api_webhook_require_httpsfilter. - Respond with any 2xx status within 8 seconds. Anything else counts as a failed delivery.
- Do the real work asynchronously. Acknowledge first, process afterwards.
Deliveries & Security
Payload format
Every delivery is a JSON POST with the same envelope. The data block is rebuilt from the live object at delivery time, so a retry never carries stale figures:
POST /your-endpoint HTTP/1.1
Content-Type: application/json
User-Agent: CouponAffiliates-Webhook/8.2.0
X-WCUsage-Event: payout.paid
X-WCUsage-Delivery: whd_66b4a1e2c3d4f5.12345678
X-WCUsage-Signature: t=1786457100,v1=5f8a2b...
{
"event": "payout.paid",
"created": "2026-08-07T14:05:00+00:00",
"site": "https://example.com",
"data": {
"payout_id": 321,
"user_id": 1456,
"coupon_id": 8338,
"amount": 40.46,
"method": "PayPal",
"method_type": "paypal",
"status": "paid",
"date": "2026-08-07T09:00:00",
"date_paid": "2026-08-07T14:04:58"
}
}
| Envelope field | Description |
|---|---|
| event | The webhook event name, e.g. payout.paid. |
| created | When the delivery was built, RFC 3339 in UTC. |
| site | The sending site's home URL — useful when one receiver serves several stores. |
| data | Event-specific payload; shapes below. |
Payload shapes by event
| Event(s) | data fields |
|---|---|
referral.created | order_id, coupon, user_id, commission |
commission.added, commission.removed, commission.mla_added, commission.mla_removed | coupon_id, coupon, user_id, note, order_id (parsed from the note; null when there is none) |
payout.requested, payout.paid, payout.reversed, payout.cancelled | payout_id, user_id, coupon_id, amount, method, method_type, status, date, date_paid |
registration.created, registration.accepted, registration.declined | registration_id, user_id, coupon_code, status, type, date |
affiliate.created | user_id, coupon, coupon_id, coupon_ids (every coupon they now hold) |
affiliate.payout_details_updated | user_id, method_type, has_details |
directlink.created | directlink_id, coupon_id, coupon, user_id, website, campaign, status |
mla.invite_created | invite_id, user_id, status, date |
mla.sub_registered | user_id, parent_user_id, coupon |
reward.earned, campaign.created | object_id, info |
ping (test delivery) | message |
mla.invite_created, for example, deliberately omits the invitee's email address. Add fields for your own trusted receiver with the wcusage_api_webhook_payload filter.Verifying signatures
Each webhook has a secret (whsec_…), shown in the admin UI and returned once on creation through the API. Every delivery is signed with HMAC-SHA256 over "<timestamp>.<raw body>":
X-WCUsage-Signature: t=<unix timestamp>,v1=<hex hmac>
Verify it before trusting anything in the payload:
<?php
$secret = 'whsec_your_webhook_secret';
$body = file_get_contents( 'php://input' );
$header = $_SERVER['HTTP_X_WCUSAGE_SIGNATURE'] ?? '';
$parts = [];
foreach ( explode( ',', $header ) as $pair ) {
[ $k, $v ] = array_pad( explode( '=', $pair, 2 ), 2, '' );
$parts[ trim( $k ) ] = trim( $v );
}
$expected = hash_hmac( 'sha256', ( $parts['t'] ?? '' ) . '.' . $body, $secret );
if ( ! hash_equals( $expected, $parts['v1'] ?? '' ) ) {
http_response_code( 401 );
exit; // signature mismatch
}
if ( abs( time() - (int) ( $parts['t'] ?? 0 ) ) > 300 ) {
http_response_code( 401 );
exit; // replayed or stale delivery
}
http_response_code( 200 );
// then process json_decode( $body, true ) out of band
hash_equals), sign over the raw body rather than a re-encoded copy, and reject timestamps older than a few minutes to prevent replay.Retries and automatic disabling
- Deliveries are queued asynchronously, so your endpoint is never in the critical path of a checkout or an admin action.
- A failed delivery is retried up to 5 attempts with exponential backoff: roughly 1 minute, 4 minutes, 16 minutes, then about an hour.
- Après 25 consecutive failures the endpoint is automatically disabled. Re-enable it from the admin page or with a
PATCHrequest; either resets the failure counter and clears the last error. - Delivery is at-least-once. Duplicates are possible, so use
X-WCUsage-Deliveryto deduplicate if your handler is not idempotent. - Ordering is not guaranteed. Two events raised close together may arrive in either order, and a retried delivery may land after a newer one.
- The last failure message and the failure count are visible on the admin page and in
GET /webhooks.
Tune the two limits with the wcusage_api_webhook_max_attempts et wcusage_api_webhook_max_failures filters.
Webhook Management API
All routes require an admin with the manage scope, except the event catalog, which needs read. PRO only — none of these routes are registered in the free build, so they answer 404 rest_no_route there.
List webhooks
GET /wp-json/wcusage/v2/webhooks
[
{
"id": 1,
"name": "Ops Slack relay",
"url": "https://hooks.example.net/coupon-affiliates",
"events": ["payout.requested", "payout.paid"],
"status": "active",
"failures": 0,
"last_delivery": "2026-08-07T14:05:01",
"last_error": "",
"date_created": "2026-07-30T11:00:00"
}
]
Signing secrets are not included in list responses. Full administrators can read them on the admin page; other plugin admins see them masked.
Create a webhook
POST /wp-json/wcusage/v2/webhooks
| Body param | Type | Description |
|---|---|---|
| name | string | Label for the webhook. Up to 100 characters. |
| url | string | Required. HTTPS delivery URL. Validated against loopback and private addresses. |
| events | array | Required. Event names from the catalog, or ["*"] for all. Unknown names are dropped; if nothing valid remains the request fails with 400 wcusage_api_no_events. |
Returns 201 including the signing secret. Store it — it is the only way to verify deliveries, and this is the only response that carries it.
Update a webhook
PATCH /wp-json/wcusage/v2/webhooks/{id}
| Body param | Type | Description |
|---|---|---|
| status | string | active ou disabled. Re-activating resets the failure counter and clears the last error. |
| events | array | Replaces the subscribed events entirely. |
Sending neither returns 400 wcusage_api_no_fields.
Test a webhook
POST /wp-json/wcusage/v2/webhooks/{id}/test
Sends a signed ping delivery immediately and synchronously, and returns the HTTP status your endpoint answered with:
{ "code": 200 }
A transport-level failure (DNS, TLS, timeout) returns 502 wcusage_api_delivery_failed with the underlying message. Test deliveries do not count towards the failure counter.
Delete a webhook
DELETE /wp-json/wcusage/v2/webhooks/{id}
{ "deleted": true }
Event catalog
GET /wp-json/wcusage/v2/webhooks/events
Permission: admin, read scope.
[
{ "event": "referral.created", "description": "A referred order was attributed to an affiliate." },
{ "event": "payout.paid", "description": "A payout was marked as paid." }
]
Read this rather than hard-coding the list: it reflects exactly which add-ons are active on the install.
Code Samples
Nothing here needs a client library — the API is plain HTTP with a bearer token. These are complete, working starting points.
PHP (inside WordPress)
Talking to another store from a WordPress site, using the HTTP API that is already loaded:
$response = wp_remote_get(
'https://example.com/wp-json/wcusage/v2/reports/summary?top=5',
array(
'timeout' => 15,
'headers' => array(
'Authorization' => 'Bearer ' . WCUSAGE_API_KEY,
'Accept' => 'application/json',
),
)
);
if ( is_wp_error( $response ) ) {
error_log( 'Coupon Affiliates API unreachable: ' . $response->get_error_message() );
return;
}
$code = wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( 200 !== $code ) {
// Every plugin error carries a machine-readable code.
error_log( 'API error ' . $code . ': ' . ( $body['code'] ?? 'unknown' ) );
return;
}
printf( 'Unpaid commission: %s %.2f', $body['currency'], $body['totals']['unpaid_commission'] );
PHP (standalone, with pagination)
Walking a collection to the end, using the pagination headers rather than guessing:
<?php
function wcu_api_get( $path, $params = [] ) {
$url = 'https://example.com/wp-json/wcusage/v2' . $path;
if ( $params ) {
$url .= '?' . http_build_query( $params );
}
$ch = curl_init( $url );
curl_setopt_array( $ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => true,
CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv( 'WCUSAGE_KEY' ) ],
CURLOPT_TIMEOUT => 20,
] );
$raw = curl_exec( $ch );
$status = curl_getinfo( $ch, CURLINFO_RESPONSE_CODE );
$header_size = curl_getinfo( $ch, CURLINFO_HEADER_SIZE );
curl_close( $ch );
$headers = substr( $raw, 0, $header_size );
$body = json_decode( substr( $raw, $header_size ), true );
$pages = 1;
if ( preg_match( '/^X-WP-TotalPages:\s*(\d+)/mi', $headers, $m ) ) {
$pages = (int) $m[1];
}
return [ 'status' => $status, 'body' => $body, 'pages' => $pages ];
}
$page = 1;
do {
$result = wcu_api_get( '/affiliates', [ 'page' => $page, 'per_page' => 100 ] );
if ( 200 !== $result['status'] ) {
throw new RuntimeException( 'API error: ' . ( $result['body']['code'] ?? $result['status'] ) );
}
foreach ( $result['body'] as $affiliate ) {
printf( "%-30s %8.2f unpaid\n",
$affiliate['user']['display_name'],
$affiliate['unpaid_commission']
);
}
$page++;
} while ( $page <= $result['pages'] );
JavaScript / Node
Including the one piece of error handling that matters most in practice — honouring retry_after:
const BASE = 'https://example.com/wp-json/wcusage/v2';
const KEY = process.env.WCUSAGE_KEY;
async function api( path, params = {} ) {
const url = new URL( BASE + path );
Object.entries( params ).forEach( ( [ k, v ] ) => url.searchParams.set( k, v ) );
const res = await fetch( url, { headers: { Authorization: `Bearer ${KEY}` } } );
const body = await res.json();
if ( res.status === 429 ) {
const wait = ( body?.data?.retry_after ?? 60 ) * 1000;
await new Promise( r => setTimeout( r, wait ) );
return api( path, params ); // one retry after the window
}
if ( ! res.ok ) {
throw new Error( `${body.code}: ${body.message}` );
}
return { body, total: Number( res.headers.get( 'X-WP-Total' ) || 0 ) };
}
const { body: me } = await api( '/me' );
console.log( `Authenticated as ${me.user.display_name}, admin: ${me.is_admin}, scopes:`, me.auth.scopes );
Python
import os, requests
BASE = "https://example.com/wp-json/wcusage/v2"
SESSION = requests.Session()
SESSION.headers["Authorization"] = f"Bearer {os.environ['WCUSAGE_KEY']}"
def get(path, **params):
r = SESSION.get(f"{BASE}{path}", params=params, timeout=20)
if r.status_code != 200:
raise RuntimeError(f"{r.status_code} {r.json().get('code', '')}")
return r
# All referred orders for one coupon last month, newest first.
page, orders = 1, []
while True:
r = get("/coupons/8338/orders", **{"from": "2026-07-01", "to": "2026-07-31",
"page": page, "per_page": 100})
orders += r.json()
if r.headers.get("X-WCUsage-Truncated") == "1":
print("Warning: order window was capped; narrow the date range.")
if page >= int(r.headers.get("X-WP-TotalPages", 1)):
break
page += 1
print(f"{len(orders)} orders, {sum(o['commission'] for o in orders):.2f} commission")
Command line
# Quick health check
curl -s https://example.com/wp-json/wcusage/v2/me \
-H "Authorization: Bearer $WCUSAGE_KEY" | jq
# Top 5 affiliates by all-time commission
curl -s "https://example.com/wp-json/wcusage/v2/reports/summary?top=5" \
-H "Authorization: Bearer $WCUSAGE_KEY" \
| jq -r '.top_affiliates[] | "\(.user.display_name)\t\(.total_commission)"'
# Anything that needs paying out
curl -s "https://example.com/wp-json/wcusage/v2/payouts?status=pending&per_page=100" \
-H "Authorization: Bearer $WCUSAGE_KEY" \
| jq -r '.[] | "#\(.id)\t\(.user.display_name)\t\(.amount)\t\(.method)"'
No-Code Platforms
Zapier, Make, n8n, Pabbly Connect, Activepieces and similar tools all speak plain HTTP, so no special connector is needed. There are two ways to wire them up.
Push: webhook trigger (recommended, PRO)
- In your automation tool, create a scenario starting with a Webhook / Catch Hook trigger and copy the URL it gives you.
- In WordPress, go to Coupon Affiliates → Admin Tools → API → Webhooks and add an endpoint with that URL, subscribed to the events you care about.
- Press Test. Your tool receives a
pingpayload and learns the structure. - Map the fields from
datainto whatever comes next — a Slack message, a spreadsheet row, a CRM record.
This is instant, costs no polling quota, and scales to any program size.
GET /payouts/{id}) before acting on it.Pull: scheduled polling
If you are on the free version, or your tool cannot receive webhooks, poll the change feed on a schedule instead:
- Add a Scheduled trigger (every 5–15 minutes is plenty).
- Add an HTTP GET module pointed at
https://example.com/wp-json/wcusage/v2/eventswith the queryafter={{ last_id }}&per_page=100, and a headerAuthorization: Bearer wcus_…. - Store the response header
X-WCUsage-Last-Eventin a data store / variable, and feed it back aslast_idnext run. - Iterate the returned array and branch on
event.
Starting from after=0 would replay the whole history, so seed the cursor once with a single unfiltered call and store the highest id you see.
Writing back
Actions such as approving an application are ordinary HTTP requests:
Method: POST
URL: https://example.com/wp-json/wcusage/v2/registrations/512/status
Headers: Authorization: Bearer wcus_...
Content-Type: application/json
Body: { "status": "accepted", "send_email": true }
Use a key with write for this, and keep it separate from any read-only key you use elsewhere so you can revoke one without breaking the other.
Reporting & Dashboards
Google Sheets
An Apps Script bound to a sheet, refreshing a leaderboard on a timer:
function refreshAffiliates() {
const key = PropertiesService.getScriptProperties().getProperty( 'WCUSAGE_KEY' );
const url = 'https://example.com/wp-json/wcusage/v2/reports/summary?top=50';
const res = UrlFetchApp.fetch( url, {
headers: { Authorization: 'Bearer ' + key },
muteHttpExceptions: true
} );
if ( res.getResponseCode() !== 200 ) {
throw new Error( res.getContentText() );
}
const data = JSON.parse( res.getContentText() );
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName( 'Affiliates' );
sheet.clear();
sheet.appendRow( [ 'Affiliate', 'Orders', 'Sales', 'Commission' ] );
data.top_affiliates.forEach( function ( a ) {
sheet.appendRow( [ a.user.display_name, a.orders_count, a.total_sales, a.total_commission ] );
} );
sheet.getRange( 'A1:D1' ).setFontWeight( 'bold' );
}
Add a time-driven trigger for refreshAffiliates and store the key under Project Settings → Script Properties, not in the code.
Looker Studio, Power BI, Metabase
All three can read a JSON/REST source with a bearer header. Point them at:
/reports/summary?top=50for the program overview — one row of totals plus a leaderboard, already aggregated and cached for five minutes./affiliates?per_page=100for a per-affiliate table, walking pages withX-WP-TotalPages./coupons/{id}/orders?from=&to=for transaction-level detail, one coupon at a time.
/reports/summary is cached for exactly that long, so a faster schedule returns identical data (cached: true) while still spending rate limit.Warehousing the data
For an incremental ETL, drive it from /events rather than re-reading collections. Store the last event ID alongside your data, pull only what is new, and reconcile occasionally with a full pass over /affiliates. That keeps a nightly job to a handful of requests regardless of program size.
Notifications
A small receiver that verifies the signature and relays events into Slack. The same shape works for Discord, Teams, Telegram or an SMS gateway — only the last few lines change.
<?php
// webhook-receiver.php
$secret = getenv( 'WCUSAGE_WEBHOOK_SECRET' );
$body = file_get_contents( 'php://input' );
$header = $_SERVER['HTTP_X_WCUSAGE_SIGNATURE'] ?? '';
$parts = [];
foreach ( explode( ',', $header ) as $pair ) {
[ $k, $v ] = array_pad( explode( '=', $pair, 2 ), 2, '' );
$parts[ trim( $k ) ] = trim( $v );
}
$expected = hash_hmac( 'sha256', ( $parts['t'] ?? '' ) . '.' . $body, $secret );
if ( ! hash_equals( $expected, $parts['v1'] ?? '' ) || abs( time() - (int) ( $parts['t'] ?? 0 ) ) > 300 ) {
http_response_code( 401 );
exit;
}
// Acknowledge immediately - the sender allows 8 seconds and retries on anything else.
http_response_code( 200 );
fastcgi_finish_request();
$payload = json_decode( $body, true );
$data = $payload['data'] ?? [];
switch ( $payload['event'] ) {
case 'payout.requested':
$text = sprintf( ':moneybag: Payout requested: %s asked for %.2f via %s (payout #%d)',
get_display_name( $data['user_id'] ), $data['amount'], $data['method'], $data['payout_id'] );
break;
case 'registration.created':
$text = sprintf( ':wave: New affiliate application for code *%s*', $data['coupon_code'] );
break;
case 'affiliate.payout_details_updated':
$text = sprintf( ':warning: Affiliate %d changed their payout details (%s)',
$data['user_id'], $data['method_type'] ?: 'none set' );
break;
default:
return; // not interested
}
$ch = curl_init( getenv( 'SLACK_WEBHOOK_URL' ) );
curl_setopt_array( $ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [ 'Content-Type: application/json' ],
CURLOPT_POSTFIELDS => json_encode( [ 'text' => $text ] ),
CURLOPT_RETURNTRANSFER => true,
] );
curl_exec( $ch );
Two details worth copying:
- Acknowledge before working.
fastcgi_finish_request()(or a queue) returns the 2xx immediately, so a slow Slack call never turns into a retry storm. - Ignore unknown events. New event types appear in later releases; a receiver that throws on them will disable itself after 25 failures.
* and filtering in your receiver is easier to maintain than editing the subscription list every time you want another event — as long as your handler ignores what it does not recognise.AI Agents & Assistants
The API was designed with agents in mind: stable numeric IDs, plain JSON, self-describing errors, a machine-readable spec and a single "who am I" call.
Setting one up
- Create a WordPress user for the agent, at the access level it genuinely needs — an affiliate account for an affiliate-facing assistant, an admin account only for a program-management assistant.
- Create an API key against that user with the
readscope only, and an expiry date. - Give the tool the OpenAPI URL:
https://example.com/wp-json/wcusage/v2/openapi. Custom GPT Actions, most agent frameworks and any OpenAPI-aware tool builder import it directly, generating one callable function per endpoint. - Configure bearer authentication with the key.
For frameworks without OpenAPI import, hand-define a handful of tools instead — /me, /reports/summary, /affiliates/{id}/stats, /coupons/{id}/orders et /events cover the overwhelming majority of questions.
Prompting notes
- Tell the agent to call
/mefirst. It learns whether it is an admin or a single affiliate, and which scopes it holds, instead of guessing and hitting403s. - Tell it to reference coupons by ID, not code.
- Tell it that
404can mean "not yours", so it should not conclude an ID is unused. - Tell it to respect
retry_afteron429rather than retrying immediately — otherwise a loop burns the whole rate limit. - Point it at
/reports/summaryfor "how are we doing" questions. Left to itself an agent will happily page through every affiliate to compute a total the API already has.
Safety
An agent acts on the text it reads, and some of that text comes from outside your control — an applicant's "how will you promote us" answer, a campaign name, a website URL an affiliate typed in. An agent with write access can be steered by content like that into doing something you never asked for. Read-only access removes the problem entirely; anything more needs a person in the loop on the actions that matter.
manage scope. A key holding it can mint further keys with any scope and point a webhook at any server — that is full account access, in the hands of something driven by text it reads from the outside world.If an agent must act, grant write deliberately and remember what it can then do: approve or decline applications, create payout requests, and change payout statuses. Payout status changes never contact a gateway, but on a store with automatic payouts a created payout request can pay real money immediately. If that is a concern, switch the /payouts endpoints off on the API screen and let the agent read everything else.
Recipes
Sync new referrals into another system
Poll the change feed and keep a cursor — one indexed query per poll, no matter how busy the program is:
curl -s "https://example.com/wp-json/wcusage/v2/events?after=9912&event=referral&per_page=100" \
-H "Authorization: Bearer $WCUSAGE_KEY" -D headers.txt
# the new cursor for next time
grep -i '^X-WCUsage-Last-Event' headers.txt
On PRO, subscribe a webhook to referral.created and skip the polling entirely.
Build a monthly affiliate statement
# totals for the month
GET /wp-json/wcusage/v2/affiliates/1456/stats?from=2026-07-01&to=2026-07-31
# the orders behind them, one coupon at a time
GET /wp-json/wcusage/v2/coupons/8338/orders?from=2026-07-01&to=2026-07-31&per_page=100
Keep the range fixed between calls so both are served from cache rather than rescanned.
Auto-approve applications that meet your criteria
# 1. find pending applications
GET /wp-json/wcusage/v2/registrations?status=pending&per_page=100
# 2. approve the ones that qualify
POST /wp-json/wcusage/v2/registrations/512/status
{ "status": "accepted", "message": "Welcome aboard!", "send_email": true }
Needs an admin key with write. Remember that accepting is one-way through the API, so apply your criteria before calling, not after.
Give an affiliate read-only API access to their own data
POST /wp-json/wcusage/v2/keys
{ "user_id": 1456, "description": "Sarah's reporting script", "scopes": ["read"], "expires": "2027-01-01" }
The resulting key can call /me, /affiliates/1456/stats, its own coupons, their orders and click stats, and /payouts filtered to itself — and nothing else. Creating it requires manage plus the capability to edit that user.
Reconcile payouts with your accounting system
# everything paid in a period
GET /wp-json/wcusage/v2/payouts?status=paid&from=2026-07-01&to=2026-07-31&per_page=100
Match on transaction_id where your gateway provides one, and on id otherwise. date is when the payout was requested and date_paid when it was settled — use the latter for period allocation.
Flag a payout-fraud pattern
Subscribe a webhook to affiliate.payout_details_updated et payout.requested. When both arrive for the same user_id within a short window, hold the payout for manual review:
POST /wp-json/wcusage/v2/payouts/321/status
{ "status": "cancel" }
Cancelling returns the amount to the affiliate's unpaid balance, so nothing is lost; re-open it with pending once you are satisfied.
Refresh stale coupon statistics nightly
GET /wp-json/wcusage/v2/coupons/8338/stats?refresh=true
Rebuilds the stored snapshot from the order history and saves it, so the affiliate's dashboard and every later API read are current. Needs the write scope, and is limited to one rebuild per coupon per minute — space the calls out, and treat source: "throttled" as "try this one again later".
Check the health of your integration
GET /wp-json/wcusage/v2/me # what am I, and what may I do?
GET /wp-json/wcusage/v2/webhooks # failures, last_error, last_delivery
GET /wp-json/wcusage/v2/keys # last_used, status, date_expires
A webhook with a rising failures count, or a key whose last_used has gone stale, is usually the first sign that something upstream broke.
OpenAPI Document
Fetching the spec
GET /wp-json/wcusage/v2/openapi
Returns an OpenAPI 3.1 document generated live from the registered routes — every path, parameter, enum, default and required flag, plus both security schemes (bearer API key and Basic application password). Because it is generated from the actual route definitions, it never drifts from the implementation, and it automatically reflects which endpoints you have switched off and which add-ons are active.
The document is public by default, since the WordPress REST index already enumerates routes. Restrict it to admins with:
add_filter( 'wcusage_api_openapi_public', '__return_false' );
Amend the generated spec — to add descriptions, tags or examples — with the wcusage_api_openapi_spec filter.
What it contains
{
"openapi": "3.1.0",
"info": {
"title": "Coupon Affiliates REST API",
"description": "Affiliate data for WooCommerce: affiliates, coupons, referred orders, commission, payouts, registrations, clicks, events and reports.",
"version": "8.2.0"
},
"servers": [ { "url": "https://example.com/wp-json/wcusage/v2" } ],
"paths": {
"/coupons/{id}/stats": {
"get": {
"operationId": "get_coupons_id_stats",
"parameters": [
{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } },
{ "name": "from", "in": "query", "required": false, "schema": { "type": "string" } },
{ "name": "refresh", "in": "query", "required": false, "schema": { "type": "boolean", "default": false } }
],
"security": [ { "bearerApiKey": [] }, { "basicAppPassword": [] } ],
"responses": { "200": { "description": "Success." }, "401": { "description": "Authentication required or invalid." }, "403": { "description": "Insufficient permissions or scope." } }
}
}
},
"components": { "securitySchemes": { "bearerApiKey": { "type": "http", "scheme": "bearer" }, "basicAppPassword": { "type": "http", "scheme": "basic" } } }
}
Path parameters, query parameters and JSON request bodies are all derived from the routes' own argument definitions, including types, enums, defaults and required flags. Because the document is built per request, it reflects exactly what cette install exposes: endpoints switched off in the settings are absent, and so are routes belonging to add-ons that are not active.
Using it is covered under AI Agents & Assistants et No-Code Platforms — most tools that accept an OpenAPI URL will generate a working client from it directly.
Availability
Not every endpoint exists on every install. Three things decide it: the plugin edition, which add-ons are active, and which endpoints an administrator has switched on.
| Endpoint | Gratuit | PRO | Notes |
|---|---|---|---|
/me, /openapi | Oui | Oui | |
/affiliates, /coupons, /reports/summary | Oui | Oui | PRO adds mla_parents to the affiliate detail view and the paiements block to the report. |
/registrations | Oui | Oui | 501 when the registrations table is absent. |
/clicks/stats | Oui | Oui | 501 when the clicks table is absent. |
/events | Oui | Oui | 501 when the activity table is absent; 400 when the activity log is switched off. |
/keys | Oui | Oui | |
/webhooks | — | Oui | Not registered at all in the free build. Poll /events au lieu de cela. |
/payouts | — | Oui | Not registered at all in the free build; 501 when the add-on is inactive. |
MLA-related access (an upline reading a downline's coupon) and the add-on webhook events likewise exist only where those add-ons do. The safest way to discover what a given install offers is to read /openapi et /webhooks/events rather than assuming.
Error Reference
All codes are prefixed wcusage_api_ in the response. Core WordPress codes such as rest_no_route, rest_invalid_param et rest_missing_callback_param can also appear.
| Code | Statut | Meaning |
|---|---|---|
unauthorized | 401 | No authenticated user. Send an application password or an API key. |
forbidden | 403 | Authenticated, but this user may not access the resource. |
invalid_key | 401 | The API key is unknown, revoked or expired. |
invalid_key_user | 401 | The user the key acts as no longer exists. |
https_required | 401 | API keys may only be used over HTTPS. |
too_many_auth_failures | 429 | Too many invalid keys from this address; wait for the window to roll over. |
insufficient_scope | 403 | The key lacks the scope this route needs. |
key_wrong_namespace | 403 | An API key was presented to an endpoint outside the plugin's namespaces. |
rate_limited | 429 | Per-minute request limit exceeded. See retry_after. |
throttled | 429 | An expensive recalculation was requested again too soon. See retry_after. |
not_found | 404 | No such resource — or it belongs to somebody else. |
unavailable | 501 | The feature or table this endpoint reads is not present on this install. |
log_disabled | 400 | The activity log is switched off, so the events feed is empty. |
coupon_required | 400 | Non-admin callers must pass a coupon_id à /clicks/stats. |
no_change | 400 | The payout or registration already has the requested status. |
already_accepted | 400 | The registration was already accepted; reversal is admin-only. |
no_coupon_code | 409 | The registration has no coupon code, so its status cannot change. |
not_updated | 409 | Something on the site refused the registration status change. |
payouts_disabled | 400 | Payout requests are switched off in the plugin settings. |
requests_disabled | 403 | Affiliates cannot self-request payouts on this store. |
no_affiliate | 400 | The coupon has no affiliate assigned. |
no_balance | 400 | There is no unpaid commission to request. |
below_threshold | 400 | The unpaid balance is under the store's minimum payout threshold. |
no_payout_details | 400 | The affiliate has not saved payout details. |
method_disabled | 400 | The saved payout method is no longer enabled on the store. |
invoice_required | 400 | This payout method requires an invoice upload; use the affiliate dashboard. |
in_progress | 409 | A payout request for this coupon is already being processed. |
not_created | 409 | The payout request was refused by a filter or a failed write. |
invalid_transition | 400 | That payout status change is not permitted through the API. |
insufficient_unpaid | 409 | A cancelled payout cannot be re-opened; its amount is no longer in the unpaid balance. |
conflict | 409 | The payout status changed while this request was in flight. |
cannot_create_for_user | 403 | You may not create an API key acting as that user. |
cannot_manage_key | 403 | You may not revoke that user's API key. |
invalid_user | 400 | The user for this API key does not exist. |
invalid_expiry | 400 | The expiry date is not in Y-m-d format. |
invalid_url | 400 | The webhook URL is invalid or points at a disallowed host. |
no_events | 400 | No valid webhook events were supplied. |
no_fields | 400 | A webhook update supplied nothing to change. |
delivery_failed | 502 | A test delivery could not reach the endpoint. |
no_entropy | 500 | The server could not generate a secure token or secret. |
db_error | 500 | The API key could not be saved. |
Hooks & Filters
Behaviour filters
| Filter | Default | Purpose |
|---|---|---|
wcusage_api_rate_limit | 120 / 30 | Requests per minute. Receives the default and the bucket identity (key_*, user_*, ip_*). |
wcusage_api_max_auth_failures | 20 | Invalid-key attempts allowed per address per 15 minutes. Zero or less disables the check. |
wcusage_api_require_https | true outside local/dev | Whether bearer keys require HTTPS. |
wcusage_api_max_order_rows | 5000 | How many referred orders one /coupons/{id}/orders request may consider. |
wcusage_api_report_batch_size | 200 | How many coupons /reports/summary primes meta for at a time. Lower it on very large programs with tight memory. |
wcusage_api_openapi_public | vrai | Whether /openapi is publicly readable. |
wcusage_api_webhook_require_https | true outside local/dev | Whether webhook URLs must be HTTPS. |
wcusage_api_webhook_max_attempts | 5 | Delivery attempts per event. |
wcusage_api_webhook_max_failures | 25 | Consecutive failures before an endpoint is auto-disabled. |
wcusage_api_webhook_events | — | Extend or modify the webhook event catalog. |
wcusage_api_auth_failure_buckets, wcusage_api_ip_buckets | 256 | How many buckets failed attempts and anonymous callers are spread across. |
Enabling the API, and enabling individual endpoints, is done from Coupon Affiliates → Admin Tools → API rather than by a filter. Those settings live in the wcusage_api_settings option.
Response filters
| Filter | Applies to |
|---|---|
wcusage_api_coupon_summary | Every coupon object the API returns. |
wcusage_api_prepare_affiliate | Affiliate objects. Receives the detailed flag. |
wcusage_api_prepare_payout | Payout objects. |
wcusage_api_prepare_registration | Registration objects. |
wcusage_api_order_item | Referred-order rows — the place to add extra fields for a trusted integration. |
wcusage_api_report_summary | Le /reports/summary payload. |
wcusage_api_openapi_spec | The generated OpenAPI document. |
wcusage_api_webhook_payload | Webhook payloads, just before delivery. |
// Add the affiliate's country to every coupon the API returns.
add_filter( 'wcusage_api_coupon_summary', function ( $data, $coupon_id ) {
$user_id = (int) $data['user_id'];
$data['country'] = $user_id ? get_user_meta( $user_id, 'billing_country', true ) : '';
return $data;
}, 10, 2 );
Actions
| Action | Fires |
|---|---|
wcusage_activity_recorded | For every lifecycle event, with ( $event, $event_id, $info, $actor_id ). This is the same funnel the webhook dispatcher listens to, and it fires whether or not database logging is enabled — hook it to build a custom integration with no polling and no HTTP. |
add_action( 'wcusage_activity_recorded', function ( $event, $event_id, $info, $actor_id ) {
if ( 'payout_request' === $event ) {
// $event_id is the payout ID.
}
}, 10, 4 );
Legacy v1 API
The original three endpoints under woo-coupon-usage/v1 remain available for backwards compatibility. They predate scopes, pagination and schemas, and they require a full administrator account — not merely the plugin's configurable admin capability.
GET /wp-json/woo-coupon-usage/v1/coupon-info?coupon_id={id}
{
"coupon_name": "sarah10",
"unpaid_commission": 40.46,
"pending_payouts": 0,
"coupon_user_id": 1456,
"referral_url": "https://example.com/affiliate-dashboard/?couponid=sarah10"
}
GET /wp-json/woo-coupon-usage/v1/users-coupons?user={login} — an array of coupon IDs assigned to that login. An unknown login returns an empty array.
POST /wp-json/woo-coupon-usage/v1/request-payout — body coupon_id et utilisateur (login). Returns 1 when a payout request was submitted and 0 otherwise, with no explanation of why. PRO only.
How v1 relates to v2
- The v1 routes are pas affected by the API master switch, so existing integrations keep working whether or not v2 is enabled.
- API keys, however, belong to v2: while the API is switched off they authenticate nothing at all, v1 included. v1 remains reachable with an application password or cookie.
- When a key est used on v1, scopes are enforced with a fail-closed default: reads need
read, writes needwrite. - Both namespaces share the same rate limiter.
wcusage/v2. It adds affiliate self-service access, API keys and scopes, pagination and filtering, meaningful error codes, webhooks and an OpenAPI document. The v1 routes exist for existing integrations only and will not gain features.