Placing an Order

7 views Markdown

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

get/api/v1/admin/orders/product-info
Orders/GetProductInfo admin

Returns a product's prices, add-ons, meters and questions.

Query 3
product_idintreqThe product id.
client_idintThe client id. The live service counts are worked out for this client.
langstringThe language the names come back in.
Response fields data — 9
productobjectThe product itself.
idintThe product id.
typestringThe product type.
titlestringThe product name.
modulestringThe provider module behind it.
stockstringThe stock left. Empty when there is no limit.
additional_taxobjectThe additional tax definition.
taxexemptintWhether it is tax exempt.
optionsobjectThe product and module options. What is inside changes with the module and the type.
categoryintThe category id.
groupstringThe group key.
pricesarrayThe prices per period.
idintThe price record id.
cidintThe currency number.
periodstringThe period unit.
timeintThe period multiplier. 3 with a month unit means three months.
cyclestringThe cycle name worked out from the two above.
amountstringThe selling price. Text with four decimals.
setupstringThe set-up fee.
coststringThe cost.
cost_cidintThe cost currency number.
promotionstringThe promotional price.
promotion_statusintWhether the promotion is on.
statusintWhether the price is on.
discountstringThe discount value.
rankintIts order.
addonsarrayThe add-ons tied to the product.
idintThe add-on id.
namestringIts name.
descriptionstringIts description.
field_typestringThe input type: a list, a quantity, a box or a button.
statusstringWhether it is on.
categoryintThe category id.
mcategorystringThe module category.
rankintIts order.
taxexemptintWhether it is tax exempt.
override_usrcurrencyintWhether it overrides the client currency.
product_id_linkintThe product id it links to.
product_type_linkstringThe product type it links to.
icon_typestringThe icon type.
iconstringThe icon value.
list_templateintThe listing template number.
propertiesobjectThe rules for the input type: visibility, buying more than one, whether it is required, and the quantity limits.
optionsarrayThe choices and their prices. A price comes either in plain fields or per currency.
requirementsarrayThe questions belonging to the add-on. The same shape as the questions below.
addon_active_servicesintHow many live records the client has of this add-on.
metricsarrayThe meters charged by use.
idintThe meter id.
owner_idintThe product it belongs to.
typestringThe meter key.
schemestringHow it is charged.
labelstringThe name shown.
unitstringIts unit.
max_valueintIts ceiling.
includedstringHow much the package includes.
sort_orderintIts order.
pricingobjectThe tiered prices. Every tier carries a start and an end, and the prices sit under the currency CODE.
requirementsarrayThe fields asked of the client at order time.
idintThe question id.
namestringThe question name.
descriptionstringIts description.
field_typestringThe input type.
statusstringWhether it is on.
categoryintThe category id.
mcategorystringThe module category.
rankintIts order.
module_co_namesobjectThe field name it maps to per module.
propertiesobjectThe rules. Whether it is required is written here.
optionsarrayThe choices on a field that has them.
product_active_servicesintHow many live services the client has of this product.
category_active_servicesintHow many live services in this category.
group_active_servicesintHow many live services in this group.
total_active_servicesintHow many live services the client has in all.
Errors 3
invalid_product422No product id was given.
not_found404No such product.
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/admin/orders/check-domain
Orders/CheckDomain admin it asks the registrar

Returns whether a domain can be taken and what the extension costs.

Body 2
domainstringreqThe full domain name.
client_idintThe client id. It is needed for the live service counts.
Response fields data — 9
domainstringThe domain asked about.
sldstringThe body of the name.
tldstringIts extension.
availableboolWhether it can be registered. The registrar is asked live.
tld_infoobjectThe extension's settings.
idintThe extension id.
namestringThe extension.
min_yearsintThe shortest term.
max_yearsintThe longest term.
dns_manageintWhether name management is offered.
forwardingintWhether forwarding is offered.
whois_privacyintWhether registration privacy is offered.
epp_codeintWhether a transfer needs a code.
modulestringThe registrar module behind it.
pricesobjectThe register, transfer and renewal prices. Each is a list of price rows, one per term.
idintThe price record id.
cidintThe currency number.
periodstringThe period unit.
timeintThe period multiplier. 3 with a month unit means three months.
amountstringThe selling price. Text with four decimals.
setupstringThe set-up fee.
coststringThe cost.
cost_cidintThe cost currency number.
promotionstringThe promotional price.
promotion_statusintWhether the promotion is on.
statusintWhether the price is on.
discountstringThe discount value.
rankintIts order.
addonsobjectThe extension's add-on prices. The price rows come in the shape above.
domain_active_servicesintHow many live services the client has on this extension.
total_active_servicesintHow many live services the client has in all.
Errors 3
invalid_domain422The domain is missing or not valid.
tld_not_found422The extension is not defined on the installation.
insufficient_scope403The key lacks the required scope.
Request
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

get/api/v1/admin/orders/coupons
Orders/GetCoupons admin

Returns the coupons an order can take, along with their rules.

Query 2
searchstringSearches the coupon code.
client_idintThe client id. The reasons a coupon is closed fill only with this.
Response fields data[] — 22
idintThe coupon id.
codestringThe coupon code.
typestringThe discount kind: a rate or a fixed amount.
ratefloatThe discount percentage.
amountfloatThe fixed discount amount.
currency_idintThe currency number of the fixed amount.
pservicesstringThe scope string. Empty means it applies to every service.
min_amountfloatThe least the basket must hold.
min_amount_currencyintThe currency number of that least amount.
recurringintWhether it holds on renewals too.
recurring_numintFor how many renewals.
taxfreeintWhether it applies before tax.
use_mergeintWhether it joins another coupon.
auto_applyintWhether it applies by itself.
maxusesintThe use limit. Zero means no limit.
usesintHow often it was used.
validity_cyclesstring[]The billing cycles it holds for.
required_productsstringThe products the basket must hold.
required_product_cyclesstring[]The cycles of those products.
statusstringWhether the coupon is on.
disabledboolWhether it cannot be used in this context.
disabled_reasonsstring[]The reasons it cannot be used. Expired, over the limit, under the least amount and the like.
notesstringThe coupon note.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/admin/orders/validate-coupons
Orders/ValidateCoupons admin

Says whether the coupons picked hold for this basket.

Body 3
coupon_idsint[]reqThe coupon ids to try.
client_idintThe client id.
cartobjectThe basket context. The least-amount and product conditions are checked from here.
itemsarrayThe basket lines. Each carries a kind, group, product, category, extension, cycle and amount.
subtotalfloatThe basket subtotal.
user_currencyintThe client's currency number.
is_dealerboolWhether the client is a dealer.
Response fields data — 3
keptint[]The coupon ids found good.
invalidarrayThe ones turned down. Each carries an id, a code and a reason.
auto_apply_candidatesarrayThe ones that could apply by themselves.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/admin/orders
Orders/CreateOrder admin it makes services

Builds the order from the basket lines and brings the services into being.

Body 11
client_idintreqThe client the order belongs to.
productsarrayreqThe basket lines. A product line and a domain line carry different fields.
groupstringThe line's group. On a domain line this value marks it as one.
product_idintThe product id.
billing_cyclestringThe billing cycle.
quantityintHow many.
price_overridefloatSetting the price by hand.
currencyintThe currency number.
addonsobjectThe add-ons picked.
addons_qtyobjectThe add-on quantities.
metricsobjectThe meters switched on.
requirementsobjectThe answers to the questions.
requirement_filesobjectThe content for questions wanting a file. It goes as encoded content or as an address.
domain_namestringThe domain name.
domain_actionstringA registration or a transfer.
domain_periodintFor how many years.
domain_addonsstring[]The domain add-ons.
epp_codestringThe transfer code.
statusstringThe state to start in. It is worked out again from the services made.
payment_methodstringThe payment module.
promo_codesint[]The coupon ids to apply.
tax_exemptionboolWhether it is tax exempt.
billing_profile_idintThe billing address profile. The tax rate comes from here.
generate_invoiceboolWhether an invoice is raised too.
invoice_statusstringThe invoice state.
send_notificationboolWhether the client is told.
notesstringThe order note.
Response fields 201 — data
dataobjectThe order made. Same shape as the read endpoint in the order records article.
Errors 6
invalid_client422No client id was given.
no_products422The basket holds no valid line.
not_found404No such client.
file_upload_failed422A question's file could not be read.
create_failed500The order could not be built.
insufficient_scope403The key lacks the required scope.
Request
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

The state you send is not kept

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 coupon list comes thin without a client

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.

Checking without a basket checks less

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.

A transfer code is not wanted on every extension

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.

The required questions show in the product information alone

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 availability answer is a snapshot

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.

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.