Building an app
Building an app
An Eekaam app is a web service you host. Eekaam sends merchants to it during install, gives it an access token scoped to what the merchant approved, embeds its UI inside the store admin, and posts events to it as the store changes. Nothing runs on Eekaam's servers.
This page is the whole path: create, configure, authenticate, call the API, receive webhooks, test, and submit for review.
Reference: every endpoint, header, and scope named below is what the platform actually enforces. Where a value is a limit or an exact string, it is copied from the implementation rather than from a plan.
1. Create the app
Two ways in, and they produce the same record.
The CLI is the one that leaves you with a project:
Or create it in the partner dashboard under Apps → Create app, which asks only for a name and issues credentials immediately. Use eekaam app link <handle> afterwards to bind a local manifest to it.
| Command | What it does |
|---|---|
eekaam app init | Scaffold an eekaam.app.toml in the current directory |
eekaam app link [handle] | Bind the local manifest to an existing app |
eekaam app deploy | Push the local manifest to Eekaam |
eekaam app info | Show the server-side state of the linked app |
eekaam app install | Install the current draft on one of your dev stores |
eekaam app submit | Submit the current draft for admin review |
eekaam app deploy overwrites server configuration with what the manifest declares. Anything you edited in the dashboard but did not put in the manifest is replaced on the next deploy — keep the manifest as the source of truth, or edit in the dashboard only.
npm i -g @eekaam/cli
eekaam auth login
eekaam app init # writes eekaam.app.toml in the current directory
eekaam app deploy # pushes the manifest to Eekaam, creating or updating the app2. Access scopes
An app requests scopes; a merchant approves them at install. Every Admin API route checks the scope for that route, so requesting less than you need fails at call time, and requesting more than you need costs you approvals.
| Scope | Grants |
|---|---|
read_settings | Shop details |
read_products | List and read products |
write_products | Create, update, delete products |
read_orders | List and read orders |
read_customers | List and read customers |
read_webhooks | List webhook subscriptions |
write_webhooks | Create and delete webhook subscriptions |
3. The install flow
Standard OAuth 2.0 authorization code. The endpoints live at /admin/oauth (also reachable at /api/admin/oauth):
| Endpoint | Purpose |
|---|---|
GET /admin/oauth/authorize | Send the merchant here to approve scopes |
POST /admin/oauth/access_token | Exchange the returned code for a token |
POST /admin/oauth/session/verify | Verify an embedded session token |
POST /admin/oauth/register | RFC 7591 dynamic client registration |
Four things the exchange enforces, each of which is a real rejection rather than a guideline:
- The code is single use. A second exchange with the same code fails, as does one past its expiry.
- PKCE is mandatory for public clients. An app with no usable client secret must send
code_challengeon authorize andcode_verifieron exchange. - The
resourceparameter must match the one sent to authorize, when it was sent. - Suspended apps cannot exchange. Credentials for a suspended app are rejected before the code is even read.
The response is a bearer token and the scopes actually granted, which can be narrower than the scopes requested:
Register the redirect target under Configuration → URLs & permissions. A redirect URI not on that list is refused.
curl -X POST https://api.eekaam.com/admin/oauth/access_token \
-H 'Content-Type: application/json' \
-d '{
"client_id": "$EEKAAM_CLIENT_ID",
"client_secret": "$EEKAAM_CLIENT_SECRET",
"code": "<code from the redirect>",
"code_verifier": "<PKCE verifier>"
}'{ "access_token": "…", "token_type": "Bearer", "scope": "read_products,read_orders" }4. Calling the Admin API
Every resource route sits under a dated prefix and takes the merchant's access token:
| Method | Path | Scope |
|---|---|---|
GET | /shop | read_settings |
GET | /products | read_products |
POST | /products | write_products |
GET | /products/:id | read_products |
PUT | /products/:id | write_products |
DELETE | /products/:id | write_products |
GET | /orders · /orders/:id | read_orders |
GET | /customers · /customers/:id | read_customers |
GET | /webhooks | read_webhooks |
POST | /webhooks | write_webhooks |
DELETE | /webhooks/:id | write_webhooks |
The token is scoped to one store. An app installed on forty stores holds forty tokens; never reuse one across stores.
curl https://api.eekaam.com/admin/api/2026-05/products \
-H "Authorization: Bearer $EEKAAM_ACCESS_TOKEN"5. Embedded apps
An embedded app renders inside the store admin in an iframe. Two settings make that work, both under Configuration → URLs & permissions:
- Embedded app URL — what Eekaam loads in the frame
- Allowed origins — the origins permitted to frame it; an origin missing here is refused by the browser, not by us, so the failure looks like a blank frame
Eekaam appends shop, host, tenant and session_token to the frame URL. The session token identifies the merchant for that page load and must be verified server-side before you trust it:
Never treat the shop or tenant query parameters as authentication. They are hints; the session token is the proof.
curl -X POST https://api.eekaam.com/admin/oauth/session/verify \
-H 'Content-Type: application/json' \
-d '{
"client_id": "$EEKAAM_CLIENT_ID",
"client_secret": "$EEKAAM_CLIENT_SECRET",
"session_token": "<token from the query string>"
}'6. Extensions
An extension is a surface your app adds to the merchant's store. Declare them in the manifest or under Configuration. Five types exist:
| Type | Where it appears |
|---|---|
admin_page | A full page inside the store admin |
admin_link | An entry point into your app from elsewhere in the admin |
sales_channel | Your app as a sales channel |
theme_block | A block a merchant can add to a theme section |
theme_embed | A site-wide embed injected into the storefront |
At least one extension is required before an app can be submitted — an app with no surface has nothing for a merchant to open.
7. Webhooks
Subscribe to topics and Eekaam posts JSON to your endpoint as things change.
| Group | Topics |
|---|---|
| App | app/uninstalled |
| Products | products/create · products/update · products/delete |
| Orders | orders/create · orders/updated · orders/paid · orders/fulfilled · orders/cancelled · orders/refunded · orders/payment_failed |
| Customers | customers/create · customers/update |
Every delivery carries these headers:
| Header | Value |
|---|---|
X-Eekaam-Topic | The topic that fired |
X-Eekaam-Webhook-Id | Event id, stable across retries — use it to dedupe |
X-Eekaam-Hmac-Sha256 | HMAC-SHA256 of the raw body, hex encoded |
User-Agent | Eekaam-Webhooks/1.0 |
Verify the signature before parsing the body. It is computed over the exact bytes received with your subscription secret, so compute it before any JSON round-trip:
Operational rules worth designing around:
- Deliveries time out after 10 seconds. Acknowledge with a 2xx immediately and do the work asynchronously; slow handlers are recorded as failures.
- Any non-2xx is a failure. Only 200–299 counts as delivered.
- Five recent failures disable the subscription. It stops firing until you re-enable it, so a broken deploy silently costs you events. Watch delivery history after every release.
app/uninstalledis the one you cannot skip. Stop billing and delete the store's data when it arrives.
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}8. Pricing
An app can ship several plans. A free plan is activated at install with no billing step. A paid plan must be connected to a billing provider variant before the app can be submitted — a paid plan with no variant is the most common reason a submission is blocked.
If your app has both a free and a paid plan, merchants choose at install. Ship the free plan first if you want installs before revenue.
9. Test before you submit
Install the app on one of your own development stores and walk the real flow — the partner dashboard's Testing screen does this without needing admin approval, and eekaam app install does the same from the terminal.
Confirm on that store:
- The authorize screen lists the scopes you expect, and no more
- The redirect lands on a URL in your allowlist and the code exchanges once
- The embedded page loads in the admin frame, and the session token verifies server-side
- Every Admin API call your app makes succeeds with only the scopes you requested
- Each webhook you subscribe to arrives, verifies, and is acknowledged inside 10 seconds
app/uninstalledstops billing and clears the store's data
10. Submit for review
Submission is blocked until all of the following are true. The partner dashboard shows them as a checklist on the Release screen:
- Listing details are complete — name and short description
- OAuth URLs and iframe origins are configured
- Embedded app URL is set
- Permissions are selected
- At least one extension is declared
- Paid plans are connected to billing
- Support and privacy links are ready
A release is a snapshot of the configuration at the moment you submit. Merchants keep running the currently released version until a new one is approved, so editing configuration after submitting does not change what they have — it creates the next draft.
11. Credentials and security
- The client secret is shown once, when the app is created or the secret is rotated. Store it as
EEKAAM_CLIENT_SECRET; there is no way to read it back. - Rotating invalidates the old secret immediately. Deploy the new one to every server that uses it before rotating, not after.
- Never ship the client secret to a browser. An app that cannot keep a secret is a public client and must use PKCE.
- Access tokens are per store and per install. Treat them as credentials belonging to that merchant, and delete them when
app/uninstalledarrives.
Updated 8 September 2026