Updated July 28, 2026
Eekaam App Developer API Reference
The Eekaam App Ecosystem lets third-party developers build apps that merchants install from the Eekaam marketplace. Once installed, an app authenticates with an OAuth-style access token and calls the Admin API to read or write a store's products, orders, customers, themes, and more — all scoped to the merchant that installed it.
This document is the canonical reference for every endpoint an app developer can call.
1. Concepts
| Term | Meaning |
|---|---|
| App | A piece of software built by a developer and listed on the Eekaam marketplace. Each app has a client_id and client_secret. |
| Install | A specific merchant's installation of an app. Each install carries its own access token and is locked to one tenant (store). |
| Tenant / Shop | A merchant's store. All Admin API responses are automatically scoped to the installed tenant — you never pass tenant_id in a request. |
| Scope | A capability the app requested at install time (e.g. read_products, write_inventory). The API rejects calls outside granted scopes. |
| Access token | An opaque bearer token returned by the OAuth access_token exchange. Used as Authorization: Bearer <token> on every Admin API call. |
| Webhook subscription | A URL the app registers to receive POST callbacks when events happen in the store. Signed with HMAC-SHA256. |
| Embedded session token | A short-lived JWT issued by Eekaam that proves the calling merchant user is logged in. Apps can verify it server-side via the session/verify endpoint. |
Hosts and base URLs
The platform is multi-tenanted by hostname. All API URLs in this document use
the merchant store's host — for example, a merchant on acme.eekaam.com will
hit https://acme.eekaam.com/admin/api/2026-05/.... The Admin API is also
mirrored under /api/apps/admin/2026-05/... for compatibility, and the OAuth
endpoints are mirrored under /api/admin/oauth/....
API version
The current Admin API version is 2026-05. It is embedded in the URL path:
https://{shop}.eekaam.com/admin/api/2026-05/{resource}
2. Registering an app
Apps are created from the developer portal (/dev/apps in the web app, backed
by /api/developer/apps). When you create an app you get back:
client_id— public identifier (UUID-shaped)client_secret— shown once, store it immediatelyredirect_urls— at least one HTTPS URL the OAuth flow will redirect torequested_scopes— the scope list merchants will be asked to approve
The developer-portal endpoints (auth: developer session cookie) are:
| Method | Path | Purpose |
|---|---|---|
GET | /api/developer/apps | List your apps |
POST | /api/developer/apps | Create an app |
GET | /api/developer/apps/:id | Get a specific app |
PUT | /api/developer/apps/:id | Update app metadata / scopes / webhook definitions |
POST | /api/developer/apps/:id/credentials/rotate | Rotate client_secret |
POST | /api/developer/apps/:id/test-install | Install the app on one of your dev stores |
POST | /api/developer/apps/:id/submit | Submit app for marketplace review |
GET | /api/developer/apps/:id/webhooks/deliveries | List recent webhook delivery attempts |
POST | /api/developer/apps/:id/webhooks/deliveries/:delivery_id/replay | Manually replay a failed delivery |
GET | /api/developer/apps/billing/revenue | Your aggregated app revenue |
3. OAuth installation flow
Apps use a standard OAuth 2.0 authorization-code flow. There are two mirrored prefixes — pick one and stick with it:
/admin/oauth/.../api/admin/oauth/...
3.2 Exchange code for access token
POST https://{shop}.eekaam.com/admin/oauth/access_token
Content-Type: application/json
{
"client_id": "{client_id}",
"client_secret": "{client_secret}",
"code": "{authorization_code}"
}
Response:
The token is stored hashed server-side; save it immediately because it cannot be retrieved later.
{
"access_token": "shpat_...",
"token_type": "Bearer",
"scope": "read_products,write_inventory,read_orders"
}3.3 Verify embedded session (optional, for embedded apps)
If your app runs inside an iframe in the merchant dashboard, Eekaam mints a short-lived JWT that proves the calling user is logged in. Verify it server-side before trusting the request:
POST https://{shop}.eekaam.com/admin/oauth/session/verify
Content-Type: application/json
{
"client_id": "{client_id}",
"client_secret": "{client_secret}",
"session_token": "{jwt_from_the_iframe}"
}
Response includes app_id, install_id, tenant_id, user_id, shop,
scopes, and expires_at.
4. Authentication
Every Admin API request must include the bearer access token:
Authorization: Bearer shpat_xxxxxxxxxxxxxxxxxxxx
On every request, the server:
- Looks up the install by hashed token.
- Confirms the install is
active. - Confirms the install was granted the scope required by the route.
Failures return:
401 Unauthorized— missing/invalid token, install uninstalled403 Forbidden— scope not granted, body includes"scope": "<required>"
Tenant ID is always derived from the install record; you cannot escape
your tenant by sending a different tenant_id.
5. Rate limiting
The Admin API is rate-limited to 40 requests per second per access token.
When you exceed the limit Eekaam responds with:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
{ "error": "API rate limit exceeded", "retry_after": "1s" }
Back off and retry. Bursts within the budget are fine.
6. Scope reference
The exact scope strings the API recognises:
| Scope | Grants |
|---|---|
read_settings | GET /shop |
read_webhooks | List webhook subscriptions |
write_webhooks | Create / delete webhook subscriptions |
read_products | List / get products |
write_products | Create / update / delete products |
read_orders | List / get orders |
read_customers | List / get customers (PII subset) |
read_inventory | List inventory levels |
write_inventory | Set or adjust inventory |
read_files | List / get media files |
write_files | Upload / update / delete media files |
read_themes | List themes and theme assets |
write_themes | Upsert theme assets |
read_discounts | List / get discounts |
write_discounts | Create / update / delete discounts |
read_markets | List / get markets |
write_markets | Create / update markets |
read_metafields | List metafield definitions and values |
write_metafields | Upsert / delete metafield values |
read_metaobjects | List metaobject definitions and entries |
write_metaobjects | Create / update metaobject entries |
read_fulfillments | List fulfillments on an order |
write_fulfillments | Create a fulfillment on an order |
read_analytics | Analytics summary & full breakdown |
Request only the scopes you need — fewer scopes means higher merchant install conversion and a smaller blast radius if the token leaks.
7. Admin API reference
Base URL: https://{shop}.eekaam.com/admin/api/2026-05 (or
/api/apps/admin/2026-05). All endpoints require Authorization: Bearer.
Pagination convention for list endpoints:
- Query params:
page(default1),limit(default50, max250) - Response:
{ "<resource>": [...], "pagination": { "page", "limit", "total", "pages" } }
7.1 Shop
| Method | Path | Scope | Response |
|---|---|---|---|
GET | /shop | read_settings | { "shop": { id, name, domain, subdomain, email, plan, status, created_at, updated_at } } |
7.2 Products
| Method | Path | Scope |
|---|---|---|
GET | /products?page=&limit=&search=&status= | read_products |
GET | /products/:id | read_products |
POST | /products | write_products |
PUT | /products/:id | write_products |
DELETE | /products/:id | write_products |
POST body:
PUT accepts the same fields, all optional — only fields you send are
updated. List + get responses preload Images and Variants.
{
"title": "Required",
"description": "",
"price": 0,
"sku": "",
"quantity": 0,
"status": "draft | active | archived",
"is_published": false
}7.3 Orders (read-only)
| Method | Path | Scope |
|---|---|---|
GET | /orders?page=&limit=&fulfillment_status=&payment_status= | read_orders |
GET | /orders/:id | read_orders |
Preloads Items and ShippingAddress.
7.4 Customers (read-only)
| Method | Path | Scope |
|---|---|---|
GET | /customers?page=&limit=&search= | read_customers |
GET | /customers/:id | read_customers |
Returns a PII-limited projection: id, tenant_id, first_name, last_name, email, phone, accepts_marketing, created_at, updated_at.
7.5 Inventory
| Method | Path | Scope |
|---|---|---|
GET | /inventory_levels?product_id=&variant_id= | read_inventory |
POST | /inventory_levels/set | write_inventory |
POST | /inventory_levels/adjust | write_inventory |
Set body:
Adjust body:
Either product_id or variant_id is required. Variant-level updates always
win when both are provided.
{ "product_id": "uuid", "variant_id": "uuid|optional", "available": 25,
"reason": "manual", "reference_id": "po-1234" }{ "variant_id": "uuid", "adjustment": -3, "reason": "shrinkage" }7.6 Files (media library)
| Method | Path | Scope |
|---|---|---|
GET | /files?page=&limit=&search= | read_files |
GET | /files/:id | read_files |
POST | /files | write_files |
PUT | /files/:id | write_files |
DELETE | /files/:id | write_files |
POST body (you upload the asset to your own CDN/S3 first and register the
URL here):
{ "filename": "hero.jpg", "url": "https://...", "content_type": "image/jpeg",
"size": 184213, "alt_text": "Hero banner" }7.7 Themes
| Method | Path | Scope |
|---|---|---|
GET | /themes | read_themes |
GET | /themes/:id/assets | read_themes |
GET | /themes/:id/assets/* | read_themes |
PUT | /themes/:id/assets/* | write_themes |
The asset key (the * wildcard) must start with assets/ and may not
contain ... PUT body:
{ "content": "body { color: red; }", "mime_type": "text/css" }7.8 Discounts
| Method | Path | Scope |
|---|---|---|
GET | /discounts | read_discounts |
GET | /discounts/:id | read_discounts |
POST | /discounts | write_discounts |
PUT | /discounts/:id | write_discounts |
DELETE | /discounts/:id | write_discounts |
Body fields match the Discount model — title is required; method
defaults to "code"; starts_at defaults to now if omitted.
7.9 Markets
| Method | Path | Scope |
|---|---|---|
GET | /markets | read_markets |
GET | /markets/:id | read_markets |
POST | /markets | write_markets |
PUT | /markets/:id | write_markets |
Responses preload ShippingZones and PaymentConfigs. On create, name
is required; slug is auto-generated from name if omitted, and
currency_code defaults to PKR.
7.10 Metafields
| Method | Path | Scope |
|---|---|---|
GET | /metafield_definitions?owner_type= | read_metafields |
GET | /metafields?owner_type=&owner_id= | read_metafields |
PUT | /metafields | write_metafields |
DELETE | /metafields/:id | write_metafields |
Upsert body (definition must already exist):
{
"definition_id": "uuid",
"owner_type": "product | variant | customer | order | ...",
"owner_id": "uuid",
"namespace": "my_app",
"key": "warranty_months",
"value_json": "12"
}7.11 Metaobjects
| Method | Path | Scope |
|---|---|---|
GET | /metaobject_definitions | read_metaobjects |
GET | /metaobjects?definition_id= | read_metaobjects |
POST | /metaobjects | write_metaobjects |
PUT | /metaobjects/:id | write_metaobjects |
Create body requires definition_id and handle; status defaults to
active.
7.12 Fulfillments
| Method | Path | Scope |
|---|---|---|
GET | /orders/:id/fulfillments | read_fulfillments |
POST | /orders/:id/fulfillments | write_fulfillments |
POST body:
{ "status": "fulfilled | shipped | delivered | cancelled | unfulfilled",
"tracking_number": "1Z...",
"shipping_method": "FedEx Ground",
"notify_customer": true }7.13 Analytics
| Method | Path | Scope | Notes |
|---|---|---|---|
GET | /analytics/summary?days=30 | read_analytics | days clamped to [1, 365], default 30 |
GET | /analytics?days=30 | read_analytics | Full breakdown |
7.14 Webhook subscriptions (managed by the app)
| Method | Path | Scope |
|---|---|---|
GET | /webhooks | read_webhooks |
POST | /webhooks | write_webhooks |
DELETE | /webhooks/:id | write_webhooks |
POST body:
- Only
format: "json"is currently supported. callback_urlmust be a valid absolute URL.- The response includes a one-time
secretand asecret_notice— store it, you will use it to verify HMAC signatures.
DELETE does a soft disable (sets status to disabled); the subscription
record is retained for delivery history.
See dev/apps/{id}/webhooks/docs in the dashboard for the full retry-policy
and HMAC verification reference — the next section is a condensed mirror.
{ "topic": "orders/create", "callback_url": "https://app.example.com/hooks", "format": "json" }8. Webhooks
8.1 Supported topics
app/uninstalled — the merchant uninstalled your app (always delivered, even after delete)
orders/create — a new order was placed
orders/updated — an order changed (fulfillment, status, line items, etc.)
products/create
products/update
products/delete
customers/create
customers/update
Trying to subscribe to anything else returns 400 Unsupported webhook topic.
8.2 Delivery payload
Every webhook POST body has this shape:
{
"id": "evt_...",
"topic": "orders/create",
"app_id": "uuid",
"tenant_id": "uuid",
"created_at": "2026-05-22T12:00:00Z",
"data": { /* topic-specific payload */ }
}8.3 Headers Eekaam sends
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | Eekaam-Webhooks/1.0 |
X-Eekaam-Topic | e.g. orders/create |
X-Eekaam-Webhook-Id | unique event ID (use it to dedupe) |
X-Eekaam-Hmac-Sha256 | HMAC-SHA256 of the raw body, hex-encoded |
8.4 Verifying the signature
Compute HMAC-SHA256(raw_body, subscription_secret), hex-encode it, and
compare against X-Eekaam-Hmac-Sha256 with a timing-safe comparison.
Always sign the raw bytes before any JSON parsing or re-serialisation.
import crypto from "node:crypto";
export function verifyEekaamWebhook(rawBody, secret, headerSignature) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(headerSignature, "hex"),
);
}8.5 Retry policy
If the endpoint responds with a non-2xx status, or fails to respond within
10 seconds, Eekaam retries with exponential backoff up to 8 attempts
over roughly a 16-hour window. Any 2xx response — including an empty body
— counts as success. Payloads are retained for 72 hours; failed
deliveries can be manually replayed from the dev portal's deliveries page.
A sustained-high failure rate auto-disables the subscription and fires the
app/webhook_subscription_disabled event.
9. Error format
All errors return JSON of the form:
403 Forbidden responses for missing scopes additionally include
"scope": "<scope-name>".
Standard HTTP status codes apply:
| Code | Meaning |
|---|---|
400 | Malformed JSON, missing required field, invalid UUID, unsupported value |
401 | Missing/invalid bearer token |
403 | Token valid but lacks the required scope |
404 | Resource does not exist in this tenant |
409 | Conflict (e.g. handle already exists, re-enable embed on uninstalled app) |
429 | Rate limit exceeded |
500 | Server error — safe to retry with backoff |
{ "error": "Human-readable message" }10. Quick start — a complete install + first call
# 1. Send the merchant to the authorize URL
open "https://acme.eekaam.com/admin/oauth/authorize?client_id=APP_ID&redirect_uri=https://app.example.com/install&state=$(uuidgen)"
# 2. After approval, Eekaam redirects to your URL with ?code=...&state=...
# 3. Exchange the code for an access token
curl -X POST https://acme.eekaam.com/admin/oauth/access_token \
-H 'Content-Type: application/json' \
-d '{"client_id":"APP_ID","client_secret":"APP_SECRET","code":"AUTH_CODE"}'
# {"access_token":"shpat_...","token_type":"Bearer","scope":"read_products"}
# 4. Call the Admin API
curl https://acme.eekaam.com/admin/api/2026-05/products \
-H 'Authorization: Bearer shpat_...'
# 5. Subscribe to a webhook (need write_webhooks scope)
curl -X POST https://acme.eekaam.com/admin/api/2026-05/webhooks \
-H 'Authorization: Bearer shpat_...' \
-H 'Content-Type: application/json' \
-d '{"topic":"orders/create","callback_url":"https://app.example.com/hooks/orders"}'
# Response includes a one-time "secret" — store it, it won't be shown again.