Orders

8 vues Markdown

The four endpoints that show past orders, quote a price and place the order.

Overview

A customer can order on their own behalf, and that order completes without a browser. Payment is kept narrow for that reason: the account balance or a card the account has saved.

The flow runs in three steps: read the schema from the product detail, see the price with the preview, then place the order. The preview runs the same chain as the order endpoint and writes nothing.

Taking a domain does not belong here. A new domain is taken from the domain endpoints first and then named in the order as one you already own.

Reference

Listing the Orders

get/api/v1/client/orders
Orders/GetOrders the key's owner

Returns the account's orders, newest first.

Query 3
pageintWhich page.
limitintRows per page. 100 at the most.
statusstringThe state filter: waiting, in process, active or cancelled.
Response fields data[] — 8 + meta — 4
idintThe order id.
numberstringThe order number the customer sees.
statusstringWhere the order stands.
totalobjectThe order total. It is in the wallet currency.
payment_methodstringThe payment method recorded.
invoice_idintThe order invoice. It comes empty when there is none.
items_countintHow many lines the order holds.
created_atstringThe day it was placed.
total_countintHow many orders there are. It comes back under meta as the total.
pageintThe page you are on.
limitintThe page size.
next_pageintThe next page.
Errors 2
status_invalid422The state is not one the orders use.
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/client/orders?status=active' \
  -H "Authorization: Bearer $CLIENT_KEY"
const res = await fetch('https://panel.example.com/api/v1/client/orders?status=active', {
  headers: { Authorization: `Bearer ${clientKey}` },
});

const { data, meta } = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/client/orders?status=active');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $clientKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The listing carries NO LINES: the items and the services born come on the detail endpoint.
$rows = Kernel::internal('client:Orders/GetOrders', ['owner_id' => $uid])['data'];

Reading an Order in Full

get/api/v1/client/orders/{id}
Orders/GetOrder the key's owner

Returns an order's lines, the services born and its invoice.

Response fields data — 12
idintThe order id.
numberstringThe order number.
statusstringWhere the order stands.
totalobjectThe order total.
payment_methodstringThe payment method recorded.
invoice_idintThe order invoice id.
items_countintHow many lines.
created_atstringThe day it was placed.
itemsobject[]The order lines. Each carries a label, a quantity and a line total.
servicesobject[]The services born from the order. Each carries an id, a name, a type and a state, and the id goes to the service endpoint.
invoiceobjectThe order invoice summary. It carries the id, number, state, total and the dates.
notesstringThe note written while ordering.
Errors 2
not_found404No such order, or it belongs to another customer.
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/client/orders/918' \
  -H "Authorization: Bearer $CLIENT_KEY"
const res = await fetch(`https://panel.example.com/api/v1/client/orders/${id}`, {
  headers: { Authorization: `Bearer ${clientKey}` },
});

const { data } = await res.json();
const ids = data.services.map((s) => s.id);
$ch = curl_init('https://panel.example.com/api/v1/client/orders/' . $id);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $clientKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The services can already exist while the order still reads WAITING; follow the service state.
$o = Kernel::internal('client:Orders/GetOrder', ['owner_id' => $uid, 'id' => $id])['data'];
$live = array_filter($o['services'], fn ($s) => $s['status'] === 'active');

Pricing an Order

post/api/v1/client/orders/preview
Orders/PreviewOrder nothing is written

Prices an order body without writing a single row.

Body 4
itemsobject[]reqThe order lines. One to twenty, in the same shape as the order endpoint.
product_idintreqThe id of the product ordered.
cyclestringreqThe billing cycle. It has to be one the product prices.
quantityintHow many. It counts only where the product allows more than one.
addonsobjectThe add-ons picked. It maps an add-on id to a choice id.
addons_qtyobjectThe count of add-ons priced by quantity.
requirementsobjectThe answers to the product's questions. Every field marked required has to be sent.
requirement_filesobjectThe content for fields wanting a file. It goes as encoded content and an address is refused.
metricsint[]The ids of the meters to switch on.
domainobjectThe line's domain axis. It carries the choice, the name, the subdomain root and the licence fields.
couponsstring[]The coupon codes to try.
billing_profile_idintThe billing profile id. It sets the tax context, and the account default stands in when left out.
paymentobjectThe payment block. Sending it adds the method's fee to the total, and the balance is not checked here.
Response fields data — 6
itemsobject[]The lines priced. Each carries a kind, product, name, cycle, quantity, domain, unit price, set-up fee and line total.
subtotalobjectThe total before discount and tax.
discountobjectThe discount in all.
taxobjectThe tax on the discounted base.
feeobjectThe payment method fee. It is worked out only when the payment block is sent.
totalobjectThe grand total. It is what the order endpoint will charge.
Errors 8
items_required422No line was sent, or there are more than twenty.
product_not_orderable422The product is absent, closed, hidden or its group is shut to orders.
cycle_invalid422The cycle is not one the product prices.
requirement_missing422A required order field was not sent.
domain_acquisition_not_here422Registering or transferring a domain does not happen here.
coupon_invalid422The coupon is invalid or does not apply to these lines.
not_found404The billing profile or card belongs to another account.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/client/orders/preview' \
  -H "Authorization: Bearer $CLIENT_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"items":[{"product_id":55,"cycle":"monthly","quantity":1}],"coupons":["WELCOME10"]}'
const res = await fetch('https://panel.example.com/api/v1/client/orders/preview', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${clientKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ items, coupons }),
});

const { data } = await res.json();
showSummary(data.subtotal, data.discount, data.tax, data.total);
$ch = curl_init('https://panel.example.com/api/v1/client/orders/preview');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $clientKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['items' => $items]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The preview DOES NOT run the ordering gates: the terms, verification and balance go unchecked here.
$q = Kernel::internal('client:Orders/PreviewOrder', ['owner_id' => $uid, 'items' => $items])['data'];
$due = $q['total']['amount'];

Placing the Order

post/api/v1/client/orders
Orders/CreateOrder the balance or a saved card

Places the order and collects payment in the same call.

Body 7
termsboolreqAccepting the terms. It has to be sent true.
itemsobject[]reqThe order lines. One to twenty.
product_idintreqThe id of the product ordered.
cyclestringreqThe billing cycle. It has to be one the product prices.
quantityintHow many. It counts only where the product allows more than one.
addonsobjectThe add-ons picked. It maps an add-on id to a choice id.
addons_qtyobjectThe count of add-ons priced by quantity.
requirementsobjectThe answers to the product's questions. Every field marked required has to be sent.
requirement_filesobjectThe content for fields wanting a file. It goes as encoded content and an address is refused.
metricsint[]The ids of the meters to switch on.
domainobjectThe line's domain axis. It carries the choice, the name, the subdomain root and the licence fields.
couponsstring[]The coupon codes. The operator's self-applying coupons join in as well.
billing_profile_idintThe billing profile id.
notesstringThe order note. Anything past a thousand characters is cut.
paymentobjectThe payment source. It is required once the total is above zero.
methodstringThe road: the account balance or a saved card.
card_idintThe card to charge. The account's default card is charged when left out.
Response fields 201 — data — 6
order_idintThe new order id.
numberstringThe order number.
statusstringThe state right after collection. It reads waiting until provisioning moves it on.
totalobjectThe order total settled.
paymentobjectHow the collection went.
methodstringThe road used: the balance, a card or free.
statusstringWhether it was paid, failed or stands open.
cardobjectThe id and last four digits of the card charged.
transaction_idstringThe provider's transaction reference.
errorstringWhy a failed payment failed.
invoiceobjectThe order invoice summary.
Errors 9
terms_required422The terms were not accepted.
verification_required422The account is waiting on verification.
checkout_blocked422A hook refused the order.
insufficient_balance422The wallet does not cover the total.
no_stored_card422No card was named and the account holds no default.
card_not_chargeable422The card's provider cannot charge without a browser.
payment_method_restricted422The method is closed to this customer or these products.
not_found404The billing profile or card belongs to another account.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/client/orders' \
  -H "Authorization: Bearer $CLIENT_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"terms":true,"items":[{"product_id":55,"cycle":"monthly"}],"payment":{"method":"balance"}}'
const res = await fetch('https://panel.example.com/api/v1/client/orders', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${clientKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    terms: true,
    items,
    payment: { method: 'balance' },
  }),
});

const { data } = await res.json();
if (data.payment.status === 'failed') showPayLater(data.invoice);
$ch = curl_init('https://panel.example.com/api/v1/client/orders');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $clientKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'terms' => true, 'items' => $items,
        'payment' => ['method' => 'balance'],
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// A 201 DOES NOT MEAN PAID: a declined card still leaves the order and its unpaid invoice, so read the payment state.
$o = Kernel::internal('client:Orders/CreateOrder',
    ['owner_id' => $uid, 'terms' => true, 'items' => $items,
     'payment' => ['method' => 'card']])['data'];

if ($o['payment']['status'] !== 'paid') $notify($o['invoice']['invoice_id']);

Pitfalls

A successful answer does not mean paid

The order endpoint answers with success even on a failed collection, because the order was built: a declined card leaves the order and its unpaid invoice standing. Read whether it was paid from the payment state in the answer. Only the provider's plain approval counts as paid, and an answer wanting a further step fails here.

The preview does not run the ordering gates

The preview runs the whole pricing chain and skips the gates that belong to the moment of ordering: accepting the terms, account verification, the policy hook and whether the balance covers it. A body passing the preview cleanly can still be refused by the order endpoint.

Registering a domain is refused here

A line's domain axis takes a domain you already own or a free subdomain alone. Asking to register or transfer is refused with a plain error. The right order is to take it from the domain endpoints first and then name it in the order as owned.

A file field takes no address

An order field wanting a file wants encoded content, and giving an address is refused. That is a deliberate security decision: the server never downloads an address a caller hands it. Send the file with the request.

A waiting order does not mean the service is absent

An order reads waiting right after collection and stays there until provisioning moves it on. The services can already exist by then. Follow the progress from the service states in the detail rather than from the order's own.

Every amount is in the wallet currency

The product prices, the preview total and the order total all come in one currency: the account's wallet currency. No conversion is needed to compare them with the balance. The display choice on the profile does not move these figures.

Cet article vous a-t-il été utile ?

Merci pour votre retour !

Besoin d'aide supplémentaire ?

Notre équipe d'assistance est disponible 24h/24 pour tout ce que vous ne trouvez pas ci-dessus.