Tax details
The add-on collects each partner's tax form, keeps it safe, and adds up what you paid them each year so you can hand the numbers to whoever files. This page is for the store owner and for whoever does the books.
Important
KahunaCart collects and totals this information. It doesn't file or give tax advice. Every tax screen in the admin says so, and so does this page.
Before you begin
- A partner has been paid at least once if you want a year-end report. See Payouts.
- The
kahunacart.affiliates.taxpermission, to see anything on the Tax tab. - Access to the server's
.envfile, if you want full tax ID numbers stored. See Protect tax IDs with a key.
What partners fill in
Partners fill in their form in the Affiliate tab of their account. It asks one question and picks the right form.
| Partner is | Form | What is asked for |
|---|---|---|
| A US person | W-9 | Legal name, business name, federal tax classification, address, SSN, ITIN or EIN, and a typed signature with an "I certify" box. |
| A person outside the US | W-8BEN | Legal name, country of citizenship, home address, the tax ID from their country, date of birth, an optional treaty claim, and a typed signature. |
| A business outside the US | W-8BEN-E | The legal name, the country it was set up in, and the signed official form as a PDF. The form is too long to rebuild. |
Everyone may also give a VAT number and say they're registered for VAT. The add-on keeps it and shows it to you, and nothing else uses it.
A partner can upload the signed official form as a PDF alongside or instead of the fields. PDFs are checked by their first bytes rather than their name, and the limit is 10 MB.
Collect Tax Details (tax.collect) on the Tax tab of the settings switches all this on. It's on by default. With it off, partners can't fill a form in, and the payout rule below stands aside.
Protect tax IDs with a key
Full tax ID numbers are only stored when the server has a key called KAHUNACART_TAX_KEY. The key lives in the server's environment and never in the database or a settings file. Your store's other secrets sit in the same database as the data they protect, so a copy of the database would carry both. This key keeps them apart.
With the key set:
- The full US tax ID, the foreign tax ID and the date of birth are stored encrypted with it.
- Every signed PDF is stored encrypted with it too.
- Tax IDs are masked everywhere, as
•••-••-1234. The full number appears only in the year-end export.
Without the key, a W-9 keeps only the last four digits of the tax ID plus the signed PDF, and a W-8BEN keeps the signed PDF. A partner who types a full number is told why it was refused. The tax ID column of the year-end export is blank.
Set the key
- Open KahunaCart → Affiliates → Settings and go to the Tax tab.
- Under Tax ID key, click Generate a key. The key is shown once and saved nowhere. Click Copy.
- Add this line to the
.envfile in the Grav root, or set it in the server's environment:
KAHUNACART_TAX_KEY=paste-the-key-here
- Reload the settings screen. A
.envline is picked up by web requests and the command line without restarting PHP. - If partners uploaded forms before the key was set, encrypt them:
bin/plugin kahunacart-affiliates tax:rekey
The field now says Full tax ID storage is on, with the key's fingerprint.
Warning
Keep a copy of the key somewhere safe. Without it the stored tax IDs and signed forms can't be read.
Check the key
bin/plugin kahunacart-affiliates tax:status
It says whether the key is set and its fingerprint, how many forms are in each state, how many partners have to fill theirs in again, where signed forms are kept, and whether they're encrypted. Run it as the same user as the web server, or with the same .env, or it answers for a different environment.
Change the key
- Put the new key in
.env. - Run
tax:rekeywith the old one:
bin/plugin kahunacart-affiliates tax:rekey --from=OLD_KEY
Use --from-env=NAME instead to read the old key from an environment variable, which keeps it out of your shell history. Values already on the new key are left alone, so running it twice is harmless. A value that opens with neither key is counted and named and not touched, so a mistyped old key destroys nothing. The command exits with an error when any are left.
When the key is lost
A value the current key can't open is never shown as garbage. The partner's form shows Needs re-entry, and the partner is asked to fill it in again. The reason is on the partner's form page: the full number or the signed form is missing, the key has been removed, or the key has changed. The same happens to a partner restored from a store bundle, because bundles carry the last four digits but never the encrypted values or the uploaded file.
Where the signed forms are kept
Signed PDFs go in user/data/kahunacart/affiliates-tax, under random names ending in .bin. They're only handed out through the admin to someone with kahunacart.affiliates.tax, never from a path.
To keep them somewhere else, enter an absolute path in Signed Forms Folder (tax.documents_path), starting with /, of a folder the web server can write to. Move any forms already stored into it. A folder outside the web root is the safest place.
Inside the web root, the add-on writes a deny-all .htaccess into the folder, which Apache obeys. In testing, Apache and Caddy both refused the folder and its files with a 403. nginx was not checked and doesn't read .htaccess, so add this rule for nginx. The Caddy rule is there for a stricter setup.
# above the "allow public media uploads under user/data" location: nginx takes the first matching regex
location ~* ^/user/(env/[^/]+/)?data/kahunacart/affiliates-tax/ { return 403; }
@denied_affiliates_tax path_regexp (?i)^/user/(env/[^/]+/)?data/kahunacart/affiliates-tax/
respond @denied_affiliates_tax 403
Grav's stock nginx and Caddy rules already refuse .bin files under user/data, and the rule above covers the whole folder.
Review a partner's form
- Open KahunaCart → Affiliates → Tax, find the partner and click Open. Or click Open on Tax form on their partner page.
- Check the details against the Signed form (PDF).
- Click Mark verified if they match.
A form has a state.
| State | What it means |
|---|---|
| Submitted | The partner filled it in. |
| Verified | You checked it. |
| Expired | A W-8 ran out. |
| Needs re-entry | Something on it can't be read back. |
| No form | Nothing on file. |
Ask for it again… emails the partner, with a message from you if you write one, and asks them to fill it in again from their account. What's on file stays until they do.
When a W-8 form runs out
A W-8BEN or W-8BEN-E expires at the end of the third calendar year after it was signed, in your store's time zone. A form signed in 2026 runs out on 31 December 2029. The partner is emailed 60 days before and told again when it expires. A W-9 doesn't expire.
Hold back payouts until there is a form
Require Before Paying (tax.require_before_payout) decides which partners need a form before they go in a payout batch.
| Setting | Partners who need a form |
|---|---|
Nobody (off) |
None. |
US partners (us_only, the default) |
A partner with a W-9, or with no form and a US address on their customer account. |
Every partner (all) |
Everyone. |
A form passes when it's Submitted or Verified. A missing, expired or Needs re-entry form holds the partner back. The batch preview says which: "No tax form on file. This store asks for one before paying." Nobody is held back while Collect Tax Details is off.
The year-end report
KahunaCart → Affiliates → Tax adds up what each partner was paid in a calendar year.
- Pick the Year.
- Read the table. It has one row for each partner who was paid, with the form on file, their country and their totals by method and currency.
A payout counts in the year it was paid, in your store's time zone. Failed, returned and unclaimed payouts moved no money and aren't counted, and neither are commissions that are earned but not paid. The totals are split by store credit, PayPal and manual because an accountant may treat them differently. KahunaCart can't answer that for them.
The 1099-NEC flag
A US partner with a W-9 whose payments in US dollars, all methods together, reach the year's threshold shows Over threshold. The threshold is 600.00 for payments up to 2025 and 2,000.00 from 2026. Set 1099-NEC Threshold (tax.nec_threshold), in cents, to replace it for every year. It's blank by default. Payments in other currencies are listed and never converted. The flag points at the partners to look at and decides nothing.
Download the exports
| Button | What you get |
|---|---|
| 1099-NEC file (IRIS) | A CSV in the layout of the IRS IRIS 1099-NEC upload, one row for each flagged partner, with box 1 set to the US dollars paid. |
| Full table | Every partner paid in the year, with totals by method and currency. It never contains a full tax ID. |
| Statement PDF, in a row's menu | A statement of that partner's payouts for the year, with the tax ID masked, to send to them. |
The payer in the IRIS file is your seller that trades in US dollars if you've set up more than one company. See Selling through more than one company. Otherwise it's the name, address and tax ID from your invoice letterhead. The address is read as street lines and then City, ST 12345. If yours doesn't read that way, those columns are left blank for you to fill in.
Full tax IDs are written only when the key is set and the number opens, as nine digits with no hyphens. Without them the tax ID column is empty and the file starts with a line saying why. Delete that line before uploading. The file can't be uploaded without tax IDs anyway.
Important
The column headings follow the IRS's "1099-NEC Template Formatting Guidelines" for tax year 2025. The IRS changes its template from year to year, and IRIS only lets you download its own template after you sign in. Compare the file with that template before the first upload.
Every download is written to a Downloaded list under the table: when, which file, the year, how many rows, who, and whether it held full tax IDs.
Keep the records
After a partner's last paid payout, their tax records can't be erased for Keep Records For (years) (tax.retention_years), 7 by default, even if they ask to be forgotten. Once that time has passed, erasing the customer also deletes their tax form and its PDF. See Customers.
Related
- Payouts
- Settings reference
- The partner's side
- Hardening, for web-server rules