=== SheetLink Forms for Google Sheets and Excel ===
Contributors: sheetlink
Tags: google sheets, excel, form submissions, spreadsheet, automation
Requires at least: 5.8
Tested up to: 7.0
Requires PHP: 7.4
Stable tag: 1.16.1
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Send WordPress form submissions to Google Sheets and Excel automatically. Free unlimited entries, field mapping, delivery logs, and retries.

== Description ==

SheetLink Forms sends your form submissions straight to Google Sheets or Excel, automatically. Every submission becomes a properly mapped row - no manual exports, no per-submission fees, and no Zapier account.

Setup takes about three minutes. Pick your form plugin, map fields to columns, and connect your Google account in one click - SheetLink creates the sheet for you, or you pick an existing one with Google's own file picker. Rows start appearing immediately.

Prefer not to connect an account? The classic Apps Script method is built in: paste the generated script into your own sheet, and nothing but your own Google script ever touches the data.

**Local-first.** Submissions go directly from your server to Google, never through SheetLink's servers. The one-click connection talks to our sign-in broker only to link and renew your Google account; your form data never passes through it. No telemetry, no activation pings, no required account.

= Supported form plugins =

Contact Form 7, WPForms, Gravity Forms, Elementor Pro, Fluent Forms, Formidable Forms, Ninja Forms, Forminator, Everest Forms, Beaver Builder, Divi and WS Form - all from one admin screen.

= How it works =

1. Create a sync rule: pick your form, map fields to columns
2. Connect Google in one click, or paste the generated Apps Script instead
3. Every submission appears in your spreadsheet

= Free features =

The whole delivery pipeline is free and unlimited:

* All 12 form integrations, unlimited sync rules, and field mapping
* One-click Google Sheets connect, using Google's restrictive `drive.file` scope, so SheetLink can only ever see sheets it creates or that you pick
* Mirroring to Excel Online, Airtable and Notion alongside your sheet
* Automatic retry queue, delivery logs and a test button
* UTM, GCLID, fbclid, msclkid, IP, page URL and referrer capture
* Conditional routing, smart mapping suggestions and a guided setup wizard
* Local analytics, stored in your own database
* Data export and deletion through the WordPress privacy tools
* Multisite network overview

= Add-ons (require an active licence) =

Activated on the SheetLink License page. See [sheetlinkwp.com/pricing](https://sheetlinkwp.com/pricing).

* **Primary Destinations** - make Excel Online, Airtable or Notion a rule's only destination. Mirroring alongside Google Sheets stays free
* **Multi-CRM Routing** - HubSpot, Salesforce, Zoho, Pipedrive
* **Two-Way Sync** - edit a row in your sheet and have the change arrive back in WordPress
* **WooCommerce Sync** - orders, refunds and customers in real time
* **Extra Integrations** - JetFormBuilder, Bricks Builder, MetForm, Kali Forms, HappyForms
* **Agency tools** - white-label admin, multisite dashboard, branded reports, role-based menu visibility
* **AI Lead Scoring** - optional 0-100 scoring through the SheetLink API, behind an explicit consent checkbox

== Installation ==

= From your WordPress admin (recommended) =

1. In your WordPress admin, go to **Plugins → Add New**.
2. Search for "SheetLink Forms".
3. Click **Install Now**, then **Activate**.
4. The setup wizard opens automatically and walks you through connecting your first form to Google Sheets in about three minutes.

= Manual upload =

1. Download the `sheetlink-forms.zip` file.
2. In WordPress, go to **Plugins → Add New → Upload Plugin** and upload the zip, or extract it into `/wp-content/plugins/sheetlink-forms/`.
3. Activate the plugin from the Plugins menu.
4. The setup wizard opens automatically. If you dismissed it, visit **SheetLink** in the admin menu and click **+ Add Rule** to configure your first sync rule manually.

= What you'll need =

* A WordPress form plugin (Elementor Pro, Contact Form 7, Gravity Forms, WPForms, Fluent Forms, Formidable, Ninja Forms, Forminator, Everest Forms, Beaver Builder, Divi, or WS Form)
* A Google account with access to Google Sheets
* About three minutes

== Frequently Asked Questions ==

= How do I send Contact Form 7 submissions to Google Sheets? =

Install SheetLink Forms, create a sync rule, choose your Contact Form 7 form, connect Google, and map the form fields to spreadsheet columns. New submissions are added as rows automatically. Step-by-step guide: [Connect Contact Form 7 to Google Sheets](https://sheetlinkwp.com/how-to/connect-contact-form-7-to-google-sheets).

= Can I connect WPForms Lite to Google Sheets for free? =

Yes. Both WPForms Lite and WPForms Pro are supported, and the free SheetLink plugin sends unlimited WPForms submissions to Google Sheets. Step-by-step guide: [Connect WPForms to Google Sheets](https://sheetlinkwp.com/how-to/connect-wpforms-to-google-sheets).

= How do I connect Elementor Forms to Google Sheets? =

Create a sync rule, select your Elementor Pro form, connect or pick a Google Sheet, and map each Elementor field to a column. Step-by-step guide: [Connect Elementor Forms to Google Sheets](https://sheetlinkwp.com/how-to/connect-elementor-forms-to-google-sheets).

= Does SheetLink support Gravity Forms to Google Sheets? =

Yes. Gravity Forms entries go to Google Sheets in real time, with custom field mapping, delivery logs, and automatic retries. Step-by-step guide: [Connect Gravity Forms to Google Sheets](https://sheetlinkwp.com/how-to/connect-gravity-forms-to-google-sheets).

= Can WordPress forms send entries to Excel Online? =

Yes. SheetLink mirrors submissions into an Excel Online workbook on Microsoft 365 or OneDrive for free, alongside your Google Sheet. To make Excel a rule's only destination, the Primary Destinations add-on is required. Note that Microsoft's Excel API supports business accounts only - personal Outlook.com OneDrive is not supported by Microsoft for this. Step-by-step guide: [WordPress forms to Microsoft 365](https://sheetlinkwp.com/how-to/wordpress-forms-to-microsoft-365).

= Do I need to create an account or register? =

No. SheetLink Forms works immediately after installation. No account, no registration, no license key required.

= Do I have to paste an Apps Script into my sheet? =

Not anymore. Connect your Google account in one click and SheetLink writes rows through Google's official Sheets API - it can create the sheet for you, or you pick an existing one with Google's own file picker. The classic Apps Script method remains fully supported for users who prefer a zero-account, script-only setup; both are free.

= Do I need a Zapier account? =

No. SheetLink Forms sends data directly from WordPress to Google Sheets - through your one-click connected Google account or via a free Google Apps Script. No third-party services needed.

= Does the plugin call home or track anything? =

No telemetry, no analytics beacons, no activation pings. Form submissions are sent only to Google (or, if you enable those mirrors, Microsoft, Airtable, or Notion) - never through SheetLink's servers. If you use the one-click Google or Microsoft connection, the sign-in handshake and periodic token renewal go through SheetLink's OAuth broker at sheetlinkwp.com; your form data never does. The optional Lead Scoring add-on (disabled by default, requires an API token and an explicit consent checkbox) is the only feature that sends submission data to SheetLink. See "External services" above for the full disclosure.

= Which form plugins are supported? =

Elementor Pro Forms, Contact Form 7, Gravity Forms, WPForms, Fluent Forms, Formidable Forms, Ninja Forms, Forminator, Everest Forms, Beaver Builder, Divi, and WS Form.

= How do I set up the Google Sheet? =

SheetLink Forms generates the Google Apps Script for you. Click "Get Script" on any rule, copy the code, and paste it into your Google Sheet's Apps Script editor (Extensions > Apps Script). Then deploy it as a web app.

= Can I send different forms to different sheets? =

Yes. Create multiple sync rules, each pointing to a different Google Sheet (a different connected sheet, or a different Apps Script URL with the classic method).

= Is my data secure? =

Form submission data is sent directly from your WordPress server to Google over HTTPS: to `sheets.googleapis.com` (Google's official Sheets API) when you use the one-click connection, or to the Google Apps Script URL you configure with the classic method (only `script.google.com` and `script.googleusercontent.com` URLs are allowed as receivers). It never passes through SheetLink's servers. If - and only if - you enable the optional Lead Scoring add-on on the Analytics page, a copy of each submission is also sent over HTTPS to `https://sheetlinkwp.com/api/sheetlinkwp/lead-score`. Every other feature of the plugin runs locally on your WordPress server.

= What happens if the Google Sheet is unavailable? =

The submission is logged as failed and automatically retried up to 3 times (every 15 minutes). You can also retry manually from the Retry Queue page.

= Is SheetLink a free Zapier alternative? =

Yes. Zapier's base plan starts around $20/month and charges per task. SheetLink delivers unlimited form submissions from your WordPress site directly to Google Sheets at no cost - no per-task fees, no subscription, no registration. You own the entire pipeline.

= Does SheetLink work with WPForms? =

Yes. WPForms is fully supported in the free core, both Pro and Lite. Every submission is mapped to your chosen Google Sheet with the fields you select.

= Does SheetLink work with Elementor Pro forms? =

Yes. Elementor Pro Forms are fully supported in the free core. Create a sync rule, map fields, connect Google in one click (or paste the generated Apps Script) - done.

= Does SheetLink work with Fluent Forms? =

Yes. Fluent Forms is fully supported in the free core, along with 11 other form plugins (Contact Form 7, WPForms, Gravity Forms, Elementor Pro, Formidable, Ninja Forms, Forminator, Everest Forms, Beaver Builder, Divi, WS Form).

= Can I send WooCommerce orders to Google Sheets? =

Yes, with the WooCommerce Sync add-on. It pushes orders (created, completed, refunded), customers, and product catalogue changes (price, SKU, stock, status) to Google Sheets in real time. This is a premium feature; the free core covers form submissions.

= How is SheetLink different from GSheetConnector and other single-form plugins? =

Most CF7-to-Sheets or form-to-Sheets plugins handle one form type. SheetLink supports 12 form plugins from a single admin interface, includes an automatic retry queue that recovers failed deliveries, captures UTM and click-ID parameters for marketing attribution, runs local analytics on your own WordPress database, and ships a guided setup wizard that takes new installs from zero to a working Google Sheet in about three minutes.

= Does SheetLink work with WordPress Multisite? =

Yes. The free core includes a network-admin overview of all subsites. Multisite-wide central management and the Agency Dashboard are available in the Agency Plus tier.

== External services ==

This plugin contacts the following external services. Google is core to the plugin's function - via the Google Sheets API (one-click connection) or a user-deployed Google Apps Script, depending on which connection method you choose per rule. The others are optional: Microsoft Graph only when you connect Excel Online, the SheetLink License Server only if you activate a paid license, and the SheetLink Lead Scoring API only behind an explicit consent checkbox (disabled by default).

= Google Sheets API + Google Drive (one-click connection, v1.12.0+) =

* **What it is:** Google's official Sheets and Drive APIs, used when you connect a Google account with the one-click method.
* **When it is called:** Only after you click "Connect Google" on the SheetLink settings page and grant consent at Google's own sign-in screen. With no connected account, no calls are made.
* **What data is sent:** Per matching submission, the form field values mapped by the user (plus optional enrichment data such as timestamps, UTM parameters, IP address, and page URL if enabled), written directly from your WordPress server to `https://sheets.googleapis.com/`. Creating a sheet and listing tabs use the same API; the file picker uses `https://www.googleapis.com/drive/v3/` and loads Google's Picker JavaScript from `https://apis.google.com/` in wp-admin only.
* **Scope:** `drive.file` only - SheetLink can access only spreadsheets it creates or that you explicitly pick with the picker. It can never see the rest of your Google Drive.
* **Token broker:** The OAuth sign-in and periodic token renewal go through SheetLink's broker at `https://sheetlinkwp.com/api/sheetlinkwp/oauth/google/` so the Google client secret stays on SheetLink's servers and not in the plugin. Your form submission data never passes through the broker. The refresh token is stored encrypted in your own WordPress database and is cleared the moment you disconnect.
* **Privacy considerations:** If your forms collect personal data and you use the one-click connection, list `sheetlinkwp.com` (the OAuth broker) and `googleapis.com` (Google's API) as data processors in your privacy notice.
* **Google Sheets API documentation:** [https://developers.google.com/sheets/api](https://developers.google.com/sheets/api)
* **Google Terms of Service:** [https://policies.google.com/terms](https://policies.google.com/terms)
* **Google Privacy Policy:** [https://policies.google.com/privacy](https://policies.google.com/privacy)

= Google Apps Script (classic method) =

* **What it is:** A user-deployed Google Apps Script web app that receives form data and writes it to your Google Sheet.
* **What data is sent:** The form field values mapped by the user, plus optional enrichment data (timestamps, UTM parameters, IP address, page URL) if enabled by the site administrator.
* **When it is called:** Each time a WordPress form submission matches a sync rule that uses the classic method.
* **Where data goes:** User-deployed Google Apps Script web app URLs hosted at `https://script.google.com/` or `https://script.googleusercontent.com/`. The plugin validates that all receiver URLs begin with these prefixes.
* **No account required:** With this classic method the plugin does not use Google API keys or OAuth - you deploy a free Google Apps Script receiver in your own Google Sheet, and nothing but Google is ever contacted.
* **Google Apps Script documentation:** [https://developers.google.com/apps-script](https://developers.google.com/apps-script)
* **Google Terms of Service:** [https://policies.google.com/terms](https://policies.google.com/terms)
* **Google Privacy Policy:** [https://policies.google.com/privacy](https://policies.google.com/privacy)

= SheetLink License Server (only if you activate a license) =

* **What it is:** The SheetLink activation server that issues and verifies entitlements for paid features.
* **When it is called:** (a) Once when you paste a license key on the SheetLink → License page, (b) every 12 hours in the background to refresh the cached entitlements, and (c) when you click "Re-check entitlements" in the admin. If you never activate a license, this server is never contacted.
* **What data is sent:** Your license key (or purchase email as a fallback) and this site's domain (`home_url()`). No form submissions, no user accounts, no site content are ever sent to this endpoint.
* **Where data goes:** `https://sheetlinkwp.com/api/sheetlinkwp/verify` and `https://sheetlinkwp.com/api/sheetlinkwp/entitlements`, over HTTPS. (Installs older than 1.13.1 contact `siteanswerai.com` for the same endpoints - a legacy domain the API used to be hosted under, which now forwards to the address above.)
* **Opt-out:** Deactivate from the License page. The free features keep working indefinitely with no further calls.

= SheetLink AI Lead Scoring (optional, off by default) =

* **What it is:** A hosted scoring service provided by SheetLink. For each captured lead it returns a 0-100 score, a hot / warm / cold band, and a one-sentence explanation of the rating. It is asynchronous: the plugin submits a job and collects the result a moment later, so nothing is added to the time a visitor waits when submitting your form.
* **When it is called:** Only when all of these are true - (a) your licence includes AI Lead Scoring, (b) you have ticked "Score incoming leads with AI" in `SheetLink Forms -> Lead Settings`, and (c) a lead is captured *after* you switched it on. It runs on WP-Cron, never during the submission itself, and it does not score leads captured before you enabled it. If any one of these is missing, no data is sent.
* **What data is sent:** The submitted field values **with contact details removed first** - names and phone numbers are replaced with the placeholder "provided", email addresses are reduced to their domain (`[redacted]@example.com`), and any email address or phone number typed into a free-text answer is redacted in place. Also sent: this site's activation token, so the service can identify your licence. **Not** sent: the form identifier, the source form plugin name, the site URL, WordPress user account data, or anything unrelated to the submission.
* **Where data goes:** `https://sheetlinkwp.com/api/sheetlinkwp/lead-score` over HTTPS - one POST to submit the job, and one GET to collect the result. Authenticated with this site's activation token, not a separately issued key.
* **Opt-in and opt-out:** Off by default. Unticking the box in Lead Settings stops all requests immediately, as does deactivating your licence.
* **Privacy considerations:** Free-text answers are sent as written, since that is what the rating is based on. If your forms collect personal data in free text, list `sheetlinkwp.com` as a data processor in your own privacy notice before enabling this feature.
* **Terms of Service:** [https://sheetlinkwp.com/terms](https://sheetlinkwp.com/terms)
* **Privacy Policy:** [https://sheetlinkwp.com/privacy](https://sheetlinkwp.com/privacy)

= SheetLink AI Analytics extras (optional, off by default) =

* **What it is:** Three optional per-submission extras on the Analytics page - sentiment, summary, and category - computed by the same hosted service.
* **When it is called:** Only when the free Local Analytics feature is enabled, the specific extra is switched on, AND AI consent is enabled in `SheetLink Forms -> Lead Settings`. Everything else on the Analytics page is computed locally in your own database and sends nothing.
* **What data is sent:** The submitted field values, scrubbed of contact details exactly as described for AI Lead Scoring above; your own category list when the category extra is enabled; and this site's activation token.
* **Where data goes:** `https://sheetlinkwp.com/api/sheetlinkwp/analytics` over HTTPS, authenticated with this site's activation token.
* **Opt-in and opt-out:** Off by default. Switching off the individual extra, or AI consent in Lead Settings, stops all requests immediately.
* **Privacy considerations:** Same as AI Lead Scoring - free text is sent as written, so list `sheetlinkwp.com` as a data processor before enabling.

**3. Microsoft Graph API (only when the Excel Online destination is connected, v1.10.0+)**

* **What it is:** Microsoft's REST API for OneDrive + Excel Online. Used to insert new rows into a customer-selected Excel table after each form submission.
* **When it is called:** Only after a customer (a) clicks "Connect Excel" in `SheetLink Forms -> Destinations -> Excel Online` and grants consent at Microsoft's sign-in page, (b) picks a workbook + table, and (c) enables the Excel mirror on a specific rule. With no connection, no calls are made.
* **What data is sent:** Per submission, the form field values mapped to your chosen Excel columns. No WordPress user account data, admin passwords, or any field you have not explicitly mapped.
* **Where data goes:** `https://graph.microsoft.com/v1.0/me/drive/items/{workbook}/workbook/tables/{table}/rows` over HTTPS, authenticated with an OAuth access token. Token refresh goes through SheetLink's OAuth broker at `https://sheetlinkwp.com/api/sheetlinkwp/oauth/microsoft/refresh` so the Azure client secret stays on SheetLink's servers and not in the plugin.
* **Opt-in and opt-out:** The feature is off by default. Disconnecting the Microsoft account immediately stops all calls and clears the encrypted refresh token from `wp_options`. Field mappings are preserved for one-click reconnection.
* **Token storage:** The Microsoft refresh token is encrypted with AES-256-GCM (key derived from `wp_salt('auth')` via HKDF) before being written to `wp_options`. The access token is short-lived (~1 hour) and is also stored in `wp_options`.
* **Privacy considerations:** If your forms collect personal data and you enable the Excel mirror, list `sheetlinkwp.com` (the OAuth broker) and `graph.microsoft.com` (the Microsoft 365 API) as data processors in your privacy notice.
* **Microsoft Privacy Policy:** [https://privacy.microsoft.com/](https://privacy.microsoft.com/)
* **Terms of Service:** [https://sheetlinkwp.com/terms](https://sheetlinkwp.com/terms)
* **Privacy Policy:** [https://sheetlinkwp.com/privacy](https://sheetlinkwp.com/privacy)

No other external services are contacted by this plugin. All other features - smart mapping suggestions, local analytics charts, duplicate detection, conditional and advanced routing, branded reports, role-based views, white-label, and the agency dashboard - run entirely on your own WordPress server.

== Screenshots ==

1. WordPress forms to Google Sheets dashboard - submission totals, active rules, delivery status, and estimated savings vs Zapier.
2. Send WordPress form submissions to Google Sheets, and mirror them to Excel Online, Airtable, or Notion.
3. Map WordPress form fields to Google Sheets columns without writing code, with a live delivery log and one-click test.
4. Review failed form deliveries and retry Google Sheets submissions automatically - every 15 minutes, up to 3 attempts.
5. Capture GCLID, wbraid, gbraid, fbclid, and msclkid from WordPress form submissions and export offline conversions.

== Changelog ==

= 1.16.1 =
* Changed: the plugin's display name is now "SheetLink Forms for Google Sheets and Excel" in both the readme and the plugin header, so the two match and the name leads with the plugin's own brand rather than a trademark.
* Changed: the readme description has been shortened. It was long enough to be trimmed when the directory parsed it, which meant part of it was never displayed.
* Changed: changelog entries older than 1.14.8 moved to changelog.txt, bundled with the plugin. No functional change.
* Fixed: five places where a value was printed into admin HTML or a digest email without escaping, including the white-label name and accent colour.
* Fixed: failed deliveries to Meta CAPI, Twilio SMS and Excel wrote the provider's error message or response body to the PHP error log on every failure, in production. Those can carry the request URL and its credentials, or the submitted lead's own details. Now logged only under WP_DEBUG, and only the status - matching what the Airtable and Notion destinations already did.

= 1.16.0 =
* Added: **the Lead Inbox now actually fills up.** The Leads, Lead Settings and Lead Reports screens arrived in 1.15.0, and nothing ever appeared in them, because SheetLink never announced a submission for them to record. Every submission now becomes a lead - **including submissions that match no sync rule at all, and ones whose delivery fails**, which are exactly the enquiries that used to vanish without trace. Existing leads are unaffected; capture starts from this update.
* Added: **AI Lead Scoring, working end to end.** Each new lead is rated 0-100 with a hot / warm / cold band and a one-line explanation of the rating, shown in the Leads list, on the lead itself, and in the CSV export. You can sort the list by score. Off until you switch it on in Lead Settings, and it only ever scores leads captured after you do - it will not work through your back catalogue or spend your monthly allowance on old enquiries.
* Added: **contact details are removed before anything is sent for scoring.** Names and phone numbers are replaced with a placeholder, email addresses are reduced to their domain (so "works at a company" survives but the person does not), and an email address or phone number typed into a message is redacted in place. The rating quality is unaffected - scoring reads intent, not identity.
* Added: **a Setup item in the SheetLink menu, so the setup guide is always one click away.** Activating the plugin already takes you straight into it, but that only happens once - and it is skipped entirely if you activate several plugins together, install over WP-CLI, or switch SheetLink off and back on. Anyone who missed that first-run screen previously had no way back to it.
* Added: **delivery outcomes now attach to the lead, including recovery.** A delivery that failed and then succeeded on a later retry used to read as "failed" on that lead forever, even though the row was sitting in your sheet. The lead now shows it recovered, and its history says when.
* Changed: **AI scoring is switched on in Lead Settings.** It was previously only offered on the Analytics page, which does not exist unless you have separately enabled the free Local Analytics feature - so a customer whose plan included scoring had no way to reach the switch. The Analytics page now shows whether AI consent is on and links to it.
* Changed: **scoring and the AI extras use your existing licence, not a second token.** The "API Token" field on the Analytics page is gone. It asked you to paste in a value SheetLink already had, and leaving it blank silently disabled features you were paying for.
* Fixed: **uninstalling left settings behind in your database.** Sixteen options survived a full uninstall, including a stored Google Sheets two-way state, every Lead Inbox setting, and cached diagnostics. Deleting the plugin now removes them. **Your captured leads are still kept on purpose** - removing a plugin should not delete the enquiries you collected, so the three lead tables are only dropped if you explicitly ask for that.
* Fixed: hidden `_sheetlink_id` column groundwork for linking a spreadsheet row back to its lead. It is only sent to sheets whose Apps Script is current, so nothing appears in your existing sheets and no redeploy is required; if you do update your script it arrives hidden.

= 1.15.1 =
* Changed: **Elementor columns now use your field labels.** A field you added to an Elementor form arrived under a generated key, so a field labelled "phone" produced a column headed `field_d9587fa`. Elementor was the last integration needing manual mapping for this reason. **What you will see:** existing sheets gain new label-named columns, and data already collected stays in the old `field_xxx` columns - nothing is moved, renamed or deleted. If you would rather keep the old headings, map the fields explicitly on the rule. Only Elementor is affected.
* Fixed: **accounts created through the WordPress REST API were never synced.** Registration sync checked the new account's role at the moment it was created, but the REST API assigns the role a fraction of a second later - so the check found no role and skipped the account. Affects headless sites, mobile apps and any API-driven signup; the admin form and WooCommerce checkout were never affected.
* Fixed: **MemberPress signups delivered nothing at all - no row, no error, no queue entry.** MemberPress completes a signup earlier in the page load than SheetLink starts listening, so the event fired before the plugin was ready and nothing was ever attempted. This affected every transaction on that path, free and paid alike, and the same timing could affect LearnDash and Easy Digital Downloads. SheetLink now starts listening before any of them can run.
* Fixed: **Update Mode erased columns your submission did not include.** Updating a matched row rewrote the whole row from the submission, so a notes column you added yourself, a column filled by another rule, or a field left empty on that particular submission were all blanked. Updates now only touch the columns the submission actually carried. Submitting a field empty still clears it.
* Fixed: **Update Mode added duplicates instead of updating.** Matching was case-sensitive, so "John@example.com" and "john@example.com" were treated as different people, and a key column holding dates or numbers never matched at all - producing exactly the duplicate rows the feature exists to prevent.
* Fixed: **Duplicate Prevention silently overrode Update Mode.** With both switched on and keyed on the same field - the natural way to keep one row per customer up to date - repeat submissions were filed as duplicates and the update never happened. Update Mode now takes precedence when both use the same key.
* Added: the delivery log now shows whether each delivery **updated an existing row or added a new one**, and warns when the key column is missing from your sheet - previously a rule that never once updated anything looked identical to one working perfectly.
* Added: **WooCommerce order rows now carry the net amount and total refunded on every event**, not just refunds. Note these are event rows - one order produces several - so totals need the latest row per order, or Update Mode keyed on `order_id`. The WooCommerce page explains this.
* Added: **SheetLink now tells you when your site cannot send email.** Most form plugins only show their success message once every action has finished, and their notification email is one of those actions - so on a site with no mail set up, visitors are told the submission failed while the row reaches your sheet perfectly, and they submit again. That is where most duplicate rows come from. Webhook Health now reports it, and says plainly that your SheetLink deliveries are unaffected.
* Fixed: **a secondary destination could report a red cross for a delivery that actually arrived.** Google Apps Script answers a successful write with a redirect, which the secondary path mistook for an error - so a working destination looked broken, and the row was in the sheet all along.
* Fixed: **"Retry Now" on a secondary destination marked it dead instead of retrying it.** One click took an item straight to "3/3 max retries" without contacting the destination at all.
* Fixed: **"Last Error" showed the destination's raw HTML error page** instead of the error. A Google 404 filled the column with page markup while the retry of the same failure said "HTTP 404" - now both read the same, on primary and secondary deliveries alike.
* Fixed: **a failed secondary delivery was never retried.** It was recorded and then dropped, so a destination that was briefly unreachable lost the submission outright, with nothing in the retry queue. Secondary failures now queue and retry like primary ones.
* Fixed: **Multi-Destination Routing saved nothing and delivered nothing.** Adding a second destination to a rule reported success, then vanished when you reopened the rule, and submissions went only to the primary. Three separate faults, now all fixed - the destinations were never saved, the feature was switched off by an internal setting with no control anywhere, and only webhooks had any delivery code. Email and Slack destinations now work too, with Slack messages formatted for Slack.
* Fixed: **saving a rule could quietly discard settings configured elsewhere.** The rule was rebuilt from scratch on every save, so anything the rule editor did not know about - set by a vertical pack, another page, or a filter - was lost. Renaming a rule was enough to trigger it.
* Fixed: **creating a product never produced a "Product created" row.** Publishing a product from the WordPress editor was reported as an update instead, so a sheet or filter built on product creation stayed empty while the event was offered in the settings. Creating a product through the API always worked, and still does - it is not reported twice.
* Added: **SheetLink checks your Apps Script as soon as the plugin updates**, instead of waiting for the next hourly check. Without this, a site that upgrades and takes a submission straight away could lose data before being told the script needs redeploying.
* Added: **a LearnDash "student enrolled" source.** SheetLink could already sync course completions; enrolments now sync too, so a free course used as a lead magnet puts each new student in your sheet as they join. Covers both admin-side enrolment and front-end self-enrolment, and does not fire when you remove a student's access.
* Changed: **the LearnDash, MemberPress and Easy Digital Downloads sources now say which event they sync.** Each listens for exactly one thing - a completed course, a completed transaction, a completed purchase - but the rule editor named only the plugin, so picking "LearnDash LMS" and then enrolling a student looked like a broken integration rather than the wrong event. They now read "LearnDash - course completed" and so on.
* Renamed in the interface: **Role-Based Access is now Admin Menu Visibility.** The feature hides SheetLink menus from other administrators; it cannot grant access to editors or authors, and the old name suggested otherwise.
* Added: **SheetLink now warns you if Update Mode is on while your Google Apps Script is out of date** - that combination empties columns your submission does not include, on every update, and nothing in the sheet records it. Webhook Health flags the affected rules and the dashboard shows a notice until it is resolved.
* Note for existing Google Apps Script users: the script has been updated. Webhook Health will tell you if yours is out of date - open your sheet, choose Extensions then Apps Script, replace the code with the current script, and deploy it as a **new version of the existing deployment**.

= 1.15.0 =
* Fixed: **partial refunds never reached your sheet, leaving the total overstated.** WooCommerce only marks an order "refunded" when it is refunded in full, so a partial refund changed nothing SheetLink was listening for - the row kept the original amount and nothing indicated it was wrong. Because full refunds do change the status, the gap only ever swallowed partial ones. Refund rows now arrive for both, and carry the refunded amount, the running total refunded, the net amount kept, and whether the refund was partial.
* Fixed: **the Airtable field-mapping help said the opposite of what happens.** It stated that unmapped form fields "pass through", which is only true when the mapping is empty or you have ticked the option below it. With a mapping set, unmapped fields are not sent - so people saw columns arrive blank in Airtable while the same submission was complete in their Google Sheet, and read it as data loss. Both the Airtable and Notion pages now say how many fields are mapped and state plainly that the rest are left out.
* Fixed: **White-Label left the SheetLink name in the page headings.** The menu was renamed but the Dashboard and Sync Rules headings still read "SheetLink Forms", so a client handed a rebranded site saw the real product name at the top of the first page they opened.
* Fixed: **the Post Type / ACF sync source was hard to find and its setup link went to the wrong page.** "Configure rule" opened the dashboard rather than the rule editor, and the source was labelled "ACF: ..." even on sites without ACF, so people looking for a post-type source concluded there wasn't one. It now reads "Post Type: ..." when ACF is not installed, and the button opens the rule editor.
* Fixed: the Retry Queue counted deliveries that had used all their attempts as still "pending retry". They are now reported separately as failed permanently, since nothing further will be attempted for them.
* Fixed: **retrying a failed delivery used the old receiver URL, even after you corrected it.** The retry queue stored the address at the moment of failure and never re-read the rule, so the fix Webhook Health tells you to make had no effect - you corrected the URL, pressed Retry, and watched the same deliveries fail until they hit the retry limit. Retries now use the rule's current receiver.
* Fixed: **saving a rule with a non-Google Receiver URL looked like nothing happened.** The error was displayed at the top of the page, above the fold on a long rule editor, so the Save button appeared dead. The message now also appears beside the button and highlights the field.
* Fixed: **the Features screen and upgrade prompts still quoted the old add-on prices.** Individual monthly prices and an "Agency Growth" tier that no longer exists were shown in the plugin - including on the message you see when a delivery is refused for lacking an add-on, which is the worst possible place to quote a price nobody can pay. Everything now names the plan that includes the feature, matching sheetlinkwp.com/pricing.
* Renamed in the interface: **CRM Fan-Out is now Multi-CRM Routing**, and **Advanced Routing is now Multi-Destination Routing**, matching the names used on the website and in your licence. Nothing to reconfigure - settings, licences and rules are unaffected.
* Added: the Features screen now shows Form Abandonment's live state - how many partial submissions are held and when they will be sent - because they are deliberately held until a visitor has been inactive for 30 minutes, and a feature you cannot see working is indistinguishable from one that is broken.
* Added: Analytics now says it starts counting from the moment you switch it on, instead of showing a page of zeros on a site that has been taking submissions all day.
* Fixed: **Two-Way Sync was sending SheetLink's own bookkeeping columns back into WordPress.** If your sheet had ever been written to by an out-of-date Apps Script, that script added `_sheetlink_ping`, `rule_id` and `ping_at` columns to it - and every later edit sent those columns back as though you had typed them. Updating the script stops new junk rows but cannot remove columns already created, so this kept happening after the apparent fix. Those three are now filtered out on the way in. Your attribution columns and the WooCommerce event marker still come through, since they describe the row rather than SheetLink's plumbing.
* Fixed: **an out-of-date Google Apps Script could add a junk row to your sheet every hour, and the health check called it healthy.** Older versions of the script do not recognise SheetLink's hourly health check and treat it as a form submission, so they append a row to your sheet roughly 720 times a month. Webhook Health only looked at whether the script replied, not what it said, so it reported the rule as reachable throughout. It now identifies which version of the script your sheet is running and tells you plainly when the script is writing those rows, with the exact steps to replace it. **If you copied our template sheet, you are likely affected - check SheetLink Forms -> Webhook Health.**
* Changed: **a visitor's campaign is now credited for 30 days instead of 90.** When someone arrives from an ad, SheetLink remembers the campaign so a form filled in later still credits the click that earned it. Ninety days meant a single click could be credited for every lead from that browser for three months, including ones that later arrived from search or typed your address in directly - which quietly overstates what your ads produced. Thirty days matches Google Ads' own default. Sites with long sales cycles can raise it (up to Google's 90-day limit) with the `sheetlink_attribution_ttl_days` filter.
* Changed: **on opt-in sites, click IDs and campaign data are now only recorded for visitors who accept marketing cookies.** If your consent plugin is set to opt-in (the default in the EU and UK), a visitor who declines - or who has not answered the banner yet - now has their submission saved without attribution. Their click ID would otherwise have been written to your sheet and uploaded to Google as an offline conversion for advertising measurement they did not agree to. **Expect fewer attributed conversions than leads on these sites: this is the change causing it, not a fault.** Sites set to opt-out are unaffected except that an explicit refusal is now honoured, and sites with no consent plugin are unaffected entirely. Both behaviours are available via the `sheetlink_attribution_consent` filter, and the Conversions screen states which rule is in force on your site.
* Note for sites using a page cache: consent is now checked in the visitor's own browser rather than trusted from the page HTML. This matters because caches share one stored copy across visitors, and often across query strings too - with W3TC's "Cache URI with query string variables" enabled, a request for one campaign URL is served the copy generated for a different one. Without this, a page first generated for a visitor who had accepted could have caused the next visitor to be treated as having accepted as well.
* Added: **campaign tracking now respects your cookie consent banner.** If your site runs a consent plugin that supports the WordPress Consent API, the campaign cookie is only stored for visitors who accept marketing cookies, and it waits if they answer after the page loads. **Sites with no consent plugin are unaffected and keep working exactly as before.** Tracking parameters on the link a visitor actually clicked are still recorded either way - the consent step covers storing their campaign for later, not the link they arrived on. The Dashboard says which of these applies to your site.
* Fixed: **UTM and click-ID attribution now reaches your sheet on the one-click Google connection.** Visitor source was always captured and sent, but this connection method never created columns for it, so it was written nowhere - while the classic Apps Script method had always recorded it. A submission that arrives with utm_source, gclid and the rest now gets those columns, added only when a visit actually carries them, so a sheet never fills with empty attribution headers. If your sheet's header was already fixed before this release, the columns are appended the next time a submission carries them.
* Fixed: **choosing a Google Sheet did not stick.** Creating or picking a sheet only filled in the form on screen - the choice was not saved until you pressed Save, and the next step in setup (creating a rule) navigated away first. The page then said "No sheet chosen yet" even though the spreadsheet existed in your Drive, and you had to pick it again. Creating or picking a sheet now saves it immediately.
* Fixed: saving the Google Sheets screen can no longer blank the destination sheet.
* Fixed: **finishing setup the one-click way now counts as finishing setup.** Only the wizard's own last step marked a site as set up, and the one-click route deliberately ends on the Google Sheets screen instead - so a fully configured site stayed flagged as never-set-up, and the first-run wizard could reappear after an unrelated deactivate and reactivate months later. Creating a rule against a connected sheet now completes setup, whichever route you took to get there.
* Improved: **the one-click Google route no longer loses your answers.** It finishes on the Google Sheets screen rather than in the wizard, and now carries your form plugin and rule name with it, pre-filled, with a note explaining where you are. Previously both were discarded and you retyped them.
* Improved: the welcome screen described the manual Apps Script route as the only path. It now leads with one-click Google, with Apps Script as the optional alternative it actually is - and the progress step is labelled "Connect Google" rather than "Google Script".
* Added: **the Lead Inbox is now part of SheetLink Forms.** Every submission becomes a lead you can own, answer and qualify - including the ones that never reached your sheet - with a response-time clock, notes, assignment and reports. It was previously a separate plugin; it is now built in, like every other add-on, and appears under SheetLink Forms when your licence includes it.
* If you installed the separate "SheetLink Lead Inbox" plugin, nothing breaks: SheetLink Forms leaves it in charge while it is active, and you can deactivate and delete it whenever you like. Your leads, notes and settings live in the same place either way and are not touched.
* Note on removal: deleting SheetLink Forms does **not** delete your leads. They are your records of real people you have spoken to, so they are kept unless you explicitly opt into deletion.

= 1.14.9 =
* Added: **a way past "Approval required" when connecting Excel Online.** Many Microsoft 365 organizations require an administrator to approve an app before anyone can connect it, so business users saw an approval wall instead of a sign-in screen with nothing on this end to do about it. The Excel Online page now provides a link you can send to whoever administers your Microsoft 365 tenant: they approve SheetLink Forms once for the whole organization, and everyone connects normally after that. They need no SheetLink licence and no login on your site.
* Improved: if Microsoft blocks the connection for that reason, the page you land on now explains why and carries the same link, instead of only saying sign-in did not complete.

= 1.14.8 =
* Renamed: **the Multi-Node Routing add-on is now Multi-Destination Routing.** Same add-on, same price, same settings - the new name says what it does: route one submission to several destinations with priority order and fallback webhooks. Nothing to reconfigure; existing licences and rules are unaffected.
* Improvement: the license key field no longer names a specific key format. Keys are validated by the licensing service, so paste whatever key your purchase email delivered.
* Documentation: clarified an internal code comment in the Excel destination about where the license gate is enforced.

Older entries (1.14.7 down to 1.7.0) are in changelog.txt, bundled with the plugin.

== Upgrade Notice ==

= 1.16.1 =
Readme and naming only. Coming from 1.15.x: the Lead Inbox now records leads on its own, AI Lead Scoring stays off until you enable it in Lead Settings, and if you still have the separate Lead Inbox add-on from the pilot, deactivate it after updating.
