Introduction
The AtlasAR Cloud API turns 3D files and product photos into light, true-size models for the web and AR (Android Scene Viewer, iOS Quick Look, WebXR), hosts them on a CDN, and tells your server when they are ready. It is a REST API: resources have predictable URLs, requests and responses are JSON (model files are sent as their own bytes), and errors use standard HTTP status codes with a stable machine-readable code.
| Base URL | https://cloud.wpaugmentedreality.com |
| Version | v1 in the path. Fields may be added to responses at any time; nothing is removed or renamed within v1. |
| Formats | JSON (UTF-8). Uploads: model/gltf-binary for GLB, image/jpeg|png|webp for photos. |
| Time | ISO 8601, UTC. |
Quick start
Optimise a GLB and get a hosted, true-size model in two calls: upload it, then read it back (or wait for the webhook).
# 1. Upload a GLB: made exactly 30 cm tall, optimised at the default level
curl -X POST "https://cloud.wpaugmentedreality.com/v1/models/upload?name=Vase&height_cm=30" \
-H "Authorization: Bearer $ATLASAR_KEY" \
-H "Content-Type: model/gltf-binary" \
--data-binary @vase.glb
# 2. Read it until "status" is "ready" (or use a webhook)
curl "https://cloud.wpaugmentedreality.com/v1/models/mdl_9yX2…" -H "Authorization: Bearer $ATLASAR_KEY"
// Node.js 18+ (fetch is built in)
import { readFile } from 'node:fs/promises';
const res = await fetch('https://cloud.wpaugmentedreality.com/v1/models/upload?name=Vase&height_cm=30', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.ATLASAR_KEY}`, 'Content-Type': 'model/gltf-binary' },
body: await readFile('vase.glb'),
});
const model = await res.json(); // 202: { id: "mdl_…", status: "queued", … }
$res = wp_remote_post( 'https://cloud.wpaugmentedreality.com/v1/models/upload?name=Vase&height_cm=30', array(
'headers' => array( 'Authorization' => 'Bearer ' . ATLASAR_KEY, 'Content-Type' => 'model/gltf-binary' ),
'body' => file_get_contents( 'vase.glb' ),
'timeout' => 60,
) );
$model = json_decode( wp_remote_retrieve_body( $res ), true ); // 202: [ 'id' => 'mdl_…', 'status' => 'queued' ]
Authentication
Every /v1 request carries the site's API key as a bearer token. Keys look like ar_live_… (32 random characters) and are stored only as a SHA-256 hash: they are shown once, when created.
Authorization: Bearer ar_live_Xq3…
- One key per site. A key sees only its own site's models, webhook and statistics.
- Server side only. Never put a key in browser code: the viewer SDK needs none.
- Active or pending. A new email gets an active key at once. A new site for an existing account gets a pending key: every call answers
403 approval_requireduntil the account owner approves the emailed link. - WordPress sites connect from AtlasAR › Integrations in the plugin, which registers the site and stores the key encrypted; no key needs to be copied.
Errors
Successful requests answer 2xx. Errors answer 4xx (your request) or 5xx (ours), always with the same body, so code can branch on error.code. The full list is under Error codes.
{
"error": {
"code": "invalid_height",
"message": "height_cm must be a number from 0.5 to 1000 (the real product height), or left out to keep the size of the model."
}
}
Idempotency
Send an Idempotency-Key header (8–80 characters: letters, digits, _, -) with model-creating requests. A retry with the same key never creates or bills a second model: it answers 200 with the model of the first request.
Idempotency-Key: order-1042-vase
Plans, limits and billing
Usage is counted in units: optimising a GLB costs 1 unit, photo to 3D costs 10. Models are billed on success: a request is accepted at 0 units (202) and billed only when its model is ready. A model that fails, or that you delete before it is ready, is never billed.
| Plan | Optimisations a month | Hosted models | Features |
|---|---|---|---|
| Free | 20 | 10 | optimize, webhooks |
| Premium (an AtlasAR Pro licence seat on the site) | No monthly limit | No limit | optimize, photo_to_3d, webhooks |
- At most 5 models per account are processed at a time (
429 too_many_in_progress). - The monthly allowance is checked when a model is accepted, so the last accepted model may finish just past the limit.
- Busy servers answer
503 server_busywithRetry-After; such requests are never billed.
Sites and usage
Register a site
POST/v1/projects/registerNo key
Creates a site (project) and its API key. With a new email, the account is created at once on the Free plan and the key is active. With the email of an existing account, the key is pending until the owner approves the site.
Body
| Name | Description |
|---|---|
platformrequiredstring | wordpress or custom. |
site_urlrequiredstring | The site's public address, e.g. https://shop.example. |
emailrequiredstring | The account email. Approval links and receipts go there. |
curl -X POST "https://cloud.wpaugmentedreality.com/v1/projects/register" \
-H "Content-Type: application/json" \
-d '{ "platform": "custom", "site_url": "https://shop.example", "email": "owner@shop.example" }'
{
"api_key": "ar_live_Xq3…",
"key_prefix": "ar_live_Xq3",
"project_id": 41,
"plan": "free",
"approval": "active"
}
A WordPress site already registered with another email answers 409 project_exists with a masked owner_hint. The plugin can then move it with a one-time proof read from the site itself (POST /v1/projects/takeover and /takeover/verify; public sites only).
Read usage
GET/v1/usageSite key
The site's plan and this month's units.
curl "https://cloud.wpaugmentedreality.com/v1/usage" -H "Authorization: Bearer $ATLASAR_KEY"
{
"usage": {
"plan": "free",
"period": "2026-10",
"units_used": 7,
"units_limit": 20,
"units_remaining": 13,
"resets_at": "2026-11-01T00:00:00Z"
}
}
Models
The model object
| Field | Description |
|---|---|
idstring | Starts with mdl_. |
namestring | Your name for it (1–200 characters). |
statusstring | queued, processing, ready or failed. |
height_cmnumber | null | The real height it was made at; null = its own size. |
sourcestring | glb or photo: what it was made from. |
files.glbstring | null | When ready: the URL of the model file. Public, immutable, CORS *, served by the CDN. |
statsobject | When ready: triangles, glb_mb, texture_px, texture_count, level. |
budgetstring | Page-weight grade: pass (≤ 3 MB), warn (≤ 5 MB) or fail. |
viewerobject | null | Settings the viewer SDK applies (see Viewer settings). |
errorobject | null | When failed: { code }, e.g. too_complex. |
created_atstring | ISO 8601. |
expires_atstring | null | Only for temporary models (made on the Create page of the dashboard); API models never expire. |
Optimise a GLB
POST/v1/models/uploadSite key
Optimises and hosts a GLB: real-world scale (optional), base on the floor, simplified to the level's triangle budget, textures resized to WebP, meshopt compression. Answers at once with the queued model; the work runs in the background.
Query parameters
| Name | Description |
|---|---|
namerequiredstring | A name for the model (1–200 characters). |
height_cmoptionalnumber | The product's real height, 0.5–1000. The model is scaled to it with its base on the floor. Left out, the model keeps its own size. |
leveloptionalstring | How far to optimise: maximum, high (default), balanced, low or minimum. See Optimisation levels. |
Body
The GLB file (≤ 50 MB) with Content-Type: model/gltf-binary. Larger files: use a direct upload.
curl -X POST "https://cloud.wpaugmentedreality.com/v1/models/upload?name=Vase&height_cm=30&level=balanced" \
-H "Authorization: Bearer $ATLASAR_KEY" \
-H "Content-Type: model/gltf-binary" \
-H "Idempotency-Key: vase-2026-10" \
--data-binary @vase.glb
{
"id": "mdl_9yX2kQ4LrT8vN1pA6sZ3bW",
"name": "Vase",
"source": "glb",
"status": "queued",
"height_cm": 30,
"files": null,
"units": 0,
"usage": { "plan": "free", "units_used": 7, "units_limit": 20, … }
}
Photo to 3D
PremiumPOST/v1/modelsSite key
Makes a 3D model from one product photo with AI, then optimises and hosts it like a GLB. A photo model costs 10 units when it is ready.
Query parameters
| Name | Description |
|---|---|
namerequiredstring | A name for the model. |
height_cmrequirednumber | The product's real height, 0.5–1000 (an AI model has no real size of its own). |
Body
A JPEG, PNG or WebP photo (≤ 25 MB) with its Content-Type. A plain background and the whole product in view give the best result.
curl -X POST "https://cloud.wpaugmentedreality.com/v1/models?name=Mug&height_cm=10" \
-H "Authorization: Bearer $ATLASAR_KEY" \
-H "Content-Type: image/jpeg" \
--data-binary @mug.jpg
List models
GET/v1/modelsSite key
This site's models, newest first.
Query parameters
| Name | Description |
|---|---|
limitoptionalinteger | 1–100 (default 50). |
beforeoptionalstring | A model id: models older than it (from next_before). |
{
"data": [ { "id": "mdl_9yX2…", "name": "Vase", "status": "ready", … } ],
"next_before": "mdl_7aB1…"
}
Retrieve a model
GET/v1/models/{id}Site key
One model. Poll it until status is ready or failed, or use a webhook instead.
{
"id": "mdl_9yX2kQ4LrT8vN1pA6sZ3bW",
"name": "Vase",
"status": "ready",
"height_cm": 30,
"source": "glb",
"files": { "glb": "https://cloud.wpaugmentedreality.com/files/p/41/mdl_9yX2kQ4LrT8vN1pA6sZ3bW/3f9c8a1d2b7e4f60.glb" },
"stats": { "triangles": 29871, "glb_mb": 0.77, "texture_px": 1024, "texture_count": 2, "level": "balanced" },
"budget": "pass",
"viewer": null,
"error": null,
"created_at": "2026-10-18T09:12:44Z",
"expires_at": null
}
The file you sent is deleted once the model is ready or has failed for good; the finished model stays until you delete it.
Save viewer settings
PUT/v1/models/{id}/viewerSite key
Settings the viewer SDK applies wherever the model is shown with <atlasar-viewer model="…">: change the title, background, AR placement or try-on once, and every site showing the model updates within a minute. Keys are the SDK's option names; null clears them.
{
"viewer": {
"title": "Oak chair",
"background": "#f5f5f4",
"arPlacement": "floor",
"hotspots": [ { "position": "0 0.4 0.2", "normal": "0 1 0", "label": "Solid oak seat" } ]
}
}
Unknown keys or invalid values: 400 invalid_viewer (64 KB at most). Premium settings (hotspots, dimensions, items, variants, tryonCalibration) are saved on any plan and shown while the site is Premium.
Delete a model
DELETE/v1/models/{id}Site key
Deletes the model and its files; its public URL answers 404 at once. A model deleted before it is ready is not billed.
{ "ok": true }
Optimisation levels
The level of an upload sets how far a model is simplified. high is the default and suits most product pages. The browser optimiser in the SDK uses the same table, so a model looks the same wherever it was made. A quality from 0–100 is also accepted (90+ maximum, 75+ high, 60+ balanced, 40+ low, below that minimum).
| Level | Triangles | Largest texture | Image quality | Use it for |
|---|---|---|---|---|
maximum | 150,000 | 4096 px | 95 | Close-up detail: jewellery, watches |
high default | 50,000 | 2048 px | 85 | Most products |
balanced | 30,000 | 1024 px | 80 | Large catalogues, faster pages |
low | 15,000 | 1024 px | 70 | Slow connections |
minimum | 8,000 | 512 px | 60 | Simple shapes, the lightest files |
<model-viewer>, set self.ModelViewerElement = { meshoptDecoderLocation: '/js/meshopt_decoder.js' } before it loads; with three.js, GLTFLoader.setMeshoptDecoder(MeshoptDecoder).Direct uploads
Large files go straight from your server or the browser to storage, never through the API. Three steps:
- Ask for an upload URL with the exact size and type.
- PUT the file to
upload_urlwith exactly theheadersgiven (type and size are signed). - Create the model with
POST /v1/models/upload(or/v1/models) and a JSON body{ "upload_id", "name", "height_cm" }. An upload is used once, by the site that asked for it, within 15 minutes.
Create an upload
POST/v1/uploadsSite key
Body
| Name | Description |
|---|---|
kindrequiredstring | glb or photo. |
sizerequiredinteger | The exact size in bytes. |
content_typerequiredstring | e.g. model/gltf-binary. |
{
"id": "upl_4Hq…",
"upload_url": "https://cloud.wpaugmentedreality.com/…",
"method": "PUT",
"headers": { "Content-Type": "model/gltf-binary", "Content-Length": "48211330" },
"expires_at": "2026-10-18T09:27:44Z"
}
20 open uploads per account at most.
Free 3D model library
Ready-made, optimised models published by AtlasAR (made by us, or CC0 / CC BY). Any site key may use them, on any plan, at no cost: nothing is metered.
Search the library
GET/v1/librarySite key
Published models, newest first.
Query parameters
| Name | Description |
|---|---|
qoptionalstring | Words that must all be in the name, tags or description. |
categoryoptionalstring | A category key from categories. |
pageoptionalinteger | Default 1. |
per_pageoptionalinteger | 1–60 (default 24). |
{
"data": [ {
"id": "lib_2Kd…", "slug": "oak-dining-chair", "name": "Oak dining chair",
"category": "furniture", "tags": ["chair", "wood"], "height_cm": 82,
"size": 1048576, "triangles": 31204, "license": "CC0-1.0",
"credit": "", "poster_url": "https://cloud.wpaugmentedreality.com/library/posters/lib_2Kd….webp"
} ],
"total": 118,
"categories": { "furniture": "Furniture", "decor": "Decor", "…": "…" }
}
Download a model
GET/v1/library/{id}/fileSite key
The GLB itself (model/gltf-binary, as an attachment). GET /v1/library/{id} returns one model's details. For CC BY models, show the credit.
200 downloads per site a day; then 429 rate_limited.
Viewer analytics
How shoppers use your models' 3D views on every website that shows them. The viewer SDK counts by itself for <atlasar-viewer model="mdl_…"> and sends only totals (no cookies, no IP address, no visitor or page data). A page can turn it off with analytics="none".
Events: view, turn, open, ar, tryon, tryon_sample, snapshot, and with Premium modules hotspot, other_model, variant. Daily totals are kept 400 days.
Read analytics
GET/v1/statsSite key
Totals for this site's models: by event, by day, and per model (most 3D views first).
Query parameters
| Name | Description |
|---|---|
daysoptionalinteger | 7, 30 (default), 90 or 365. |
{
"days": 30,
"totals": { "view": 1840, "turn": 612, "ar": 233, "tryon": 41 },
"daily": { "2026-10-17": { "view": 71, "ar": 9 } },
"models": [ { "id": "mdl_9yX2…", "name": "Vase", "counts": { "view": 902, "ar": 120 } } ]
}
The SDK sends its counts to POST /sdk/v1/stats (public, CORS *); you never call it yourself.
Webhooks
AtlasAR Cloud calls your HTTPS endpoint when a model is ready or failed, so you never need to poll. One endpoint per site. Events: model.ready, model.failed (data.model is the model object) and ping.
Set the endpoint
PUT/v1/webhookSite key
Creates or changes the endpoint. The signing secret is shown once, when the endpoint is created.
Body
| Name | Description |
|---|---|
urlrequiredstring | An https URL that resolves to public addresses only (checked when saved and at every send). |
{ "url": "https://shop.example/atlasar-webhook", "secret": "whsec_…" }
| Also | Description |
|---|---|
GET /v1/webhook | The endpoint, the secret's prefix, when it was created. |
POST /v1/webhook/rotate-secret | A new secret; the old one stops at once. |
POST /v1/webhook/test | Sends a ping (10 an hour). |
GET /v1/webhook/deliveries | Recent deliveries: type, status, attempts, last HTTP status. |
DELETE /v1/webhook | Removes the endpoint and cancels pending deliveries. |
Verify a delivery
Each delivery carries AtlasAR-Event-Id, AtlasAR-Event-Type and AtlasAR-Signature: t=<unix time>,v1=<hex>, where v1 is HMAC-SHA256 of "<t>.<raw body>" with your secret. Check it on the raw body, refuse deliveries older than 5 minutes, and answer 2xx within 10 seconds. Delivery is at-least-once: drop repeated event ids. Failed deliveries are retried after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h; redirects are never followed.
$body = file_get_contents( 'php://input' );
parse_str( str_replace( ',', '&', $_SERVER['HTTP_ATLASAR_SIGNATURE'] ?? '' ), $sig );
$fresh = isset( $sig['t'] ) && abs( time() - (int) $sig['t'] ) <= 300;
$valid = isset( $sig['v1'] ) && hash_equals( hash_hmac( 'sha256', $sig['t'] . '.' . $body, $secret ), $sig['v1'] );
if ( ! $fresh || ! $valid ) {
http_response_code( 401 );
exit;
}
$event = json_decode( $body, true ); // [ 'type' => 'model.ready', 'data' => [ 'model' => … ] ]
import crypto from 'node:crypto';
// Express: app.post('/atlasar-webhook', express.raw({ type: 'application/json' }), handler)
function handler(req, res) {
const sig = Object.fromEntries(String(req.get('AtlasAR-Signature') || '').split(',').map((p) => p.split('=')));
const expected = crypto.createHmac('sha256', SECRET).update(`${sig.t}.${req.body}`).digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(sig.t)) <= 300;
const valid = sig.v1 && expected.length === sig.v1.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig.v1));
if (!fresh || !valid) return res.sendStatus(401);
const event = JSON.parse(req.body); // { type: 'model.ready', data: { model } }
res.sendStatus(204);
}
Error codes
| Code | Status | Meaning |
|---|---|---|
missing_api_keyinvalid_api_key | 401 | No key, or an unknown or revoked key. |
approval_required | 403 | The site waits for the account owner's approval. |
suspended | 403 | The site or account is suspended. |
feature_not_in_plan | 403 | e.g. photo to 3D on the Free plan. |
model_limit | 403 | The plan's hosted-model limit is reached. |
not_found | 404 | No such model for this key. |
upload_not_foundupload_incompleteupload_size_mismatch | 404 / 409 | Direct-upload problems. |
project_exists | 409 | The site is registered with another email (see Register a site). |
file_too_large | 413 | Over 50 MB (GLB) or 25 MB (photo). |
unsupported_media_type | 415 | Wrong Content-Type. |
invalid_nameinvalid_heightinvalid_levelinvalid_idempotency_keyinvalid_days | 422 | A bad parameter; the message says which values are allowed. |
not_glbinvalid_glbinvalid_gltfexternal_referenceout_of_boundstoo_complexno_geometryunsupported_* | 422 | The GLB was refused before processing (for a direct upload the model becomes failed with that code). |
invalid_viewer | 400 | Unknown viewer setting or invalid value. |
quota_exceeded | 429 | This month's units are used up (a usage block is included). |
rate_limited | 429 | Too many requests; wait for Retry-After. |
too_many_in_progress | 429 | 5 models are already processing for this account. |
server_busy | 503 | At capacity; retry after Retry-After. Never billed. |
generation_unavailable | 503 | Photo to 3D is not available right now. |