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.
paymenter:provision scope. The key acts on behalf of the issuing owner and is rate-limited at 600 req/min.Quick start
- Sign into VPSCtrl as an admin or owner. Go to /api-keys.
- Enter a label (e.g.
paymenter-prod), pick an expiry, tick thepaymenter:provisionscope, generate the key. Copy thevfc_live_…secret — it is shown once. - In your Paymenter installation, copy the extension scaffold below into
extensions/Servers/Vpsctrl/(see Build an extension). - Set two env values on Paymenter:
VPSCTRL_HOST=https://<your-panel-host> VPSCTRL_KEY=vfc_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
- Create a product in Paymenter that uses the Vpsctrl extension. Map the product's
package_id/group_id/os_idto VPSCtrl's package, hypervisor group, and OS template ids. - Place a test order on Paymenter. Confirm the extension calls
user.create→ server gets provisioned on VPSCtrl. The Paymenter invoice payment firesinvoice.paidback, which lands in VPSCtrl's audit ledger atAdmin → Resellers → Recent credit events.
Authentication
Authorization: Bearer vfc_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
Create a downstream user
{
"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:
{ "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.
{ "service_id": "svc_abc123" }
{ "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
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
- Auth probe.
curl -i -H "Authorization: Bearer $KEY" https://<host>/api/v1/meshould return your account JSON. A401means the key is wrong or revoked. - 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}. - Lifecycle. Run
server.suspendon a known service id, confirm in VPSCtrl admin the row goes tosuspendedand the VirtFusion VM is suspended. Thenserver.unsuspendto restore. - invoice.paid. Post a sample event, verify it appears under Admin → Resellers → Recent credit events. Re-post the same
invoice_id— should returnduplicate:true. - 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
- 401 "invalid API key" — the key was rotated or the scope
paymenter:provisionis missing. Mint a fresh key. - 404 "service not found" on suspend/terminate — Paymenter is sending the wrong
service_id. Make sure you stored VPSCtrl'ssvc_…on the Paymenter order row at provision time. - 409 / "already exists" from
user.createis not an error — the response includesduplicate:trueand is safe to ignore. - 429 rate limited — Paymenter is retrying too fast. The endpoint allows 600 req/min/key/IP. Back off and retry with jitter.
- Credit event missing from the VPSCtrl audit feed — confirm the event listener fired (Paymenter logs) and that your
invoice_idis stable. Different ids each call defeats dedupe and may still 200, but admins will see duplicates.