Skip to content

Partner API

Embed 3D dishes and the AI Concierge

Two custom elements, one script tag each. The viewer renders your dish in 3D, with a one-tap AR button on phones. The concierge widget puts the grounded menu assistant on your page as a chat launcher. No build step, no framework, no data leaves your page except to TruePlate.

See it live

This is the stock viewer, rendered right here with a bundled sample dish. It is exactly what the snippet below produces before you touch anything, and it adapts to the device on its own: web visitors get an inline 3D view, phones go straight to AR.

On the web

Tap to load the interactive 3D view: orbit, zoom, inspect. The 3D runtime only downloads after the tap.

On phones

The same embed on a phone is AR-first: one tap opens the dish in the visitor's space, true to size, via AR Quick Look (iOS) or Scene Viewer (Android). Reading this on your phone? The "On the web" demo on this page already behaves this way.

The demo ships with the script: it needs no key, makes no API call, and never counts against anyone's embed quota. You can drop <trueplate-viewer demo> on your own page to try the look before creating a key.

Get a publishable key

One key serves both embeds. In your dashboard: Developers, then API keys, then New key, then Publishable. Add every website origin that embeds the viewer or the concierge to the key's allow-list:

Exact origin
https://www.example.com
Wildcard subdomain
https://*.example.com
Publishable keys are safe to ship in your page's HTML: they can only reach the embed endpoints, and only from the origins you list. They can't read your full menu. They do receive short-lived signed model URLs for the specific items you embed (that is what the viewer renders), so embedding an item makes its 3D model viewable to that page's visitors while the page is open.

Building on a staging site? Create the key in the Test environment instead: a tp_pk_test_… key behaves identically but is never billed, and the viewer shows a visible Test mode badge. See Test mode.

Install the viewer

Copy the default embed, swap in your item id and the publishable key you just created, and it renders the same viewer for your real dish:

Your page
<script src="https://www.trueplate.ai/embed/v1.js" async></script>

<trueplate-viewer
  item-id="ITEM_ID"
  publishable-key="tp_pk_live_YOUR_PUBLISHABLE_KEY">
</trueplate-viewer>
React / Vue: use publishable-key (or data-key). The bare key attribute is reserved by both frameworks and never reaches the DOM.

The default look is intentionally neutral so it sits well on any menu page. Everything beyond that is yours to change on your end:

Size and placement
It's a plain block element. Set a width/height (or grid/flex constraints) on <trueplate-viewer> and it fills the box; unstyled it renders a square.
Corners and framing
Wrap it in your own card, override border-radius on the element, add your shadow.
Skip the poster step
Add the autoload attribute and desktop visitors get the rotating 3D view immediately instead of a poster + button.

Attributes

Four attributes on <trueplate-viewer>, read once when the element connects, plus one on the script tag. Anything else is ignored.

AttributeValueWhat it does
item-idItem idRequired. The menu item to render. item is accepted as an alias.
publishable-keytp_pk_live_…Required. data-key and key are accepted as aliases, but key never reaches the DOM from React or Vue.
autoloadPresent or absentOpens straight into the interactive 3D view instead of a poster and a "View in 3D" button. Boolean: the value is not read.
demoPresent, or phoneRenders the bundled sample dish with no key, no API call and no quota usage. demo="phone" also forces the AR-first phone presentation on a desktop, which is what the second demo above is doing.
data-apiAPI base URLOn the <script> tag, not the element. Overrides the API origin the script derives from its own src. Only needed against a non-standard host.
There is no styling attribute. Size, corners and framing are CSS on your side, as Install describes: the element is a plain block and inherits whatever box you give it.

How it behaves

  • Shows a poster image first; loads the 3D runtime only when the visitor taps it.
  • Desktop: an orbit-controls 3D viewer. iOS: AR Quick Look. Android: Scene Viewer.
  • Inside an in-app browser (Instagram, Facebook…), where AR can't launch, it prompts the visitor to open in their real browser.
  • Refetches signed model URLs automatically when they expire.

Every load counts as one embed view against your plan's included quota. See the limits.

Add the concierge

The second embed is a chat surface: a floating launcher that opens the same grounded menu assistant guests get on a TruePlate menu. It answers only from the restaurant's published dishes and house notes, and its recommendation cards deep-link to the public TruePlate menu, AR included.

Closed

All a visitor sees until they ask something. On a real page it floats in the bottom-right corner, clear of your own layers, and it appears only once the config endpoint says the concierge is servable.

Open

A scripted sample thread, streamed word by word the way a real turn arrives. Ask it anything: the demo answers from its bundled script rather than a restaurant. The disclosure under the header is the live one.

Your page
<script src="https://www.trueplate.ai/embed/concierge.v1.js" async></script>

<trueplate-concierge
  restaurant-id="RESTAURANT_ID"
  publishable-key="tp_pk_live_YOUR_PUBLISHABLE_KEY">
</trueplate-concierge>

restaurant-id is the restaurant id, the one GET /partner/restaurants returns, not the menu slug. The key is the same publishable key the viewer uses, under the same origin allow-list, so a page that already embeds a dish needs no second key.

Nothing renders until the concierge is servable. The element boots from the config endpoint and shows its launcher only when that answers enabled: true. A restaurant whose subscription has lapsed, one whose owner has switched the assistant off, and one caught by the platform kill switch all render no button at all rather than a dead one. If nothing appears on your page, check that the restaurant's concierge is switched on before you check your markup.

A tp_pk_test_… key behaves identically, streams the deterministic sandbox provider instead of the model, spends no message allowance, and labels the panel with a TEST MODE badge. See Test mode.

React / Vue: the same caveat as the viewer. Pass the key as publishable-key or data-key. The bare key attribute is reserved by both frameworks and never reaches the DOM.

Concierge attributes

Three attributes on <trueplate-concierge>, read once when the element connects, plus the same script-tag override the viewer takes. The first two are required outside demo mode: with either missing, the element renders nothing at all. Anything else is ignored.

AttributeValueWhat it does
restaurant-idRestaurant idRequired. The restaurant whose menu the assistant answers from. data-restaurant is accepted as an alias.
publishable-keytp_pk_live_…Required. data-key and key are accepted as aliases, but key never reaches the DOM from React or Vue.
demoPresent, or launcherRenders the bundled sample thread with no key, no API call and nothing metered, docked into the page instead of pinned to the corner. demo="launcher" renders the closed launcher, which is what the two demos above are doing.
data-apiAPI base URLOn the <script> tag, not the element. Overrides the API origin the script derives from its own src. Only needed against a non-standard host.
There is no styling attribute. The panel renders in a shadow root, pinned to the bottom-right corner of the viewport, and follows the visitor's light or dark preference on its own.

What it calls

Two endpoints, both cross-origin from your page, both authenticated with the publishable key in an Authorization: Bearer header:

GET /partner/embed/restaurants/{restaurantId}/concierge
The bootstrap the launcher waits on: display name, welcome message, the AI disclosure string, the currency its cards price in, and the public menu URL they link to. Cached private, max-age=60. Every not-servable state answers enabled: false, never an error shape.
POST /partner/embed/restaurants/{restaurantId}/concierge/chat
One turn, streamed back as Server-Sent Events (meta, delta, cards, detail, done, error), the exact protocol TruePlate's own menu pages consume.

Publishable keys carry no scopes: the origin allow-list is their boundary. A secret key reaching the same two routes must hold concierge:chat, though a secret key has no business in a page.

Chat is capped at 20 turns a minute per key and visitor IP, far tighter than the viewer data reads above, because every turn is model-priced. A request over that line is refused before the stream opens, as an ordinary 429. Turns also meter into the restaurant's own concierge message allowance, one quota across the menu, the app and every embed; exhausting it ends the stream with an error event, which the widget renders as a short message in the thread.

These two are the widget's own wire protocol, and they are deliberately absent from the API reference: they are versioned with the script that calls them, and the publishable-key-plus-origin pairing they authenticate with is not the secret-key scheme that document describes. To drive the concierge from an interface of your own, call the server-side concierge chat endpoint with a secret key instead.

The AI disclosure stays visible

The widget pins the disclosure string from the meta event under its header, on every panel, with no way to dismiss it. Under EU AI Act Article 50 the obligation to tell a person they are talking to an AI follows the deployment surface, and on your page the deployment surface is yours. Keep it visible: do not clip, overlay or collapse the panel header, and do not reimplement the widget without carrying the same string. The concierge endpoint states the same requirement for integrations built directly on the API.

  • The panel renders inside a shadow root, so your stylesheet cannot reach the disclosure by accident, and the widget cannot repaint your page.
  • Assistant text is written as plain text nodes, never as HTML, so nothing the model returns can inject markup into your page.
  • Allergen answers are drawn from declared data only and always defer to the restaurant's staff. Do not present the assistant as a medical or dietary authority.