Skip to content

REST API ​

Ambikly registers its routes on WordPress's own REST infrastructure. Everything the admin app does — listing products, saving an order, applying a coupon — goes through these routes, and so can your code.

This page covers how to authenticate, what a response looks like, and how to page through a collection. The routes themselves are on the Endpoint reference.

Base URLs ​

There are two namespaces.

NamespaceBase URLProvided by
ambikly/v1https://example.com/wp-json/ambikly/v1Free core
ambikly-pro/v1https://example.com/wp-json/ambikly-pro/v1Ambikly Pro, including add-on routes

The free core registers 117 register_rest_route() calls, which expand to 123 routes at runtime because two controllers register in a loop. Pro registers 94 calls, expanding to 99. That is over 200 REST routes in total across both namespaces.

If your site does not use pretty permalinks, the same routes are reachable as https://example.com/?rest_route=/ambikly/v1/products.

Authentication ​

Ambikly does not implement its own authentication. It relies entirely on WordPress's REST authentication. There is no custom auth layer, no API key mechanism, and no JWT support of any kind in either plugin.

That leaves two practical options.

A logged-in browser session authenticates with its WordPress cookie, which core only honors for REST when the request also carries a valid wp_rest nonce in the X-WP-Nonce header.

Ambikly creates that nonce for you and hands it to its own scripts: the admin app receives it as rest_nonce, and the storefront receives it as nonce, both from wp_create_nonce('wp_rest'). In your own enqueued script, create your own with wp_localize_script().

php
add_action('wp_enqueue_scripts', function () {
    wp_enqueue_script('acme-store', plugins_url('acme.js', __FILE__), [], '1.0', true);
    wp_localize_script('acme-store', 'AcmeStore', [
        'root'  => esc_url_raw(rest_url('ambikly/v1/')),
        'nonce' => wp_create_nonce('wp_rest'),
    ]);
});
js
const res = await fetch(`${AcmeStore.root}cart`, {
  method: 'GET',
  credentials: 'same-origin',
  headers: { 'X-WP-Nonce': AcmeStore.nonce },
});
const body = await res.json();
console.log(body.data.items);

credentials: 'same-origin' is required — without it the browser does not send the cookie and the request is treated as logged out.

Nonces expire

A wp_rest nonce is valid for roughly 24 hours. A long-lived page will start getting 403 rest_cookie_invalid_nonce after that. Refresh the page or fetch a new nonce rather than retrying with the old one.

Application Passwords — for server-to-server ​

For a script, a cron job or an external system, use WordPress Application Passwords over HTTP Basic auth. This is a WordPress core feature, available on any site running WordPress 5.6 or newer over HTTPS — not something Ambikly adds. Create one under Users → Profile → Application Passwords for a user who holds the capability the route requires.

bash
curl -u 'storemanager:abcd EFGH ijkl MNOP qrst UVWX' \
  'https://example.com/wp-json/ambikly/v1/orders?per_page=5&status=processing'
bash
curl -u 'storemanager:abcd EFGH ijkl MNOP qrst UVWX' \
  -X POST 'https://example.com/wp-json/ambikly/v1/products' \
  -H 'Content-Type: application/json' \
  -d '{"name":"Blue Mug","price":18.00,"sku":"MUG-BLUE","status":"published"}'

Application Passwords are per-user and revocable individually. Revoking one does not affect the user's real password or their other application passwords.

Treat an application password as a full credential

It carries every capability the user has, on every REST endpoint on the site, not just Ambikly's. Create a dedicated user with the narrowest role that works — usually Support Agent for read-only integrations, Store Manager for write ones — rather than reusing an administrator.

What is not supported ​

  • No bearer tokens, no OAuth, no JWT.
  • No per-application scopes. Access is whatever the authenticating user's capabilities allow.
  • No separate API rate limiting. Standard WordPress and host-level limits apply.

Capabilities and permission callbacks ​

Every Ambikly route declares a permission_callback. There are two Ambikly capabilities, held by two custom roles plus Administrator.

CapabilityGranted toMeans
ambikly_manage_storeAdministrator, Store ManagerFull read and write on store operations
ambikly_view_storeAdministrator, Store Manager, Support AgentRead-only store access

Routes resolve those through a small set of named callbacks. Both BaseController (the legacy controllers) and RestController (the newer ones) expose the same helpers.

CallbackPasses whenTypical routes
store_permission()ambikly_manage_store or manage_optionsEvery create, update and delete on products, orders, customers, coupons, categories, variations, attributes, tax rates, shipping, reviews; POST /jobs/{id}/retry
store_view_permission()ambikly_view_store, or anything store_permission() passesEvery admin list and detail read: products, orders, customers, coupons, categories, companies, reports, downloads for an order or customer, GET /jobs/status, GET /jobs/failed
admin_permission() / check_permissions()manage_options onlySettings, payment gateway config, webhooks, system status, import/export, email templates, the setup wizard steps
customer_permission()Any logged-in userGET /account/downloads, the /companies/me* self-service routes
public_permission() / __return_trueEveryone, including logged outCart, checkout, storefront reads, gateway callbacks, token-gated documents

admin_permission() and check_permissions() are identical in effect — current_user_can('manage_options'). The two names are an artifact of the two controller base classes.

Which capability a route needs

The Endpoint reference lists the permission callback for every single route, and flags the ones that require no authentication at all.

The response envelope ​

Every successful Ambikly response is a JSON object with a success key. It is not a bare resource, and it is not core's standard REST shape.

Success ​

Controllers extending BaseController return success, message and data:

json
{
  "success": true,
  "message": "Operation completed successfully",
  "data": {
    "id": 42,
    "name": "Blue Mug",
    "sku": "MUG-BLUE",
    "price": "18.00",
    "status": "published"
  }
}

Controllers extending RestController return success and data, with no message:

json
{
  "success": true,
  "data": {
    "id": 1044,
    "order_number": "AMB-1044",
    "status": "processing",
    "total": "63.50",
    "currency": "USD"
  }
}

A response with nothing to return omits data entirely — a successful DELETE /webhooks/12 is just {"success":true}.

success_response() always returns HTTP 200

BaseController::success_response() accepts a $status argument but never applies it. Every response it produces is HTTP 200, whatever status the calling code asked for. Do not treat a 201 as meaning "created" — you will not see one. Branch on the body's success key and on the presence of an error code, not on the numeric status of a successful call.

Errors ​

Errors are WP_Error objects, serialized by core into its standard error shape. There is no success: false envelope — an error body has no success key at all.

json
{
  "code": "missing_fields",
  "message": "Missing required fields: name, price",
  "data": {
    "status": 400,
    "data": { "missing_fields": ["name", "price"] }
  }
}

The HTTP status is real on errors — data.status matches it. The nested data.data is present only when the controller attached extra context.

Handling both shapes ​

js
async function ambikly(path, options = {}) {
  const res = await fetch(`${AcmeStore.root}${path}`, {
    credentials: 'same-origin',
    headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': AcmeStore.nonce },
    ...options,
  });
  const body = await res.json();
  if (body.code && body.message) {
    throw new Error(`${body.code}: ${body.message}`);
  }
  return body;
}

Pagination ​

Pagination is body-only. Ambikly never sends X-WP-Total or X-WP-TotalPages headers. If you are porting code from another WordPress REST client that reads those headers, it will see nothing.

A paginated list response puts the counts alongside data:

json
{
  "success": true,
  "total": 214,
  "page": 3,
  "per_page": 25,
  "total_pages": 9,
  "data": [
    { "id": 1044, "order_number": "AMB-1044", "status": "processing" },
    { "id": 1043, "order_number": "AMB-1043", "status": "completed" }
  ]
}
FieldMeaning
totalTotal rows matching the filters, across all pages
pageThe page you asked for (1-based)
per_pageRows per page actually used
total_pagesceil(total / per_page)

To walk a whole collection, request page 1 and keep going while page < total_pages.

bash
curl -u 'user:app password' \
  'https://example.com/wp-json/ambikly/v1/products?page=1&per_page=100' \
  | python3 -c 'import json,sys; b=json.load(sys.stdin); print(b["page"], "of", b["total_pages"], "-", b["total"], "products")'

Per-page defaults ​

Defaults differ by controller, and only some of them clamp the maximum.

RouteDefault per_pageMaximum
GET /products50not clamped
GET /categories100not clamped
GET /orders25set by the repository
GET /customers25set by the repository
GET /coupons25set by the repository
GET /companies25set by the repository
GET /reviews25100
GET /customers/{id}/orders100100

Routes not listed here return a complete list with no pagination at all — GET /webhooks, GET /tax/rates, GET /shipping/zones, GET /tags, GET /brands and the reports endpoints all return every row.

GET /webhooks/{id}/deliveries and GET /jobs/failed each return a fixed, unpaginated most-recent 200 rows.

Common query parameters ​

Collection endpoints share a common vocabulary.

ParameterTypeApplies toNotes
pageintAll paginated lists1-based, defaults to 1
per_pageintAll paginated listsSee the table above
searchstringProducts, categories, orders, customers, coupons, companies, reviewsSubstring match
statusstringProducts, categories, orders, coupons, reviews, exportsExact match
orderbystringProducts, categories, orders, customers, couponsAllow-listed per controller; an unknown value falls back to the default
orderASC|DESCSame as orderbyAnything else falls back to the default

Controller-specific filters:

ParameterRouteNotes
category_idGET /productsPass all to skip the filter
typeGET /products, GET /export/productsProduct type
parent_idGET /categoriesRestrict to one parent
payment_statusGET /ordersIndependent of status
customer_idGET /ordersOrders for one customer
date_from, date_toGET /ordersYYYY-MM-DD
product_idGET /reviewsReviews for one product
idsGET /export/productsComma-separated id list
formatGET /orders/{id}/invoicehtml (default) or pdf
tokenGET /orders/{id}/invoice, GET /orders/{id}/packing-slipGrants access without being logged in

GET /products accepts orderby values id, name, sku, price, stock_quantity, status, type, created_at, updated_at — default created_at DESC.

GET /categories accepts id, name, slug, parent_id, sort_order, status, created_at, updated_at and the computed products count — default sort_order ASC.

Error codes ​

code values are stable strings. The most common ones:

CodeStatusMeaning
missing_fields400One or more required fields absent or empty; data.data.missing_fields lists them
invalid_sale_price400A sale price was not lower than the regular price
duplicate_sku400The SKU is already used by another product or variation
invalid_url400A webhook URL was not a valid http/https URL
not_found404No record with that id
order_not_found404No order with that id
database_error500The underlying $wpdb write failed
license_required403Enabling a Pro add-on without a valid license

Core adds its own codes on top: rest_no_route (404), rest_forbidden (401 or 403), rest_cookie_invalid_nonce (403), rest_invalid_param (400).

Reading the order status vocabulary ​

Do not hard-code status strings. GET /orders/statuses returns the store's live vocabulary for both order status and payment status:

bash
curl -u 'user:app password' \
  'https://example.com/wp-json/ambikly/v1/orders/statuses'
json
{
  "success": true,
  "data": {
    "order_statuses": { "pending": "Pending", "processing": "Processing", "completed": "Completed" },
    "payment_statuses": { "pending": "Pending", "paid": "Paid", "refunded": "Refunded" }
  }
}

Next ​

The Endpoint reference lists every route in both namespaces, grouped by resource, with the permission each one requires.