Reseller API
The partner interface for buying, provisioning and managing services wholesale: keys, request signing, wholesale pricing, ordering and service lifecycle.
What the reseller API is for
The reseller API is the machine interface a partner's own platform calls to buy, provision and manage your services wholesale. It is a different product from the Customer REST API: that one lets an end customer drive services they already own, this one lets a business partner order new services at your wholesale prices, attribute them to their own customers, and run them from their own shop, billing system or provisioning module.
The relationship is one level deep. You sell to the partner at wholesale; the partner sells to their own customers at whatever price they choose. The partner's customers never touch your platform — they exist on it only as reference records, with no login. Every service the partner buys is an ordinary service on your platform, owned by their client account and billed to them on its cycle.
Note: The whole reseller area, portal and API alike, depends on the Resellers feature being enabled for your company (Settings → Feature Toggles → Resellers). With it off, every call is refused with 403 and the code FEATURE_DISABLED. See Reseller Program for the settings behind it.
Getting a partner onto the API
1. The partner applies
The partner first registers an ordinary client account on your platform. In the client portal they then open Reseller programme, which shows your discount tiers, and apply with their company name, registration number and tax identifier where you ask for them.
2. You approve
Applications land in Resellers in the admin panel, where you approve, reject, suspend or reactivate them. If you have turned auto-approval on, the profile is active the moment the partner applies. On approval the partner is emailed and a Reseller section appears in their portal sidebar.
A profile is in one of three states, and they matter to the API:
| State | What it does |
|---|---|
| pending | Applied, not yet approved. API keys do not work. |
| active | Full portal and API access. |
| suspended | Portal and API refuse. Services already bought keep running and keep being billed; outbound event deliveries are paused. |
A key on a pending or suspended profile answers 401 Invalid API key — exactly the same answer as an unknown, revoked or expired key. The two are deliberately not distinguished.
3. The partner mints keys
Everything to do with credentials lives on one page: Reseller → API Settings in the client portal, headed API Access. The page is only reachable while the profile is active. You can also mint and revoke a partner's keys from their record in the admin panel.
Managing API keys
Keys are a list, not a single credential. A partner can hold up to 20 active keys at once, each with its own label, its own scopes, an optional expiry and an optional customer binding.
Creating one
- On API Settings, select Create key.
- Give it a Label (for example
Billing integration). - Tick the Scopes it needs. At least one is required; with none chosen the key gets read.
- Optionally Bind to customer. A bound key sees and acts on only that customer's orders and services, and every order it places is attributed to them — safe to hand to an end customer. A bound key may not carry the account or customers scopes; the form disables them.
- Optionally set an Expiry date. It must be in the future.
- Select Create.
The full key is then shown once, under Save Your API Key Now — “This is the only time you'll see the full key.” It has the form rsk_ plus an eight-character prefix, an underscore and a 64-character secret. Only a one-way hash is stored, so it cannot be recovered afterwards.
The key list
The table shows Label, Scopes, Customer (the bound customer, or All customers), Expires, Last Used and Created, with Revoked and Expired badges where they apply. Each row can be edited (label and expiry only — scopes are fixed for the life of a key) or revoked. A Revoke all keys button at the top invalidates every outstanding key at once; the confirmation warns that all integrations stop working immediately.
Scopes
| Scope | What it does |
|---|---|
| read | Products, orders, services, capabilities, operating-system templates, statistics, backup lists and the customer list. |
| orders | Placing orders and retrying a failed payment. |
| services | Reversible service actions: power, suspend, unsuspend, change package, create a backup, open a console. |
| destructive | Terminate a service, reinstall the operating system, reset the root password. |
| customers | Create, update and delete end-customer records. |
| account | Saved payment methods, the outbound event URL, and the test delivery. |
Two read operations are deliberately not covered by read: opening a console needs services, because it starts a session, and listing payment methods needs account. A call to a route whose scope the key lacks is refused with 403 API key is missing the required scope: <scope>. The scope check runs before the signature check, so a key without the scope gets 403 even when the signature is perfect.
The signing secret
Beside the key list, the Webhooks panel holds the Signing secret. This is not the API key. It is a separate shared secret used in both directions: the partner signs their requests with it, and your platform signs the event deliveries it sends them with it.
- It exists from the moment the partner applies, so a partner approved automatically does not have to generate one first.
- It is masked; a show/hide control reveals it and a copy button copies it.
- Rotate issues a new one. The old one stops verifying immediately — the confirmation says so — so every integration has to be updated straight after.
- If a profile somehow has none, the panel shows “No signing secret yet” and offers Generate secret. Until one exists, every signed call answers
400 Webhook secret not configured.
Calling the API
The base path is https://your-panel-domain/api/v1/reseller. Three documentation routes need no key at all and are the canonical reference for an integrator:
GET /docs— the rendered reference.GET /guide.md— the partner guide, the API reference and the signing guide as one Markdown file.GET /openapi.json— the machine-readable route contract.
Every other call carries the key as a bearer token:
Authorization: Bearer rsk_<prefix>_<secret>
Signing a request
Every mutating call must carry an HMAC signature. “Mutating” means POST, PUT, PATCH and DELETE; GET is never signed. A missing signature on a mutation is 401 Request signature required for this operation.
| Header | What it does |
|---|---|
Authorization | Required on every call: Bearer rsk_<prefix>_<secret>. |
X-Reseller-Signature | Required on mutations: hmac-sha256= followed by 64 lower-case hexadecimal characters. |
X-Reseller-Timestamp | Strongly recommended. Unix time, in seconds or milliseconds. |
Content-Type | Required whenever there is a body: application/json. Without it the body is not parsed and the signature cannot match. |
What exactly is signed
The signature is HMAC-SHA256 keyed on the signing secret, rendered as lower-case hexadecimal. The message is:
- with a timestamp header: the timestamp exactly as sent, then a full stop, then the raw request body;
- without one: the raw request body alone.
The message is the exact bytes of the body you transmit, not a re-encoding of them. Serialise the payload once, keep that string, send that string as the body and sign that same string. Two encoders do not always agree — escaping of slashes, escaping of non-ASCII characters and the rendering of 1.0 versus 1 all differ between languages, and any of those differences produces a signature mismatch with no other symptom.
For a request with no body — terminating a service, for example — the raw body is the empty string when you send none, or {} if you send an empty JSON object. Sign whichever you actually transmit; both forms verify.
Warning: The timestamp is the header value character for character. If you send
1735689600, sign1735689600.{"a":1}— not the millisecond form of the same instant.
Replay protection and clock skew
Sending X-Reseller-Timestamp is what makes a captured request stop being replayable. When the header is present it is enforced: more than 300 seconds of drift in either direction is rejected with 401. Seconds and millisecond values are both accepted and told apart automatically. A value that is not a number is a 400.
The header is optional so that integrations written before it existed keep working. Without it, the signed content of a body-less call is a constant, and one captured signature stays valid on that route indefinitely. Send it on every call, and above all on body-less ones.
Worked vectors
Check an implementation against these before touching live data. The secret is topsecret.
| Signed content | Signature |
|---|---|
{"productId":"p1","quantity":2} (body only, no timestamp) | d507d1886ee899eec5da5d6879119341287bfed619bc4620781d6e7511be9cd0 |
1735689600.{"productId":"p1","quantity":2} | c2dd5122cfe40dc33bb2c83ae0024db0f6661e0d4ea5cdae20ea2cad184c1dd1 |
1735689600. (body-less call, no body sent) | 3dd148f773b8ec61c6a175856cf072395f5d51185b63ca5f4e218a211c87c32d |
1735689600.{} (the same call sending an empty object) | 047fcd1bc819b9d1d20d6a57c7ff7b59662dd9171838e4e6abd50f400c575f71 |
On the command line: printf '%s' '1735689600.{"productId":"p1","quantity":2}' | openssl dgst -sha256 -hmac topsecret
Once the vectors match, make a harmless signed call such as POST /webhooks/test.
Signature failures
| Response | What it does |
|---|---|
401 Missing or invalid Authorization header | No bearer header at all. |
401 Invalid API key format | The token does not start with rsk_. |
401 Invalid API key | Unknown, revoked or expired key — or the profile is not active. |
401 Request signature required for this operation | A mutation with no signature header. |
400 Invalid signature format | Not in the form hmac-sha256=<hex>. |
401 Invalid signature | The digest does not match, or is not 64 hexadecimal characters. Nearly always the body was re-encoded between signing and sending. |
400 Webhook secret not configured | The profile has no signing secret. Generate one in API Settings. |
400 Invalid X-Reseller-Timestamp | The header is not a number. |
401 Request timestamp outside the allowed window | More than 300 seconds of clock drift. |
Rate limits
The limit is a sliding one-hour window, applied per key and against a ceiling for the whole partner, so minting more keys does not raise total throughput. The default ceiling is 1,000 requests per hour; you can raise or lower it for the programme as a whole or for an individual partner. Every authenticated request counts, including ones refused for scope, signature or validation.
Four headers come back on every response: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used and X-RateLimit-Reset (Unix seconds; always one hour ahead, not the true edge of the sliding window). Over the limit answers 429 Rate limit exceeded. The partner's API Settings page shows the same counters as a Rate Limit meter.
Response envelope and pagination
A success is { "success": true, "data": { … } }; a refusal is { "success": false, "error": "message" }. Request-validation failures are a 400 whose error is an object instead of a string: { "message": "Validation failed", "details": [ { "field": "billingCycle", "message": "Invalid billing cycle" } ] }.
Paginated endpoints add a pagination object, in one of two shapes: the order list uses total, limit, offset and hasMore; the service and customer lists use page, limit, total and totalPages.
Every operation
“Signed” marks the routes that require the signature header.
| Operation | Scope and signing |
|---|---|
GET /products | read |
GET /products/{productId} | read |
GET /products/{productId}/schema | read |
POST /orders | orders, signed |
GET /orders | read |
GET /orders/{orderId} | read |
POST /orders/{orderId}/retry | orders, signed |
GET /services | read |
GET /services/{serviceId} | read |
GET /services/{serviceId}/capabilities | read |
GET /services/{serviceId}/os-templates | read |
GET /services/{serviceId}/stats | read |
GET /services/{serviceId}/backups | read |
GET /services/{serviceId}/console | services |
POST /services/{serviceId}/power | services, signed |
POST /services/{serviceId}/suspend | services, signed |
POST /services/{serviceId}/unsuspend | services, signed |
POST /services/{serviceId}/change-package | services, signed |
POST /services/{serviceId}/backups | services, signed |
POST /services/{serviceId}/reinstall | destructive, signed |
POST /services/{serviceId}/password | destructive, signed |
POST /services/{serviceId}/backups/{backupId}/restore | destructive, signed |
DELETE /services/{serviceId}/backups/{backupId} | destructive, signed |
GET /services/{serviceId}/document | read |
GET /services/{serviceId}/traffic | read |
GET /services/{serviceId}/health | read |
GET /services/{serviceId}/network | read |
PUT /services/{serviceId}/rdns | services, signed |
GET /services/{serviceId}/ssh-keys | read |
POST /services/{serviceId}/ssh-keys | services, signed |
DELETE /services/{serviceId}/ssh-keys/{keyId} | services, signed |
POST /services/{serviceId}/console-session | services, signed |
GET /services/{serviceId}/options | read |
PUT /services/{serviceId}/options | orders, signed |
DELETE /services/{serviceId}/options/{serviceOptionId} | orders, signed |
GET /services/{serviceId}/upgrades | read |
POST /services/{serviceId}/rescue | services, signed |
POST /services/{serviceId}/unrescue | services, signed |
PUT /services/{serviceId}/transit-config | services, signed |
GET /services/{serviceId}/provider | read |
GET /services/{serviceId}/ui-schema | read |
POST /services/{serviceId}/ui-action/{action} | services, signed |
GET /services/{serviceId}/game and every | read / services / destructive (per route, see the API reference) |
DELETE /services/{serviceId} | destructive, signed |
GET /customers | read |
POST /customers | customers, signed |
PUT /customers/{customerId} | customers, signed |
DELETE /customers/{customerId} | customers, signed |
GET /payment-methods | account |
PUT /payment-methods/{methodId}/default | account, signed |
PUT /account/webhook | account, signed |
POST /webhooks/test | account, signed |
GET /docs, GET /guide.md, GET /openapi.json | No key needed |
Wholesale pricing
Every product you have marked reseller-available is in the partner's catalogue automatically. GET /products returns each one with the partner's own price per cycle:
{ "id": "…", "name": "VPS 4G", "slug": "vps-4g", "category": "VPS", "currency": "EUR", "pricing": { "currency": "EUR", "monthly": 8.5, "quarterly": 24, "semiAnnual": null, "annual": 90, "hourly": null, "setupFee": 0 }, "specs": { } }
For each product and cycle the wholesale price is resolved in this order:
- An override price you set for that partner on that product, per cycle plus setup fee.
- Their tier's price, from your tier price book.
- Retail minus a percentage — a per-product percentage you set for them, otherwise the better of their tier's discount and the default discount on their profile.
A cycle with no price in any of the three cannot be ordered on that cycle; the order is refused with 400 Product is not sold on the '…' billing cycle. Nothing is ever priced at zero by accident. A cycle priced null in the listing is simply not on sale.
Prices are quoted in your base currency, which is what currency on each product names. Where the partner's account currency differs, the order is charged in theirs at the rate in force at order time, and that rate is stored so the invoice and the charge always reconcile.
Add-on options bought with an order are charged at your tier option price where one is set for that tier, otherwise at retail; option setup fees are always retail. Datacentre surcharges you price on a product apply to partners too.
Tiers, caps and minimums
Tiers are your own configurable ladder (Bronze, Silver, Gold and Platinum by default). A partner's tier follows their monthly run-rate with you: the recurring value of everything they currently hold, normalised to a month, plus one-off spend in the last 30 days. Terminated and cancelled services do not count; suspended ones still do, because the partner is still billed for them. Their dashboard shows the tier, the run-rate and the distance to the next one.
You can also cap how many units of a product a partner may hold at once — exceeding it is 409 Quantity limit reached — and set a programme-wide minimum order amount, below which an order is refused with 400 before anything is charged.
Reading what an order needs
GET /products/{productId}/schema describes the form an order for that product has to fill in:
billingCycles— the exact cycle valuesPOST /orderswill accept.requiredFields— always at leastbillingCycle, pluslocationwhen the product prices datacentres, plus any required field of a provider's order form.osImagesandhostnamePattern— on platform-provisioned VPS products, the images that may be named and the pattern a hostname must match.locationPricingwithlocationRequired— the datacentres and their surcharge; send the entry's value as the order'slocation.orderSchemaandpluginDefaults— the provider's own order form when the product is provisioned by one, otherwise empty.
Ordering on behalf of a customer
A partner's customers are records, not accounts. A record carries the partner's own customer identifier, and optionally a name and an email; it creates no login on your platform and gives its subject no access to anything.
Records can be created explicitly with POST /customers (idempotent on the external identifier, always answering 200 with the record), or implicitly — an order that carries an externalCustomerId creates or reuses the record automatically, with the orders scope alone. PUT /customers/{id} updates the email, the name and a metadata object (which is replaced wholesale, not merged). DELETE /customers/{id} removes it permanently; it is refused with 400 while the customer still has a service that is neither terminated nor cancelled, and past orders and terminated services are detached and kept. Any key bound to that customer is revoked with it.
Placing an order
POST /orders takes:
{ "productId": "…", "billingCycle": "monthly", "quantity": 1, "externalOrderId": "your-own-id", "externalCustomerId": "your-own-customer-id", "externalCustomerEmail": "[email protected]", "externalCustomerName": "Jane Doe", "configuration": { "hostname": "web01", "location": "de1", "osImageId": "…" }, "options": [ { "optionId": "…", "quantity": 1 } ], "paymentMethodId": "…" }
billingCycleis one ofmonthly,quarterly,semi-annually,annually,hourly. Note the spelling: the product listing reports that cycle's price under the keysemiAnnual, but the value sent here issemi-annually.quantityis 1 to 100 and creates that many separate services; the setup fee applies to each.optionsholds up to 50 add-ons, each of which must belong to the product. They come back with the unit price actually charged.configurationis passed to provisioning. A product that prices datacentres refuses an order without alocation(400 A datacentre must be selected). A hostname and an image are validated before anything is charged. AnexternalServiceIdhere is stored on the service as the partner's own reference.paymentMethodIdcharges a specific saved method instead of the default; it matters only where you collect by saved payment method.
Idempotency — always send externalOrderId
Provisioning can take longer than a partner's HTTP timeout. Without an idempotency key, a client that retries gets a second order and a second charge. With externalOrderId set, a repeat call returns the original order with HTTP 200 and "idempotent": true instead of 201. The value is unique per partner, so their own order number works. Two genuinely concurrent creates with the same key resolve the same way: one lands and the other gets the 200. If the response was lost entirely, reconcile with GET /orders?externalOrderId=… rather than retrying blind.
How the partner is charged
You choose one collection mode for the whole programme, and the order response reports it as paymentMode:
| Mode | What it does |
|---|---|
| saved_method | The partner's saved card or wallet is charged immediately and provisioning starts. |
| credit | The amount is deducted from their credit balance with you and provisioning starts. |
| invoice | An invoice or proforma is raised with a due date. Services are created in pending_payment and provisioned when it is paid. |
Both pay-now modes still raise a paid document for the partner's records. Renewals then follow the ordinary billing lifecycle: each service renews on its cycle and the partner is invoiced for it like any other customer.
What comes back
{ "success": true, "data": { "orderId": "…", "orderNumber": "RSL-…", "externalOrderId": "your-own-id", "status": "paid", "paymentStatus": "paid", "paymentMode": "saved_method", "amount": 12.5, "currency": "EUR", "invoiceId": "…", "services": [ { "serviceId": "…", "status": "pending" } ], "options": [ ], "message": "Order created and paid. Service will be provisioned." } }
| HTTP status | What it does |
|---|---|
| 201 | Created. Read data.status. |
| 200 | An order with this externalOrderId already existed; the reconciliation row comes back with idempotent: true. |
| 400 / 409 | Validation, pricing, location or quantity-cap refusal. Nothing was charged. |
| 502 | The service could not be created after payment. Where money moved, the charge stands and you have been alerted. Do not retry; quote the order identifier. |
| Value | What it does |
|---|---|
| paid | Charged; provisioning is under way. Wait for the service.provisioned event or poll the service. |
| invoiced | A document was raised. Nothing provisions until it is paid. |
| payment_failed | The order exists, the payment did not go through. Use the retry route. |
| provisioning_failed | Returned together with the 502 above. |
Retrying a failed payment
POST /orders/{orderId}/retry charges exactly what was quoted, at the order-time rate, and then creates the services. An optional paymentMethodId picks a different saved method. It answers 400 Order is already paid when there is nothing to retry, 400 when the order is invoiced (the document that was raised must be paid instead — a retry does not raise another), 400 with the gateway's reason when the charge fails again, and 502 under the same contract as an order when the charge succeeds but the service cannot be created. A success re-emits the order.paid event.
Provisioning is asynchronous
An accepted order does not mean a running service. There are three ways for the partner to learn the outcome, and a robust integration uses all three:
- Events. service.provisioned fires when a service reaches active; service.provisioning_failed fires when creation or provisioning fails after payment, including when the failure happens in the background well after the order was accepted. See Webhooks.
- Polling the order.
GET /orders/{orderId}carriesprovisioning_status— provisioned, pending or failed — and, on the detail route,provisioning_errorwith the reason. - Polling the service. Every order row carries a
servicesarray with the service identifier, status, hostname and address, so a reconcile after a timed-out create links the service without a second call.
Warning: Events are only sent to an endpoint that is already on file. Events raised while no URL is configured are dropped, not queued. A partner should set their endpoint before placing their first order.
Status values
Three different status fields appear on an order row, and they answer different questions.
| Field | What it does |
|---|---|
status | The platform order status, from pending through to active. |
payment_status | pending, processing, paid, failed or refunded. This is what GET /orders?status= filters on. |
provisioning_status | provisioned, pending or failed. |
Services have their own set, which is what GET /services?status= filters on: pending (created, provisioning not started), pending_payment (invoice mode, waiting for the partner's payment), provisioning and installing, active, suspended, terminated, cancelled and provisioning_failed.
Managing services
GET /services accepts ?page=, ?limit= (20 by default), ?status= and ?externalCustomerId=. Rows carry the service identifier, the partner's own reference, the customer reference, the product name, status, address, hostname, cycle, wholesale amount per cycle, next due date and creation date; the detail route adds the location and port.
Lifecycle
| Operation | What it does |
|---|---|
POST /services/{id}/suspend | Suspends the service. Optional { "reason": "…" }. Answers with the new status. |
POST /services/{id}/unsuspend | Lifts the suspension. No body needed. |
DELETE /services/{id} | Terminates. Idempotent: on an already-terminated service it succeeds with alreadyTerminated: true, so a retry after a timeout is safe. |
POST /services/{id}/change-package | Moves the service to another product. Body { "productId": "…" }. Answers 202. |
Warning: Suspension is the partner acting against their own customer. It does not pause what they owe you. Terminating is the only call that stops the wholesale charge.
A package change answers 202 with an upgrade identifier, a status, and the billing document it raised (type, identifier and number, or nothing at all if the change costs nothing). The change applies once that document is settled. Where you require your own approval first, the status is awaiting_admin and the partner should poll the service. A package change runs your upgrade engine, so it is only possible between two products you have linked with an upgrade path — without one the call answers 409 and says so.
Two general refusals apply across the service routes: 404 means “not yours, or does not exist”, deliberately indistinguishable, and 409 means the service is in the wrong state for the transition.
Device control
A service the partner resells sits on one of several backends, and what it can be asked to do depends on which — and on the switches you can turn off per product. The partner should read it rather than assume it:
GET /services/{id}/capabilities → { "backend": "vps" | "dedicated" | "upstream" | "plugin" | null, "power": true, "powerActions": ["start","stop","restart"], "stats": true, "console": true, "reinstall": true, "password": true, "backups": false }
A backend of null means the service has no remote controls at all. A terminated service reports every control as false. Dedicated servers report no backups.
- Power.
POST /services/{id}/powerwith{ "action": "start" | "stop" | "restart" | "shutdown" };shutdownis the graceful form ofstop. A service with no backend answers 400; a control you have switched off for that product answers 403; a dedicated server whose management controller accepted the call but then failed answers 424. A suspended service refusesstart, and a terminated one refuses everything. - Reinstall.
GET /services/{id}/os-templatesfirst — it returns the valid values and names the platform. On platform-provisioned VPS and dedicated servers the value is an image identifier (osImageIdis accepted as the explicit spelling), and a VPS also requires a password of at least 8 characters. On a provider-backed service it is that provider's template name. Bare metal ignores a supplied password, generates its own and returns it from the password route. - Console.
GET /services/{id}/consoleopens a one-shot, short-lived session. The shape depends on the backend: a platform VPS returns a websocket session descriptor to point a VNC-over-websocket client at, a dedicated server returns a remote-console launch descriptor, and a provider-backed service may return its provider's own URL. A stopped VPS answers 409 — start it first. There is no plain browser URL to redirect to for a VPS or a dedicated server; embed a client, or open the console from the reseller portal. - Statistics and backups.
GET /services/{id}/statsreturns the backend's usage object.GET /services/{id}/backupslists snapshots or backups, andPOSTcreates one with an optional name.
Suspended services refuse console, reinstall, password reset and backup creation, and refuse to start — they still stop, restart and report statistics.
The service page, add-ons and the rest
Everything the platform's own client panel shows for a service is available to the partner, so their storefront can show the customer the same page:
- The service document.
GET /services/{id}/documentreturns the whole page in one read:kind(dedicated, vps, ip-transit, colocation, game, plugin, generic), the per-product panel switches, the kind section (hardware, location, interfaces and storage for a dedicated server; addresses and OS for a VPS; port and BGP for IP transit; device and cross-connects for colocation; the game document), the network, and the add-ons and upgrade paths priced at the partner's wholesale. The live parts have their own reads because they hit hardware:traffic,health,network. Reverse DNS (PUT .../rdns) and SSH keys (GET/POST/DELETE .../ssh-keys, applied on the next reinstall) are writable; a partner attaching a key for an end customer sends the public key material, which is stored under the partner's own account. - Console for a foreign viewer.
POST /services/{id}/console-sessionanswers a session plusorigin— this platform's address — so a viewer served by the partner's own panel connects its websocket here. Sessions are one-shot and short-lived. - Add-ons after the sale.
PUT /services/{id}/optionsstates the add-ons the service must carry (an absolute set). New add-ons and quantity increases are charged to the partner at wholesale through their payment mode exactly like an order (first cycle on the service's cycle plus setup fee; a top-up at the unit price, no setup fee); removals are free and immediate. 402 means nothing changed and names the order to retry.DELETE .../options/{serviceOptionId}drops one add-on. Delivery to the server is the same path a customer's own purchase takes here. - Rescue and BGP.
POST .../rescue/.../unrescueboot a dedicated server into and out of the rescue environment (the one-time password is returned);PUT .../transit-configapplies the self-service BGP edits you allow on IP transit. - Game servers. Every
/services/{id}/game/…route of the client panel — files (uploads as base64 JSON, downloads as bytes), backups, schedules, mods, SFTP, power, variables and the console with itsorigin— is served for the partner by the same handlers. - Provider-built services.
GET .../provider,GET .../ui-schemaandPOST .../ui-action/{action}hand the partner the plugin's own client page, so a service built by one of your provider plugins renders the same page on their storefront.
A FluxBilling storefront connected to you as an upstream connection uses all of this automatically; a WHMCS partner's client area renders the document.
Account operations
GET /payment-methods lists the partner's saved methods with you (identifier, type, label, card brand, last four digits, whether it is the default, and when it was last used). PUT /payment-methods/{id}/default chooses the one every automatic charge lands on — it is signature-gated precisely because it decides where money goes. Methods themselves are added in the client portal, never through the API.
PUT /account/webhook sets the endpoint for outbound events, and POST /webhooks/test sends a test delivery. Both are covered in Webhooks.
What the API does not cover
- There are no renewal or invoice events, and no invoice listing on this surface. A partner watches their billing in the client portal; the first machine-readable signal of an unpaid balance is a service suspension.
- There is no rehearsal mode. Integration testing places real orders against real payment methods — use a product priced at zero for that partner.
- Version selection is the
/v1path segment and nothing else. - Reselling through the API is one level deep. A partner cannot appoint sub-resellers.
Related articles
- Webhooks — the events this API raises and how to verify them.
- Reseller Portal — the pages a partner uses by hand.
- Reseller Program — tiers, discounts, collection mode and approval settings.
- Upstream Connections — reselling another platform’s catalogue from your own store, the other side of this API.
- Resellers and Reseller Details — approving and managing partners.
- Customer REST API — the separate interface for an end customer's own services.
