Paymenter API

A drop-in surface for Paymenter extensions to provision and bill VPS service on this panel. Scope: paymenter:provision.

Overview

Base URL: https://<your-panel-host>. All paths under /api/v1/paymenter. Designed so a Paymenter extension can drive the full server lifecycle from a single bearer key.

Issue a Paymenter key from /api-keys with the paymenter:provision scope. The key acts on behalf of the issuing owner and is rate-limited at 600 req/min.

Quick start

  1. Sign into VPSCtrl as an admin or owner. Go to /api-keys.
  2. Enter a label (e.g. paymenter-prod), pick an expiry, tick the paymenter:provision scope, generate the key. Copy the vfc_live_… secret — it is shown once.
  3. In your Paymenter installation, copy the extension scaffold below into extensions/Servers/Vpsctrl/ (see Build an extension).
  4. Set two env values on Paymenter:
    VPSCTRL_HOST=https://<your-panel-host>
    VPSCTRL_KEY=vfc_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
  5. Create a product in Paymenter that uses the Vpsctrl extension. Map the product's package_id / group_id / os_id to VPSCtrl's package, hypervisor group, and OS template ids.
  6. Place a test order on Paymenter. Confirm the extension calls user.create → server gets provisioned on VPSCtrl. The Paymenter invoice payment fires invoice.paid back, which lands in VPSCtrl's audit ledger at Admin → Resellers → Recent credit events.

Authentication

Authorization: Bearer vfc_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx

Create a downstream user

POST/api/v1/paymenter/user.createpaymenter:provision
{
  "email": "customer@example.com",
  "name": "Customer Name",
  "password": "optional-8-plus-chars"
}

Idempotent on email — if a user already exists, returns { "ok": true, "duplicate": true, "id": ... }. If password is omitted, a temp password is generated and returned once as temp_password.

// 200
{ "ok": true, "id": 125, "email": "customer@example.com" }

Server lifecycle

Three POSTs, identical payload. Operate on a service that was provisioned through this panel (paid invoice). The Paymenter extension stores our service_id on its side and passes it back here:

POST/api/v1/paymenter/server.suspendpaymenter:provision
{ "service_id": "svc_abc123" }

Marks the service as admin-suspended on this panel and calls VirtFusion to suspend the underlying VM. End users cannot self-unsuspend.

POST/api/v1/paymenter/server.unsuspendpaymenter:provision
{ "service_id": "svc_abc123" }
POST/api/v1/paymenter/server.terminatepaymenter:provision
{ "service_id": "svc_abc123" }

Schedules the VirtFusion server for deletion (5s delay). The local service row is marked terminated and remains for billing history.

Report an invoice payment

POST/api/v1/paymenter/invoice.paidpaymenter:provision

Push a paid invoice from Paymenter. Accepted in either flat or Paymenter's nested webhook envelope. Deduped by invoice_id.

// flat
{ "invoice_id": "INV-123", "amount": 25.00, "user_email": "customer@example.com" }

// or nested (matches Paymenter's outbound event format)
{ "event": "invoice.paid", "data": {
    "id": "INV-123", "total": 25.00, "user": { "email": "customer@example.com" }
} }
// 200
{ "ok": true, "id": "ev_..." }

Re-posting the same invoice returns { "ok": true, "duplicate": true }.

Build a Paymenter extension

Paymenter is a Laravel app. Server-type extensions live under extensions/Servers/<Name>/. The minimum surface is one class implementing create / suspend / unsuspend / terminate plus a config map that the admin UI uses to collect package_id, group_id, and os_id per product.

1. Create the directory layout:

extensions/Servers/Vpsctrl/
├── index.php           # extension entry, returns config
├── Vpsctrl.php         # main class
└── routes.php          # optional, for admin views

2. The extension entry — extensions/Servers/Vpsctrl/index.php:

<?php
return [
    'name'        => 'Vpsctrl',
    'description' => 'Provision VPS servers via VPSCtrl panel',
    'version'     => '1.0.0',
    'author'      => 'You',
    'settings'    => [
        ['name' => 'host', 'label' => 'VPSCtrl host', 'type' => 'text', 'required' => true],
        ['name' => 'key',  'label' => 'API key (vfc_live_…)', 'type' => 'password', 'required' => true],
    ],
    'product_settings' => [
        ['name' => 'package_id', 'label' => 'VPSCtrl package id', 'type' => 'number', 'required' => true],
        ['name' => 'group_id',   'label' => 'Hypervisor group id', 'type' => 'number', 'required' => true],
        ['name' => 'os_id',      'label' => 'OS template id', 'type' => 'number', 'required' => true],
    ],
];

3. The main class — extensions/Servers/Vpsctrl/Vpsctrl.php:

<?php
namespace Paymenter\Extensions\Servers\Vpsctrl;

use App\Classes\Extension\Server;
use Illuminate\Support\Facades\Http;

class Vpsctrl extends Server
{
    private function call($method, $path, $body = [])
    {
        $host = rtrim($this->config('host'), '/');
        $req  = Http::withToken($this->config('key'))
                    ->acceptJson()
                    ->timeout(20);
        $r = $method === 'GET'
            ? $req->get($host . $path)
            : $req->{strtolower($method)}($host . $path, $body);
        if (!$r->ok()) throw new \Exception('VPSCtrl ' . $path . ': ' . $r->body());
        return $r->json();
    }

    /** Called by Paymenter when an invoice is paid and the service must be provisioned. */
    public function createServer($user, $params, $order, $product, $configurableOptions)
    {
        // 1) Make sure the customer exists on VPSCtrl
        $u = $this->call('POST', '/api/v1/paymenter/user.create', [
            'email' => $user->email,
            'name'  => $user->name,
        ]);

        // 2) Provision the server via a one-shot signed order on VPSCtrl.
        //    Reuse VPSCtrl's reseller signup path or your panel-internal
        //    provisioning endpoint here. Store the returned service_id
        //    on the Paymenter service row.
        $svc = $this->call('POST', '/api/v1/reseller/provision-server', [
            'email'      => $user->email,
            'package_id' => (int) $product->settings->get('package_id'),
            'group_id'   => (int) $product->settings->get('group_id'),
            'os_id'      => (int) $product->settings->get('os_id'),
            'hostname'   => $params['hostname'] ?? null,
        ]);

        // Persist the VPSCtrl service id back onto the Paymenter row.
        $order->update(['external_id' => $svc['service_id']]);
        return true;
    }

    public function suspendServer($user, $params, $order, $product)
    {
        $this->call('POST', '/api/v1/paymenter/server.suspend', ['service_id' => $order->external_id]);
        return true;
    }

    public function unsuspendServer($user, $params, $order, $product)
    {
        $this->call('POST', '/api/v1/paymenter/server.unsuspend', ['service_id' => $order->external_id]);
        return true;
    }

    public function terminateServer($user, $params, $order, $product)
    {
        $this->call('POST', '/api/v1/paymenter/server.terminate', ['service_id' => $order->external_id]);
        return true;
    }
}

4. Report invoice payments back so VPSCtrl's owner can reconcile. In a service provider (or EventServiceProvider):

use App\Events\Invoice\Paid;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Http;

Event::listen(Paid::class, function (Paid $e) {
    Http::withToken(config('services.vpsctrl.key'))
        ->post(config('services.vpsctrl.host') . '/api/v1/paymenter/invoice.paid', [
            'invoice_id' => $e->invoice->id,
            'amount'     => $e->invoice->total,
            'user_email' => $e->invoice->user->email,
        ])
        ->throw();
});

5. Activate the extension from Paymenter admin → Extensions → Vpsctrl, fill in host + key, then create products that use it.

Testing checklist

  1. Auth probe. curl -i -H "Authorization: Bearer $KEY" https://<host>/api/v1/me should return your account JSON. A 401 means the key is wrong or revoked.
  2. user.create. curl -X POST https://<host>/api/v1/paymenter/user.create -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"email":"qa@example.com","name":"QA"}' — first call {ok:true, id:…}, second call {ok:true, duplicate:true}.
  3. Lifecycle. Run server.suspend on a known service id, confirm in VPSCtrl admin the row goes to suspended and the VirtFusion VM is suspended. Then server.unsuspend to restore.
  4. invoice.paid. Post a sample event, verify it appears under Admin → Resellers → Recent credit events. Re-post the same invoice_id — should return duplicate:true.
  5. End-to-end. Place a real Paymenter test order with a $0 coupon or a small amount, watch the VPSCtrl service get created and the credit event arrive.

Troubleshooting