Skip to main content

Updates and Downloads

Once a customer's license is valid, Code Heaven also serves your plugin's updates. There are two endpoints: one asks whether a newer version exists, the other hands back a short-lived signed URL to download it. Together they replace the update server you would otherwise have to run.

Step 1 — Check for an update

Call GET /licenses/{licenseKey}/updates with the product, the version currently installed, and the domain. Code Heaven compares the installed version against the latest published release for that product.

curl -G https://api.code-heaven.com/v1/licenses/CH-7K2P-9XQ4-LM83/updates \
-H "X-CH-Vendor-Key: $CH_VENDOR_KEY" \
--data-urlencode "product=acme-forms-pro" \
--data-urlencode "installedVersion=2.4.1" \
--data-urlencode "domain=example.com"

When a newer version exists, hasUpdate is true and you get the changelog for everything between the installed version and the latest:

{
"product": "acme-forms-pro",
"latestVersion": "2.6.0",
"hasUpdate": true,
"changelog": [
{
"version": "2.6.0",
"date": "2026-05-28",
"notes": "New conditional-logic builder. Fixes reCAPTCHA v3 edge case."
},
{
"version": "2.5.0",
"date": "2026-04-30",
"notes": "Multi-step forms. Performance: 40% faster render on large forms."
}
]
}

When the customer is already current, hasUpdate is false and latestVersion equals what they have installed:

{
"product": "acme-forms-pro",
"latestVersion": "2.4.1",
"hasUpdate": true,
"changelog": []
}

Poll this daily, not on every page load. A once-a-day check is the WordPress norm and keeps you well inside the rate limit. Cache latestVersion and the changelog between checks.

The domain parameter matters: updates are gated by the license, so a 403 domain_not_activated here means the customer needs to activate this site before they can pull updates.

Step 2 — Get the signed download URL

When the customer chooses to update, call GET /licenses/{licenseKey}/download to mint a signed package URL. Pass the exact version you want (usually latestVersion from the updates call) and the domain.

curl -G https://api.code-heaven.com/v1/licenses/CH-7K2P-9XQ4-LM83/download \
-H "X-CH-Vendor-Key: $CH_VENDOR_KEY" \
--data-urlencode "product=acme-forms-pro" \
--data-urlencode "version=2.6.0" \
--data-urlencode "domain=example.com"
{
"url": "https://dl.code-heaven.com/pkg/acme-forms-pro/2.6.0/CH-7K2P...sig=9f2c&exp=1717500000",
"version": "2.6.0",
"expiresAt": "2026-06-04T11:45:00Z"
}

The url is a short-lived signed URL. Three rules follow from that:

  1. Fetch it now. It expires at expiresAt (minutes, not hours). Request it immediately before you download; never store it for later.
  2. Do not cache or share it. The signature ties it to this license and this moment. A stale or shared URL fails.
  3. The vendor key is not used to fetch the package. You GET the url directly — it carries its own signature. Do not attach X-CH-Vendor-Key to the download request.

A complete update flow in PHP

Here is the whole loop: check daily, and when the customer clicks update, mint the URL, download, and hand the zip to WordPress.

<?php

// 1. Daily check — cache the result.
function ch_check_for_update(string $key, string $domain, string $installed): array
{
$query = http_build_query([
'product' => 'acme-forms-pro',
'installedVersion' => $installed,
'domain' => $domain,
]);

$resp = ch_get("/licenses/{$key}/updates?{$query}"); // sends X-CH-Vendor-Key
set_transient('acme_update_info', $resp, DAY_IN_SECONDS);
return $resp;
}

// 2. When the customer updates — mint the signed URL and pull the package.
function ch_download_package(string $key, string $domain, string $version): string
{
$query = http_build_query([
'product' => 'acme-forms-pro',
'version' => $version,
'domain' => $domain,
]);

$info = ch_get("/licenses/{$key}/download?{$query}"); // { url, version, expiresAt }

// Fetch the signed URL DIRECTLY — no vendor key on this request.
$tmp = download_url($info['url']); // WP helper; returns a temp file path
if (is_wp_error($tmp)) {
throw new RuntimeException('Package download failed: ' . $tmp->get_error_message());
}
return $tmp; // hand this zip to WP_Upgrader / Plugin_Upgrader
}

Wiring into the WordPress update UI

To make updates appear on the customer's Plugins screen, hook the transient WordPress uses to discover updates:

<?php

add_filter('pre_set_site_transient_update_plugins', function ($transient) {
$info = get_transient('acme_update_info') ?: ch_check_for_update(
get_option('acme_license_key', ''),
parse_url(home_url(), PHP_URL_HOST),
ACME_FORMS_VERSION
);

if (!empty($info['hasUpdate'])) {
$plugin = 'acme-forms-pro/acme-forms-pro.php';
$transient->response[$plugin] = (object) [
'slug' => 'acme-forms-pro',
'plugin' => $plugin,
'new_version' => $info['latestVersion'],
// package is fetched on demand via the signed URL when WP installs
'package' => '', // resolved in upgrader_pre_download
];
}

return $transient;
});

Because the package URL is short-lived, resolve it at install time (in an upgrader_pre_download filter), not when you build the transient — mint the URL only when WordPress is actually about to download.

Error handling

CodeWhenWhat to do
403 expiredLicense lapsedBlock the update, prompt renewal — expired licenses don't get new versions
403 domain_not_activatedSite has no seatActivate the domain, then retry
404 not_foundUnknown product or versionVerify the product slug and that the version was published
429 rate_limitedPolling too oftenBack off; move to a daily check

The PHP SDK wraps both endpoints — checkUpdate() and download() — including the daily caching and the signed-URL handling shown above.