docs
Apps

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.

CommandWhat it does
eekaam app initScaffold an eekaam.app.toml in the current directory
eekaam app link [handle]Bind the local manifest to an existing app
eekaam app deployPush the local manifest to Eekaam
eekaam app infoShow the server-side state of the linked app
eekaam app installInstall the current draft on one of your dev stores
eekaam app submitSubmit 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 app

2. 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.

ScopeGrants
read_settingsShop details
read_productsList and read products
write_productsCreate, update, delete products
read_ordersList and read orders
read_customersList and read customers
read_webhooksList webhook subscriptions
write_webhooksCreate 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):

EndpointPurpose
GET /admin/oauth/authorizeSend the merchant here to approve scopes
POST /admin/oauth/access_tokenExchange the returned code for a token
POST /admin/oauth/session/verifyVerify an embedded session token
POST /admin/oauth/registerRFC 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_challenge on authorize and code_verifier on exchange.
  • The resource parameter 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:

MethodPathScope
GET/shopread_settings
GET/productsread_products
POST/productswrite_products
GET/products/:idread_products
PUT/products/:idwrite_products
DELETE/products/:idwrite_products
GET/orders · /orders/:idread_orders
GET/customers · /customers/:idread_customers
GET/webhooksread_webhooks
POST/webhookswrite_webhooks
DELETE/webhooks/:idwrite_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:

TypeWhere it appears
admin_pageA full page inside the store admin
admin_linkAn entry point into your app from elsewhere in the admin
sales_channelYour app as a sales channel
theme_blockA block a merchant can add to a theme section
theme_embedA 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.

GroupTopics
Appapp/uninstalled
Productsproducts/create · products/update · products/delete
Ordersorders/create · orders/updated · orders/paid · orders/fulfilled · orders/cancelled · orders/refunded · orders/payment_failed
Customerscustomers/create · customers/update

Every delivery carries these headers:

HeaderValue
X-Eekaam-TopicThe topic that fired
X-Eekaam-Webhook-IdEvent id, stable across retries — use it to dedupe
X-Eekaam-Hmac-Sha256HMAC-SHA256 of the raw body, hex encoded
User-AgentEekaam-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/uninstalled is 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/uninstalled stops 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/uninstalled arrives.

Updated 8 September 2026