Project Sharing
Generate public share links for projects. Supports password protection and embed-domain restrictions.
Get Share Configuration
const response = await fetch(BASE_URL + '/projects/proj_123/share', {
headers: { 'Authorization': 'Bearer ' + accessToken }
});
const data = await response.json();
// { "success": true, "settings": {
// "isEnabled": true,
// "shareToken": "a1b2c3...", // build the URL yourself:
// // https://YOUR_APP_DOMAIN/share/{projectId}/{shareToken}
// "domainRestrictions": [], // allowed embed hosts; [] = any
// "requirePassword": false
// } }Enable Sharing
const response = await fetch(BASE_URL + '/projects/proj_123/share/enable', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + accessToken }
});Update Share Settings
// WARNING: this REPLACES the whole settings object.
// Any field you omit reverts to its default — omitting isEnabled sets it to
// false, which takes the live share link and every embed offline.
// Always send the full object. shareToken is preserved server-side; rotate it
// with POST /projects/:id/share/regenerate.
const response = await fetch(BASE_URL + '/projects/proj_123/share', {
method: 'PUT',
headers: {
'Authorization': 'Bearer ' + accessToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({
isEnabled: true,
requirePassword: true,
password: 'secret123',
domainRestrictions: ['shop.example.com']
})
});
// { "success": true, "settings": { ...the stored object... } }Regenerate Share Token
const response = await fetch(BASE_URL + '/projects/proj_123/share/regenerate', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + accessToken }
});Access Shared Project (Public)
const response = await fetch(BASE_URL + '/share/proj_123/abc123', {
headers: {
// Required when calling Supabase Edge Functions directly from browser clients
'apikey': SUPABASE_ANON_KEY,
'Authorization': 'Bearer ' + SUPABASE_ANON_KEY,
// Only when the share is password-protected:
'X-Share-Password': 'secret123'
}
});
const data = await response.json();
// Scene is already decompressed server-side, and every model carries a
// pre-signed download url. Full schema below.Submit Shared Form (Async Processing)
const response = await fetch(BASE_URL + '/share/proj_123/abc123/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'apikey': SUPABASE_ANON_KEY,
'Authorization': 'Bearer ' + SUPABASE_ANON_KEY
},
body: JSON.stringify({
formData: { name: 'Jane Smith', email: '[email protected]' },
selectedOptions: {},
configurationSummary: '...',
screenshots: []
})
});
const data = await response.json();
// Immediate acknowledgment:
// { "success": true, "requestId": "..." }
// Screenshots/PDF/emails/webhooks are processed asynchronously after response.Public share response schema
This is the endpoint to build a headless or native integration against. It is the only one that hands you a directly fetchable model file: every entry in models carries a pre-signed storage URL the browser or device downloads straight from storage, so the binary never proxies through the API. The scene is decompressed server-side, so — unlike GET /projects/:id — there are no large-project variants to feature-detect.
{
"success": true,
"project": {
"id": "proj_123",
"name": "Modern Office Chair",
"type": "configurator", // 'configurator' | 'viewer' | 'modular' | ...
"data": {
"models": [
{
"id": "mdl_7f2a", // opaque model id — the keyspace targetObjectId uses
"fileName": "Main_roof_3x3.glb",
"url": "https://...supabase.co/storage/v1/object/sign/...",
// PRE-SIGNED (1 year). Fetch the GLB directly.
"assetId": "ast_991", // present for asset-library models
"uploadedModelPath": "user_1/project-models/...",
// present for project-uploaded models
"transform": { }, // raw stored transform
"position": { "x": 0, "y": 0, "z": 0 }, // normalized to OBJECTS here
"rotation": { "x": 0, "y": 1.57, "z": 0 },
"scale": { "x": 1, "y": 1, "z": 1 },
"partTransforms": { },
"deletedParts": [],
"pivotData": { }
}
],
"sceneData": {
"optionBlocks": [ ],
"projectSettings": { }, // REDACTED — see the security note below
"formFields": [ ],
"animationBlocks": [ ],
"pricingBlocks": [ ],
"pricingFormula": [ ]
},
"camera": { // may be undefined — fall back to your own framing
"position": { "x": 0, "y": 2, "z": 5 },
"target": { "x": 0, "y": 0, "z": 0 }
},
"renderQuality": "optimised", // 'optimised' | 'low' | 'high'
"saveConfigEnabled": false // whether the Save Configuration flow is available
}
}
}Request headers
| Header | Value |
|---|---|
| apikey | SUPABASE_ANON_KEY — required. This is the public anon key, safe to ship in a browser bundle or app binary; it is not an API token. |
| Authorization | Bearer <SUPABASE_ANON_KEY> — required, the same key again. |
| X-Share-Password | Only when the share is password-protected (requirePassword: true). Omitting it returns an unauthorized response rather than the project. |
Caching, limits and failure states
- • ETag / 304. The response carries an
ETag. Send it back asIf-None-Matchand an unchanged project answers304 Not Modifiedwith no body — keep your last payload. This is the cheapest way to poll for updates. - • Rate limit: 120 requests per 5 minutes, per IP. Ample for real shoppers; easy to exceed from a server that re-fetches per render. Cache the payload and revalidate with
If-None-Match. - • Only the first 20 models are re-signed per request. A scene with more than 20 models will have entries beyond that limit without a fresh
url. Handle a missingurlrather than assuming every entry has one. - •
402when the owner’s billing is hard-locked — body{ "error": "unavailable", "reason": "owner_billing" }. Render a neutral “configurator unavailable” state and stop; never retry in a loop, and never surface the billing reason to a shopper.
projectSettings is redacted on this public surface
This endpoint is unauthenticated, so the owner’s credentials are stripped from projectSettings before it is returned — including smtpPassword and the other smtp* fields, emailAdminAddress, the webhook* keys (both webhookSecret and webhookUrl), and the woocommerce* / shopify* keys. Do not build against those fields here; they are only readable through an authenticated GET /projects/:id. Everything a viewer actually needs — branding, lighting, camera, AR, display and pricing-display settings — is present.
Async Submit Semantics
Shared-form submit endpoints persist the request first and respond immediately. Heavy side effects (screenshot upload, PDF generation, SMTP notifications, webhook dispatch) run in background tasks, so they are eventually consistent and may complete after the initial 200 response.
