Browse the documentation

Requests and responses

Base URL

https://api.freshperf.fr/v1. Every route in this section is relative to that base, and only HTTPS is served.

Requests

RequestRule
BodyJSON, with Content-Type: application/json
Path parametersthe public identifiers in the table below
Filters and paginationquery parameters
Unknown fields in a bodyignored

The API refuses a malformed value rather than silently correcting it:

CaseAnswer
?limit=abc400 VALIDATION_ERROR:limit
?limit=0, ?limit=500400 VALIDATION_ERROR:limit
?cursor= unreadable400 VALIDATION_ERROR:cursor
?status= outside the enum400 VALIDATION_ERROR:status
Body field missing or outside its enum400 VALIDATION_ERROR:<field>

Responses

A success is an envelope:

{ "data": { "code": "SRV-TJUAQ1-1951", "status": "ACTIVE" } }

A list adds pagination:

{
  "data": [ { "code": "SRV-TJUAQ1-1951", "status": "ACTIVE" } ],
  "pagination": { "nextCursor": "MTg4", "limit": 20 }
}
FieldTypeMeaning
pagination.nextCursorstring or nullpass back as ?cursor=. null on the last page
pagination.limitintegerthe page size actually applied

201 on a creation, 200 otherwise. The two PDF routes answer the file, not JSON.

Pagination

?limit= runs 1 to 100, 20 by default. Out of range answers 400 VALIDATION_ERROR:limit.

The cursor is opaque and rests on two different mechanics depending on the route. Do not build one yourself.

MechanicRoutesBehaviour
Last id seenGET /services, GET /orders, GET /invoicesan insert mid-walk does not shift the pages
OffsetGET /balance/transactions, GET /credit-notes, GET /ticketsan insert mid-walk shifts the later pages

Lists run newest to oldest, except GET /tickets, sorted on the last update.

The exception: notifications

GET /account/notifications follows none of these rules.

NotificationsRule
Pagination?page= (1 by default, minimum 1) and ?limit= (20 by default, maximum 50)
Envelope{"data": {"items": [...], "total": n, "unread": n}}, with no pagination block

Errors

Every error, on every route, has the same shape:

{
  "error": {
    "code": "API_SCOPE_MISSING:services.power:write",
    "message": "This API key does not hold the scope this operation needs.",
    "docs": "https://freshperf.fr/api-reference"
  }
}
  • code is a stable marker. Branch your code on it.
  • message is an English sentence for humans. It can change; do not parse it.

Some markers carry a suffix after a colon:

MarkerSuffix
API_SCOPE_MISSING:<scope>the missing permission
API_ACTION_NOT_ALLOWED:<action>the refused provider action
VALIDATION_ERROR:<field>the offending field or parameter
PRODUCT_NOT_FOUND:<shortname>, PRODUCT_NOT_ORDERABLE:<shortname>, RECURRENCE_REQUIRED:<shortname>, RECURRENCE_NOT_OFFERED:<shortname>the product of the order line
INVALID_METADATA_KEY:<key>, INVALID_METADATA_VALUE:<key>, MISSING_REQUIRED:<key>the characteristic of the line

SERVICE_NOT_FOUND never carries a suffix.

Status classes:

StatusMeaning
400the request is malformed, or the state does not allow it
401key absent or unknown
402a payment did not go through
403permission, cap or address
404does not exist, or is outside the key's reach
409conflict with the current state, or a provider refusal
429too many calls, or too frequent an action
500on our side
502the infrastructure behind the service failed
503a PDF is not ready yet

The full list is in Error reference.

Identifiers

ResourceFieldShapeWhere it comes from
ServicecodeSRV- + base36 + - + 4 digits, e.g. SRV-TJUAQ1-1951GET /services
OrderorderNumberORD-YYYYMMDD-NNNN, e.g. ORD-20260828-0001GET /orders, or the POST /orders response
InvoicecodeINV-YYYYMM-NNNN, e.g. INV-202608-0083GET /invoices
Credit notecodeAV-YYYYMM-NNNN, e.g. AV-202608-0001GET /credit-notes
TicketticketNumberTKT- + base36 + - + 4 digits, e.g. TKT-TKH21B-6604GET /tickets, or the POST /tickets response
Payment methodidintegerGET /payment-methods
SSH keyidintegerGET /ssh-keys
BackupbackupIdstring, the provider's own, e.g. 9b5d3938-cbd9-4946-8357-05badcdf6c33GET /services/{code}/backupsresult
PaymentidintegerGET /invoices/{code}payments[]

Treat them as opaque. Infrastructure identifiers (instanceId, ownerId, vmid, node, cluster) are stripped from every response, at every depth.

Amounts and dates

  • Amounts are integer cents: "totalCents": 1199 is 11.99 €. Never a float.
  • currency is an ISO 4217 code, and can be null on a document with no amount.
  • Dates are epoch milliseconds, UTC: "nextBillingAt": 1789437385056.
  • Catalogue prices and a service's recurring price are excluding tax; taxCents travels beside them and totalCents includes both.
  • amountCents on a balance transaction is signed: positive on a credit, negative on a debit.

Headers

What you send:

HeaderWhen
Authorization: Bearer fpk_...always
Content-Type: application/jsonwhenever there is a body
Idempotency-Keyon the four routes that move money (see Rate limits and idempotency)

What you get back:

HeaderWhenMeaning
X-Request-Idevery responsethe call's identifier, also in the key's log. Quote it to support
Cache-Control: no-storeevery response except /openapi.jsonnothing here is cacheable
X-RateLimit-Limit, -Remaining, -Resetauthenticated responsesthe tightest budget of the current minute. Reset is an epoch timestamp in seconds, not a delay
Retry-After429, 409 IDEMPOTENCY_IN_PROGRESS, 503 PDF_NOT_AVAILABLEseconds to wait
WWW-Authenticate401Bearer realm="freshperf-api"
Idempotent-Replayed: truereplaythe response comes from an earlier identical call

GET /v1/openapi.json answers Cache-Control: public, max-age=300 with an ETag, and carries no rate-limit headers.

Browsers

Only the FreshPerf site's origin is allowed through CORS, for the reference's try-it console. An API key does not belong in a web page: call /v1 from your servers.

Compatibility

The API is versioned in the path (/v1). Within v1 we add fields, routes and enum values; we do not remove or rename them.

So write clients that ignore unknown fields and tolerate a new enum value. A breaking change would be a /v2, announced ahead of time.