Code HeavenCode Heaven
Developer API

Updates and Downloads: Shipping New Versions

Check for plugin updates with GET /licenses/{key}/updates and deliver them through the short-lived signed package URL from GET /licenses/{key}/download. A complete update flow with JSON examples.

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

| Code | When | What to do | |---|---|---| | 403 expired | License lapsed | Block the update, prompt renewal — expired licenses don't get new versions | | 403 domain_not_activated | Site has no seat | Activate the domain, then retry | | 404 not_found | Unknown product or version | Verify the product slug and that the version was published | | 429 rate_limited | Polling too often | Back 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.