Managing services
Every route on this page takes the service code (SRV-TJUAQ1-1951), given by GET /services. A service outside the key's restriction answers 404 SERVICE_NOT_FOUND.
GET /services
Permissions: services:read
| Parameter | Type | Required | Values |
|---|---|---|---|
limit | integer | no, default 20 | 1 to 100 |
cursor | string | no | pagination.nextCursor from the previous page |
status | string | no | ACTIVE, SUSPENDED, CANCELLED, DELETED |
curl -H "Authorization: Bearer $FRESHPERF_KEY" \ "https://api.freshperf.fr/v1/services?limit=2&status=ACTIVE"
{ "data": [ { "code": "SRV-TJUAQ1-1951", "name": "Minecraft 1", "customName": null, "productShortname": "minecraft-1", "productTitle": "Minecraft 1", "category": "minecraft", "status": "ACTIVE", "providerType": "pterodactyl", "autoRenew": true, "nextBillingAt": 1789437385056, "recurrence": "monthly", "createdAt": 1786845385066, "activatedAt": 1786845385056 } ], "pagination": { "nextCursor": null, "limit": 2 } }
| Field | Type | Notes |
|---|---|---|
code | string | the identifier every service route takes |
name | string | the custom name when set, otherwise the product title |
customName | string or null | the name you gave it |
productShortname | string | pass it back as product in an order |
productTitle | string or null | product label |
category | string or null | category shortname, e.g. vps, minecraft |
status | string | ACTIVE, SUSPENDED, CANCELLED, DELETED |
providerType | string or null | vps or pterodactyl |
autoRenew | boolean | |
nextBillingAt | integer or null | epoch milliseconds |
recurrence | string or null | billing cycle, e.g. monthly |
createdAt, activatedAt | integer or null | epoch milliseconds |
GET /services/{code}
Permissions: services:read
Carries the fields above and adds:
| Field | Type | Notes |
|---|---|---|
provisioned | boolean | false while no infrastructure exists |
priceCents | integer | recurring price of one cycle, excluding tax |
taxCents | integer | tax on the same cycle |
currency | string or null | ISO 4217 |
dunningState | string | NONE, RETRY, SUSPENSION_TRIGGERED, DELETION_TRIGGERED |
restartRequired | boolean | a resource change is waiting for a restart |
restartRequiredReason | string or null | |
configuration | object string → string | the configuration as ordered. Credentials and internal keys are stripped |
cancelledAt | integer or null | epoch milliseconds |
{ "data": { "code": "SRV-TIYMZT-8349", "name": "VPS 1", "status": "ACTIVE", "providerType": "vps", "provisioned": true, "autoRenew": true, "nextBillingAt": 1787960297645, "recurrence": "monthly", "priceCents": 500, "taxCents": 100, "currency": "EUR", "dunningState": "NONE", "restartRequired": false, "restartRequiredReason": null, "configuration": { "os": "debian", "osVersion": "13", "location": "paris", "cpuCores": "2", "ramSize": "2048", "diskSize": "20" }, "cancelledAt": null } }
GET /services/{code}/status
Permissions: services.metrics:read
Asks the infrastructure live. Allow up to two seconds.
Every field can be null: each provider reports what it knows.
| Field | Type | Notes |
|---|---|---|
status | string | the provider's own text: running, stopped, starting, stopping, restoring... Not a closed enum |
uptimeSeconds | integer | |
cpu | number | a fraction or a percentage depending on the provider |
cpuCount | number | |
memoryBytes, memoryMaxBytes | integer | bytes |
diskBytes, diskMaxBytes | integer | bytes |
networkInBytes, networkOutBytes | integer | bytes |
ipv4 | string | on a game server, address:port |
ipv6 | string | |
extraIpv4 | array of string | additional addresses |
hostname | string | |
extra | object | the provider's own figures. Free-form, unversioned |
INSTANCE_NOT_PROVISIONED (409) while no infrastructure exists.
GET /services/{code}/metrics/history
Permissions: services.metrics:read
{ "data": { "samples": [ { "ts": 1787903520000, "cpu": 0.1364, "mem": 1069400678, "maxmem": 2147483648, "netin": 4550, "netout": 69 } ] } }
samples is an array of free-form objects: the window, the step and the keys come from the provider. It is empty when no history is available, which is the case on game servers.
POST /services/{code}/power
Permissions: services.power:write
Rate: 20 a minute, per key and per service.
| Field | Type | Required | Values |
|---|---|---|---|
action | string | yes | start, stop, restart, kill |
kill exists only on game servers; on a VPS it answers 400 ACTION_UNSUPPORTED.
curl -X POST -H "Authorization: Bearer $FRESHPERF_KEY" \ -H "Content-Type: application/json" -d '{"action":"restart"}' \ https://api.freshperf.fr/v1/services/SRV-TJUAQ1-1951/power
{ "data": { "status": "SUCCESS", "message": "Power signal accepted" } }
| Field | Values |
|---|---|
status | SUCCESS when done, PENDING when the infrastructure is still working on it |
message | the provider's sentence, or null |
On PENDING, read /status again every 5 to 10 seconds.
| Error | Status | When |
|---|---|---|
VALIDATION_ERROR:action | 400 | action absent or outside the list |
ACTION_UNSUPPORTED | 400 | kill on a VPS |
SERVICE_NOT_ACTIVE | 409 | service suspended or cancelled |
INSTANCE_NOT_PROVISIONED | 409 | no infrastructure yet |
POST /services/{code}/console/command
Permissions: services.console:write
Game servers only.
| Field | Type | Required |
|---|---|---|
command | string | yes, non-blank |
curl -X POST -H "Authorization: Bearer $FRESHPERF_KEY" \ -H "Content-Type: application/json" -d '{"command":"say hello"}' \ https://api.freshperf.fr/v1/services/SRV-TJUAQ1-1951/console/command
{ "data": { "status": "SUCCESS", "message": "Command sent" } }
On a VPS: 400 ACTION_UNSUPPORTED. On a stopped server the panel refuses the command and the answer is 502 ACTION_FAILED.
Backups
The four routes relay the provider. The response always has the same shape: {"data": {"status", "message", "result"}}, where result is the provider's payload.
GET /services/{code}/backups
Permissions: services.backups:read
Answers even on a suspended service.
{ "data": { "status": "SUCCESS", "message": "OK", "result": { "customerEnabled": true, "limits": { "max": 6, "used": 2 }, "customer": [ { "id": "9b5d3938-cbd9-4946-8357-05badcdf6c33", "note": null, "status": "COMPLETED", "createdAt": "2026-08-15T19:50:45.282411", "sizeBytes": 21474837581 } ], "internal": [] } } }
A backup id is a string, the provider's own. It is what delete and restore take.
POST /services/{code}/backups
Permissions: services.backups:write
| Field | Type | Required | Values |
|---|---|---|---|
note | string or null | no | 200 characters at most |
{ "data": { "status": "PENDING", "message": "VPS backup in progress", "result": { "taskId": "d3b780a2-308f-40bf-95a2-0316d8872721", "status": "PENDING" } } }
| Error | Status | When |
|---|---|---|
NOTE_TOO_LONG | 400 | note beyond 200 characters |
SERVICE_NOT_ACTIVE | 400 | service suspended or cancelled |
SERVICE_IN_DUNNING | 400 | service in dunning |
INSTANCE_NOT_PROVISIONED | 400 | no infrastructure yet |
LIMIT_REACHED | 409 | backup quota reached |
BACKUP_IN_PROGRESS | 400 | a backup or a restore is already running |
DELETE /services/{code}/backups/{backupId}
Permissions: services.backups:write
backupId comes from result.customer[].id. An unknown one answers 400 BACKUP_NOT_FOUND.
POST /services/{code}/backups/restore
Permissions: services.backups:write
Overwrites the disk.
| Field | Type | Required | Values | Where it comes from |
|---|---|---|---|---|
source | string | yes | CUSTOMER or INTERNAL | - |
backupId | string | yes when source is CUSTOMER | - | result.customer[].id |
volid | string | yes when source is INTERNAL | - | result.internal[].volid |
Same refusals as creation, plus 400 MISSING_FIELDS when source is absent or its matching identifier is missing.
POST /services/{code}/reinstall
Permissions: services.reinstall:write
Rate: 2 a minute, per key and per service. Refused calls count.
Destroys the disk. On a game server it resets the server and the body may be empty.
| Field | Type | Required | Values | Where it comes from |
|---|---|---|---|---|
os | string | yes on a VPS | the composite <family>-<version> value | GET /catalog/products/{shortname} → the kind: "os-picker" characteristic, options field |
authType | string | yes on a VPS | PASSWORD or SSH_KEY | - |
authValue | string | yes on a VPS | the password, or an OpenSSH public key line | - |
authValue is stored encrypted, never returned, and this call is never logged with its body.
curl -X POST -H "Authorization: Bearer $FRESHPERF_KEY" \ -H "Content-Type: application/json" \ -d '{"os":"debian-13","authType":"SSH_KEY","authValue":"ssh-ed25519 AAAAC3Nza... admin@laptop"}' \ https://api.freshperf.fr/v1/services/SRV-TIYMZT-8349/reinstall
{ "data": { "status": "PENDING", "message": "reinstalling" } }
| Error | Status | When |
|---|---|---|
MISSING_FIELDS | 400 | one of the three fields is missing on a VPS |
INVALID_OS | 400 | os outside the catalogue values |
OS_FAMILY_MISMATCH | 400 | family and version do not go together |
INVALID_AUTH_TYPE | 400 | authType outside the list |
SERVICE_NOT_ACTIVE, SERVICE_IN_DUNNING, INSTANCE_NOT_PROVISIONED | 400 | state of the service |
API_RATE_LIMITED | 429 | more than 2 calls in the minute |
GET /services/{code}/addons
Permissions: services.addons:read
{ "data": { "addons": [ { "shortname": "extra-saves", "title": "Additional saves", "kind": "BACKUP_SLOTS", "status": "ACTIVE", "quantity": 1, "unitPriceCents": 399, "unitTaxCents": 80, "linePriceCents": 399, "lineTaxCents": 80, "nextCycleLinePriceCents": 399, "nextCycleLineTaxCents": 80, "pendingRemovalQuantity": 0 } ], "available": [ { "shortname": "extra-ram-1gb", "title": { "fr-fr": "+1 Go RAM", "en-us": "+1 Gb RAM" }, "kind": "RESOURCE_DELTA", "resourceKey": "ramSize", "unitDelta": 1024, "unitPriceCents": 200, "maxQuantity": 10 } ], "recurringTotalCents": 479 } }
| Field | Contents |
|---|---|
addons | the add-ons the service already carries |
available | those it may still take. maxQuantity of 0 means unlimited |
recurringTotalCents | the service's recurring total including add-ons, excluding tax |
Adding or removing an add-on happens in the dashboard.
PATCH /services/{code}
Permissions: services:write
| Field | Type | Required | Values |
|---|---|---|---|
customName | string or null | no | 32 characters at most. An empty string clears the name |
autoRenew | boolean | no | true only |
curl -X PATCH -H "Authorization: Bearer $FRESHPERF_KEY" \ -H "Content-Type: application/json" -d '{"customName":"prod-eu-1"}' \ https://api.freshperf.fr/v1/services/SRV-TJUAQ1-1951
Answers the whole service, like GET /services/{code}.
| Error | Status | When |
|---|---|---|
CUSTOM_NAME_TOO_LONG | 400 | beyond 32 characters |
RENEWAL_CANCELLATION_NOT_AVAILABLE_VIA_API | 400 | autoRenew: false. Stopping a renewal happens in the dashboard |
POST /services/{code}/renew
Permissions: services:write and billing.invoices:pay, plus billing.saved_methods:charge as soon as the funding touches a card.
Required header: Idempotency-Key.
Charges the next cycle now and moves the billing date forward.
| Field | Type | Required | Values | Where it comes from |
|---|---|---|---|---|
method | string | no, default account_default | account_default, balance, saved_method | - |
paymentMethodId | integer | yes when saved_method | > 0 | GET /payment-methods → id |
method | Effect |
|---|---|
account_default | applies the account's rule: the balance when opted in and sufficient, otherwise the default payment method. Going through the card needs billing.saved_methods:charge |
balance | the prepaid balance alone. No fallback to a card |
saved_method | charges the named method |
curl -X POST -H "Authorization: Bearer $FRESHPERF_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: renew-SRV-TJUAQ1-1951-2026-08" \ -d '{"method":"balance"}' \ https://api.freshperf.fr/v1/services/SRV-TJUAQ1-1951/renew
{ "data": { "status": "CHARGED", "serviceCode": "SRV-TJUAQ1-1951", "nextBillingAt": 1793091315189, "amountCents": 360 } }
status is only ever CHARGED: any other outcome is an error. amountCents is what was charged, tax included.
| Error | Status | When |
|---|---|---|
VALIDATION_ERROR:method | 400 | method outside the list |
PAYMENT_METHOD_ID_REQUIRED | 400 | saved_method without paymentMethodId |
PAYMENT_METHOD_NOT_FOUND | 404 | that method is not on the account |
INSUFFICIENT_BALANCE | 402 | balance too short |
NO_PAYMENT_METHOD | 402 | account_default with no usable method |
PAYMENT_METHOD_NOT_ACTIVE | 402 | method expired or blocked |
RENEWAL_FAILED | 402 | the charge failed for another reason |
API_KEY_SPENDING_CAP_EXCEEDED | 403 | the key's cap does not cover the cycle |
SERVICE_NOT_ACTIVE | 409 | service neither active nor reactivatable |
RENEWAL_IN_PROGRESS | 409 | a renewal of the same service is already running |
RENEWAL_TOO_FREQUENT | 429 | less than a minute since the previous one |
POST /services/{code}/actions/{action}
Permissions: they depend on the action sent.
The raw gateway to the provider. It is how the firewall and reverse DNS are reached, since neither has a dedicated route.
| Action | Permission |
|---|---|
power, start_vps, stop_vps, reboot_vps | services.power:write |
send_console_command | services.console:write |
backup_list | services.backups:read |
backup_create, backup_delete, backup_restore | services.backups:write |
firewall_list | services.firewall:read |
firewall_create, firewall_update, firewall_move, firewall_delete | services.firewall:write |
rdns | services.rdns:write, including for op: "get" |
features | services:read |
No other action is reachable. There is no firewall_add, firewall_set or firewall_remove: a rule is created, edited and deleted with firewall_create, firewall_update and firewall_delete.
The game-panel actions (files, databases, allocations, schedules, startup, versions, content, subdomains, sftp, subusers) answer 403 API_ACTION_NOT_ALLOWED whatever the key holds.
| Field | Type | Required |
|---|---|---|
arguments | object | no. Contents specific to each action |
curl -X POST -H "Authorization: Bearer $FRESHPERF_KEY" \ -H "Content-Type: application/json" \ -d '{"arguments":{"op":"set","address":"203.0.113.224","hostname":"vps.example.com"}}' \ https://api.freshperf.fr/v1/services/SRV-TIYMZT-8349/actions/rdns
{ "data": { "status": "SUCCESS", "message": "Action completed", "result": { "configured": true, "entries": [ { "family": "ipv4", "address": "203.0.113.224", "hostname": "vps.example.com", "zoneConfigured": true } ] } } }
rdns takes op as get, set or delete, plus address (one of the addresses /status returns) and, for set, hostname. The arguments of the firewall_* actions depend on the hypervisor: read firewall_list to see the shape of a rule before writing one.
| Error | Status | When |
|---|---|---|
INVALID_ACTION | 400 | action empty or starting with _ |
API_ACTION_NOT_ALLOWED:<action> | 403 | action the platform does not classify, or one with no public permission |
SERVICE_NOT_ACTIVE | 409 | service suspended or cancelled |
a provider marker, e.g. FIREWALL_INVALID_ARGUMENT | 409 | the provider refused |
ACTION_FAILED | 502 | the provider failed or did not answer |
Prefer the typed routes (/power, /backups, /reinstall, /console/command) wherever they exist: they are stable and provider-neutral.