Building Your App: Runtime & Data Access¶
Your app is a web service you host. Once a merchant installs it, the dashboard embeds it in a sandboxed iframe and hands it a one-time launch code. Your app's server exchanges that code (with its client credentials) for a short-lived access token, then reads the shop's data from the Merchant API. Your app never talks to the platform database directly.
This page covers what happens after install: how your app is embedded, how it authenticates (OAuth authorization-code style), and how it reads shop data.
The embed model¶
When a merchant opens your app (App Store → Installed → Manage), the dashboard loads your Redirect URL in a sandboxed iframe and appends:
| Param | Example | Meaning |
|---|---|---|
code |
a1b2c3… (64 hex) |
One-time launch code, valid ~60s, single use. Exchange it for a token. |
api |
https://api.bitcommerz.com |
Base URL for the OAuth token endpoint and data calls. |
shop_id |
117 |
The shop your app is serving (context only — never trust it for auth). |
So your app is opened at, e.g.:
https://your-app.example.com/?code=a1b2c3…&api=https://api.bitcommerz.com&shop_id=117
A long-lived token never travels in the URL — only a short, single-use code.
Authenticating: exchange the code for a token¶
On your server, exchange the code for tokens at the OAuth endpoint, using the
client_id / client_secret you received when your app was approved:
POST {api}/app-oauth/token
Content-Type: application/json
{ "grant_type": "authorization_code",
"code": "<code from the iframe URL>",
"client_id": "app_…",
"client_secret": "<your secret>" }
Response
{ "token_type": "Bearer",
"access_token": "eyJhbGciOi…", // use this on data calls (~10 min)
"refresh_token": "eyJhbGciOi…", // swap for a new access token later
"expires_in": 600,
"scope": "read_orders read_analytics" }
- The access token is short-lived (~10 min) and carries your granted scopes.
- The refresh token (long-lived) gets a new access token via
grant_type=refresh_tokenwith the same client credentials. - Codes are single-use — a reused or expired code returns
400.
Keep the secret on your server
Your client_secret and both tokens must stay server-side. The launch code
is the only thing that appears in the browser, and it is useless without your
secret. If your app is opened with no code, show a "open me from your
dashboard" message rather than failing.
The access token¶
- A JWT bound to your app (
aud = client_id) and scoped to one shop (shopId), carrying your grantedscopes. - Revoked on uninstall: every data call re-checks that your app is still active on the shop, so access stops the moment a merchant uninstalls — you don't wait for expiry.
- Treat any
401as "get a fresh token" (refresh, or re-open from the dashboard for a new code).
Reading shop data: the Merchant API¶
Call the Merchant API at the api base with the token as a Bearer credential.
Every response is automatically scoped to the token's shop — you never pass a
shop_id to the API yourself.
Orders¶
GET {api}/merchant-order?perPage=50¤tPage=1
Authorization: Bearer {token}
| Query | Default | Notes |
|---|---|---|
perPage |
10 |
Page size. |
currentPage |
1 |
1-based page number. |
Response
{
"data": [
{
"id": 143588,
"order_number": "1783048837902",
"status": "Order Placed",
"payment_status": "unpaid",
"grand_total": 4580,
"created_at": "2026-07-01T14:40:53.572Z"
}
],
"perPage": 50,
"currentPage": 1,
"totalPage": 3,
"totalResult": 134
}
Aggregate on your side for reports (totals, status breakdown, trends). Use
totalResult for the true order count and page through with currentPage when
you need every row.
Scopes still apply
You can only read what your app requested at submission time (e.g.
read_orders). See App Configuration → Permissions.
Running without a merchant present (client_credentials)¶
Everything above is the embed flow: a merchant opens your app, you get a launch code, you act inside their session. If your backend needs to work when nobody is looking — a nightly sync, a scheduled report — there is a separate server-to-server flow on the platform API.
POST {platform_api}/api/v2/app-oauth/token
Content-Type: application/json
{ "grant_type": "client_credentials",
"client_id": "<client_id>",
"client_secret": "<client_secret>" }
Returns an app token valid for 1 hour. client_credentials is the only
grant this endpoint accepts.
GET {platform_api}/api/v2/app-api/orders?shop_id=117&limit=50
Authorization: Bearer {app_token}
Two differences that matter:
- The token is not bound to a shop. You pass
shop_idon every call, and the platform re-checks that your app is installed and active on that shop each time. Passing a shop you are not installed on fails. - It is a different host and a different base path from the Merchant API used by the embed flow. Do not mix the two tokens — a merchant-api access token will not authenticate here, and vice versa.
Scopes work the same way: the route requires the scope you requested at
submission (read_orders for the route above).
Narrow surface
Orders read is the only route exposed on this flow today. If you need more, use the embed flow, or tell us what you need.
A minimal app (Node, no framework)¶
The whole app: exchange the code for a token, call the Merchant API, render.
const http = require("http");
const { URL } = require("url");
const { CLIENT_ID, CLIENT_SECRET } = process.env;
http.createServer(async (req, res) => {
const url = new URL(req.url, "http://localhost");
const code = url.searchParams.get("code");
const api = url.searchParams.get("api");
if (!code || !api) {
res.writeHead(200, { "content-type": "text/html" });
return res.end("<p>Open this app from your BitCommerz dashboard.</p>");
}
// 1) Swap the one-time code for a short-lived access token (server-side).
const t = await fetch(`${api}/app-oauth/token`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
grant_type: "authorization_code",
code, client_id: CLIENT_ID, client_secret: CLIENT_SECRET,
}),
}).then((r) => r.json());
// 2) Read orders with the access token — no direct DB access.
const { data = [], totalResult = 0 } = await fetch(
`${api}/merchant-order?perPage=1000¤tPage=1`,
{ headers: { Authorization: `Bearer ${t.access_token}` } }
).then((r) => r.json());
const revenue = data.reduce((s, o) => s + Number(o.grand_total || 0), 0);
res.writeHead(200, { "content-type": "text/html" });
res.end(`<h1>Order Report</h1>
<p>${totalResult} orders · ৳${revenue.toLocaleString()} revenue</p>`);
}).listen(process.env.PORT || 8080);
Serve it so the dashboard can iframe it — don't set X-Frame-Options: DENY
(the platform embeds you with a sandbox; a hard frame-deny blanks the panel).
End-to-end lifecycle¶
Build Host a web service (any stack) that reads ?code &api and
renders your UI.
|
Submit Partner Panel -> Apps -> Create App. Set App URL and Redirect
URL to your service, request only the scopes you use.
(status: pending)
|
Approve A super-admin publishes it. You receive OAuth client_id +
client_secret (secret shown once) and the app enters the
App Store catalog.
|
Install Merchant installs from their App Store. Free installs
instantly; paid goes through checkout.
|
Run Dashboard embeds your Redirect URL with code + api + shop_id.
Your server exchanges the code and calls the Merchant API.
See App Distribution & Lifecycle for the publishing side, and App Configuration for the submission fields.
Checklist¶
- [ ] App reads
code+apifrom the iframe URL, server-side. - [ ] Exchanges the code for a token at
/app-oauth/tokenwith yourclient_id/client_secret(kept server-side). - [ ] All shop data comes from the Merchant API with the Bearer access token — no direct database access.
- [ ] Graceful "open from dashboard" state when there's no code.
- [ ]
401handled as "get a fresh token" (refresh, or re-open for a new code). - [ ] Only the scopes you actually use are requested at submission — data routes reject tokens missing the required scope.
- [ ] Your App URL is iframe-embeddable (no
X-Frame-Options: DENY).