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.
<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
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:
<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>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-radiuson the element, add your shadow. - Skip the poster step
- Add the
autoloadattribute 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.
| Attribute | Value | What it does |
|---|---|---|
item-id | Item id | Required. The menu item to render. item is accepted as an alias. |
publishable-key | tp_pk_live_… | Required. data-key and key are accepted as aliases, but key never reaches the DOM from React or Vue. |
autoload | Present or absent | Opens straight into the interactive 3D view instead of a poster and a "View in 3D" button. Boolean: the value is not read. |
demo | Present, or phone | Renders 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-api | API base URL | On the <script> tag, not the element. Overrides the API origin the script derives from its own src. Only needed against a non-standard host. |
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.
<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.
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.
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.
| Attribute | Value | What it does |
|---|---|---|
restaurant-id | Restaurant id | Required. The restaurant whose menu the assistant answers from. data-restaurant is accepted as an alias. |
publishable-key | tp_pk_live_… | Required. data-key and key are accepted as aliases, but key never reaches the DOM from React or Vue. |
demo | Present, or launcher | Renders 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-api | API base URL | On the <script> tag, not the element. Overrides the API origin the script derives from its own src. Only needed against a non-standard host. |
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 answersenabled: 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.
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.