API keys and the webhook are managed by you in the Partner section of the Rivervo panel. OpenAPI 3.1 (JSON)
Guide
Authentication
Create an API key in the Partner section of the Rivervo panel. The key is shown once — keep it on your server, never in a browser or a mobile app. Send it on every call:
http
Authorization: Bearer 12|3c9f…
Requests and answers are JSON. Amounts are integer US cents (6300 = $63.00); dates are ISO 8601.
The key works only on the Partner API (not in the customer panel), and the Partner API accepts no other token.
If your account has an IP allowlist, calls from other addresses get 403 ip_not_allowed. A suspended partner account gets 403 partner_inactive.
A leaked key: revoke it in the panel and create a new one.
Every call that changes something (POST, PUT, PATCH, DELETE) must carry an Idempotency-Key header — your own unique value for this change, 1–128 characters of letters, digits, -_.:. Your order or invoice number works well.
http
Idempotency-Key: inv-2026-0042
Same key + same request → the first answer again, with the header Idempotent-Replayed: true. Never a second charge, so a timeout is safe to retry.
Same key + a different request → 409 idempotency_conflict. While the first is still running → 409 idempotency_in_progress.
No key → 400 idempotency_key_required.
Secrets (a new password, a sign-in link, a root password) are only in the first answer; a replay returns them as null.
Errors
Every error has the same shape:
json
{"error": {
"code": "insufficient_funds",
"message": "Not enough partner balance: this costs 63.00 USD, available 10.00 USD (balance plus credit limit)."
}}
HTTP
code
meaning
400
idempotency_key_required, bad_request
Fix the request.
401
unauthenticated
Missing or wrong API key.
402
insufficient_funds
The partner balance does not cover it; nothing is charged.
403
forbidden, ip_not_allowed, partner_inactive
Not allowed for this key, this IP address or this partner account.
The Idempotency-Key was used for another request, or the first one is still running.
410
not_available
The feature is not available for this service.
422
validation_failed (+ fields), refused
Valid JSON that cannot be done; the message says why.
422
out_of_stock
POST /vps: the location has no room for the plan right now — see VPS availability.
429
rate_limited
Slow down; wait for the Retry-After seconds.
502
upstream_failed
Could not be done right now; nothing is charged — try again later.
500
internal_error
Our fault; we are told automatically.
validation_failed adds fields — the field names with their messages. A failed change still has an order (status: failed with error), also returned in the error answer under order.
Orders and asynchronous operations
Every change becomes an order, returned next to the result:
Creating a hosting account or a server takes a little while: the answer is 202 Accepted with the order in processing. It becomes completed when the service is ready, or failed. Follow it with GET /orders/{id} or, better, with the webhooks. GET /orders?status=processing lists the ones still running.
Every purchase and renewal through the API is paid from your partner balance at your partner price. GET /me shows the balance, the credit limit and your discounts.
If available_cents (balance plus the credit allowed) does not cover a purchase: 402 insufficient_funds, nothing is charged.
Services bought from the partner balance renew by themselves from it, 24 hours before they end, at the partner price. If the balance cannot cover a renewal, nothing is charged, you are told, and the service stops at the end of its paid period.
GET /balance/transactions?from=2026-11-01&to=2026-11-30 — every movement, newest first, with our retail price next to what you paid.
Prices
GET /prices returns everything you can buy with our retail price and your partner price, so you can compute and publish your own prices. Retail already includes our term discounts (a hosting year, yearly email); your discount is applied on top: partner_cents = round(retail_cents × (100 − discount) / 100).
The answer has an ETag and Cache-Control: private, max-age=300. Send If-None-Match with the last ETag and you get 304 Not Modified while nothing changed — so you can poll often and update your site only when our prices move:
js
// Run every few minutes (cron). Re-publishes your prices only when ours changed.
let etag = await store.get('rivervo_prices_etag');
const res = await fetch('https://api.rivervo.com/api/partner/v1/prices', {
headers: {
Authorization: `Bearer ${process.env.RIVERVO_API_KEY}`,
...(etag ? { 'If-None-Match': etag } : {}),
},
});
if (res.status === 304) return; // nothing changed
const { data } = await res.json();
for (const plan of data.hosting) {
for (const term of plan.terms) {
// your price: our partner price plus your margin, in cents
const yours = Math.ceil(term.partner_cents * 1.25);
await store.setPrice(`hosting-${plan.id}-${term.months}m`, yours);
}
}
await store.set('rivervo_prices_etag', res.headers.get('ETag'));
Catalog helpers for your order forms: GET /catalog/hosting-plans, GET /catalog/mail-plans, GET /catalog/vps-plans, GET /catalog/regions, GET /catalog/os.
VPS availability
Each server plan in GET /catalog/vps-plans and GET /prices has locations (where we sell it) and available_locations (where it can be made right now; refreshed every 5 minutes). GET /catalog/vps-availability returns the same availability on its own — a light call to check before showing a location to your client.
A location in locations but not in available_locations has no room now: POST /vps there answers 422 out_of_stock and nothing is charged.
To buy it anyway, send "wait": true. The server is paid now, gets status: waiting_for_room and waiting_until, and is made as soon as room appears — the order stays processing, then you get order.completed.
If no room appears by waiting_until (14 days), the payment goes back to the partner balance and the order fails with out_of_stock. Deleting a server that is still waiting cancels it and returns the payment.
Every price for you: retail and partner, per term and region (cache it; ETag / If-None-Match)
Responses
200 OKFields
dataPriceListrequired
currencystring
updated_atstring (date-time) | null
discounts_pctobject
hostingarray of object
idinteger
namestring
termsarray of object
monthsinteger
retail_centsinteger
partner_centsinteger
mailarray of object
planstring
namestring
mailbox_quota_gbinteger
min_mailboxesinteger
per_mailboxobject
monthlyPrice | null
retail_centsinteger
partner_centsinteger
yearlyPrice
retail_centsinteger
partner_centsinteger
extra_storage_per_gb_per_mailboxobject
monthlyPrice
retail_centsinteger
partner_centsinteger
yearlyPrice
retail_centsinteger
partner_centsinteger
vpsarray of object
idinteger
namestring
cpuinteger
ram_gbnumber
disk_gbinteger
traffic_tbnumber
cpu_typestring
Values: shared · dedicated
archstring
Values: x86 · arm
regionstring
locationsarray of string
Where the plan is sold.
available_locationsarray of string
Where it can be made right now (in GET /catalog/vps-plans and GET /prices only; refreshed every 5 minutes). Elsewhere in `locations` POST /vps answers out_of_stock unless "wait": true.
Where it can be made right now (in GET /catalog/vps-plans and GET /prices only; refreshed every 5 minutes). Elsewhere in `locations` POST /vps answers out_of_stock unless "wait": true.
Create a hosting account (paid from the partner balance)
Parameters
Idempotency-Keyheaderstringrequired
Your unique key for this change (1–128 of A–Z a–z 0–9 - _ . :). The same key with the same request returns the first answer (header Idempotent-Replayed: true) and never charges twice; with another request: 409 idempotency_conflict.
Request body application/json
domainstringrequired
plan_idintegerrequired
term_monthsintegerrequired
Responses
202 Accepted: the order is processing; follow it with GET /orders/{id} or the webhook.Fields
dataHosting
idinteger
domainstring
usernamestring
planobject | null
idinteger
namestring
term_monthsinteger | null
statusstring
Values: provisioning · active · expired · suspended · deleted
Your unique key for this change (1–128 of A–Z a–z 0–9 - _ . :). The same key with the same request returns the first answer (header Idempotent-Replayed: true) and never charges twice; with another request: 409 idempotency_conflict.
Your unique key for this change (1–128 of A–Z a–z 0–9 - _ . :). The same key with the same request returns the first answer (header Idempotent-Replayed: true) and never charges twice; with another request: 409 idempotency_conflict.
idpathintegerrequired
The hosting id.
Request body application/json
term_monthsinteger
Responses
200 OKFields
dataHosting
idinteger
domainstring
usernamestring
planobject | null
idinteger
namestring
term_monthsinteger | null
statusstring
Values: provisioning · active · expired · suspended · deleted
Your unique key for this change (1–128 of A–Z a–z 0–9 - _ . :). The same key with the same request returns the first answer (header Idempotent-Replayed: true) and never charges twice; with another request: 409 idempotency_conflict.
idpathintegerrequired
The hosting id.
Request body application/json
plan_idintegerrequired
term_monthsintegerrequired
Responses
200 OKFields
dataHosting
idinteger
domainstring
usernamestring
planobject | null
idinteger
namestring
term_monthsinteger | null
statusstring
Values: provisioning · active · expired · suspended · deleted
New password for the hosting account (shown in this answer only)
Parameters
Idempotency-Keyheaderstringrequired
Your unique key for this change (1–128 of A–Z a–z 0–9 - _ . :). The same key with the same request returns the first answer (header Idempotent-Replayed: true) and never charges twice; with another request: 409 idempotency_conflict.
One-time sign-in link to the website control panel (2 minutes)
Parameters
Idempotency-Keyheaderstringrequired
Your unique key for this change (1–128 of A–Z a–z 0–9 - _ . :). The same key with the same request returns the first answer (header Idempotent-Replayed: true) and never charges twice; with another request: 409 idempotency_conflict.
Where it can be made right now (in GET /catalog/vps-plans and GET /prices only; refreshed every 5 minutes). Elsewhere in `locations` POST /vps answers out_of_stock unless "wait": true.
Create a server (one month, paid from the partner balance)
Parameters
Idempotency-Keyheaderstringrequired
Your unique key for this change (1–128 of A–Z a–z 0–9 - _ . :). The same key with the same request returns the first answer (header Idempotent-Replayed: true) and never charges twice; with another request: 409 idempotency_conflict.
The location has no room now (out_of_stock): buy anyway; the server is made as soon as there is room (status waiting_for_room until then, the order stays processing). If no room appears by waiting_until, the payment is returned to the partner balance and the order fails with out_of_stock.
Default: false
Responses
202 Accepted: the order is processing; follow it with GET /orders/{id} or the webhook.Fields
dataVps
idinteger
hostnamestring
planVpsPlan
idinteger
namestring
cpuinteger
ram_gbnumber
disk_gbinteger
traffic_tbnumber
cpu_typestring
Values: shared · dedicated
archstring
Values: x86 · arm
regionstring
locationsarray of string
Where the plan is sold.
available_locationsarray of string
Where it can be made right now (in GET /catalog/vps-plans and GET /prices only; refreshed every 5 minutes). Elsewhere in `locations` POST /vps answers out_of_stock unless "wait": true.
Where it can be made right now (in GET /catalog/vps-plans and GET /prices only; refreshed every 5 minutes). Elsewhere in `locations` POST /vps answers out_of_stock unless "wait": true.
Your unique key for this change (1–128 of A–Z a–z 0–9 - _ . :). The same key with the same request returns the first answer (header Idempotent-Replayed: true) and never charges twice; with another request: 409 idempotency_conflict.
Your unique key for this change (1–128 of A–Z a–z 0–9 - _ . :). The same key with the same request returns the first answer (header Idempotent-Replayed: true) and never charges twice; with another request: 409 idempotency_conflict.
idpathintegerrequired
The server id.
actionpathstringrequired
Values: on · off · reboot
Responses
200 OKFields
dataVps
idinteger
hostnamestring
planVpsPlan
idinteger
namestring
cpuinteger
ram_gbnumber
disk_gbinteger
traffic_tbnumber
cpu_typestring
Values: shared · dedicated
archstring
Values: x86 · arm
regionstring
locationsarray of string
Where the plan is sold.
available_locationsarray of string
Where it can be made right now (in GET /catalog/vps-plans and GET /prices only; refreshed every 5 minutes). Elsewhere in `locations` POST /vps answers out_of_stock unless "wait": true.
Reinstall the server from its image (data lost; the new root password is in this answer only)
Parameters
Idempotency-Keyheaderstringrequired
Your unique key for this change (1–128 of A–Z a–z 0–9 - _ . :). The same key with the same request returns the first answer (header Idempotent-Replayed: true) and never charges twice; with another request: 409 idempotency_conflict.
idpathintegerrequired
The server id.
Responses
200 OKFields
dataVps
idinteger
hostnamestring
planVpsPlan
idinteger
namestring
cpuinteger
ram_gbnumber
disk_gbinteger
traffic_tbnumber
cpu_typestring
Values: shared · dedicated
archstring
Values: x86 · arm
regionstring
locationsarray of string
Where the plan is sold.
available_locationsarray of string
Where it can be made right now (in GET /catalog/vps-plans and GET /prices only; refreshed every 5 minutes). Elsewhere in `locations` POST /vps answers out_of_stock unless "wait": true.
Your unique key for this change (1–128 of A–Z a–z 0–9 - _ . :). The same key with the same request returns the first answer (header Idempotent-Replayed: true) and never charges twice; with another request: 409 idempotency_conflict.
idpathintegerrequired
The server id.
Responses
200 OKFields
dataVps
idinteger
hostnamestring
planVpsPlan
idinteger
namestring
cpuinteger
ram_gbnumber
disk_gbinteger
traffic_tbnumber
cpu_typestring
Values: shared · dedicated
archstring
Values: x86 · arm
regionstring
locationsarray of string
Where the plan is sold.
available_locationsarray of string
Where it can be made right now (in GET /catalog/vps-plans and GET /prices only; refreshed every 5 minutes). Elsewhere in `locations` POST /vps answers out_of_stock unless "wait": true.
Buy the email subscription (paid now from the partner balance)
Parameters
Idempotency-Keyheaderstringrequired
Your unique key for this change (1–128 of A–Z a–z 0–9 - _ . :). The same key with the same request returns the first answer (header Idempotent-Replayed: true) and never charges twice; with another request: 409 idempotency_conflict.
Your unique key for this change (1–128 of A–Z a–z 0–9 - _ . :). The same key with the same request returns the first answer (header Idempotent-Replayed: true) and never charges twice; with another request: 409 idempotency_conflict.
Set the number of paid mailboxes (more: charged for the rest of the period)
Parameters
Idempotency-Keyheaderstringrequired
Your unique key for this change (1–128 of A–Z a–z 0–9 - _ . :). The same key with the same request returns the first answer (header Idempotent-Replayed: true) and never charges twice; with another request: 409 idempotency_conflict.
Your unique key for this change (1–128 of A–Z a–z 0–9 - _ . :). The same key with the same request returns the first answer (header Idempotent-Replayed: true) and never charges twice; with another request: 409 idempotency_conflict.
Your unique key for this change (1–128 of A–Z a–z 0–9 - _ . :). The same key with the same request returns the first answer (header Idempotent-Replayed: true) and never charges twice; with another request: 409 idempotency_conflict.
Your unique key for this change (1–128 of A–Z a–z 0–9 - _ . :). The same key with the same request returns the first answer (header Idempotent-Replayed: true) and never charges twice; with another request: 409 idempotency_conflict.
Your unique key for this change (1–128 of A–Z a–z 0–9 - _ . :). The same key with the same request returns the first answer (header Idempotent-Replayed: true) and never charges twice; with another request: 409 idempotency_conflict.
Your unique key for this change (1–128 of A–Z a–z 0–9 - _ . :). The same key with the same request returns the first answer (header Idempotent-Replayed: true) and never charges twice; with another request: 409 idempotency_conflict.
Your unique key for this change (1–128 of A–Z a–z 0–9 - _ . :). The same key with the same request returns the first answer (header Idempotent-Replayed: true) and never charges twice; with another request: 409 idempotency_conflict.
Your unique key for this change (1–128 of A–Z a–z 0–9 - _ . :). The same key with the same request returns the first answer (header Idempotent-Replayed: true) and never charges twice; with another request: 409 idempotency_conflict.
Your unique key for this change (1–128 of A–Z a–z 0–9 - _ . :). The same key with the same request returns the first answer (header Idempotent-Replayed: true) and never charges twice; with another request: 409 idempotency_conflict.