# Coupon Affiliates — API Documentation > Machine-readable version of the API documentation. Human-readable version: https://couponaffiliates.com/es/api-docs/ # Getting Started ## 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": "admin@example.com" }, "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. - **Affiliates** — 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 - Coupon Affiliates **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, use `https://example.com/?rest_route=/wcusage/v2/me` instead: 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 `501` rather than pretending to be empty — see [Availability](#). > **Warning:** Before connecting anything to a live store, read Security & Disclaimers. This API returns personal data about real people and can perform actions that cannot be undone through the API — and on some configurations, actions that move real money. ## 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. - Go to **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) | > **Warning:** Disabling the API stops outbound webhook deliveries as well as inbound requests. If a system depends on webhooks, switching the API off silently stops feeding it. ## 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/v2` and `woo-coupon-usage/v1`. Presenting one to a core or WooCommerce endpoint fails with `403 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. | > **Warning:** 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 **only** to API keys. With an application password or a logged-in cookie, `auth.scopes` is `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` and `OPTIONS` need `read`, anything else needs `write`. > **Tip:** When connecting an AI agent or a third-party SaaS, create a dedicated read-only key on a dedicated account. It cannot change anything on your site whatever the client does, and you can revoke it without disturbing anything else. ### 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()` reports `local` or `development`. Such a request fails with `401 wcusage_api_https_required`. The `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. `GET` parameters go in the query string; `POST` and `PATCH` parameters go in a JSON body with `Content-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/summary` reports 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 current `per_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`, `to`, `expires`) use `Y-m-d`, e.g. `2026-08-01`. Anything else is rejected with `400 rest_invalid_param`. - **Response fields** use ISO 8601 in the site's timezone, e.g. `2026-08-07T14:03:22`. Empty dates are `null`. - Ranges include both ends. When `from` is given without `to`, `to` 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`. | Status | 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). | > **Note:** A resource owned by somebody else answers 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`/`to`, or `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` and `retry_after: 60`. The one exception is all-time coupon stats, which return the stored snapshot with `source: "throttled"` rather than failing. > **Tip:** Poll a fixed date range rather than a moving one. A client that keeps asking for yesterday is always served from cache; a client that shifts the range slightly on every call forces a fresh scan each minute and will start seeing 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/summary` replaces walking every affiliate. - Use `per_page=100` on collections instead of many small pages. - Prefer all-time stats (no `from`/`to`) 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. > **Warning:** Test on staging first. Do not point a new integration at a live store until you have watched it run end to end somewhere disposable. Several endpoints write data that cannot be undone through the API — accepting a registration creates a coupon, and on a store with automatic payouts a payout request may pay real money immediately. ### 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. > **Note:** This page is general guidance about how the software behaves, not legal advice. If you are unsure whether a particular integration is lawful in your jurisdiction, take proper advice before building it. ### 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 **not** 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 `source` and `last_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 `code` in an error response is the stable part; the `message` is not, and is translated. - Rate limits, cache windows and throttles exist to protect your site and may be adjusted in future releases. Handle `429` gracefully 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`, `/openapi` and `/webhooks` endpoints 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](https://www.gnu.org/licenses/gpl-3.0.html); 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. > **Tip:** Two habits prevent most serious incidents: give every integration its own key with the narrowest scope that works, and keep a current backup before running anything that writes in bulk. ## 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 | Access | Scope | Purpose | | **GET** `/me` | Any logged-in user | — | Identify the caller, its access level and scopes. | | **GET** `/affiliates` | Admin | 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` | Admin | 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` | Admin | write | Change a payout status. Bookkeeping only. | | **GET** `/registrations` | Admin | read | List affiliate applications. | | **GET** `/registrations/{id}` | Admin | read | One application. | | **POST** `/registrations/{id}/status` | Admin | write | Approve or decline an application. | | **GET** `/clicks/stats` | Admin, or owner with `coupon_id` | read | Clicks, conversions and conversion rate. | | **GET** `/events` | Admin | read | Change feed with a cursor for polling. | | **GET** `/reports/summary` | Admin | read | Store-wide totals and top affiliates. | | **GET** `/keys` | Admin | manage | List API keys. | | **POST** `/keys` | Admin | manage | Create an API key. | | **DELETE** `/keys/{id}` | Admin | manage | Revoke an API key. | | **GET** `/webhooks` | Admin | manage | **PRO.** List webhook endpoints. | | **POST** `/webhooks` | Admin | manage | **PRO.** Create a webhook endpoint. | | **PATCH** `/webhooks/{id}` | Admin | manage | **PRO.** Change status or subscribed events. | | **DELETE** `/webhooks/{id}` | Admin | manage | **PRO.** Delete a webhook endpoint. | | **POST** `/webhooks/{id}/test` | Admin | manage | **PRO.** Send a test delivery. | | **GET** `/webhooks/events` | Admin | 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](#). # Endpoints ## 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": "sarah@example.com" }, "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" } ``` | Field | Type | Description | | user | object | `id` and `display_name` always; `login` and `email` 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` or `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. | > **Note:** auth.scopes: null means the request is not scope-limited — it authenticated with an application password or cookie, so the user's capabilities are the only limit. ## Affiliates 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 | | search | 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": "sarah@example.com" }, "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. | | to | 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`/`to`. > **Note:** A ranged request recalculates every one of the affiliate's coupons from the order history, so one call costs as much as their whole trading history. Results are cached for 60 seconds per range, and an uncached range is limited to one per affiliate per minute — over that, the response is 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. | | search | 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" } ``` | Field | Description | | unpaid_commission | 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`. > **Note:** Always reference coupons by ID. WooCommerce coupon codes are case-insensitive and not guaranteed unique, and the stats layer is keyed by code — see 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 `false`. 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 } ``` | Field | 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 | `true` 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` and `last_refreshed` are always present, even when empty. > **Warning:** 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. `completed`. | | 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 *this* 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. > **Note:** Order rows deliberately carry no customer data — only IDs, totals and commission. A trusted server-side integration can add fields with the wcusage_api_order_item filter. ## Payouts 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`. > **Warning:** Changing a payout's status is bookkeeping only — no payment gateway is ever contacted. Creating a payout is different: if your store has automatic payouts enabled, the normal submit flow may pay it immediately through PayPal, Stripe or Wise, exactly as a dashboard request would. The response flags this with 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 } ``` > **Note:** Payout destination details — PayPal addresses, bank accounts, wallet addresses — are never returned by the API. Only the boolean 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` — `true` when auto-accept moved the payout straight to `created` or `paid`, 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` or `processing`), 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`. > **Note:** processing and 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_unpaid` rather 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`. ## Registrations 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` or `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": "new@example.com" }, "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` or `declined`. | | message | string | Optional message included in the notification email. | | send_email | boolean | Whether to send the notification email. Default `true`. | Returns the updated registration. > **Warning:** Accepting runs the full approval flow: the affiliate coupon is created from the template, roles are assigned, emails are sent and hooks fire — exactly as if approved from the admin screen. | 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. > **Note:** The IP addresses stored with clicks are never exposed through the API. If the clicks table does not exist on the install, the endpoint answers 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 | Event | `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. > **Note:** Declined registrations are not written to the activity log, so they never appear in this feed. Subscribe to the registration.declined webhook (PRO) if you need them. > **Warning:** The feed needs the activity log enabled in the plugin settings (it is by default). When it is off, this endpoint answers 400 wcusage_api_log_disabled. Webhooks are unaffected — they fire from the same funnel, before the log setting is consulted. ## Reports 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 `false`. | ``` { "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": "sarah@example.com" } } ], "cached": false } ``` `cached: true` means the response came from the 5-minute cache. The `payouts` 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. > **Note:** Totals are summed from the same stored per-coupon snapshots the affiliate dashboards show, so they move when those snapshots are rebuilt rather than order by order. Balances (unpaid_commission, 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 } ``` > **Warning:** The token is returned only once, at creation, with 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`. > **Note:** Permanent deletion of a key row is available on the admin screen only. The API offers revocation, which keeps the audit trail intact. # Webhooks ## Webhooks Overview > **Note:** Webhooks are a PRO feature. In the free build the delivery layer is not shipped at all: the /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: | Event | 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: | Event | Fires when | Add-on | | `payout.requested` | An affiliate requests a payout. | Payouts | | `payout.paid` | A payout is marked as paid. | Payouts | | `payout.reversed` | A payout is reversed. | Payouts | | `payout.cancelled` | A payout is cancelled and its amount returned to the unpaid balance. | Payouts | | `affiliate.payout_details_updated` | An affiliate changes their payout method or details. | Payouts | | `reward.earned` | An affiliate earns a reward or bonus. | Rewards | | `campaign.created` | An affiliate creates a campaign. | Campaigns | | `directlink.created` | An affiliate registers a direct-link domain. | Direct Link | | `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 *your* 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. > **Tip:** 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_https` filter. - Respond with any **2xx** status within **8 seconds**. Anything else counts as a failed delivery. - Do the real work asynchronously. Acknowledge first, process afterwards. > **Note:** Webhook events fire from the plugin's activity funnel before the activity-log setting is consulted, so they keep working even on stores that have database logging switched off. They do stop when the API master switch is off. ## 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` | > **Note:** Payloads never carry customer personal data, payout destination details, or the email address of a person who is not a user of the site. 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 `"."`: ``` X-WCUsage-Signature: t=,v1= ``` Verify it before trusting anything in the payload: ```