Skip to content

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 · OpenAPI 3.1 specification · Last reviewed: 2026-09-15

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 an API key, covered in Authentication.

# 0. Save your API key once (replace with your value).
KEY='aluy_rs_...'

# 1. List available OS templates (you only need this if you want a non-default OS).
curl -s -H "Authorization: Bearer $KEY" \
  https://aluy.net/api/reseller/v1/os-templates | jq .

# 2. Place an hourly VPS order — preset variant path.
ORDER=$(curl -s -X POST -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -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 "Authorization: Bearer $KEY" \
  "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 "Authorization: Bearer $KEY" \
  "https://aluy.net/api/reseller/v1/services?status=ACTIVE&limit=1" \
  | jq -r '.services[0].id')
curl -s -H "Authorization: Bearer $KEY" \
  https://aluy.net/api/reseller/v1/services/$SVC_ID/status | jq .

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

Authentication

Use a reseller API key with the minimum scopes your integration needs; every example below sends one as $KEY. A portal session cookie still works for reading (GET). A POST, PATCH or DELETE on a session is accepted only when the browser sends the portal's own Origin; anything else, including curl with a copied cookie, gets 403 origin_not_allowed.

curl -H "Authorization: Bearer aluy_rs_..." \
  https://aluy.net/api/reseller/v1/products

Keys are shown once and may also be sent in X-API-Key. Assign only the required scopes: catalog:read, orders:read/write, services:read/write/control, billing:read/write, tickets:read/write, ssh-keys:read/write, and webhooks:read/write. A key may also be restricted to CIDR ranges and an expiry date.

Conventions

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

Money is in cents

All monetary values use the field suffix Cents and 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 server's id on its host: a numeric string such as "1234", or "pve:…" (pve:<vm number>) on our newer hosts — treat it as an opaque string, it can change when a server moves) and SSH key ids (numeric integers; keys saved in the customer portal are numbered from 10,000,000 up).

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, code, requestId }. Treat the stable code as machine-readable and quote requestId when troubleshooting.

A server action the host did not carry out (power, console, password, reinstall, templates) answers 502 Upstream provider error with detail: the same plain reason your customer sees in the portal, e.g. The server's host did not answer. Try again in a minute. Changes a running install or a move to new hardware would undo answer 409 with code install_running or moving.

Idempotency

POST /orders, POST /orders/:id/pay, POST /services/:id/upgrade, POST /services/:id/remove-ipv4, POST /services/:id/reinstall, and POST /services/:id/retry-provision require a unique Idempotency-Key (8–200 printable characters). Reusing it with the same request safely replays the result; changing the request returns 409.

Secrets are never stored for a replay. A replayed POST /services/:id/reset-password, POST /webhooks or POST /webhooks/:id/rotate-secret returns password / secret as null with a note; call it again with a new key to get a new one. X-Request-Id is only echoed back for your logs.

On the endpoints that move money, a key is never recycled. If an attempt is interrupted before it completes, retrying with the same key returns 409 — idempotency_in_progress while it may still be running, or idempotency_key_unusable once it is known to have failed. Check whether the original took effect, then retry with a new key. This is deliberate: replaying a half-finished charge is worse than making you look.

Rate limits

Limits are enforced per reseller, key/session, and scope, not per client IP. A 429 response includes Retry-After and X-RateLimit-Limit.

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 (the net usage); on the 1st a usage invoice lists the month and adds VAT where it applies, also taken from the wallet — a tick leaves enough for that VAT or suspends the VPS. 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. New orders and upgrades must fit your aggregate RAM, vCPU, storage, and IPv4 quota. Use GET /quota to check usage and remaining capacity; exceeding any resource returns 429. An overdue POSTPAID invoice suspends all linked reseller services; payment restores only services suspended for that invoice, while continued non-payment follows the normal termination schedule.
  • 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 priceCents is 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 API key or sessionSuccess: 200

Example request

curl -H "Authorization: Bearer $KEY" \
  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 API key or sessionSuccess: 200

Request body (JSON)

invoiceAutoChargebooleanrequired— true to enable, false to disable.

Example request

curl -X PATCH -H "Authorization: Bearer $KEY" -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." }
GET/api/reseller/v1/quota
Return aggregate POSTPAID VPS resource usage, hard limits, remaining capacity, and automatic growth. PREPAID resource limits are null.
Auth: Reseller API key or sessionSuccess: 200

Example request

curl -H "Authorization: Bearer $KEY" \
  https://aluy.net/api/reseller/v1/quota

Successful response

{
  "billingMode": "POSTPAID",
  "resources": {
    "used":      { "ramMb": 27648, "cpuCores": 23, "storageGb": 300, "ipv4": 21 },
    "reserved":  { "ramMb": 0, "cpuCores": 0, "storageGb": 0, "ipv4": 0 },
    "limits":    { "ramMb": 131072, "cpuCores": 64, "storageGb": 1024, "ipv4": 64 },
    "remaining": { "ramMb": 103424, "cpuCores": 41, "storageGb": 724, "ipv4": 43 }
  },
  "growth": {
    "ramMb": 8192, "cpuCores": 4, "storageGb": 50, "ipv4": 4,
    "lastGrowthAt": "2026-09-12T02:00:01.505Z",
    "nextGrowthAt": "2026-10-10T02:00:01.505Z"
  }
}

Catalog

Read what you can sell. GET /products lists VPS preset variants; GET /os-templates lists OS ids for operatingSystemId; 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 active VPS products with their active variants. Use a variant id to place a preset order. Non-VPS catalog items are omitted — reseller orders are VPS-only.
Auth: Reseller API key or 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 "Authorization: Bearer $KEY" \
  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/os-templates
OS ids you can pass as operatingSystemId on POST /orders. They depend on the location: a Proxmox location takes the images of its servers, a VirtFusion location VirtFusion's templates. Ask per location.
Auth: Reseller API key or sessionSuccess: 200
  • Without location: templates (VirtFusion's list, as before) plus locations, one entry per orderable location and product: { location, product, platform, templates }.
  • An id from another location may not exist there: POST /orders refuses it with 400 os_unavailable.

Query parameters

locationstring— Location code from GET /vm-config. Returns only the OS ids an order there takes.
productstring— custom (default) or storage.

Example request

curl -H "Authorization: Bearer $KEY" \
  "https://aluy.net/api/reseller/v1/os-templates?location=nl&product=custom"

Successful response

{
  "location": "nl",
  "product": "custom",
  "platform": "proxmox",
  "templates": [
    { "id": 1048123, "label": "Debian 13" },
    { "id": 1052911, "label": "Ubuntu 24.04 LTS" }
  ]
}

Common errors

400The location is not orderable for the product, or product is not custom/storage.
{ "error": "Location \"xx\" is not orderable for custom VPS. GET /api/reseller/v1/vm-config lists the locations." }
502The location's servers cannot be read right now.
{ "error": "No OS images can be read for nl right now. Try again later." }
503No OS templates are available on this deployment.
{ "error": "Server management is not configured. Contact support." }
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 API key or 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.
  • ipv6Prefix: the monthly net price in cents of a bigger IPv6 prefix per size (null: not offered). Every VPS that has IPv6 gets a /64 included; a bigger size is changed with ipv6PrefixLen on POST /services/:id/upgrade.
  • Each product lists only the locations it is sold in (Storage VPS is not sold everywhere Custom VPS is), in-stock first, each with an inStock flag. Only inStock: true locations are orderable — the rest are sold out right now and reject with a 400 until they come back in stock. available is true when at least one location is in stock.
  • platform: where new VPS in that location are built (proxmox or virtfusion). osTemplates: the request that lists the OS ids an order there takes.
  • ipv4Free (where present): the most IPv4 addresses one VPS ordered there can get right now, its included address plus extraIpv4. An order asking for more is rejected with a 400.

Example request

curl -H "Authorization: Bearer $KEY" \
  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": "hk", "label": "Hong Kong", "inStock": true, "platform": "virtfusion",
        "osTemplates": "/api/reseller/v1/os-templates?location=hk&product=custom" },
      { "id": "nl", "label": "Netherlands", "inStock": true, "ipv4Free": 37, "platform": "proxmox",
        "osTemplates": "/api/reseller/v1/os-templates?location=nl&product=custom" },
      { "id": "bg", "label": "Bulgaria", "inStock": false, "platform": "virtfusion",
        "osTemplates": "/api/reseller/v1/os-templates?location=bg&product=custom" }
    ]
  },
  "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": "nl", "label": "Netherlands", "inStock": true, "platform": "proxmox",
      "osTemplates": "/api/reseller/v1/os-templates?location=nl&product=storage" }]
  },
  "extraIpv4": { "monthlyCents": 100, "max": 4 },
  "ipv6Prefix": { "56": 200, "48": null }
}
POST/api/reseller/v1/promo/validate
Quote the discount a promo code would apply, optionally for a specific variant.
Auth: Reseller API key or 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)

codestring— The promo code to test. Whitespace is trimmed.
variantIdstring— Optional. When supplied, the discount is computed against this variant's price and category.

Example request

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: kai-nl-test-001" \
  -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 onlyIdempotency-Key required
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 API key or sessionSuccess: 201
  • The two paths are mutually exclusive. Send either variantId or type with specs — never both.
  • POSTPAID orders atomically reserve RAM, vCPU, combined storage, and IPv4. A request exceeding any remaining resource returns 429 resource_quota_exceeded.
  • Every sshKeys id must be one of your own keys (GET /ssh-keys); an unknown id, or one that belongs to another account, returns 400 before anything is ordered. operatingSystemId must be one the location takes (GET /os-templates?location=…); one it does not have returns 400 os_unavailable before anything is ordered. A build that still fails is rebuilt by itself on our newer hosts (POST /services/:id/retry-provision answers 202 rebuild_scheduled); cancelling a server that was never delivered ends it at once and frees its quota.
  • A valid promoCode on an hourly order discounts the meter rate (hourlyRateCents). It is not applied as an order-line discount because the order total is €0.
  • maxSpendCents is an optional lifetime usage limit for this hourly VPS. At the limit the VPS is powered off; raise it or set it to null before starting again.

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"
  "maxSpendCents": 1000,         // optional €10 lifetime usage limit
  "operatingSystemId": 42,       // optional template id (GET /os-templates)
                                 // (see GET /os-templates,
                                 //  or omit to use the package default)
  "sshKeys": [12, 17]            // optional SSH key ids (GET /ssh-keys)
                                 // (manage via /api/reseller/v1/ssh-keys)
}

// Path 2 — custom VPS
{
  "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",              // 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"
  "maxSpendCents": null,         // optional; null/omitted = unlimited
  "operatingSystemId": 42,       // optional
  "sshKeys": [12, 17]            // optional
}

Example request

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{
    "type": "vm-custom",
    "ramGb": 4, "cpuCores": 2, "ssdGb": 60,
    "location": "fi",
    "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,
    "maxSpendCents": 1000,
    "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 \"xx\". Available: nl, fi, bg, hk" }
400operatingSystemId is not a positive integer, or sshKeys contains non-positive integers / has more than 50 entries.
{ "error": "operatingSystemId must be a positive integer" }
400The location's servers do not have the operatingSystemId image.
{ "error": "Operating system 13 is not available in nl. GET /api/reseller/v1/os-templates?location=nl&product=custom lists the ones that are.", "location": "nl", "code": "os_unavailable" }
400An sshKeys id is not one of your own keys.
{ "error": "Unknown SSH key id: 99. List yours with GET /api/reseller/v1/ssh-keys." }
400maxSpendCents is invalid or supplied for a monthly order.
{ "error": "maxSpendCents must be a positive integer or null" }
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" }
422Monthly or postpaid-hourly order and the billing profile is incomplete.
{ "error": "…", "missingProfileFields": ["street", "city"] }
429The projected VPS exceeds one or more resource quotas.
{ "error": "Resource quota exceeded (ramMb). Contact support to request more capacity.", "code": "resource_quota_exceeded", "exceeded": ["ramMb"] }
451Billing country is sanctioned.
{ "error": "…" }
GET/api/reseller/v1/orders
List your orders, newest first.
Auth: Reseller API key or sessionSuccess: 200

Query parameters

statusstring— Filter. One of DRAFT, PENDING_PAYMENT, PAID, PROVISIONING, ACTIVE, CANCELLED, REFUNDED.
limitnumber— 1–100 (default 50).
offsetnumber— ≥ 0 (default 0).

Example request

curl -H "Authorization: Bearer $KEY" \
  "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 API key or sessionSuccess: 200

Example request

curl -H "Authorization: Bearer $KEY" \
  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 API key or sessionSuccess: 200

Request body (JSON)

method"balance"required— Only "balance" is accepted today.

Example request

curl -X POST -H "Authorization: Bearer $KEY" -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 API key or sessionSuccess: 200
  • Resource limits are null for PREPAID resellers and for any explicitly unlimited POSTPAID dimension. Use GET /quota for growth dates.
  • 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

kindstring— Filter. One of VPS, DEDICATED, HETZNER_SERVER, HETZNER_STORAGEBOX, LIR, CUSTOM.
statusstring— Filter. One of PENDING, ACTIVE, SUSPENDED, TERMINATED.
limitnumber— 1–100 (default 50).
offsetnumber— ≥ 0 (default 0).

Example request

curl -H "Authorization: Bearer $KEY" \
  "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,
      "maxSpendCents": 1000,
      "resourceAllocation": {
        "ramMb": 4096, "cpuCores": 2, "storageGb": 60, "ipv4": 1,
        "releasedAt": null
      },
      "lastBilledAt": "2026-04-23T15:00:00.000Z",
      "billedHourlyCents": 7,
      "pendingInvoiceCents": 0,
      "pendingInvoiceSeconds": 0,
      "pendingInvoiceSince": null
    }
  ],
  "total": 12,
  "limit": 50,
  "offset": 0,
  "quota": {
    "resources": {
      "used": { "ramMb": 27648, "cpuCores": 23, "storageGb": 300, "ipv4": 21 },
      "reserved": { "ramMb": 0, "cpuCores": 0, "storageGb": 0, "ipv4": 0 },
      "limits": { "ramMb": 131072, "cpuCores": 64, "storageGb": 1024, "ipv4": 64 },
      "remaining": { "ramMb": 103424, "cpuCores": 41, "storageGb": 724, "ipv4": 43 }
    },
    "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 API key or sessionSuccess: 200
  • metadata is the customer view of the service: connection details (hostname, location, IPs), specs, and for a suspended service suspendReason / suspendedAt / scheduledTerminationAt. Internal provisioning and billing keys are never included.
  • A failed provisioning attempt is not exposed as text; check hasProvisionIssue and call retry-provision.
  • externalId is null once a service is TERMINATED.
  • Cancelling a VPS that was never delivered (still PENDING) ends it at once: its status becomes TERMINATED and its quota is released.

Example request

curl -H "Authorization: Bearer $KEY" \
  https://aluy.net/api/reseller/v1/services/clxsvc...

Successful response

{
  "service": {
    "id": "clxsvc...",
    "kind": "VPS",
    "status": "ACTIVE",
    "label": "edge-fi-01",
    "externalId": "1234",
    "metadata": {
      "hostname": "edge-fi-01.example.net",
      "location": "fi",
      "ip": "203.0.113.10",
      "ipv4List": ["203.0.113.10"],
      "ipv6List": ["2001:db8::/64"],
      "cpu": 2, "ramGb": 4, "diskGb": 60
    },
    "createdAt": "2026-04-23T14:08:11.123Z",
    "nextDueAt": null,
    "subscriptionBillingPeriod": "HOURLY",
    "subscriptionCancelledAt": null,
    "hourlyRateCents": 7,
    "maxSpendCents": 1000,
    "resourceAllocation": {
      "ramMb": 4096, "cpuCores": 2, "storageGb": 60, "ipv4": 1,
      "releasedAt": null
    },
    "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 or an hourly VPS lifetime spend limit. Set maxSpendCents to null for unlimited usage.
Auth: Reseller API key or sessionSuccess: 200

Request body (JSON)

labelstring— 1–200 chars after trim.
maxSpendCentsnumber | null— Positive euro-cent limit for an hourly VPS, above billedHourlyCents; null removes the limit.

Example request

curl -X PATCH -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"maxSpendCents":2500}' \
  https://aluy.net/api/reseller/v1/services/clxsvc...

Successful response

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

Common errors

400label is empty after trim.
{ "error": "label cannot be empty" }
400maxSpendCents is invalid or the service is not an hourly VPS.
{ "error": "maxSpendCents must be a positive integer or null" }
400No supported field was sent.
{ "error": "No valid fields to update" }
409maxSpendCents is not above what the VPS has already used (it would stop at once).
{ "error": "This server has already used €15.00. Set a limit above that, or remove the limit.", "code": "spend_limit_already_used" }
POST/api/reseller/v1/services/:id/cancel
Cancel automatic renewal. Monthly services stay active until nextDueAt. Hourly services keep running for one more hour, then terminate. Response includes scheduledTerminationAt. Send {"when": "now"} to end an active VPS right away instead: it is deleted with its data at once and the unused part of the paid term is credited pro rata (PREPAID: to your balance; POSTPAID: a credit note lowers your invoice; hourly: nothing billed ahead). The response then carries the credit instead of scheduledTerminationAt.
Auth: Reseller API key or sessionSuccess: 200

Example request

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

# End it now instead of at the end of the term:
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"when":"now"}' https://aluy.net/api/reseller/v1/services/clxsvc.../cancel

Successful response

{
  "service": {
    "id": "clxsvc...",
    "label": "edge-fi-01",
    "status": "ACTIVE",
    "subscriptionCancelledAt": "2026-09-14T14:30:00.000Z",
    "scheduledTerminationAt": "2026-09-14T15:30:00.000Z"
  }
}

Common errors

400Service is already TERMINATED.
{ "error": "Service is already terminated" }
400when is not "now" or "end_of_term", or "now" for a service that is not a VPS.
{ "error": "when must be \"now\" or \"end_of_term\"", "code": "invalid_request" }
409"now" for a VPS that is suspended, moving or held.
{ "error": "Service is not active", "code": "conflict" }
POST/api/reseller/v1/services/:id/resume
Reverse a cancellation that hasn't taken effect yet. Clears subscriptionCancelledAt so billing continues. Works for any service kind you cancelled.
Auth: Reseller API key or sessionSuccess: 200

Example request

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

Successful response

{ "ok": true }

Common errors

400The service was never cancelled.
{ "error": "Subscription is not cancelled" }
409Service is already TERMINATED.
{ "error": "Service is terminated and cannot be resumed. Place a new order." }
POST/api/reseller/v1/services/:id/suspendVPS only
Power off the VPS and mark it SUSPENDED. This is a pause — it does not start the 7-day termination clock. Hourly billing stops while suspended. Undo with POST …/unsuspend.
Auth: Reseller API key or sessionSuccess: 200

Example request

curl -X POST -H "Authorization: Bearer $KEY" \
  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 VPS you suspended with POST …/suspend, or a PREPAID hourly VPS suspended for insufficient balance once your balance covers at least one hour at its rate. Marks the service ACTIVE and powers it back on. A termination already scheduled on the service (a cancellation, or one set by Aluy) stays in place. Other suspensions made by Aluy (abuse, support, unpaid invoices, billing details) cannot be lifted through the API.
Auth: Reseller API key or sessionSuccess: 200

Example request

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

Successful response

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

Common errors

402Suspended for insufficient balance and your balance does not yet cover one hour. Top up and retry.
{ "error": "Insufficient balance to resume (need at least €0.07 for one hour, have €0.00). Top up your balance and try again.", "code": "insufficient_balance" }
403The service was suspended by Aluy (abuse, support, unpaid invoice, billing details), not through the reseller API. Contact support.
{ "error": "Suspended by provider", "code": "provider_suspension" }
409The hourly VPS's lifetime spend limit is exhausted.
{ "error": "Hourly spend limit reached. Raise or remove the limit before starting this server.", "code": "spend_limit_exhausted" }
502The hypervisor rejected the unsuspend request.
{ "error": "<upstream message>" }
POST/api/reseller/v1/services/:id/reset-passwordVPS only
Reset the login password for the VPS: root, or Administrator on a Windows server (username says which). Returns the new password in plaintext exactly once — store it securely; we do not retain it.
Auth: Reseller API key or sessionSuccess: 200

Example request

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

Successful response

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

Common errors

409The service is not ACTIVE (e.g. suspended).
{ "error": "Service is suspended", "code": "not_active" }
409This action is not available for the service right now. Contact support.
{ "error": "This action is currently unavailable, please contact support", "code": "action_unavailable" }
409The server is in the last step of a move to new hardware (a few minutes).
{ "error": "Your server is in the last step of its move. This works again in a few minutes.", "code": "moving" }
502The host did not carry it out. detail says why, in plain words.
{ "error": "Upstream provider error", "detail": "The server is not running." }
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 API key or 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 "Authorization: Bearer $KEY" \
  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" }
202A failed build on our newer hosts (pve:) is rebuilt by itself: nothing to do, check the status after nextAttemptAt.
{ "ok": true, "code": "rebuild_scheduled", "serviceId": "clxsvc...", "status": "PENDING", "nextAttemptAt": "2026-10-16T22:00:00.000Z" }
409A pve: build whose automatic rebuilds did not take: our team finishes it or refunds the order.
{ "error": "This build could not be completed automatically. Our team has been notified and will finish it or refund the order.", "code": "rebuild_needs_staff" }
409A server was (or may have been) created upstream before the failure. Retrying would create a second one; our team reconciles it instead.
{ "error": "Provisioning may already exist at the upstream provider and must be reconciled before retrying", "code": "upstream_reconciliation_required" }
POST/api/reseller/v1/services/:id/upgradeVPS only
Change RAM, CPU, SSD, and/or add extra IPv4 on an ACTIVE VPS. Hourly services apply immediately and the meter rate updates on the next tick (no prorata invoice). Monthly services create a prorata upgrade order. RAM/CPU may go down (immediate, with a prorata refund on monthly). Storage cannot shrink.
Auth: Reseller API key or sessionSuccess: 200
  • Hourly: applied: true and no order ids. Monthly: upgradeOrderId / extraIpOrderId when that branch created a prorata charge.

Request body (JSON)

ramGbnumber— New RAM in GB.
cpuCoresnumber— New vCPU count.
ssdGbnumber— New SSD in GB. Must be greater than or equal to current.
extraIpv4number— Number of additional IPv4 addresses to add.
ipv6PrefixLennumber— IPv6 prefix size on servers that support it: 64 (included), 56 or 48 (monthly add-ons). Bigger creates a prorata order (ipv6OrderId); smaller applies at once with a prorata refund to the balance. Send it on its own.

Example request

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

Successful response

{
  "ok": true,
  "applied": true,
  "refundedToBalanceCents": 0,
  "upgradeOrderId": "clxord...",
  "extraIpOrderId": "clxord...",
  "paidFromBalance": true,
  "extraIpPaidFromBalance": true
}

Common errors

400No upgrade fields supplied.
{ "error": "At least one upgrade field is required (ramGb, cpuCores, ssdGb, extraIpv4, ipv6PrefixLen)" }
400Resource upgrade path failed validation/availability.
{ "error": "Upgrade not available" }
400Extra IP path failed.
{ "error": "Quantity must be 1–4" }
409Service is not ACTIVE.
{ "error": "Service must be active to upgrade" }
POST/api/reseller/v1/services/:id/remove-ipv4VPS only
Detach one extra IPv4 and drop its recurring charge. Hourly: the meter rate drops immediately (no refund — you were never prepaid). Monthly: unused cycle days are credited to balance.
Auth: Reseller API key or sessionSuccess: 200

Request body (JSON)

ipAddressstring— Optional. Specific extra IPv4 to remove (not the primary).
quantitynumber— Optional. How many extras to drop when ipAddress is omitted (default 1).

Example request

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"quantity":1}' \
  https://aluy.net/api/reseller/v1/services/clxsvc.../remove-ipv4

Successful response

{
  "ok": true,
  "removedIps": ["203.0.113.8"],
  "refundedToBalanceCents": 0,
  "billedIpsRemoved": 1
}
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 API key or sessionSuccess: 200

Request body (JSON)

operatingSystemIdnumberrequired— Template id (see GET /os-templates or GET /services/:id/templates).
hostnamestring— Optional. Hostname to set on the new install.
sshKeysnumber[]— Optional. Ids of your own SSH keys (see GET /ssh-keys).

Example request

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"operatingSystemId":42, "hostname":"edge-fi-01.example.com", "sshKeys":[12]}' \
  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 (number) is required" }
400An sshKeys id is not one of your own keys.
{ "error": "Unknown SSH key id: 99. List yours with GET /api/reseller/v1/ssh-keys." }
409The service is not ACTIVE (e.g. suspended).
{ "error": "Only active services can be reinstalled", "code": "not_active" }
409This action is not available for the service right now. Contact support.
{ "error": "This action is currently unavailable, please contact support", "code": "action_unavailable" }
409An install of this server is still running.
{ "error": "An install is still running. Try again once it is done.", "code": "install_running" }
409The server is being moved to new hardware.
{ "error": "Your server is being moved to new hardware. This is available again once the move is done — we email you when it is.", "code": "moving" }
502The host did not carry it out. detail says why, in plain words.
{ "error": "Upstream provider error", "detail": "That operating system is not available on this server." }

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 API key or sessionSuccess: 200
  • state: running, shutoff, paused, suspended, or installing while an OS install (the ordered OS or a reinstall) runs. allocated: the server has no OS yet — install one with POST /services/:id/reinstall.

Example request

curl -H "Authorization: Bearer $KEY" \
  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": null,
  "ipAddresses": [
    { "address": "203.0.113.5",   "type": "ipv4" },
    { "address": "2001:db8::/64", "type": "ipv6" }
  ]
}

Common errors

409The service is terminated.
{ "error": "Service is terminated" }
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 API key or sessionSuccess: 200
  • On a pve: server, shutdown / restart wait up to 3 minutes for the guest; one that ignores them is left running — use stop.

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 "Authorization: Bearer $KEY" -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. Must be one of: start, shutdown, stop, restart" }
409The service is not ACTIVE (e.g. suspended).
{ "error": "Service is suspended", "code": "not_active" }
409Starting or restarting an hourly VPS whose lifetime spend limit is exhausted.
{ "error": "Hourly spend limit reached. Raise or remove the limit before starting this server.", "code": "spend_limit_exhausted" }
409The server is in the last step of a move to new hardware (a few minutes).
{ "error": "Your server is in the last step of its move. This works again in a few minutes.", "code": "moving" }
502The host did not carry it out. detail says why, in plain words.
{ "error": "Upstream provider error", "detail": "The server's host did not answer. Try again in a minute." }
503The server's host is down right now (also on reinstall, reset-password and console).
{ "error": "…", "code": "host_down" }
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 API key or sessionSuccess: 200
  • On a server whose externalId starts with pve: the answer has no ip / port: { wssUrl, password, enabled: true }. wssUrl is a WebSocket (VNC) for one session, valid for 60 seconds — connect right away, and ask again for the next session; password is its VNC password.

Example request

curl -H "Authorization: Bearer $KEY" \
  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
  }
}

Common errors

409The service is not ACTIVE (e.g. suspended).
{ "error": "Service is suspended", "code": "not_active" }
409This action is not available for the service right now. Contact support.
{ "error": "This action is currently unavailable, please contact support", "code": "action_unavailable" }
502The console could not be opened. detail says why, in plain words.
{ "error": "Upstream provider error", "detail": "The console could not be opened. Is the server running?" }
POST/api/reseller/v1/services/:id/console
Enable or disable VNC console access on the VPS.
Auth: Reseller API key or sessionSuccess: 200
  • On a server whose externalId starts with pve: this is accepted and changes nothing: no console port stays open there, every session is a one-time ticket from GET /services/:id/console.

Request body (JSON)

action"enable" | "disable"required— Whether to turn console access on or off.

Example request

curl -X POST -H "Authorization: Bearer $KEY" -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..." }

Common errors

409The service is not ACTIVE (e.g. suspended).
{ "error": "Service is suspended", "code": "not_active" }
409This action is not available for the service right now. Contact support.
{ "error": "This action is currently unavailable, please contact support", "code": "action_unavailable" }
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 API key or sessionSuccess: 200
  • For order-time OS selection use GET /os-templates — this endpoint needs an existing VPS.

Example request

curl -H "Authorization: Bearer $KEY" \
  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" }
  ]
}

Common errors

502The host could not list them. detail says why, in plain words.
{ "error": "Upstream provider error", "detail": "The server's host did not answer. Try again in a minute." }

SSH keys

Manage your SSH public keys. Reference the ids returned here in the sshKeys field on POST /orders and POST /services/:id/reinstall to put them into authorized_keys.

Keys are saved with your account (ids from 10,000,000 up, the same as keys saved in the customer portal) and work on every server. Older keys with lower ids keep working too.

GET/api/reseller/v1/ssh-keys
List your SSH keys. When older keys (ids below 10,000,000) cannot be read right now, the rest are listed with olderKeysUnavailable: true.
Auth: Reseller API key or sessionSuccess: 200

Example request

curl -H "Authorization: Bearer $KEY" \
  https://aluy.net/api/reseller/v1/ssh-keys

Successful response

{
  "sshKeys": [
    {
      "id": 10000012,
      "name": "laptop-ed25519",
      "publicKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...",
      "type": "OpenSSH",
      "enabled": true,
      "createdAt": "2026-03-01T08:21:00.000Z"
    }
  ]
}
POST/api/reseller/v1/ssh-keys
Save a new SSH public key (one key line). At most 20 keys.
Auth: Reseller API key or sessionSuccess: 201

Request body (JSON)

namestringrequired— Display name. Max 120 chars.
publicKeystringrequired— OpenSSH-format public key, e.g. "ssh-ed25519 AAAA... user@host".

Example request

curl -X POST -H "Authorization: Bearer $KEY" -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": 10000012,
    "name": "laptop-ed25519",
    "publicKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...",
    "type": "OpenSSH",
    "enabled": true,
    "createdAt": "2026-04-23T14:08:11.123Z"
  }
}

Common errors

400name missing/too long, publicKey is malformed or more than one line, or you have 20 keys already.
{ "error": "publicKey must be one OpenSSH public key line" }
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.
Auth: Reseller API key or sessionSuccess: 200

Example request

curl -X DELETE -H "Authorization: Bearer $KEY" \
  https://aluy.net/api/reseller/v1/ssh-keys/10000012

Successful response

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

Common errors

400id is not a positive integer.
{ "error": "Invalid key id" }
404The key id doesn't belong to you.
{ "error": "SSH key not found" }
502The key could not be removed everywhere yet; it is kept. Try again.
{ "error": "<friendly upstream message>" }

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 API key or sessionSuccess: 200

Query parameters

limitnumber— 1–100 (default 50).
offsetnumber— ≥ 0 (default 0).

Example request

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

Successful response

{
  "invoices": [
    {
      "id": "clxinv...",
      "number": "INV-2026-001",
      "status": "PAID",
      "kind": "SERVICE",
      "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 API key or sessionSuccess: 200

Example request

curl -H "Authorization: Bearer $KEY" \
  https://aluy.net/api/reseller/v1/invoices/clxinv...

Successful response

{
  "invoice": {
    "id": "clxinv...",
    "number": "INV-2026-001",
    "status": "PAID",
    "kind": "SERVICE",
    "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. The default response is a ZUGFeRD/Factur-X 2.x file (EN 16931 profile): visually identical to a plain PDF in any modern reader, but `factur-x.xml` is embedded for one-click import into DATEV/SAP/Lexware/etc., and the file is PDF/A-3 archival-grade — also satisfies the German B2G mandate from 1 Jan 2027. Append `?format=plain` if you specifically need the bare visual PDF without the embedded XML attachment. Returns application/pdf bytes with a Content-Disposition: attachment header. Cached as private; not stored or proxied.
Auth: Reseller API key or sessionSuccess: 200

Example request

curl -H "Authorization: Bearer $KEY" -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" }

Abuse reports

Abuse reports against your services. An OPEN report carries a deadlineAt: without a statement (filed at statementUrl) by then, the service is suspended. Subscribe to abuse.report_opened and abuse.report_suspended to pass the deadline on to your own customers in time. Scope: services:read.

GET/api/reseller/v1/abuse-reports
List abuse reports on your services, newest first.
Auth: Reseller API key or sessionSuccess: 200

Query parameters

statusstring— Filter. One of OPEN, UNDER_REVIEW, SUSPENDED, RESOLVED, CLOSED.
serviceIdstring— Only reports on this service.
limitnumber— 1–100 (default 50).
offsetnumber— ≥ 0 (default 0).

Example request

curl -H "Authorization: Bearer $ALUY_API_KEY" \
  "https://aluy.net/api/reseller/v1/abuse-reports?status=OPEN"

Successful response

{
  "abuseReports": [
    {
      "id": "clxabr...",
      "status": "OPEN",
      "category": "uceprotect",
      "source": "uceprotect",
      "subject": "UCEPROTECT Level 1 listing — 203.0.113.10",
      "serviceId": "clxsvc...",
      "ips": ["203.0.113.10"],
      "createdAt": "2026-09-23T01:00:00.000Z",
      "deadlineAt": "2026-09-24T01:00:00.000Z",
      "suspendedAt": null,
      "resolvedAt": null,
      "statementUrl": "https://aluy.net/portal/abuse/clxabr..."
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}

Support tickets

Create, reply, close, and reopen 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. messageCount counts the messages of the conversation; staff-only notes are not counted.
Auth: Reseller API key or sessionSuccess: 200

Query parameters

statusstring— Filter. One of OPEN, AWAITING_CUSTOMER, AWAITING_STAFF, CLOSED.
limitnumber— 1–100 (default 50).
offsetnumber— ≥ 0 (default 0).

Example request

curl -H "Authorization: Bearer $KEY" \
  "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 API key or 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)

subjectstringrequired— Short summary. Trimmed; cannot be empty.
messagestringrequired— First message body. Trimmed; cannot be empty.
serviceIdsstring[]— Optional. Service ids to attach. Non-owned ids are silently dropped.

Example request

curl -X POST -H "Authorization: Bearer $KEY" -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. The messages are the conversation as the portal shows it: staff-only notes are never included.
Auth: Reseller API key or sessionSuccess: 200

Example request

curl -H "Authorization: Bearer $KEY" \
  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 API key or sessionSuccess: 201

Request body (JSON)

messagestringrequired— Reply body. Trimmed; cannot be empty.

Example request

curl -X POST -H "Authorization: Bearer $KEY" -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.
{ "error": "Ticket is closed — reopen it first" }
POST/api/reseller/v1/tickets/:id/close
Close a ticket. Idempotent if it is already closed.
Auth: Reseller API key or sessionSuccess: 200

Example request

curl -X POST -H "Authorization: Bearer $KEY" \
  https://aluy.net/api/reseller/v1/tickets/clxtkt.../close

Successful response

{ "ticket": { "id": "clxtkt...", "status": "CLOSED" } }
POST/api/reseller/v1/tickets/:id/reopen
Reopen a CLOSED ticket (sets status back to OPEN). Idempotent if it is not closed.
Auth: Reseller API key or sessionSuccess: 200

Example request

curl -X POST -H "Authorization: Bearer $KEY" \
  https://aluy.net/api/reseller/v1/tickets/clxtkt.../reopen

Successful response

{ "ticket": { "id": "clxtkt...", "status": "OPEN" } }

Webhooks

Subscribe an HTTPS endpoint to lifecycle events. Manage endpoints with GET/POST /webhooks, PATCH/DELETE /webhooks/:id, and rotate a secret with POST /webhooks/:id/rotate-secret.

curl -X POST -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"production","url":"https://example.com/aluy","events":["service.provisioned","service.terminated"]}' \
  https://aluy.net/api/reseller/v1/webhooks
  • The HMAC secret is returned only on creation or rotation.
  • Verify X-Aluy-Signature as the hex HMAC-SHA256 of <X-Aluy-Timestamp>.<raw request body>. Reject stale timestamps.
  • Deliveries include X-Aluy-Event and X-Aluy-Event-Id. De-duplicate by event id.
  • Non-2xx responses are retried with exponential backoff. Redirects are never followed.
  • When webhooks are switched off on our side, adding or re-pointing an endpoint returns 503 with code webhooks_unavailable; poll the API meanwhile.

Events: order.paid, service.provisioned, service.suspended, service.resumed, service.terminated, invoice.paid, abuse.report_opened, and abuse.report_suspended. service.provisioned is sent when the server is ready: for an order with an operatingSystemId, once that OS is installed.

Errors

New API responses use { error, code, requestId }. Switch on stable codes such as authentication_required, insufficient_scope, rate_limit_exceeded, origin_not_allowed, invalid_request, not_found, conflict, and provider_error. Human-readable wording may change.

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" }
                                Also returned when the account is payment-blocked.
  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).
  422  Unprocessable          — Billing profile incomplete. POST /orders may include
                                missingProfileFields. Update the profile in the portal.
  429  Rate / quota exceeded  — Rate limit or POSTPAID resource quota exceeded.
                                Quota errors include code, exceeded, usage, limits, remaining, and requested.
  451  Unavailable            — Billing country is sanctioned; new orders are refused.
  500  Server error           — Invoice PDF generation failed (body may include message).
  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 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.