មូលដ្ឋានចំណេះដឹង

លក់សេវាបង្ហោះវេបសាយ Zinn® ពី HostBill, Blesta ឬប្រព័ន្ធរបស់អ្នកផ្ទាល់

លក់សេវាបង្ហោះគេហទំព័រ Zinn ពី HostBill, Blesta ឬប្រព័ន្ធផ្ទាល់ខ្លួនរបស់អ្នក៖ ម៉ូឌុល HostBill ឥតគិតថ្លៃ, កម្មវិធីគំរូ PHP ឯកសារតែមួយ, បទបញ្ជាពីរដែលត្រូវបង់ប្រាក់នៅពេលបំពាន, និងវាលសំណើពីរដែលងាយនឹងភ្លេចដាក់បញ្ចូល។

If you run WHMCS, use the WHMCS module instead — this page is for everything else. There are two options, both free and both GPL-2.0-or-later.

HostBill

A hosting module with the same six actions as the WHMCS one.

  1. Download it and unzip.
  2. Upload the zinn directory into includes/modules/Hosting/ so the files land at
  3. includes/modules/Hosting/zinn/class.zinn.php and includes/modules/Hosting/zinn/ZinnHostBillClient.php.

  4. Settings → Modules → Hosting, activate Zinn Digital®, and add a connection:
  • API address: https://api.zinndigital.com
  • API key: your Zinn® API key (a password field, so HostBill stores it encrypted)
  1. On the product set Product line (e.g. mainstream), Stack (wordpress by
  2. default), and optionally Application and PHP version.

  3. Press Test connection.

Do not rename the zinn directory. HostBill's loader derives the class it looks for from the directory name, so a rename produces a module the panel lists and never calls.

Anything else — the PHP client

One file, no dependencies, no panel assumptions. Drop ZinnProvisioning.php into your own codebase and call six methods.

require_once 'ZinnProvisioning.php';

$zinn = new ZinnProvisioning(getenv('ZINN_API_KEY'));

// When an order is paid — once per CUSTOMER, then once per SERVICE:
$orgId  = $zinn->account('customer-4211', 'Acme Ltd');
$siteId = $zinn->provision('service-9915', $orgId, 'acme.com', 'mainstream', 'wordpress');
// Store $orgId against your customer and $siteId against your service.

$zinn->suspend($siteId, 'INV-2026-114 unpaid');   // an invoice goes unpaid
$zinn->unsuspend($siteId);                        // they pay
$zinn->terminate($siteId);                        // they cancel — schedules a deletion

// When they click "log in to my hosting" — mint it on the CLICK, never on page render:
header('Location: ' . $zinn->signInLink($siteId));

Not writing PHP? The same calls are in the API reference and you can make them from anything.

The API key

Seven permissions, and no more: org.create, org.read, sites.create, sites.view, sites.delete, reseller.view, reseller.provision.

Not reseller.manage. A key you paste into a billing panel should be able to hold a client for non-payment and sign them in; it should not be able to rewrite your own payment-gateway credentials.

The two rules that cost money when they are broken

  1. Key the account on your CUSTOMER, and the site on your SERVICE. A customer's second
  2. order must land in the account they already have. Key both on the service and one customer ends up with three unrelated accounts and three separate control panels.

  3. Send an idempotency key on every create, derived from your own id for the thing. Every
  4. billing system retries — a gateway callback arrives twice, an admin re-runs a failed provision, a customer double-clicks. Without it the second attempt creates a second site and you are billed for it. Both modules and the PHP client do this for you; if you are calling the API directly, send Idempotency-Key.

Three fields that are easy to leave out

Each one breaks a whole verb, and none shows up as anything but a validation error — except the first, which shows up as nothing at all:

  • ⛔⛔ A client must be put on a plan BEFORE their first site is provisioned, with
  • POST /v1/reseller/clients/{orgId}/plan, and its subscription_id passed to POST /v1/sites. This is the one with no error to read. A new client account holds no subscription and POST /v1/sites has no plan field, so a site provisioned without one carries no plan at all: your wholesale statement is built from your clients' live subscriptions, so no line is raised and Zinn® bills you nothing for as long as the hosting runs; your client inherits no allowances, so no quota is applied to their site; and there is nothing for an upgrade to change. The order returns 201, the site provisions, and it serves perfectly. Nothing at either end reports any of it.

The order matters: POST /v1/sites records whatever subscription exists at the moment it runs, so a plan set afterwards does not attach retrospectively.

  • POST /v1/sites requires stack_type. Omit it and no order can succeed.
  • **DELETE /v1/sites/{siteId} requires a body `{"confirm_domain": "<the site's own
  • domain>"}** — a typed confirmation, because this is the destructive verb. Omit it and no cancellation can succeed, so the service keeps running and you keep being billed for it. Read the domain back from GET /v1/reseller/services/{siteId}` rather than using your own record: it is compared against the site's own primary domain and never against an alias.

Read the whole error, not just the sentence

A validation refusal's message is deliberately generic — "The request was well-formed but failed validation." — because the actionable half is in details, which names the field and why. Both modules and the PHP client append it to the message they show your admin. If you are calling the API yourself, read error.details; some entries carry a bare value rather than a sentence, which is how max_sites: 0 reaches you when your reseller plan has no room.

Before your first order

Sites your clients buy count against your reseller plan's site allowance, so you need a plan with room in it. A connection test cannot detect this — it reads your programme and provisions nothing — so it reports a healthy account that cannot yet sell. An order placed without an allowance is refused with a message saying exactly that.

Verify your download

Both archives are served over TLS from our own hostname, never redirected elsewhere, with their SHA-256 checksums printed beside the download links on the downloads page.

នៅតែជាប់គាំងមែនទេ?

ការគាំទ្រត្រូវបានរួមបញ្ចូលនៅលើគ្រប់កញ្ចប់ទាំងអស់ និងការឆ្លើយតបជាភាសាផ្ទាល់ខ្លួនរបស់អ្នក។

ទំនាក់ទំនងផ្នែកជំនួយ អត្ថបទទាំងអស់