Reseller API

RESTful JSON API for approved resellers. Browse the catalog, place orders, manage VPS instances, SSH keys, invoices, and support tickets — all programmatically. Every endpoint returns JSON unless otherwise noted (the only binary endpoint is the invoice PDF download).

Base URL: https://aluy.net/api/reseller/v1 · Last reviewed: 2026-04-23

Quick start

The five-call flow most resellers automate first: pick an OS, place a VPS order, watch it provision, fetch its IP, then power it on/off as needed. The block below uses the cookie auth covered in Authentication.

# 0. Save your session cookie once (replace with your value).
SESSION='__Secure-next-auth.session-token=eyJhbGciOi...'

# 1. List available OS templates that ship with the catalog package
#    (you only need this if you want to pick a non-default OS).
curl -s -H "Cookie: $SESSION" \
  https://aluy.net/api/reseller/v1/products | jq .

# 2. Place an hourly VPS order — preset variant path.
ORDER=$(curl -s -X POST -H "Cookie: $SESSION" \
  -H "Content-Type: application/json" \
  -d '{"variantId":"clxabc...","label":"edge-fi-01","sshKeys":[12]}' \
  https://aluy.net/api/reseller/v1/orders)
echo "$ORDER" | jq .

# 3. Find the service that was created from this order.
curl -s -H "Cookie: $SESSION" \
  "https://aluy.net/api/reseller/v1/services?status=ACTIVE&limit=5" | jq .

# 4. Fetch the live status (IP addresses, power state, resources).
SVC_ID=$(curl -s -H "Cookie: $SESSION" \
  "https://aluy.net/api/reseller/v1/services?status=ACTIVE&limit=1" \
  | jq -r '.services[0].id')
curl -s -H "Cookie: $SESSION" \
  https://aluy.net/api/reseller/v1/services/$SVC_ID/status | jq .

# 5. Power-cycle the box.
curl -s -X POST -H "Cookie: $SESSION" \
  -H "Content-Type: application/json" \
  -d '{"action":"restart"}' \
  https://aluy.net/api/reseller/v1/services/$SVC_ID/power | jq .

Authentication

Every endpoint requires an authenticated reseller session — a regular sign-in to the portal that has the reseller flag enabled on the account. There is no separate API token today; you authenticate by replaying the NextAuth session cookie from a logged-in browser.

  1. Sign in to the portal in your browser.
  2. Open DevTools → Application → Cookies and copy the value of __Secure-next-auth.session-token (or next-auth.session-token on plain HTTP).
  3. Send it on every request as a Cookie header.
curl -H "Cookie: __Secure-next-auth.session-token=eyJhbGciOi..." \
  https://aluy.net/api/reseller/v1/products

Sessions follow the portal's rolling expiry. If a request returns 403 { "error": "Reseller access required" }, sign in again and re-copy the cookie. Non-reseller users hit the same 403 even when authenticated.

Conventions

Read this once and the rest of the reference will read cleanly.

Money is in cents

All monetary values use the field suffix Centsand are integer cents in the order's currency (today, always EUR). Divide by 100 to display.

Timestamps are ISO-8601

All *At fields are ISO strings in UTC, e.g. 2026-04-23T14:08:11.123Z.

IDs are CUIDs (services, orders, ...)

Most resource ids are 25-char CUID strings. The exceptions are externalId on a service (the upstream hypervisor id, numeric string) and SSH key ids (numeric integers from VirtFusion).

Pagination

List endpoints accept limit (1–100, default 50) and offset (default 0) and return { items, total, limit, offset }.

Errors

Errors return JSON of shape { "error": "<message>" } and an HTTP status code from the table in Errors.

Idempotency

GET requests are safe and read-only. Other methods are not idempotent unless explicitly noted — assume each POST creates a new resource or kicks off a fresh side effect.

Billing model

Two account-level dimensions decide how an order is billed: billing mode (PREPAID vs POSTPAID, set on your reseller account) and period (hourly vs monthly, chosen per order).

  • billingMode: "PREPAID" — your wallet is debited as you spend. Hourly VPS draw down balanceCents every cron tick; monthly VPS deduct the full amount up-front. An order that would leave you below the cost of one hour is rejected with 402 Insufficient balance.
  • billingMode: "POSTPAID" — usage rolls up into a single invoice on the 1st of every month. Hourly VPS still meter per running hour; monthly VPS are added to that invoice at their monthly price. The only hard limit is resellerVpsLimit (returned as quota.vps.limit by GET /services); exceeding it returns 429.
  • period: "hourly"default for resellers. The order is created with totalCents: 0 (the meter starts from provisioning) and the response includes hourlyRateCents. Powered-off hours are not charged.
  • period: "monthly" — legacy upfront-then-renew. The full priceCentsis charged immediately (PREPAID: deducted now; POSTPAID: added to this month's invoice).

Always trust the billingMode / period / hourlyRateCents fields in the POST /orders response — they reflect what was actually applied.

Account

Your wallet balance, billing mode, recent transactions, and the invoice auto-pay toggle.

GET/api/reseller/v1/balance
Return wallet balance, billing mode, and the 50 most recent balance transactions (newest first).
Auth: Reseller sessionSuccess: 200

Example request

curl -H "Cookie: $SESSION" \
  https://aluy.net/api/reseller/v1/balance

Successful response

{
  "balanceCents": 4250,
  "billingMode": "POSTPAID",
  "invoiceAutoCharge": { "enabled": true, "locked": false },
  "transactions": [
    {
      "id": "clxbal...",
      "amountCents": -700,
      "type": "USAGE",
      "description": "Hourly VPS — edge-fi-01 (Apr 22 14:00–15:00 UTC)",
      "createdAt": "2026-04-22T15:00:00.000Z"
    }
  ]
}
PATCH/api/reseller/v1/balance
Toggle automatic charging of new invoices from your balance.
Auth: Reseller sessionSuccess: 200

Request body (JSON)

invoiceAutoChargebooleanrequiredtrue to enable, false to disable.

Example request

curl -X PATCH -H "Cookie: $SESSION" -H "Content-Type: application/json" \
  -d '{"invoiceAutoCharge": true}' \
  https://aluy.net/api/reseller/v1/balance

Successful response

{
  "invoiceAutoCharge": { "enabled": true, "locked": false }
}

Common errors

400Body is not valid JSON or invoiceAutoCharge is not a boolean.
{ "error": "Field \`invoiceAutoCharge\` must be a boolean." }
409PREPAID resellers cannot disable auto-charge.
{ "error": "Auto balance charge cannot be disabled while billing mode is PREPAID. Switch to POSTPAID first." }

Catalog

Read what you can sell. GET /products lists the preset variants; GET /vm-config describes the live custom-VM limits, pricing, and locations you can plug into POST /orders; POST /promo/validate checks a promo code for a given variant.

GET/api/reseller/v1/products
List all active products with their active variants. Use a variant id to place a preset order.
Auth: Reseller sessionSuccess: 200
  • The list only contains products with at least one active variant. Inactive variants are hidden.
  • period here is the catalog list price unit (always MONTHLY today) — it is not the reseller billing cadence. To order hourly use period: "hourly" on POST /orders.

Example request

curl -H "Cookie: $SESSION" \
  https://aluy.net/api/reseller/v1/products

Successful response

{
  "products": [
    {
      "id": "clxprd...",
      "slug": "vps-starter",
      "name": "VPS Starter",
      "description": "Entry-tier KVM",
      "category": "VPS",
      "features": ["1 vCPU", "1 GB RAM", "20 GB SSD"],
      "variants": [
        {
          "id": "clxvar...",
          "sku": "vps-1g",
          "name": "1 GB",
          "priceCents": 499,
          "period": "MONTHLY",
          "displayOrder": 1
        }
      ]
    }
  ]
}
GET/api/reseller/v1/vm-config
Live limits, per-resource pricing, and available datacenter locations for the custom VPS path. Use this to validate specs and quote prices client-side before posting an order.
Auth: Reseller sessionSuccess: 200
  • Hourly orders charge nothing at order time, but the meter rate is derived from the same per-resource pricing returned here.
  • custom.available / storage.available can flip to false if a hypervisor pool is fully allocated; planned orders against an unavailable pool are rejected with a 400.

Example request

curl -H "Cookie: $SESSION" \
  https://aluy.net/api/reseller/v1/vm-config

Successful response

{
  "custom": {
    "available": true,
    "pricing": {
      "ramPer1GbCents": 200,
      "cpuPerCoreCents": 300,
      "ssdPer10GbCents": 100
    },
    "limits": {
      "minRamGb": 1, "maxRamGb": 64,
      "minCpuCores": 1, "maxCpuCores": 16,
      "minSsdGb": 20, "maxSsdGb": 1000
    },
    "locations": [
      { "id": "fi-hel", "label": "Helsinki, FI" },
      { "id": "de-fra", "label": "Frankfurt, DE" }
    ]
  },
  "storage": {
    "available": true,
    "pricing": { "ramPer1GbCents": 200, "cpuPerCoreCents": 300, "hddPer1TbCents": 500 },
    "limits": { "minRamGb": 1, "maxRamGb": 32, "minCpuCores": 1, "maxCpuCores": 8, "minHddTb": 1, "maxHddTb": 16 },
    "locations": [{ "id": "fi-hel", "label": "Helsinki, FI" }]
  },
  "extraIpv4": { "monthlyCents": 100, "max": 4 }
}
POST/api/reseller/v1/promo/validate
Quote the discount a promo code would apply, optionally for a specific variant.
Auth: Reseller sessionSuccess: 200
  • An empty code, an unknown code, or a code that is not eligible always returns 200 with { valid: false, error } — never a 4xx. Treat valid as the source of truth.

Request body (JSON)

codestringThe promo code to test. Whitespace is trimmed.
variantIdstringOptional. When supplied, the discount is computed against this variant's price and category.

Example request

curl -X POST -H "Cookie: $SESSION" -H "Content-Type: application/json" \
  -d '{"code":"SUMMER10","variantId":"clxvar..."}' \
  https://aluy.net/api/reseller/v1/promo/validate

Successful response

{
  "valid": true,
  "code": "SUMMER10",
  "discountPercent": 10,
  "discountCents": 50,
  "newSubtotalCents": 449
}

Orders

POST /orders creates and provisions a new VPS in one call. Two paths are supported: preset (pick a catalog variant by id) and custom (build a VPS to spec). For per-hour billing send period: "hourly" (the default for resellers).

POST/api/reseller/v1/ordersVPS onlynot idempotent
Create and provision a VPS. Returns 201 with the order; provisioning runs synchronously inline so by the time the response arrives the service row exists (poll GET /services/:id/status to know when the VM is reachable).
Auth: Reseller sessionSuccess: 201
  • The two paths are mutually exclusive. Send either variantId or type with specs — never both.
  • POSTPAID resellers are capped at resellerVpsLimit active VPS — exceeding it returns 429 with the current count and limit.
  • operatingSystemId and sshKeys are not validated against VirtFusion at order time. If they are wrong, provisioning fails downstream and the service ends up PENDING with hasProvisionIssue: true — use POST /services/:id/retry-provision after fixing the input.

Request body (JSON)

// Path 1 — preset catalog variant
{
  "variantId": "clxvar...",      // required (from GET /products)
  "label": "edge-fi-01",         // optional display label
  "promoCode": "SUMMER10",       // optional
  "period": "hourly",            // "hourly" (default) | "monthly"
  "operatingSystemId": 42,       // optional VirtFusion template id
                                 // (see GET /services/:id/templates,
                                 //  or omit to use the package default)
  "sshKeys": [12, 17]            // optional VirtFusion key ids
                                 // (manage via /api/reseller/v1/ssh-keys)
}

// Path 2 — custom VPS (VirtFusion-backed)
{
  "type": "vm-custom",           // "vm-custom" | "vm-storage"
  "ramGb": 4,                    // required (limits in /vm-config)
  "cpuCores": 2,                 // required
  "ssdGb": 60,                   // required for type:"vm-custom"
  "hddTb": 1,                    // required for type:"vm-storage"
  "location": "fi-hel",          // required (id from /vm-config.locations)
  "extraIpv4": 1,                // optional, max from /vm-config
  "label": "edge-fi-01",         // optional
  "promoCode": "SUMMER10",       // optional
  "period": "hourly",            // "hourly" (default) | "monthly"
  "operatingSystemId": 42,       // optional
  "sshKeys": [12, 17]            // optional
}

Example request

curl -X POST -H "Cookie: $SESSION" -H "Content-Type: application/json" \
  -d '{
    "type": "vm-custom",
    "ramGb": 4, "cpuCores": 2, "ssdGb": 60,
    "location": "fi-hel",
    "label": "edge-fi-01",
    "sshKeys": [12]
  }' \
  https://aluy.net/api/reseller/v1/orders

Successful response

// 201 Created — hourly POSTPAID example
{
  "order": {
    "id": "clxord...",
    "status": "PAID",
    "totalCents": 0,
    "discountCents": 0,
    "billingMode": "POSTPAID",
    "period": "hourly",
    "hourlyRateCents": 7,
    "note": "Service is metered per running hour and rolled up into one invoice on the 1st of each month. Powered-off hours are not charged.",
    "lines": [{ "label": "edge-fi-01", "totalCents": 0 }],
    "createdAt": "2026-04-23T14:08:11.123Z"
  }
}

Common errors

400Body is not valid JSON.
{ "error": "Invalid JSON body" }
400Neither variantId nor a custom type was supplied.
{ "error": "Provide either variantId (preset) or type with specs (custom). type must be \"vm-custom\" or \"vm-storage\"." }
400Custom build with an unknown or out-of-stock location.
{ "error": "Invalid location \"fi-hel\". Available: de-fra, us-ash" }
400operatingSystemId is not a positive integer, or sshKeys contains non-positive integers / has more than 50 entries.
{ "error": "operatingSystemId must be a positive integer" }
400promoCode is invalid or not eligible.
{ "error": "Promo code expired" }
400variantId points at a non-VPS product.
{ "error": "Reseller orders are limited to VPS products" }
402PREPAID + hourly with insufficient balance for one hour.
{ "error": "Insufficient balance for hourly billing (need at least €0.07 for one hour, have €0.00). Top up your balance and try again." }
402PREPAID + monthly with insufficient balance for the order.
{ "error": "Insufficient balance" }
404variantId points at a missing or inactive variant.
{ "error": "Variant not found or inactive" }
429POSTPAID VPS limit reached.
{ "error": "VPS limit reached (3/3). Contact support to increase your limit.", "current": 3, "limit": 3 }
GET/api/reseller/v1/orders
List your orders, newest first.
Auth: Reseller sessionSuccess: 200

Query parameters

statusstringFilter. One of DRAFT, PENDING_PAYMENT, PAID, PROVISIONING, ACTIVE, CANCELLED, REFUNDED.
limitnumber1–100 (default 50).
offsetnumber≥ 0 (default 0).

Example request

curl -H "Cookie: $SESSION" \
  "https://aluy.net/api/reseller/v1/orders?status=PAID&limit=20"

Successful response

{
  "orders": [
    {
      "id": "clxord...",
      "status": "PAID",
      "currency": "EUR",
      "subtotalCents": 0,
      "vatCents": 0,
      "totalCents": 0,
      "reverseCharge": false,
      "lines": [
        { "label": "edge-fi-01", "quantity": 1, "unitCents": 0, "totalCents": 0 }
      ],
      "createdAt": "2026-04-23T14:08:11.123Z"
    }
  ],
  "total": 42,
  "limit": 20,
  "offset": 0
}

Common errors

400Unknown status value.
{ "error": "Invalid status. Valid values: DRAFT, PENDING_PAYMENT, PAID, PROVISIONING, ACTIVE, CANCELLED, REFUNDED" }
GET/api/reseller/v1/orders/:id
Detailed view of a single order, including its lines and payment history.
Auth: Reseller sessionSuccess: 200

Example request

curl -H "Cookie: $SESSION" \
  https://aluy.net/api/reseller/v1/orders/clxord...

Successful response

{
  "order": {
    "id": "clxord...",
    "status": "PAID",
    "currency": "EUR",
    "subtotalCents": 0,
    "vatCents": 0,
    "totalCents": 0,
    "reverseCharge": false,
    "lines": [
      { "id": "clxln...", "label": "edge-fi-01", "quantity": 1, "unitCents": 0, "totalCents": 0 }
    ],
    "payments": [
      {
        "id": "clxpay...",
        "provider": "MANUAL",
        "providerRef": "reseller-hourly",
        "amountCents": 0,
        "currency": "EUR",
        "status": "COMPLETED",
        "createdAt": "2026-04-23T14:08:11.123Z"
      }
    ],
    "createdAt": "2026-04-23T14:08:11.123Z",
    "updatedAt": "2026-04-23T14:08:11.123Z"
  }
}

Common errors

404Order does not exist or is not yours.
{ "error": "Order not found" }
POST/api/reseller/v1/orders/:id/pay
Pay a PENDING_PAYMENT order from your wallet balance. Marks any prior pending payment attempts on the order as FAILED, creates a fresh COMPLETED MANUAL payment, then runs provisioning.
Auth: Reseller sessionSuccess: 200

Request body (JSON)

method"balance"requiredOnly "balance" is accepted today.

Example request

curl -X POST -H "Cookie: $SESSION" -H "Content-Type: application/json" \
  -d '{"method":"balance"}' \
  https://aluy.net/api/reseller/v1/orders/clxord.../pay

Successful response

{ "ok": true }

Common errors

400Body is not valid JSON, or method is anything other than "balance".
{ "error": "Only balance payment is supported" }
402Wallet does not cover the order total.
{ "error": "Insufficient balance" }
404Order does not exist, is not yours, or is not in PENDING_PAYMENT state.
{ "error": "Pending order not found" }

Services

Lifecycle and metadata for every service on your account. Filter by kind to focus on VPS, dedicated, storage boxes, LIRs, etc. The list response also surfaces your quota and a real-time hourly spend summary.

GET/api/reseller/v1/services
List your services, newest first, plus current quota and hourly billing summary.
Auth: Reseller sessionSuccess: 200
  • quota.vps.limit is null for PREPAID resellers and for POSTPAID resellers configured with no cap.
  • hourly.hoursRemaining is a number for PREPAID with non-zero rate (balance ÷ rate), and null otherwise.
  • nextDueAt is null for hourly services and for storage boxes; it is set on monthly subscriptions only.

Query parameters

kindstringFilter. One of VPS, DEDICATED, HETZNER_SERVER, HETZNER_STORAGEBOX, LIR, CUSTOM.
statusstringFilter. One of PENDING, ACTIVE, SUSPENDED, TERMINATED.
limitnumber1–100 (default 50).
offsetnumber≥ 0 (default 0).

Example request

curl -H "Cookie: $SESSION" \
  "https://aluy.net/api/reseller/v1/services?kind=VPS&status=ACTIVE"

Successful response

{
  "services": [
    {
      "id": "clxsvc...",
      "kind": "VPS",
      "status": "ACTIVE",
      "label": "edge-fi-01",
      "externalId": "1234",
      "createdAt": "2026-04-23T14:08:11.123Z",
      "nextDueAt": null,
      "subscriptionBillingPeriod": "HOURLY",
      "subscriptionCancelledAt": null,
      "hourlyRateCents": 7,
      "lastBilledAt": "2026-04-23T15:00:00.000Z",
      "billedHourlyCents": 7,
      "pendingInvoiceCents": 0,
      "pendingInvoiceSeconds": 0,
      "pendingInvoiceSince": null
    }
  ],
  "total": 12,
  "limit": 50,
  "offset": 0,
  "quota": {
    "vps": { "used": 12, "limit": 25 },
    "billingMode": "POSTPAID"
  },
  "hourly": {
    "billingMode": "POSTPAID",
    "balanceCents": 4250,
    "totalHourlyRateCents": 84,
    "hoursRemaining": null,
    "activeHourlyServices": 12,
    "pendingInvoiceCents": 1260
  }
}

Common errors

400Unknown kind or status.
{ "error": "Invalid kind. Valid values: VPS, DEDICATED, HETZNER_SERVER, HETZNER_STORAGEBOX, LIR, CUSTOM" }
GET/api/reseller/v1/services/:id
Single-service detail with parsed metadata and a flag that surfaces failed provisioning attempts (so you can decide whether to call retry-provision).
Auth: Reseller sessionSuccess: 200
  • metadata.provisionError is stripped from this response; check hasProvisionIssue to know if it's present internally.

Example request

curl -H "Cookie: $SESSION" \
  https://aluy.net/api/reseller/v1/services/clxsvc...

Successful response

{
  "service": {
    "id": "clxsvc...",
    "kind": "VPS",
    "status": "ACTIVE",
    "label": "edge-fi-01",
    "externalId": "1234",
    "metadata": {
      "vmType": "custom",
      "vmLocation": "fi-hel",
      "ramGb": 4, "cpuCores": 2, "ssdGb": 60
    },
    "createdAt": "2026-04-23T14:08:11.123Z",
    "nextDueAt": null,
    "subscriptionBillingPeriod": "HOURLY",
    "subscriptionCancelledAt": null,
    "hourlyRateCents": 7,
    "billedRunningSeconds": 3600,
    "billedHourlyCents": 7,
    "lastBilledAt": "2026-04-23T15:00:00.000Z",
    "pendingInvoiceCents": 0,
    "pendingInvoiceSeconds": 0,
    "pendingInvoiceSince": null,
    "hasProvisionIssue": false
  }
}

Common errors

404Service does not exist or is not yours.
{ "error": "Service not found" }
PATCH/api/reseller/v1/services/:id
Update the display label of a service. For VPS, the new label is also pushed to the hypervisor as the server name (best-effort — failures are logged but do not fail the request).
Auth: Reseller sessionSuccess: 200

Request body (JSON)

labelstringrequired1–200 chars after trim.

Example request

curl -X PATCH -H "Cookie: $SESSION" -H "Content-Type: application/json" \
  -d '{"label":"client-acme-edge-01"}' \
  https://aluy.net/api/reseller/v1/services/clxsvc...

Successful response

{
  "service": {
    "id": "clxsvc...",
    "label": "client-acme-edge-01",
    "kind": "VPS",
    "status": "ACTIVE"
  }
}

Common errors

400No label was supplied (or the value is empty/too long).
{ "error": "No valid fields to update" }
POST/api/reseller/v1/services/:id/cancel
Cancel automatic renewal. The service stays active until the end of the current billing period; sets subscriptionCancelledAt to now.
Auth: Reseller sessionSuccess: 200

Example request

curl -X POST -H "Cookie: $SESSION" \
  https://aluy.net/api/reseller/v1/services/clxsvc.../cancel

Successful response

{
  "service": {
    "id": "clxsvc...",
    "label": "edge-fi-01",
    "status": "ACTIVE",
    "subscriptionCancelledAt": "2026-04-23T14:30:00.000Z"
  }
}

Common errors

400Service is already TERMINATED.
{ "error": "Service is already terminated" }
POST/api/reseller/v1/services/:id/resumeVPS only
Reverse a cancellation that hasn't taken effect yet. Clears subscriptionCancelledAt so the next billing cycle renews normally.
Auth: Reseller sessionSuccess: 200

Example request

curl -X POST -H "Cookie: $SESSION" \
  https://aluy.net/api/reseller/v1/services/clxsvc.../resume

Successful response

{ "ok": true }

Common errors

400The service was never cancelled.
{ "error": "Service is not cancelled" }
POST/api/reseller/v1/services/:id/suspendVPS only
Power off the VPS and mark it suspended. Use this to pause a downstream client without terminating the server.
Auth: Reseller sessionSuccess: 200

Example request

curl -X POST -H "Cookie: $SESSION" \
  https://aluy.net/api/reseller/v1/services/clxsvc.../suspend

Successful response

{ "ok": true, "serviceId": "clxsvc..." }

Common errors

409The service has not been provisioned yet (no externalId).
{ "error": "Service not yet provisioned" }
502The hypervisor rejected the suspend request.
{ "error": "<upstream message>" }
POST/api/reseller/v1/services/:id/unsuspendVPS only
Restore a suspended VPS. Marks the service ACTIVE and powers it back on.
Auth: Reseller sessionSuccess: 200

Example request

curl -X POST -H "Cookie: $SESSION" \
  https://aluy.net/api/reseller/v1/services/clxsvc.../unsuspend

Successful response

{ "ok": true, "serviceId": "clxsvc..." }

Common errors

502The hypervisor rejected the unsuspend request.
{ "error": "<upstream message>" }
POST/api/reseller/v1/services/:id/reset-passwordVPS only
Reset the root/admin password for the VPS. Returns the new password in plaintext exactly once — store it securely; we do not retain it.
Auth: Reseller sessionSuccess: 200

Example request

curl -X POST -H "Cookie: $SESSION" \
  https://aluy.net/api/reseller/v1/services/clxsvc.../reset-password

Successful response

{ "ok": true, "password": "Q9!t2R-u-pVnR3mJh8zX" }

Common errors

502The hypervisor rejected the request.
{ "error": "Upstream provider error" }
POST/api/reseller/v1/services/:id/retry-provisiondestructive
Re-run provisioning for a service that was created but failed to deploy upstream. Deletes the current service row, then re-issues the provisioning side-effects from the original order. Only valid while status is PENDING.
Auth: Reseller sessionSuccess: 200
  • Use this only when GET /services/:id reports hasProvisionIssue: true. The original id stops existing — capture serviceId from this response and use that going forward.
  • Calling twice in quick succession can interleave badly with the still-running first attempt. Wait for the first response before retrying.

Example request

curl -X POST -H "Cookie: $SESSION" \
  https://aluy.net/api/reseller/v1/services/clxsvc.../retry-provision

Successful response

{
  "ok": true,
  "serviceId": "clxsvcNEW...",
  "status": "ACTIVE"
}

Common errors

400Service is not in PENDING state.
{ "error": "Only pending services can be retried" }
400The original order can't be located in metadata.
{ "error": "Cannot retry — missing order information" }
409The service is still being set up (no provisionError yet).
{ "error": "Service is still being set up — please wait" }
POST/api/reseller/v1/services/:id/upgradeVPS only
Increase the resources of an ACTIVE VPS. Send only the dimensions you want to grow; downgrades are not supported. Resource upgrades create a new upgrade order; extra IPs create a separate add-on order.
Auth: Reseller sessionSuccess: 200
  • upgradeOrderId and extraIpOrderId are only present when that branch ran. At least one is always present on a 200.

Request body (JSON)

ramGbnumberNew RAM in GB. Must be greater than current.
cpuCoresnumberNew vCPU count. Must be greater than current.
ssdGbnumberNew SSD in GB. Must be greater than current.
extraIpv4numberNumber of additional IPv4 addresses to add (each creates a recurring line).

Example request

curl -X POST -H "Cookie: $SESSION" -H "Content-Type: application/json" \
  -d '{"ramGb":8, "extraIpv4":1}' \
  https://aluy.net/api/reseller/v1/services/clxsvc.../upgrade

Successful response

{
  "ok": true,
  "upgradeOrderId": "clxord...",
  "extraIpOrderId": "clxord..."
}

Common errors

400No upgrade fields supplied.
{ "error": "At least one upgrade field is required (ramGb, cpuCores, ssdGb, extraIpv4)" }
400Resource upgrade path failed validation/availability.
{ "error": "Upgrade not available" }
400Extra IP order failed.
{ "error": "Extra IP order failed" }
409Service is not ACTIVE.
{ "error": "Service must be active to upgrade" }
POST/api/reseller/v1/services/:id/reinstallVPS onlydestructive
Wipe and reinstall the VPS with a different OS template. Destructive — all data on the server is lost. Triggers the standard reinstall notification email in the background.
Auth: Reseller sessionSuccess: 200

Request body (JSON)

operatingSystemIdnumberrequiredVirtFusion template id (see GET /services/:id/templates).
hostnamestringOptional. Hostname to set on the new install.

Example request

curl -X POST -H "Cookie: $SESSION" -H "Content-Type: application/json" \
  -d '{"operatingSystemId":42, "hostname":"edge-fi-01.example.com"}' \
  https://aluy.net/api/reseller/v1/services/clxsvc.../reinstall

Successful response

{ "ok": true, "serviceId": "clxsvc..." }

Common errors

400operatingSystemId missing or not a number.
{ "error": "operatingSystemId is required" }
502Upstream rejected the reinstall.
{ "error": "Upstream provider error" }

VPS management

Live operations against a provisioned VPS. These all require kind: "VPS" and the service to have an externalId (i.e. provisioning has completed).

GET/api/reseller/v1/services/:id/status
Live server state, resources, and IP addresses straight from the hypervisor.
Auth: Reseller sessionSuccess: 200

Example request

curl -H "Cookie: $SESSION" \
  https://aluy.net/api/reseller/v1/services/clxsvc.../status

Successful response

{
  "serviceId": "clxsvc...",
  "externalId": "1234",
  "state": "running",
  "hostname": "edge-fi-01.example.com",
  "name": "vps-1234",
  "cpuCores": 2,
  "memoryMb": 4096,
  "storageGb": 60,
  "trafficGb": 1000,
  "ipAddresses": [
    { "address": "203.0.113.5",   "type": "ipv4" },
    { "address": "2001:db8::1",   "type": "ipv6" }
  ]
}

Common errors

502Hypervisor returned no server.
{ "error": "Could not retrieve server status." }
POST/api/reseller/v1/services/:id/power
Run a power action against the VPS.
Auth: Reseller sessionSuccess: 200

Request body (JSON)

{
  "action": "start"   // start | shutdown | stop | restart
}

Semantics:
  start     — Boot the server.
  shutdown  — Graceful OS shutdown (ACPI). Returns immediately.
  stop      — Force power off (equivalent to pulling the cord).
  restart   — Reboot.

Example request

curl -X POST -H "Cookie: $SESSION" -H "Content-Type: application/json" \
  -d '{"action":"restart"}' \
  https://aluy.net/api/reseller/v1/services/clxsvc.../power

Successful response

{ "ok": true, "action": "restart", "serviceId": "clxsvc..." }

Common errors

400Unknown action.
{ "error": "Invalid action. Use: start | shutdown | stop | restart" }
GET/api/reseller/v1/services/:id/console
VNC console connection details for the VPS — IP, port, password, plus a panel-issued WebSocket URL when available.
Auth: Reseller sessionSuccess: 200

Example request

curl -H "Cookie: $SESSION" \
  https://aluy.net/api/reseller/v1/services/clxsvc.../console

Successful response

{
  "serviceId": "clxsvc...",
  "console": {
    "ip": "192.168.4.2",
    "port": 5903,
    "password": "Z3eK..u9V",
    "wssUrl": "wss://panel.host/vnc/?token=...",
    "enabled": true
  }
}
POST/api/reseller/v1/services/:id/console
Enable or disable VNC console access on the VPS.
Auth: Reseller sessionSuccess: 200

Request body (JSON)

action"enable" | "disable"requiredWhether to turn console access on or off.

Example request

curl -X POST -H "Cookie: $SESSION" -H "Content-Type: application/json" \
  -d '{"action":"enable"}' \
  https://aluy.net/api/reseller/v1/services/clxsvc.../console

Successful response

{ "ok": true, "action": "enable", "serviceId": "clxsvc..." }
GET/api/reseller/v1/services/:id/templates
OS templates available for reinstalling this VPS. Use the returned ids on POST /services/:id/reinstall or POST /orders { operatingSystemId }.
Auth: Reseller sessionSuccess: 200
  • Returns the package's default template list even when the service has no externalId yet — useful for choosing an OS before placing an order.

Example request

curl -H "Cookie: $SESSION" \
  https://aluy.net/api/reseller/v1/services/clxsvc.../templates

Successful response

{
  "templates": [
    { "id": 1, "label": "Ubuntu 24.04 LTS" },
    { "id": 2, "label": "Debian 12" },
    { "id": 3, "label": "Rocky Linux 9" }
  ]
}

SSH keys

Manage SSH public keys on your panel account. Reference the ids returned here in the sshKeys field on POST /orders to inject them into authorized_keys at first boot of a new VPS. The firstPOSThere lazily provisions a panel account if you don't have one yet — there is nothing else you need to do.

GET/api/reseller/v1/ssh-keys
List the SSH keys registered on your panel account. Returns an empty list when you don't have a panel account yet (we don't lazy-create on read).
Auth: Reseller sessionSuccess: 200

Example request

curl -H "Cookie: $SESSION" \
  https://aluy.net/api/reseller/v1/ssh-keys

Successful response

{
  "sshKeys": [
    {
      "id": 12,
      "name": "laptop-ed25519",
      "publicKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...",
      "type": "OpenSSH",
      "enabled": true,
      "createdAt": "2026-03-01T08:21:00.000Z"
    }
  ]
}

Common errors

503VirtFusion credentials are not configured for this deployment.
{ "error": "Server management is not configured. Contact support." }
POST/api/reseller/v1/ssh-keys
Register a new SSH public key. Lazily creates a panel account on first call.
Auth: Reseller sessionSuccess: 201

Request body (JSON)

namestringrequiredDisplay name. Max 120 chars.
publicKeystringrequiredOpenSSH-format public key, e.g. "ssh-ed25519 AAAA... user@host".

Example request

curl -X POST -H "Cookie: $SESSION" -H "Content-Type: application/json" \
  -d '{
    "name": "laptop-ed25519",
    "publicKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI..."
  }' \
  https://aluy.net/api/reseller/v1/ssh-keys

Successful response

{
  "sshKey": {
    "id": 12,
    "name": "laptop-ed25519",
    "publicKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...",
    "type": "OpenSSH",
    "createdAt": "2026-04-23T14:08:11.123Z"
  }
}

Common errors

400name missing/too long, or publicKey is malformed.
{ "error": "Public key must be a valid OpenSSH key" }
502VirtFusion rejected the key (already registered, malformed, etc.).
{ "error": "<friendly upstream message>" }
503VirtFusion credentials are not configured for this deployment.
{ "error": "Server management is not configured. Contact support." }
DELETE/api/reseller/v1/ssh-keys/:id
Remove one of your SSH keys. Already-deployed VMs keep the key in authorized_keys; only future builds are affected. We verify ownership against your panel keyring before deleting.
Auth: Reseller sessionSuccess: 200

Example request

curl -X DELETE -H "Cookie: $SESSION" \
  https://aluy.net/api/reseller/v1/ssh-keys/12

Successful response

{ "ok": true, "id": 12 }

Common errors

400id is not a positive integer.
{ "error": "id must be a positive integer" }
404You have no panel account, or the key id doesn't belong to you.
{ "error": "SSH key not found" }

Invoices

Read your invoices and download their PDFs. Invoices issued before the current PDF schema (v2) cannot be re-rendered as PDF; the metadata endpoints still serve them.

GET/api/reseller/v1/invoices
List all invoices on your account, newest first.
Auth: Reseller sessionSuccess: 200

Query parameters

limitnumber1–100 (default 50).
offsetnumber≥ 0 (default 0).

Example request

curl -H "Cookie: $SESSION" \
  "https://aluy.net/api/reseller/v1/invoices?limit=20"

Successful response

{
  "invoices": [
    {
      "id": "clxinv...",
      "number": "INV-2026-001",
      "issuedAt": "2026-04-01T00:00:00.000Z",
      "currency": "EUR",
      "subtotalCents": 4990,
      "vatCents": 0,
      "totalCents": 4990
    }
  ],
  "total": 12,
  "limit": 20,
  "offset": 0
}
GET/api/reseller/v1/invoices/:id
Single invoice with the full snapshot (line items, tax breakdown, billing party) and the originating order id.
Auth: Reseller sessionSuccess: 200

Example request

curl -H "Cookie: $SESSION" \
  https://aluy.net/api/reseller/v1/invoices/clxinv...

Successful response

{
  "invoice": {
    "id": "clxinv...",
    "number": "INV-2026-001",
    "issuedAt": "2026-04-01T00:00:00.000Z",
    "currency": "EUR",
    "subtotalCents": 4990,
    "vatCents": 0,
    "totalCents": 4990,
    "vatLines": [{ "rateBps": 0, "baseCents": 4990, "vatCents": 0 }],
    "snapshot": {
      "schemaVersion": 2,
      "invoiceNumber": "INV-2026-001",
      "lines": [{ "label": "VPS Starter — 1 GB", "totalCents": 4990 }],
      "billTo": { "name": "Acme Ltd", "country": "DE" }
    },
    "orderId": "clxord..."
  }
}

Common errors

404Invoice does not exist or is not yours.
{ "error": "Invoice not found" }
GET/api/reseller/v1/invoices/:id/pdf
Download the invoice as a PDF. Returns application/pdf bytes with a Content-Disposition: attachment header. Cached as private; not stored or proxied.
Auth: Reseller sessionSuccess: 200

Example request

curl -H "Cookie: $SESSION" -OJ \
  https://aluy.net/api/reseller/v1/invoices/clxinv.../pdf
# saves INV-2026-001.pdf to the working directory

Successful response

HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="INV-2026-001.pdf"
Cache-Control: private, no-store

<PDF binary>

Common errors

400Invoice is on a legacy schema and cannot be rendered.
{ "error": "PDF generation is only available for current-format invoices" }
404Invoice does not exist or is not yours.
{ "error": "Invoice not found" }

Support tickets

Create and reply to support tickets, optionally linking them to one or more services so the support team has the full context.

GET/api/reseller/v1/tickets
List your support tickets, newest first.
Auth: Reseller sessionSuccess: 200

Query parameters

statusstringFilter. One of OPEN, AWAITING_CUSTOMER, AWAITING_STAFF, CLOSED.
limitnumber1–100 (default 50).
offsetnumber≥ 0 (default 0).

Example request

curl -H "Cookie: $SESSION" \
  "https://aluy.net/api/reseller/v1/tickets?status=OPEN"

Successful response

{
  "tickets": [
    {
      "id": "clxtkt...",
      "subject": "Reverse DNS for vps-42",
      "status": "OPEN",
      "priority": "NORMAL",
      "messageCount": 3,
      "createdAt": "2026-04-23T10:00:00.000Z",
      "updatedAt": "2026-04-23T11:00:00.000Z"
    }
  ],
  "total": 5,
  "limit": 50,
  "offset": 0
}
POST/api/reseller/v1/tickets
Create a new support ticket. Optionally link related services so we can pull metadata and access into the conversation.
Auth: Reseller sessionSuccess: 201
  • Creating a ticket also pings staff via Telegram and writes an audit log entry. There is no rate limit on creation but please don't abuse it.

Request body (JSON)

subjectstringrequiredShort summary. Trimmed; cannot be empty.
messagestringrequiredFirst message body. Trimmed; cannot be empty.
serviceIdsstring[]Optional. Service ids to attach. Non-owned ids are silently dropped.

Example request

curl -X POST -H "Cookie: $SESSION" -H "Content-Type: application/json" \
  -d '{
    "subject": "Reverse DNS for vps-42",
    "message": "Please set rDNS for 203.0.113.5 to mail.mydomain.com",
    "serviceIds": ["clxsvc..."]
  }' \
  https://aluy.net/api/reseller/v1/tickets

Successful response

{
  "ticket": {
    "id": "clxtkt...",
    "subject": "Reverse DNS for vps-42",
    "status": "OPEN",
    "messages": [
      {
        "id": "clxmsg...",
        "body": "Please set rDNS for 203.0.113.5 to mail.mydomain.com",
        "createdAt": "2026-04-23T14:08:11.123Z"
      }
    ],
    "createdAt": "2026-04-23T14:08:11.123Z"
  }
}
GET/api/reseller/v1/tickets/:id
Full ticket detail with the message history and any attached services.
Auth: Reseller sessionSuccess: 200

Example request

curl -H "Cookie: $SESSION" \
  https://aluy.net/api/reseller/v1/tickets/clxtkt...

Successful response

{
  "ticket": {
    "id": "clxtkt...",
    "subject": "Reverse DNS for vps-42",
    "status": "AWAITING_CUSTOMER",
    "priority": "NORMAL",
    "relatedServices": [
      { "id": "clxsvc...", "label": "edge-fi-01", "kind": "VPS" }
    ],
    "messages": [
      {
        "id": "clxmsg...",
        "author": "you@example.com",
        "body": "Please set rDNS for 203.0.113.5 to mail.mydomain.com",
        "staff": false,
        "createdAt": "2026-04-23T10:00:00.000Z"
      },
      {
        "id": "clxmsg...",
        "author": "support",
        "body": "Done — rDNS now points at mail.mydomain.com.",
        "staff": true,
        "createdAt": "2026-04-23T11:00:00.000Z"
      }
    ],
    "createdAt": "2026-04-23T10:00:00.000Z",
    "updatedAt": "2026-04-23T11:00:00.000Z"
  }
}

Common errors

404Ticket does not exist or is not yours.
{ "error": "Ticket not found" }
POST/api/reseller/v1/tickets/:id/reply
Reply to an existing ticket. Sets the ticket status to AWAITING_STAFF and pings on-call via Telegram.
Auth: Reseller sessionSuccess: 201

Request body (JSON)

messagestringrequiredReply body. Trimmed; cannot be empty.

Example request

curl -X POST -H "Cookie: $SESSION" -H "Content-Type: application/json" \
  -d '{"message":"Thanks, confirmed working."}' \
  https://aluy.net/api/reseller/v1/tickets/clxtkt.../reply

Successful response

{
  "message": {
    "id": "clxmsg...",
    "body": "Thanks, confirmed working.",
    "createdAt": "2026-04-23T14:08:11.123Z"
  }
}

Common errors

409The ticket is CLOSED and can't accept replies. Open a new ticket instead.
{ "error": "Ticket is closed" }

Errors

All errors return JSON of shape { "error": "<message>" }. Some endpoints add extra fields (e.g. POST /orders returns current and limit on a 429). The exact wording can change — switch on the HTTP status code, not the message.

HTTP status codes:
  400  Bad request           — Missing/invalid parameters or malformed JSON body.
  402  Payment required      — Insufficient wallet balance for the requested action.
  403  Reseller access       — Not authenticated, or authenticated user is not a reseller.
                                Body: { "error": "Reseller access required" }
  404  Not found              — Resource does not exist or you don't own it.
  409  Conflict               — State doesn't allow the action (e.g. cancel an already-terminated
                                service, suspend an unprovisioned one, reply to a closed ticket,
                                or disable auto-charge while PREPAID).
  429  Rate / quota exceeded  — POSTPAID VPS limit reached. Body includes "current" and "limit".
  502  Upstream error         — Hypervisor / payments gateway / third-party provider returned
                                an error. Retrying after a short delay is usually safe.
  503  Service unavailable    — A required integration (e.g. VirtFusion creds) is not configured
                                in this deployment. Contact support.

Retry guidance

  • 4xx errors should not be retried without changes — the input is the problem.
  • 5xx errors are usually transient. Back off (2s, 4s, 8s) and retry up to 3 times.
  • If a POST /orders times out, fetch GET /orders before retrying — the order may already exist.