Quickstart
Five commands, and your app is running inside a real store's dashboard.
npm install -g @tanqory/cli
tanqory login
tanqory app init order-notifier
cd order-notifier
tanqory app dev --open
That is the whole thing. A browser opens on your app, framed inside a development store.
localhost:3000 on its own will say Loading… foreverThat is correct, not a bug. Your app only ever runs inside an iframe, and opened directly there is no dashboard around it to say which store it is serving, so the handshake never completes.
Use tanqory app dev --open to look at your app. Never the plain localhost
URL.
What those commands did
tanqory app dev is not a dev server wrapper — it does the whole setup:
Registered your app and wrote TANQORY_CLIENT_ID / TANQORY_CLIENT_SECRET into .env.local | the secret is shown once |
Pushed tanqory.app.json | your manifest: URLs, permissions, webhooks, menu |
| Test-installed it on one of your development stores | no review needed for this |
| Started your dev server and opened the framed URL | |
| Watched the manifest and re-pushes on save | a permission change re-installs |
Doing it by hand instead — because you are adding Tanqory to a project you already have — is Manual setup. Same trip, on foot.
What you were given
order-notifier/
├── tanqory.app.json your manifest
├── AGENTS.md · CLAUDE.md notes for AI coding assistants
├── next.config.mjs frame-ancestors, already correct
└── src/app/
├── embed/[storeId]/[[...slug]]/page.tsx the page the dashboard frames
└── hooks/orders/route.ts a webhook handler that verifies its signature
Two of these ship correct rather than documented, because getting them wrong
fails silently: the frame headers in next.config.mjs, and the signature
check in the webhook handler.
If you use Claude Code, Cursor or a similar assistant, read AI
assistants — AGENTS.md is already in your project and is what stops
an assistant confidently suggesting the things that do not work here.
Add a screen
Screens go under [storeId], because the dashboard frames
<embedded.path>/<storeId>/<your route>:
src/app/embed/[storeId]/settings/page.tsx ✅ framed at /embed/<storeId>/settings
src/app/embed/settings/page.tsx ❌ never reached
Every page calls the hook for itself, and must render error:
'use client'
import { useTanqoryApp } from '@tanqory/plugin-sdk/client'
export default function Settings() {
const { ready, error, store, api } = useTanqoryApp()
if (error) return <p>{error}</p> // ← without this, every failure looks like "Loading…"
if (!ready) return <p>Loading…</p>
return <h1>{store!.storeName}</h1>
}
Put it in the merchant's menu
"navigation": {
"placement": "orders",
"items": [{ "label": "Notifications", "path": "/embed" }]
}
placement picks which menu you appear under — one of thirteen. Save the file;
tanqory app dev re-pushes it.
A path with no route behind it renders your own 404 inside the frame,
which looks like a platform bug and is not one.
Full list of placements and the rest of the manifest: Manual setup.
Call the merchant's API
From the browser, the hook hands you a client already scoped to the store:
const orders = await api.get('/orders', { limit: 5 })
From your server:
import { createTanqoryApp } from '@tanqory/plugin-sdk'
const tanqory = createTanqoryApp({
clientId: process.env.TANQORY_CLIENT_ID!,
clientSecret: process.env.TANQORY_CLIENT_SECRET!,
apiBase: 'https://app.tanqory.com', // required — always this exact host
})
const orders = await tanqory.store(storeId).get('/orders', { query: { limit: 5 } })
apiBase is always https://app.tanqory.comNever a host with a region in it (api-do-sgp1.tanqory.com and friends). Which
region a store sits on is the platform's decision and can change; a request to
the wrong one returns a 404 that looks exactly like a deleted record.
When something does not work
| What you see | Almost always |
|---|---|
Loading… forever | Opened outside the dashboard, or the page does not render error |
| Blank frame, clean console | An X-Frame-Options header — it overrides frame-ancestors. Never send it |
| Your own 404 inside the frame | A route not under [storeId], or a menu item with no page |
| Webhook signature never matches | The body was parsed and re-serialised. Pass the raw body |
403 from an API call | The merchant did not grant that permission. Not retryable — ask for it in your next version |
| Every call 404s in production but worked in testing | apiBase points at the central host or a region |
Next
- AI assistants — make Claude Code or Cursor useful on this project
- CLI reference — every
tanqory appcommand - Permissions — the 246 scopes you may request
- Submit for review — what review reads, and what it does not