Developers
Everything the affiliates add-on registers, in one place. It's for a developer who builds on the program, feeds it from a script or listens to what it does.
REST routes
Every route is under {api}/kahunacart/affiliates, where {api} is the API plugin's base, /api/v1 by default. Authentication, errors and paging work as described in REST API. Money is in minor units with a currency code beside it. Timestamps are unix seconds.
Reads take kahunacart.affiliates.view or kahunacart.affiliates.manage. Writes take manage. The payout routes take kahunacart.affiliates.payouts and the tax routes take kahunacart.affiliates.tax, and each is checked first.
Partners
| Route | Permission | What it does |
|---|---|---|
GET /kahunacart/affiliates |
view | List partners. Filter with status, tracking, customer_id and q, which searches name, email and code. Applications are partners with ?status=applied. |
POST /kahunacart/affiliates |
manage | Add a partner, with an optional coupon in the same call. |
GET /kahunacart/affiliates/{id} |
view | One partner with their coupons, balances and link. It also carries old_codes (each with code, created_at and created_by, newest first), code_request (the request waiting, or null) and coupon_percent (the discount a coupon for a new code would copy, or null). |
PATCH /kahunacart/affiliates/{id} |
manage | Change a partner's details and rate overrides. A new code keeps the old one working. Send coupon: true with it to make a coupon for the new code too. The answer's coupon says whether one was made and, if not, why. |
GET /kahunacart/affiliates/code-requests |
view | Every request for a new code still waiting, oldest first, each with its partner's id, name, code, status and coupon_percent. |
POST /kahunacart/affiliates/code-requests/{id}/approve |
manage | Give the partner the code they asked for. Send coupon: true to make a coupon for it too. The code is checked again, and a code that was taken or now has a blocked word in it is refused. |
POST /kahunacart/affiliates/code-requests/{id}/reject |
manage | Turn the request down, with an optional message for the partner. |
POST /kahunacart/affiliates/{id}/status |
manage | Move a partner with action: approve, reject, pause, resume or close, and an optional message. |
GET /kahunacart/affiliates/{id}/coupons |
view | A partner's coupons. |
POST /kahunacart/affiliates/{id}/coupons |
manage | Give a partner a coupon with coupon_id, or make one with coupon_percent and coupon_code. |
DELETE /kahunacart/affiliates/{id}/coupons/{couponId} |
manage | Stop a coupon earning for the partner. |
GET /kahunacart/affiliates/{id}/clicks |
view | A partner's daily clicks. |
GET /kahunacart/affiliates/clicks |
view | The whole program's daily clicks. |
GET /kahunacart/affiliates/orders/{orderId} |
view | An order's referral. |
PUT /kahunacart/affiliates/orders/{orderId} |
manage | Credit the order to affiliate_id by hand. |
DELETE /kahunacart/affiliates/orders/{orderId} |
manage | Take the partner off the order. |
GET /kahunacart/affiliates/attention |
any permission | The counts the section shows and which permissions the caller holds. |
Commissions and rates
| Route | Permission | What it does |
|---|---|---|
GET /kahunacart/affiliates/commissions |
view | List commissions. Filter, sort, search and page. |
GET /kahunacart/affiliates/commissions/{id} |
view | One commission. |
POST /kahunacart/affiliates/commissions/approve |
manage | Approve commissions by ids. |
POST /kahunacart/affiliates/commissions/reject |
manage | Reject commissions by ids. |
POST /kahunacart/affiliates/commissions/adjustments |
manage | Add a signed adjustment with affiliate_id, amount_minor, currency and a note. |
POST /kahunacart/affiliates/commissions/orders/{orderId} |
manage | Credit a paid order whose partner was set by hand. |
POST /kahunacart/affiliates/commissions/{id}/release |
manage | Release a disputed commission. |
GET /kahunacart/affiliates/balances |
view | Approved and other balances for each partner and currency. |
GET /kahunacart/affiliates/overview |
view | The overview's numbers. |
GET /kahunacart/affiliates/rates |
view | List product and category rates. Filter with scope, scope_id and affiliate_id, where program means rates for everyone. |
POST /kahunacart/affiliates/rates |
manage | Add a rate with scope, scope_id, rate_type, rate_value and an optional affiliate_id. |
PATCH /kahunacart/affiliates/rates/{id} |
manage | Change rate_type and rate_value. |
DELETE /kahunacart/affiliates/rates/{id} |
manage | Remove a rate. |
Payouts
| Route | What it does |
|---|---|
GET /kahunacart/affiliates/payouts |
List batches. |
POST /kahunacart/affiliates/payouts |
Create batches from method, currency and an exclude list. The preview is rebuilt on the server, and amounts are never taken from the caller. |
POST /kahunacart/affiliates/payouts/preview |
Preview who would be paid, and the reason for everyone left out. |
GET /kahunacart/affiliates/payouts/paypal/connection |
The last PayPal connection check. |
POST /kahunacart/affiliates/payouts/paypal/connection |
Check the PayPal keys now. |
POST /kahunacart/affiliates/payouts/paypal/poll |
Ask PayPal about every open batch. |
GET /kahunacart/affiliates/payouts/{id} |
One batch with its items. |
POST /kahunacart/affiliates/payouts/{id}/run |
Run a draft. |
POST /kahunacart/affiliates/payouts/{id}/resume |
Finish a store credit batch that was cut off. |
POST /kahunacart/affiliates/payouts/{id}/cancel |
Delete a draft. |
POST /kahunacart/affiliates/payouts/{id}/paid |
Mark every open item of a manual batch paid, with an optional reference. |
POST /kahunacart/affiliates/payouts/{id}/retry |
Make a new draft for failed and returned items. |
POST /kahunacart/affiliates/payouts/{id}/poll |
Ask PayPal about one batch. |
GET /kahunacart/affiliates/payouts/{id}/csv/{format} |
A file: paypal, wise or generic. |
POST /kahunacart/affiliates/payouts/{id}/items/{itemId}/paid |
Mark one item paid. For a manual batch, or a PayPal batch whose outcome is unknown. |
POST /kahunacart/affiliates/payouts/{id}/items/{itemId}/failed |
Mark one item not paid, and release its commissions. |
GET /kahunacart/affiliates/{id}/payout-settings |
A partner's payout method, with bank details masked. |
PUT /kahunacart/affiliates/{id}/payout-settings |
Change a partner's method, PayPal email and bank details. |
GET /kahunacart/affiliates/{id}/payout-settings/details |
Reveal the bank details. Each call is written to the log with who asked. |
Tax
| Route | What it does |
|---|---|
GET /kahunacart/affiliates/tax/key |
Whether KAHUNACART_TAX_KEY is set, its fingerprint, and how many forms need filling in again. |
POST /kahunacart/affiliates/tax/key/generate |
Make a new key and return its .env line once. Nothing is saved. |
GET /kahunacart/affiliates/tax/report |
The year-end report for ?year=. |
GET /kahunacart/affiliates/tax/report/iris |
The IRIS 1099-NEC CSV. |
GET /kahunacart/affiliates/tax/report/full |
The full table CSV. |
GET /kahunacart/affiliates/tax/report/statement/{id} |
A partner's statement PDF. |
GET /kahunacart/affiliates/tax/exports |
The download history. |
GET /kahunacart/affiliates/tax/profiles |
Every tax form, masked. |
GET /kahunacart/affiliates/tax/profiles/{id} |
One partner's form, masked. |
POST /kahunacart/affiliates/tax/profiles/{id}/verify |
Mark a form verified. |
POST /kahunacart/affiliates/tax/profiles/{id}/request |
Ask the partner to fill the form in again, with an optional message. |
GET /kahunacart/affiliates/tax/profiles/{id}/document |
The signed PDF, opened for the caller. |
Creatives
| Route | Permission | What it does |
|---|---|---|
GET /kahunacart/affiliates/creatives |
view | List banners, text and links. |
POST /kahunacart/affiliates/creatives |
manage | Add one with type, title, body, media and enabled. |
POST /kahunacart/affiliates/creatives/order |
manage | Set the order with a list of ids. |
PATCH /kahunacart/affiliates/creatives/{id} |
manage | Change one. |
DELETE /kahunacart/affiliates/creatives/{id} |
manage | Delete one. |
The signed-in customer's routes
These are the routes the account tab uses. They're declared public to the API plugin, because a customer is never an API user. The API plugin attaches the signed-in session's customer and refuses the session as proof on a cross-site write, and every controller answers 401 to anyone who isn't a signed-in customer. Orders are shown by number and date only. The one exception is code-check, which answers anyone, because the public application form asks it too.
| Route | What it does |
|---|---|
GET /kahunacart/affiliates/account/status |
The customer's partner status and code. |
GET /kahunacart/affiliates/account/link |
Their referral link. |
POST /kahunacart/affiliates/account/link/build |
A referral link to any page of the site. |
GET /kahunacart/affiliates/account/coupons |
Their coupons and coupon links. |
GET /kahunacart/affiliates/account/clicks |
Their daily clicks. |
POST /kahunacart/affiliates/account/apply |
Apply to the program. An optional code is the code they want. A code that can't be had gets 422 with the sentence to show. |
GET /kahunacart/affiliates/account/code-check |
Ask whether a code could be had, with ?code=. The answer has available, code (as it would be stored) and message (why not). Counted in the affiliate_code_check rate limit bucket, and over it the answer is 429. |
GET /kahunacart/affiliates/account/code-request |
The partner's own code, their waiting request or null, and can_ask. |
POST /kahunacart/affiliates/account/code-request |
Ask for a new code with code and an optional reason. One may wait at a time, and only an active partner can ask. |
DELETE /kahunacart/affiliates/account/code-request |
Withdraw the waiting request. The answer's withdrawn says whether there was one. |
GET /kahunacart/affiliates/account/commissions |
Their commissions. |
GET /kahunacart/affiliates/account/totals |
Their totals for each currency. |
GET /kahunacart/affiliates/account/payouts |
Their payouts. |
GET /kahunacart/affiliates/account/payout-settings |
Their payout method, with bank details masked. |
PUT /kahunacart/affiliates/account/payout-settings |
Change it. |
GET /kahunacart/affiliates/account/tax |
Their tax form, masked. |
POST /kahunacart/affiliates/account/tax |
Submit it, as JSON or multipart with the PDF. |
GET /kahunacart/affiliates/account/tax/form |
Which form they need and its fields. |
The add-on publishes no MCP tools and has no REST reference pages of its own yet.
Permissions
| Permission | Label |
|---|---|
kahunacart.affiliates.view |
View Affiliates |
kahunacart.affiliates.manage |
Manage Affiliates |
kahunacart.affiliates.payouts |
Pay Affiliates |
kahunacart.affiliates.tax |
See Affiliate Tax Details |
Each stands alone. None implies another. See Affiliates.
Events
Each event is fired after the change is written, and a listener that throws can't undo it. Payloads are arrays.
| Event | Carries | Fired when |
|---|---|---|
onKahunaCartAffiliateApplied |
affiliate |
A customer applied. |
onKahunaCartAffiliateStatusChanged |
affiliate, from, to, action, message, actor |
A partner was approved, rejected, paused, resumed or closed. |
onKahunaCartAffiliateCodeChanged |
affiliate, old_code, new_code, actor, request, coupon |
A partner's code was changed, by your edit or an approved request, after the change has committed. request is the approved request, or null for an edit. coupon says what became of the coupon for the new code (made, code, percent and reason), or is null when none was asked for. |
onKahunaCartAffiliateCodeRequested |
affiliate, request |
A partner asked for a new code. |
onKahunaCartAffiliateCodeRequestRejected |
affiliate, request, message, actor |
You turned a request down. |
onKahunaCartAffiliateCommissionCreated |
commission, affiliate, order |
A sale, renewal or adjustment was written. order is null for an adjustment. |
onKahunaCartAffiliateCommissionApproved |
commission, affiliate, order |
A commission became approved. |
onKahunaCartAffiliateCommissionReversed |
commission, affiliate, order, clawback, reduced_minor, why |
A refund or reversal took something back. why says which. |
onKahunaCartAffiliatePayoutSent |
payout, item, affiliate |
A partner was paid. |
onKahunaCartAffiliatePayoutFailed |
payout, item, affiliate |
An item failed or came back and its commissions returned to the balance. The item's error says why. |
onKahunaCartAffiliatePayoutUnclaimed |
payout, item, affiliate, reminder, days_left |
A PayPal payout waits to be claimed. Fired when it first does, with reminder false, and once more a week before PayPal returns it, with reminder true. |
onKahunaCartAffiliatePayoutProblem |
payout, kind, reason |
PayPal would not give the store a token, did not answer or could not take the batch, or the outcome of a batch is unknown and it now waits for you. kind is paypal or outcome_unknown, and reason is a sentence in plain words. It fires every time the runner meets the problem, so a listener that tells somebody should keep one message for each batch and kind. |
onKahunaCartAffiliatePayoutBatchFinished |
payout, counts |
Every item in a batch has an outcome. counts is items for each status. |
onKahunaCartAffiliateTaxSubmitted |
affiliate, profile, replaced |
A partner filled in or replaced their form. |
onKahunaCartAffiliateTaxRequested |
affiliate, profile, message, actor, requested_at |
You asked for the form again. |
onKahunaCartAffiliateTaxExpiring |
affiliate, profile, expires_at |
A W-8 expires within 60 days. Once for each form. |
onKahunaCartAffiliateTaxExpired |
affiliate, profile, expires_at |
A W-8 was marked expired. Once for each form. |
profile is always masked and never holds a full tax ID.
AffiliateService::changeCode() is the one way a code ever changes. It claims the new code and keeps the old one in a single transaction, then fires onKahunaCartAffiliateCodeChanged. It refuses to run inside a transaction that's already open, because the event, and the partner's email it sends, would go out for a change that might still roll back. It throws a LogicException there. Pass any write that belongs with the change as $alongside, and it runs inside the same transaction.
What the add-on listens to
| Event | What it does |
|---|---|
onKahunaCartCheckoutValidate |
Stamps the visitor's link on the order. It adds no errors. |
onKahunaCartOrderCompleted and onKahunaCartOrderPaid |
Credit a sale. A checkout that captures at once fires only the first. |
onKahunaCartSubscriptionRenewed |
Credits a renewal. |
onKahunaCartRefundApplied and onKahunaCartOrderReversed |
Take commissions back. |
onKahunaCartDisputeOpened |
Holds an unpaid commission. |
onKahunaCartDisputeClosed |
Follows the outcome: won lets the hold go, lost takes the commission back (a clawback when it was paid), and open, set by hand, holds it again. Needs KahunaCart 1.5.18. |
onConsentRegisterServices and onConsentChanged |
Declare the kc_ref cookie as marketing, and drop it when consent is withdrawn. |
Twig, shortcode and partial
| Name | What it does |
|---|---|
kahunacart_affiliate_program() |
Draws the public program block. Use it in a theme template. |
|
The same block as a shortcode. Needs the Shortcode Core plugin. |
partials/kahunacart-affiliates/program.html.twig |
The partial both draw. Copy it into your theme at the same path to change it. It is given program, apply_url and stylesheets. |
CLI
| Command | What it does |
|---|---|
bin/plugin kahunacart-affiliates tax:status |
Show whether KAHUNACART_TAX_KEY is set and how partners' tax forms stand. |
bin/plugin kahunacart-affiliates tax:rekey |
Encrypt signed forms stored before a key was set, or with --from=<old key> or --from-env=NAME move every encrypted tax detail from an old key to the current one. |
See Tax details.
Jobs
| Job | Runs | What it does |
|---|---|---|
affiliates.release_holds |
About every 15 minutes | Approves commissions whose holding period has ended. |
affiliates.poll_paypal_payouts |
Every 10 minutes | Sends unanswered PayPal requests again, reads open batches and sends unclaimed reminders. |
affiliates.email |
When queued | Sends one partner email. |
affiliates.statements |
Once a month, in the first week | Queues last month's statements. |
affiliates.payouts_due |
Daily | Sends Affiliate payouts due once a month, on the first day any partner is over the payout minimum. It starts a day after the worker first ticks. |
The tax sweep that sends expiry emails runs inside the worker's tick about once every 23 hours.
Notifications
| Code | Audience |
|---|---|
affiliates.application_received, affiliates.application_approved, affiliates.application_rejected, affiliates.commission_new, affiliates.payout_sent, affiliates.payout_failed, affiliates.payout_unclaimed, affiliates.tax_requested, affiliates.tax_expiring, affiliates.tax_expired, affiliates.statement, affiliates.code_changed, affiliates.code_request_rejected |
Partner |
affiliates.owner_application, affiliates.owner_batch_finished, affiliates.owner_payouts_due, affiliates.owner_payout_problem, affiliates.owner_tax_form, affiliates.owner_commission, affiliates.owner_commission_reversed, affiliates.owner_code_request |
Owner |
The owner rows and when each is sent are in The partner's side. The two commission rows register switched off (enabledByDefault: false, which needs KahunaCart 1.5.3). Each message has a dedupe key: affiliates.owner_payouts_due once per month in the store's time zone, affiliates.owner_payout_problem once per payout item and status or per batch and kind, affiliates.owner_tax_form once per form, the commission rows once per commission (a reversal once per amount taken back), and affiliates.owner_code_request once per request.
The add-on also opts two of its dashboard alerts in to being pushed (notify:): affiliates_paypal_payouts (high) and affiliates_tax_forms (normal). Core makes alert.affiliates_paypal_payouts and alert.affiliates_tax_forms rows for them. See Dashboard alerts → Pushing an alert to your phone.
Partner emails are templates in templates/emails/ named affiliate-*.html.twig and affiliate-*.txt.twig. See The partner's side.
Tables
The add-on adds its tables to KahunaCart's database through the base plugin's migrations, and a store reset empties all of them.
| Table | Holds |
|---|---|
kahunacart_affiliates |
Partners. |
kahunacart_affiliate_codes |
Every code each partner has had, the current one included, with code unique, so the database decides who has a code. Old codes are the rows that aren't a partner's current code. |
kahunacart_affiliate_code_requests |
Partners' requests for a new code, with the code asked for, the reason and how it was decided. |
kahunacart_affiliate_coupons |
Which coupon belongs to which partner. |
kahunacart_affiliate_clicks |
The daily click counter for each partner. |
kahunacart_affiliate_rates |
Product and category rates. |
kahunacart_affiliate_referrals |
One row for each order that has a partner, or a removal. |
kahunacart_affiliate_commissions |
Commissions, with a unique dedupe_key such as sale:<order id>. |
kahunacart_affiliate_payouts |
Batches, each with a random ref. |
kahunacart_affiliate_payout_items |
One row for each partner in a batch. |
kahunacart_affiliate_tax_profiles |
Tax forms. |
kahunacart_affiliate_tax_exports |
The download history. |
kahunacart_affiliate_creatives |
Banners, text and links. |
Export sets, merge and erasure
A store bundle carries the program in three sets, so the permission that guards each is the permission needed to take it out. See Export and restore.
| Set | Holds | Permission |
|---|---|---|
affiliates |
Partners, their codes and code requests, their coupons, rates and creatives. | kahunacart.affiliates.manage |
affiliates.commissions |
Referrals, commissions, batches and items, and click counters. | kahunacart.affiliates.payouts |
affiliates.tax |
Tax forms, without the secrets below. | kahunacart.affiliates.tax |
A bundle never carries a partner's bank details, the full tax ID, the foreign tax ID, the date of birth or the signed PDF. The last four digits and the rest of the form do travel. A restored form is marked Needs re-entry.
A merge matches a partner by code, a commission by its dedupe_key, a referral by its order and a batch by its ref. It never deletes, and it never overwrites the columns a set leaves out.
Codes are matched by the code alone, and a code is never moved from one partner to another. A code nobody has here is added for its partner. A code the same partner already has is left as it is. A code that belongs to another partner here stays theirs, is left out of the merge and is named in the report. A partner in the bundle whose code is an old code of a partner already here is matched to that partner, and keeps this store's current code. Code requests are matched by partner, code and the time they were made. After every import each partner's current code is claimed if it isn't yet, which also covers a bundle from a store that never kept old codes.
Merging two customers moves the partner record to the survivor. Erasing a customer is refused while a paid payout is inside the retention period, and afterwards the partner row is anonymised. See Customers.