Skip to main content

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.

warning
localhost:3000 on its own will say Loading… forever

That 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.localthe secret is shown once
Pushed tanqory.app.jsonyour manifest: URLs, permissions, webhooks, menu
Test-installed it on one of your development storesno review needed for this
Started your dev server and opened the framed URL
Watched the manifest and re-pushes on savea 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.

Declaring a menu item does not create the page

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 } })
danger
apiBase is always https://app.tanqory.com

Never 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 seeAlmost always
Loading… foreverOpened outside the dashboard, or the page does not render error
Blank frame, clean consoleAn X-Frame-Options header — it overrides frame-ancestors. Never send it
Your own 404 inside the frameA route not under [storeId], or a menu item with no page
Webhook signature never matchesThe body was parsed and re-serialised. Pass the raw body
403 from an API callThe merchant did not grant that permission. Not retryable — ask for it in your next version
Every call 404s in production but worked in testingapiBase points at the central host or a region

Next​