Feedback is the cornerstone of any successful product development process. For WordPress plugin developers, understanding why users deactivate a plugin can offer invaluable insights.

As I was developing another awesome plugin for WordPress using Cloudflare, I wanted a feedback system if a user deactivates my plugin. I started searching online and the solutions found were not feasible for me.

Many WordPress plugin creators rely on built-in deactivation surveys or third-party services to gather feedback. However, these methods come with limitations and issues:

  • Costs: Running servers and managing infrastructure for feedback collection can be expensive. Thus, third-party providers charge quite a sum.
  • Latency: Slow feedback mechanisms can deter users from participating. When a user de-activates a plugin, he doesn’t expect the page to load for 10 seconds.
  • Integration hassles: Many feedback systems require a steep learning curve or complex integration processes, like SDKs, and so on.
  • Privacy issues: Most of the third-party service providers gather sensitive data like email addresses and more. Storing your users sensitive data on a platform that may or may not sell this data, get hacked or worse; is just not feasible.

So how can you gather this feedback efficiently, secured and cost-effectively? Enter my API based on Cloudflare Workers , KV and D1 .

I decided to create my own REST API based on the amazing Cloudflare Workers platform, effectively interconnecting my members area deployed using Pages with the API. You can read more about how I built my members area here .

This means that I can restrict the API usage just to esteemed members, whilst offering privacy and security of the stored data. The API is blazing fast worldwide with zero downtime, whilst maintaining low cost and enhanced security. What more can someone want? 😃

And this is just the beginning, as I have already prepared several endpoints for future services both free and paid for Clients, Developers and Partners.

Getting Started

Getting started with my API is straightforward. Sign up on my members area . After registration, simply visit your profile . If you already had an account, a set of API Tokens will be generated on the fly.

You will find on this page: Account ID, Public Token and Private Token:

  • The Account ID is used to identify your account and store information accordinly.
  • The Public token is limited to certain operations, please see the scopes.
  • The Private token is not limited like the Public one, and you can use it to read, write, delete feedbacks on your account. Again, please see the scopes.

Note: There is heavy rate limitting on my API to prevent abuse. If you play around too much, you might get restricted for a minimum of one hour or more.

There is no charge to use the WordPress feedback API endpoint, this is completely FREE, so please do not try to abuse it or I will have to ban your account.

Integrate the API with your plugin

Integration is super easy, you just need to follow these steps:

  1. Hook into admin_footer and inject your feedback modal/form.
  2. Load your CSS and JS using admin_enqueue_scripts; make sure you only load these on the plugins page. Tip: Create your own custom styles, otherwise you will break the plugins page style or even clash with another plugin feedback modal.
  3. Create one admin AJAX function to process the feedbacks; for example wp_ajax_your_plugin_deactivation_feedback. Note: Always use nonce to secure your AJAX functions.

When collecting feedback, you need to gather some information and send them to the API accordinly. As I am very privacy concerned, the API will not store email addresses or anything else considered confidential.

Furthermore, to comply with GDPR as best as we can, I provide all the five locations D1 offers to store the collected data:

  • Western North America
  • Eastern North America
  • Western Europe
  • Eastern Europe
  • Asia-Pacific

With that in mind, I also provide a GEO helper function which returns the region for your hosting. This means that you can automatically store the data in the closes region where your WordPress is hosted.

An example function to get the region:

 1private function get_region($account, $token)
 3	$api_url = "$account/geo-info";
 5	$response = wp_remote_post($api_url, [
 6		'headers' => [
 7			'Authorization' => 'Bearer '.$token,
 8			'Content-Type' => 'application/json; charset=utf-8',
 9		],
10		'method' => 'GET',
11	]);
13	$response_body = wp_remote_retrieve_body($response);
14	$response_data = json_decode($response_body, true);
16	return $response_data;

This will return a JSON response like:

 2    "longitude": "8.68370",
 3    "latitude": "50.11690",
 4    "continent": "EU",
 5    "asn": 14061,
 6    "asOrganization": "Digital Ocean",
 7    "country": "DE",
 8    "isEUCountry": "1",
 9    "city": "Frankfurt am Main",
10    "region": "Hesse",
11    "timezone": "Europe/Berlin",
12    "postalCode": "60341",
13    "recommended-storage-region": "weur"

As you can see in this example, the server resides in Hesse region, thus the API recommends to store the data in the weur region. Take this with a grain of salt; the API cannot guarantee it will always return the correct “recommandation”.

Now you can proceed to create the feedback object and send to the API the following: reason, comments, wp_plugin_name, wp_plugin_version, wp_site_url, wp_version, wp_locale, wp_multisite, php_version, db_type, db_version, server_type, server_version, date_created.

An example function to send the feedback:

 1public function wp_ajax_your_plugin_deactivation_feedback()
 3	if (!wp_verify_nonce($_POST['nonce'], 'wp_ajax_your_plugin_deactivation_feedback_nonce')) {
 4		wp_send_json_error(esc_html__('Request is invalid. Please refresh the page and try again.', 'your-plugin-name'), 400, 0);
 5		exit();
 6	}
 8	$account = "....";
 9	$token = "....";
10	$api_url = "$account/wp-feedback";
12	$region = $this->get_region($account, $token);
14	$response = wp_remote_post($api_url, [
15		'headers' => [
16			'Authorization' => 'Bearer '.$token,
17			'Content-Type' => 'application/json; charset=utf-8',
18		],
19		'body' => json_encode([
20			'reason'             => sanitize_text_field($_POST['reason']),
21			'comments'           => sanitize_textarea_field($_POST['comments']),
22			'wp_plugin_name'     => $this->plugin_name,
23			'wp_plugin_version'  => $this->version,
24			'wp_site_url'        => get_bloginfo('url'),
25			'wp_version'         => $this->get_wp_version(),
26			'wp_locale'          => get_locale(),
27			'wp_multisite'       => is_multisite(),
28			'php_version'        => $this->get_php_version(),
29			'db_type'            => $this->get_db_type(),
30			'db_version'         => $this->get_db_version(),
31			'server_type'        => $this->get_server_type(),
32			'server_version'     => $this->get_server_version(),
33			'date_created'       => current_time('mysql'),
34			'region'             => $region['recommended-storage-region'],
35		]),
36		'method' => 'PUT',
37		'data_format' => 'body'
38	]);
40	$response_body = wp_remote_retrieve_body($response);
41	$response_data = json_decode($response_body);
43	if ($response_data && isset($response_data->success) && $response_data->success === false) {
44		$error_message = isset($response_data->errors[0]->message) ? esc_html($response_data->errors[0]->message) : 'Unknown error';
45		wp_send_json_error($error_message, 400);
46		exit();
47	}
48	else if ($response_data && isset($response_data->success) && $response_data->success === true && isset($response_data->error)) {
49		wp_send_json_error($response_data->error, 400);
50		exit();
51	}
53	wp_send_json_success($response_data, 200, 0);

A succesfull saved feedback response will be returned like: Feedback saved in <region_name> region..

In the above example function you can see that we gather php_version, db_type, db_version, etc. For your convenience I will provide some example functions for these:

 1private function get_wp_version() {
 2	global $wp_version;
 3	return $wp_version;
 6private function get_php_version() {
 7	return PHP_VERSION;
10private function get_db_type() {
11	global $wpdb;
13	$query = "SELECT TABLE_SCHEMA FROM information_schema.TABLES WHERE TABLE_NAME = 'wp_options'";
14	$result = $wpdb->get_row($query);
16	if ($result && isset($result->TABLE_SCHEMA)) {
17		$table_schema = $result->TABLE_SCHEMA;
19		if (strpos($table_schema, 'mysql') !== false) {
20			return 'MySQL';
21		} elseif (strpos($table_schema, 'pgsql') !== false) {
22			return 'PostgreSQL';
23		} elseif (strpos($table_schema, 'sqlite') !== false) {
24			return 'SQLite';
25		}
26	}
28	return 'Unknown';
31private function get_db_version() {
32	global $wpdb;
33	return $wpdb->db_version();
36private function get_server_type() {
37	if (isset($_SERVER['SERVER_SOFTWARE'])) {
38		return explode(' ', $_SERVER['SERVER_SOFTWARE'])[0];
39	}
40	return 'Unknown';
43private function get_server_version() {
44	$uname = php_uname('s');
46	$serverTypes = [
47		'Apache' => 'Apache',
48		'Nginx' => 'Nginx',
49		'LiteSpeed' => 'LiteSpeed'
50	];
52	if (isset($serverTypes[$uname])) {
53		return $serverTypes[$uname];
54	}
56	return '0.0.0';

Now you should have everything you need to gather your WordPress Plugin deactivation feedbacks. Amend as neccessary to fit your needs.

Start collecting deactivation feedback

If you have followed the steps above, when a user tries to deactivate your plugin the feedback modal should appear:

WordPress Plugin Deactivation Feedback

The use can enter his reasons for deactivating your plugin. You can retrieve all your gathered feedbacks from the API anytime.

API Documentation

Base Endpoint:

WordPress Feedback Endpoint:{account_id}/wp-feedback

Fetch Feedbacks

  • Method: GET
  • URL:{account_id}/wp-feedback?region=[region]
  • URL Parameters:
    • region (Required): The region code where the feedback is stored. It can be wnam, enam, weur, eeur, or apac.
  • Scope: read:wp_feedback
  • Headers:
    • Authorization: Bearer token for access.
  • Response:
    • 200 OK with feedback data if successful.
    • 400 Bad Request if the region is invalid.
    • 403 Forbidden if the token is not authorized.
    • 404 Not Found if the token doesn't exist.
    • 406 Not Acceptable if an invalid region is provided.

Save Feedback

  • Method: PUT

  • URL:{account_id}/wp-feedback

  • Scope: write:wp_feedback
  • Headers:

    • Authorization: Bearer token for access.
    • Content-Type: application/json
  • Body:

    • JSON object containing:
      • reason (Required): The User deactivation reason. Allowed format: string.
      • comments (Required): The User deactivation comments. Allowed format: string.
      • wp_plugin_name (Required): The current WordPress Plugin name. Allowed format: string.
      • wp_plugin_version (Required): The current WordPress Plugin version. Allowed format:
      • wp_site_url (Required): The current WordPress URL. Allowed format: string.
      • wp_version (Required): The current WordPress version. Allowed format:
      • wp_locale (Required): The current WordPress locale. Allowed format: string.
      • wp_multisite (Required): If the current WordPress install is multisite. Allowed format: boolean.
      • php_version (Required): The current PHP version. Allowed format:
      • db_type (Required): The current database server type. Allowed format: string.
      • db_version (Required): The current database server version. Allowed format:
      • server_type (Required): The current web server type. Allowed format: string.
      • server_version (Required): The current web server version. Allowed format:
      • date_created (Required): The date/time when the feedback was created. Allowed format: YYYY-MM-DD HH:MM:SS.
      • region (Required): The region code where the feedback is stored. It can be wnam, enam, weur, eeur, or apac.
  • Response:

    • 200 OK if the feedback is saved successfully.
    • 400 Bad Request if any of the provided parameters are invalid.
    • 403 Forbidden if the token is not authorized or revoked/banned.
    • 404 Not Found if the token doesn't exist.
    • 406 Not Acceptable if an invalid region is provided.
    • 500 Internal Server Error if there's an error saving the feedback.

Delete Feedbacks

  • Method: POST

  • URL:{account_id}/wp-feedback

  • Scope: delete:wp_feedback
  • Headers:

    • Authorization: Bearer token for access.
    • Content-Type: application/json
  • Body:

    • JSON object containing:
      • feedback_ids (Required): Array of integers. Example: [1, 2, 3]
      • region (Required): The region code where the feedback is stored. It can be wnam, enam, weur, eeur, or apac.
  • Response:

    • 200 OK if all feedbacks are deleted successfully.
    • 400 Bad Request if any of the provided parameters are invalid or if feedback_ids is not an array of integers.
    • 403 Forbidden if the token is not authorized.
    • 404 Not Found if the token doesn't exist.
    • 406 Not Acceptable if an invalid region is provided.


Having such a feedback system will make a huge difference in your plugin development. I hope this post was helpful and you can improve your WordPress plugin using my API.

I may introduce activation feedback in the future, but I am very sceptic about it because the users cannot “opt in” or “opt out” of it. So for now, we will have just deactivation feedback.

If you have any feedback or suggestions, feel free to use the comment system below.