Placing an Order
The five endpoints that prepare a basket and build the order.
Overview
Building an order is not one call. Every line in the basket rests on a product's price, its add-ons and its questions, while a domain line has to have been asked about at the registrar. This article covers that preparation and the creation at the end.
The order of work: build the line from the product information, ask about the domain when there is one, check any coupon you mean to use, then create the order. Each step feeds the next.
The create call does more than open a record: it brings the services into being and raises the invoice when asked. Undoing it costs, so do not skip the preparation.
Reference
Product Information for the Order Form
Returns a product's prices, add-ons, meters and questions.
curl 'https://panel.example.com/api/v1/admin/orders/product-info?product_id=12&client_id=94' \
-H "Authorization: Bearer $API_KEY"const url = new URL('https://panel.example.com/api/v1/admin/orders/product-info');
url.searchParams.set('product_id', productId);
url.searchParams.set('client_id', clientId);
const res = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
const { data } = await res.json();
const required = data.requirements.filter((r) => r.properties.compulsory);$qs = http_build_query(['product_id' => $productId, 'client_id' => $clientId]);
$ch = curl_init('https://panel.example.com/api/v1/admin/orders/product-info?' . $qs);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// The required questions are written HERE; fill this list before building the order.
$info = Api::Orders()->GetProductInfo([], ['product_id' => $pid, 'client_id' => $cid])['data'];
$must = array_filter($info['requirements'], fn ($r) => $r['properties']['compulsory'] ?? false);Asking Whether a Domain Is Free
Returns whether a domain can be taken and what the extension costs.
curl -X POST 'https://panel.example.com/api/v1/admin/orders/check-domain' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"domain":"example.com","client_id":94}'const res = await fetch('https://panel.example.com/api/v1/admin/orders/check-domain', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ domain, client_id: clientId }),
});
const { data } = await res.json();
if (! data.available) suggestAlternatives();$ch = curl_init('https://panel.example.com/api/v1/admin/orders/check-domain');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['domain' => $domain, 'client_id' => $cid]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// Whether a transfer needs a code depends on the extension; learn it here BEFORE ordering.
$d = Api::Orders()->CheckDomain(['domain' => $domain, 'client_id' => $cid])['data'];
$needsCode = (int) $d['tld_info']['epp_code'] === 1;Listing the Coupons
Returns the coupons an order can take, along with their rules.
curl 'https://panel.example.com/api/v1/admin/orders/coupons?client_id=94' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/orders/coupons?client_id=' + clientId, {
headers: { Authorization: `Bearer ${apiKey}` },
});
const { data } = await res.json();
const usable = data.filter((c) => ! c.disabled);$ch = curl_init('https://panel.example.com/api/v1/admin/orders/coupons?client_id=' . $cid);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// WITHOUT a client id the 'disabled' field stays thin and an unusable coupon looks open.
$rows = Api::Orders()->GetCoupons([], ['client_id' => $cid])['data'];
$usable = array_filter($rows, fn ($c) => ! $c['disabled']);Checking the Coupons
Says whether the coupons picked hold for this basket.
curl -X POST 'https://panel.example.com/api/v1/admin/orders/validate-coupons' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"coupon_ids":[19],"client_id":94,"cart":{"subtotal":100,"user_currency":4}}'const res = await fetch('https://panel.example.com/api/v1/admin/orders/validate-coupons', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
coupon_ids: picked,
client_id: clientId,
cart: { items, subtotal, user_currency: currencyId, is_dealer: false },
}),
});
const { data } = await res.json();
data.invalid.forEach((c) => showReason(c.code, c.reason));$ch = curl_init('https://panel.example.com/api/v1/admin/orders/validate-coupons');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'coupon_ids' => $picked, 'client_id' => $cid, 'cart' => $cart,
]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// WITHOUT the basket the least-amount and product conditions go unchecked; a coupon passes here and fails at ordering.
$out = Api::Orders()->ValidateCoupons([
'coupon_ids' => $picked, 'client_id' => $cid, 'cart' => $cart,
])['data'];
$use = $out['kept'];Creating the Order
Builds the order from the basket lines and brings the services into being.
curl -X POST 'https://panel.example.com/api/v1/admin/orders' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"client_id":94,"generate_invoice":true,"products":[{"group":"hosting","product_id":12,"billing_cycle":"monthly","quantity":1}]}'const res = await fetch('https://panel.example.com/api/v1/admin/orders', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
client_id: clientId,
status: 'waiting',
generate_invoice: true,
invoice_status: 'unpaid',
products: [{
group: 'hosting',
product_id: 12,
billing_cycle: 'monthly',
quantity: 1,
requirements: answers,
}],
}),
});
const { data } = await res.json();$ch = curl_init('https://panel.example.com/api/v1/admin/orders');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'client_id' => $cid,
'generate_invoice' => true,
'products' => $lines,
]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// The state you send is NOT kept: it is worked out again from the services made, so read the answer.
$order = Api::Orders()->CreateOrder([
'client_id' => $cid, 'status' => 'waiting', 'products' => $lines,
])['data'];
$real = $order['status'];Pitfalls
Even with a state named in the body, the create works the state out again from the services born. An order sent as waiting can come back active. Read what happened from the state in the answer rather than assuming what you sent.
The reasons a coupon cannot be used fill only when a client id is sent. Without one only the basic state shows, and a coupon closed to that client looks open. Pass the basket to the checking endpoint as well for the basket conditions.
The checking endpoint reads the least amount, the products needed and the cycle match from the basket. Leaving it out leaves those three unchecked: the coupon looks good here and fails while the order is built. The basket you check and the basket you order with should be the same.
Whether a domain transfer wants a code is written in the extension's settings and comes back with the availability answer. Leaving the code out where it is wanted still builds the order and never starts the transfer. Read the extension information before preparing the line.
Which questions a product makes required is written under the rules in the product information. The create call may not catch a missing answer and the service reaches the provider short of data. Filter the required list while building the line.
The domain question goes live to the registrar and the answer is that moment's state. Between the question and the order someone else can take the name, and then the order builds while the registration fails. Ask again right before ordering on a basket that sat a while.
Related Articles
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.