Browse the documentation

Ordering through the API

Three calls: read the catalogue, place the order, follow the provisioning. Payment fits inside the order or in a separate call.

GET /catalog/products

Permissions: catalog:read

ParameterTypeRequiredValues
categorystringnoa category shortname, matched exactly

Private products and retired categories are never listed, and cannot be ordered.

GET /catalog/products/{shortname}

Permissions: catalog:read

{ "data": {
  "shortname": "vps-1",
  "category": "vps",
  "title": { "fr-fr": "VPS Xeon 1", "en-us": "VPS 1" },
  "providerType": null,
  "recurrences": ["monthly", "yearly"],
  "prices": [
    { "recurrence": "monthly", "priceCents": 500 },
    { "recurrence": "yearly", "priceCents": 5500 }
  ],
  "addons": [
    { "shortname": "extra-ram-1gb",
      "title": { "fr-fr": "+1 Go RAM", "en-us": "+1 Gb RAM" },
      "kind": "RESOURCE_DELTA", "maxQuantity": 10,
      "prices": [ { "recurrence": "monthly", "priceCents": 200 } ] }
  ],
  "characteristics": [
    { "key": "os", "label": { "fr-fr": "Système", "en-us": "OS" },
      "kind": "os-picker", "required": true, "sensitive": false,
      "defaultValue": null,
      "options": ["debian-13"],
      "choices": [ { "value": "debian", "label": { "en-us": "Debian" }, "versions": ["13"] } ],
      "companionKeys": ["osVersion"] }
  ]
} }
FieldTypeNotes
shortnamestringsend it as product in an order line
categorystring or nullcategory shortname
titleobject locale → textnot a string
providerTypestring or nullinfrastructure type, when it is fixed
recurrencesarray of stringthe cycles it is sold in. Open set: read it, do not hard-code it
prices[]recurrence + priceCentsprice of one cycle, excluding tax, in cents
addons[]see belowadd-ons attachable to a line
characteristics[]see belowthe configuration a line must carry

An add-on (addons[])

FieldTypeNotes
shortnamestringsend it in addons[].shortname
titleobject locale → text
kindstringRESOURCE_DELTA, BACKUP_SLOTS, EXTRA_IPV4, FLAG
maxQuantityintegermaximum quantity per line. 0 means unlimited
prices[]recurrence + priceCentsexcluding tax

A characteristic (characteristics[])

FieldTypeNotes
keystringthe key to send under characteristics
labelobject locale → text
kindstringthe input type: text-input, select, radio-grid, os-picker, auth-credential, password-input... Open set
requiredboolean
sensitivebooleanthe value is a credential: stored encrypted, never echoed back
defaultValuestring or nullalways null when sensitive is true
optionsarray of stringthe exact values you may send. Empty for free text
choicesarraythe same values with their labels. For an os-picker, one entry per family with its versions
companionKeysarray of stringthe keys the line must carry beside this one

Two companionKeys cases:

  • authValue (kind: "auth-credential") needs authType beside it, as PASSWORD or SSH_KEY.
  • os (kind: "os-picker") needs osVersion if you send the family and the version separately. The simpler path is to send the composite value from options (debian-13) and nothing else. It is also what POST /services/{code}/reinstall takes.

POST /orders

Permissions: orders:write, plus those of the funding you pick. Required header: Idempotency-Key.

FieldTypeRequiredValuesWhere it comes from
linesarrayyes1 to 20 lines-
lines[].productstringyes-GET /catalog/productsshortname
lines[].recurrencestringyes when the product offers anya value from recurrencesGET /catalog/productsrecurrences
lines[].quantityintegerno, default 11 to 100-
lines[].characteristicsobject string → stringdepends on the productthe keys of characteristics[], values taken from optionsGET /catalog/products/{shortname}
lines[].addons[].shortnamestringyes inside an add-on-GET /catalog/products/{shortname}addons[]
lines[].addons[].quantityintegerno, default 11 to maxQuantity-
promoCodestring or nullno--
customerNotestring or nullno500 characters at most-
paymentobject or nullno, default no paymentsee below-

characteristics values are always strings, even for a number.

The payment block

FieldTypeRequiredValues
methodstringno, default nonenone, balance, saved_method
paymentMethodIdintegeryes when saved_methodGET /payment-methodsid
methodExtra permissionsEffect
none, or no blocknonethe order stays PENDING_PAYMENT. Pay it later
balancebilling.invoices:paythe prepaid balance pays. Balance too short: 402 INSUFFICIENT_BALANCE, and no order is created
saved_methodbilling.invoices:pay and billing.saved_methods:chargethe stored method is charged, within the key's cap

A funding refused before any write leaves no order behind: method not found, method inactive, balance short, cap exceeded.

A zero total, after a 100% discount, settles with no payment and the order goes straight to PAID.

Example

curl -X POST -H "Authorization: Bearer $FRESHPERF_KEY" \
     -H "Content-Type: application/json" \
     -H "Idempotency-Key: 6f1c0b62-4f4b-4f8e-9b06-0c1d7a5c9e21" \
     -d '{
       "lines": [{
         "product": "vps-1",
         "recurrence": "monthly",
         "quantity": 1,
         "characteristics": {
           "os": "debian-13",
           "location": "paris",
           "authType": "SSH_KEY",
           "authValue": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... admin@laptop"
         },
         "addons": [{ "shortname": "extra-ram-1gb", "quantity": 2 }]
       }],
       "customerNote": "Web server migration",
       "payment": { "method": "balance" }
     }' \
     https://api.freshperf.fr/v1/orders
{ "data": {
  "order": {
    "orderNumber": "ORD-20260828-0001",
    "status": "PAID",
    "currency": "EUR",
    "subtotalCents": 900, "taxCents": 180, "discountCents": 0, "totalCents": 1080,
    "amountPaidCents": 1080, "remainingDueCents": 0,
    "promoCode": null, "customerNote": "Web server migration",
    "lines": [ { "productShortname": "vps-1", "recurrence": "monthly", "quantity": 1,
                 "unitPriceCents": 500, "unitTaxCents": 100, "totalCents": 1080,
                 "addons": [ { "shortname": "extra-ram-1gb", "quantity": 2, "unitPriceCents": 200 } ],
                 "configuration": { "os": "debian-13", "location": "paris" } } ],
    "serviceCodes": ["SRV-TKH243-7763"],
    "createdAt": 1787907301139, "paidAt": 1787907315110
  },
  "promoError": null,
  "payment": { "status": "CAPTURED", "paymentId": 244, "amountCents": 1080,
               "invoiceCode": "INV-202608-0084", "balanceCents": 3320 }
} }
FieldTypeNotes
order.statusstringDRAFT, PENDING_PAYMENT, AUTHORIZED, PAID, FULFILLED, CANCELLED, FAILED, REFUNDED
order.serviceCodesarray of stringthe services the order created. Empty until the payment settles and the provisioning finishes
order.lines[].configurationobject string → stringthe configuration as ordered. sensitive values are never in it
promoErrorstring or nullset when the promo code could not apply. The order stands at full price
payment.statusstringNONE when nothing was charged, FREE on a zero total, otherwise a Payment.Status value

Placing an order through the API never touches the cart open in the dashboard.

Refusals

ErrorStatusWhen
NO_ITEMS400lines empty or absent
TOO_MANY_ITEMS400more than 20 lines
PRODUCT_NOT_FOUND:<shortname>400unknown or private product
PRODUCT_NOT_ORDERABLE:<shortname>400category withdrawn from sale
RECURRENCE_REQUIRED:<shortname>400the product sells by cycle and recurrence is missing
RECURRENCE_NOT_OFFERED:<shortname>400that cycle does not exist for this product
MISSING_REQUIRED:<key>400a required characteristic is missing
INVALID_METADATA_KEY:<key>400a key the product does not declare
INVALID_METADATA_VALUE:<key>400a value outside options
ADDON_NOT_AVAILABLE:<shortname>400add-on not offered by this product
ADDON_MAX_QUANTITY:<shortname>400quantity beyond maxQuantity
VALIDATION_ERROR:lines.quantity400quantity outside 1 to 100
VALIDATION_ERROR:customerNote400beyond 500 characters
VALIDATION_ERROR:payment.method400method outside the list
PAYMENT_METHOD_ID_REQUIRED400saved_method without paymentMethodId
PAYMENT_METHOD_NOT_FOUND404method not on the account
PAYMENT_METHOD_NOT_ACTIVE400method expired or blocked
INSUFFICIENT_BALANCE402balance too short
PAYMENT_DENIED402the bank refused. The order stays payable another way
API_KEY_SPENDING_CAP_EXCEEDED403the key's cap does not cover the total
PAYMENT_IN_PROGRESS409another payment of the same order is running
PAYMENT_RECORDED_MISMATCH409reconciliation anomaly. Contact support
ORDER_CONCURRENT_UPDATE409the order changed during the write. Retry

POST /orders/{orderNumber}/pay

Permissions: billing.invoices:pay, plus billing.saved_methods:charge with saved_method. Required header: Idempotency-Key.

Pays what is left due on an order left pending.

FieldTypeRequiredValuesWhere it comes from
methodstringyesbalance, saved_method-
paymentMethodIdintegeryes when saved_method> 0GET /payment-methodsid
curl -X POST -H "Authorization: Bearer $FRESHPERF_KEY" \
     -H "Content-Type: application/json" \
     -H "Idempotency-Key: pay-ORD-20260828-0001" \
     -d '{"method":"saved_method","paymentMethodId":14}' \
     https://api.freshperf.fr/v1/orders/ORD-20260828-0001/pay

Answers the same payment block as POST /orders.

ErrorStatusWhen
ORDER_NOT_FOUND404unknown number
ORDER_NOT_PAYABLE, ORDER_ALREADY_SETTLED, ORDER_ALREADY_PAID400there is nothing to pay
AMOUNT_INVALID400the remaining due is not a valid amount

Plus the payment refusals from the table above.

GET /orders and GET /orders/{orderNumber}

Permissions: orders:read

ParameterTypeRequiredValues
limitintegerno, default 201 to 100
cursorstringnopagination.nextCursor from the previous page

The list returns a summary (orderNumber, status, currency, totalCents, amountPaidCents, remainingDueCents, createdAt, paidAt) and omits drafts. The detail returns the whole order, with lines[] and serviceCodes.

Following the provisioning

  1. GET /orders/{orderNumber}: status moves to PAID, then to FULFILLED once every service is in place.
  2. serviceCodes fills with the codes created. It is the link between an order and the services it produced.
  3. GET /services/{code} returns provisioned: false while the infrastructure comes up, then true.

Provisioning runs in the background and takes a few minutes. Poll every 10 to 30 seconds.