Chapter 15: API Design
API Architecture
AdPriority exposes a RESTful JSON API served by Express.js with TypeScript. All endpoints except authentication callbacks and Shopify webhooks require a valid Shopify session. The API is designed for consumption by the embedded Shopify Polaris frontend via App Bridge authenticated fetch.
API ARCHITECTURE
================
Shopify Admin (iframe)
|
| App Bridge authenticated fetch
v
+---------------------------+
| Express.js Server |
| Port 3010 |
| |
| Middleware Stack: |
| 1. CORS |
| 2. JSON body parser |
| 3. Rate limiter |
| 4. Shopify auth verify |
| 5. Tenant scoping |
| 6. Request logging |
+---------------------------+
|
+----> /auth/* Shopify OAuth, session validation
+----> /api/products/* Product listing, sync triggers
+----> /api/priorities/* Score management, overrides
+----> /api/rules/* Category rule CRUD
+----> /api/seasons/* Seasonal calendar management
+----> /api/sync/* Google Sheets push, sync status
+----> /api/settings/* Tenant configuration
+----> /api/billing/* Subscription management
+----> /api/gdpr/* Mandatory GDPR compliance webhooks
+----> /webhooks/* Shopify product webhooks (HMAC verified)
Base Configuration
Base URL (Development): http://localhost:3010
Base URL (Production): https://app.adpriority.com
API Prefix: /api
Content-Type: application/json
Authentication: Shopify App Bridge session token
Rate Limit: 100 requests/minute (general), 10/minute (sync)
Authentication Endpoints
Authentication uses Shopify’s OAuth 2.0 flow. The app is embedded in the Shopify Admin, so most API requests use session tokens verified via Shopify’s App Bridge library. The OAuth callback installs the app and creates the tenant record.
POST /auth/callback
Handles the Shopify OAuth callback after the merchant approves the app installation.
| Property | Value |
|---|---|
| Method | POST |
| Path | /auth/callback |
| Auth Required | No (this creates the session) |
| Rate Limit | 10/minute |
Request Body:
{
"code": "shopify_oauth_code_here",
"shop": "nexus-clothes.myshopify.com",
"hmac": "sha256_hmac_signature",
"timestamp": "1707220800",
"state": "nonce_from_install_request"
}
Response (200 OK):
{
"success": true,
"tenant": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"shopifyShopDomain": "nexus-clothes.myshopify.com",
"planTier": "starter",
"status": "active"
},
"redirectUrl": "/app/dashboard"
}
Error Responses:
| Status | Code | When |
|---|---|---|
| 400 | INVALID_HMAC | HMAC signature verification failed |
| 400 | INVALID_NONCE | State parameter does not match stored nonce |
| 500 | TOKEN_EXCHANGE_FAILED | Shopify rejected the authorization code |
GET /auth/session
Validates the current session and returns tenant information. Called on every page load by the frontend to confirm the session is still valid.
| Property | Value |
|---|---|
| Method | GET |
| Path | /auth/session |
| Auth Required | Yes (session token) |
| Rate Limit | 100/minute |
Response (200 OK):
{
"tenant": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"shopifyShopDomain": "nexus-clothes.myshopify.com",
"planTier": "growth",
"status": "active",
"gmcMerchantId": "123456789",
"googleSheetId": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms"
},
"billing": {
"plan": "growth",
"status": "active",
"activatedAt": "2026-01-15T10:00:00Z"
},
"features": {
"seasonalAutomation": true,
"tagModifiers": true,
"newArrivalBoost": true,
"googleAdsIntegration": false,
"maxProducts": null
}
}
Error Responses:
| Status | Code | When |
|---|---|---|
| 401 | UNAUTHORIZED | Session token invalid or expired |
| 403 | TENANT_SUSPENDED | Tenant account suspended |
Products Endpoints
Product endpoints provide paginated access to the local product cache and trigger Shopify sync operations. Products are read from the AdPriority database, not from Shopify directly (the sync operation refreshes the cache).
GET /api/products
Returns a paginated, filterable list of products with their current priority scores.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/products |
| Auth Required | Yes |
| Rate Limit | 100/minute |
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
page | int | 1 | Page number (1-indexed) |
limit | int | 50 | Items per page (max 250) |
status | string | active | Filter: active, archived, draft, all |
productType | string | – | Filter by product type |
vendor | string | – | Filter by vendor |
priority | int | – | Filter by priority score (0-5) |
search | string | – | Full-text search on title |
sortBy | string | title | Sort field: title, priority, updatedAt, productType |
sortOrder | string | asc | Sort direction: asc, desc |
Response (200 OK):
{
"products": [
{
"id": "prod-uuid-001",
"shopifyProductId": "8779355160808",
"title": "Jordan Craig Stacked Jeans - Jet Black",
"productType": "Men-Bottoms-Stacked Jeans",
"vendor": "Jordan Craig",
"tags": ["NAME BRAND", "Men", "in-stock"],
"status": "active",
"variantCount": 6,
"priority": {
"score": 5,
"label": "Push Hard",
"override": false,
"calculatedAt": "2026-02-10T08:00:00Z"
},
"updatedAt": "2026-02-10T08:00:00Z"
},
{
"id": "prod-uuid-002",
"shopifyProductId": "9128994570472",
"title": "New Era NY Yankees 59FIFTY Fitted",
"productType": "Headwear-Baseball-Fitted",
"vendor": "New Era",
"tags": ["NAME BRAND", "Men"],
"status": "active",
"variantCount": 8,
"priority": {
"score": 4,
"label": "Strong",
"override": false,
"calculatedAt": "2026-02-10T08:00:00Z"
},
"updatedAt": "2026-02-10T08:00:00Z"
}
],
"pagination": {
"page": 1,
"limit": 50,
"total": 2425,
"totalPages": 49,
"hasNext": true,
"hasPrev": false
}
}
GET /api/products/:id
Returns full detail for a single product, including all variants and their individual priority scores.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/products/:id |
| Auth Required | Yes |
| Rate Limit | 100/minute |
Response (200 OK):
{
"id": "prod-uuid-001",
"shopifyProductId": "8779355160808",
"title": "Jordan Craig Stacked Jeans - Jet Black",
"productType": "Men-Bottoms-Stacked Jeans",
"vendor": "Jordan Craig",
"tags": ["NAME BRAND", "Men", "in-stock", "jordan-craig"],
"status": "active",
"createdAt": "2025-06-15T10:00:00Z",
"updatedAt": "2026-02-10T08:00:00Z",
"variants": [
{
"id": "var-uuid-001",
"shopifyVariantId": "46050142748904",
"sku": "107438",
"price": "89.99",
"inventoryQuantity": 12,
"gmcProductId": "shopify_US_8779355160808_46050142748904",
"priorityScore": {
"priority": 5,
"customLabel0": "priority-5",
"customLabel1": "winter",
"customLabel2": "jeans-pants",
"customLabel3": "in-stock",
"customLabel4": "name-brand",
"override": false,
"calculatedAt": "2026-02-10T08:00:00Z"
}
},
{
"id": "var-uuid-002",
"shopifyVariantId": "46050142781672",
"sku": "107439",
"price": "89.99",
"inventoryQuantity": 0,
"gmcProductId": "shopify_US_8779355160808_46050142781672",
"priorityScore": {
"priority": 0,
"customLabel0": "priority-0",
"customLabel1": "winter",
"customLabel2": "jeans-pants",
"customLabel3": "dead-stock",
"customLabel4": "name-brand",
"override": false,
"calculatedAt": "2026-02-10T08:00:00Z"
}
}
]
}
Error Responses:
| Status | Code | When |
|---|---|---|
| 404 | NOT_FOUND | Product ID does not exist or belongs to another tenant |
POST /api/products/sync
Triggers a Shopify product sync that fetches all products from the Shopify Admin API and updates the local cache. This is a long-running operation that returns immediately with a job reference.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/products/sync |
| Auth Required | Yes |
| Rate Limit | 5/minute |
Request Body:
{
"mode": "incremental",
"since": "2026-02-09T00:00:00Z"
}
| Field | Type | Required | Description |
|---|---|---|---|
mode | string | No | full (all products) or incremental (since date). Default: incremental |
since | ISO 8601 | No | Only fetch products updated after this time. Default: last sync time |
Response (202 Accepted):
{
"syncLogId": "sync-uuid-001",
"status": "started",
"estimatedProducts": 2425,
"message": "Shopify product sync started"
}
Error Responses:
| Status | Code | When |
|---|---|---|
| 409 | SYNC_IN_PROGRESS | Another sync is already running for this tenant |
| 429 | RATE_LIMITED | Too many sync requests |
Priorities Endpoints
Priority endpoints read and modify the calculated priority scores. The listing endpoint supports the same filtering as products but returns score-centric data. The override endpoint allows merchants to manually lock a score.
GET /api/priorities
Returns priority scores with filtering and aggregation options.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/priorities |
| Auth Required | Yes |
| Rate Limit | 100/minute |
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
page | int | 1 | Page number |
limit | int | 50 | Items per page (max 250) |
priority | int | – | Filter by exact score (0-5) |
minPriority | int | – | Filter: score >= value |
maxPriority | int | – | Filter: score <= value |
override | boolean | – | Filter: only manual overrides |
productType | string | – | Filter by product type |
categoryGroup | string | – | Filter by category group (label_2) |
needsSync | boolean | – | Filter: scores calculated after last sync |
Response (200 OK):
{
"priorities": [
{
"variantId": "var-uuid-001",
"shopifyVariantId": "46050142748904",
"gmcProductId": "shopify_US_8779355160808_46050142748904",
"productTitle": "Jordan Craig Stacked Jeans - Jet Black",
"sku": "107438",
"priority": 5,
"customLabels": {
"label0": "priority-5",
"label1": "winter",
"label2": "jeans-pants",
"label3": "in-stock",
"label4": "name-brand"
},
"override": false,
"calculatedAt": "2026-02-10T08:00:00Z"
}
],
"summary": {
"distribution": { "0": 615, "1": 45, "2": 380, "3": 820, "4": 350, "5": 215 },
"totalScored": 2425,
"totalOverrides": 12,
"lastCalculated": "2026-02-10T08:00:00Z"
},
"pagination": {
"page": 1,
"limit": 50,
"total": 2425,
"totalPages": 49
}
}
PUT /api/priorities/:variantId
Manually overrides the priority score for a specific variant. Setting override: true locks the score so it is not changed by automatic recalculation.
| Property | Value |
|---|---|
| Method | PUT |
| Path | /api/priorities/:variantId |
| Auth Required | Yes |
| Rate Limit | 100/minute |
Request Body:
{
"priority": 5,
"override": true,
"overrideReason": "Bestseller - always push hard"
}
| Field | Type | Required | Constraints |
|---|---|---|---|
priority | int | Yes | 0-5 inclusive |
override | boolean | No | Default: true when manually setting |
overrideReason | string | No | Free text explanation |
Response (200 OK):
{
"variantId": "var-uuid-001",
"priority": 5,
"previousPriority": 4,
"override": true,
"overrideReason": "Bestseller - always push hard",
"customLabels": {
"label0": "priority-5",
"label1": "winter",
"label2": "jeans-pants",
"label3": "in-stock",
"label4": "name-brand"
},
"calculatedAt": "2026-02-10T14:30:00Z"
}
Error Responses:
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Priority not in range 0-5 |
| 404 | NOT_FOUND | Variant ID not found |
POST /api/priorities/recalculate
Triggers a bulk recalculation of all priority scores for the tenant. This re-evaluates every variant against the current category rules, seasonal calendar, tag modifiers, and inventory levels. Manually overridden scores are skipped unless includeOverrides is set to true.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/priorities/recalculate |
| Auth Required | Yes |
| Rate Limit | 5/minute |
Request Body:
{
"includeOverrides": false,
"productTypes": ["Men-Bottoms-Stacked Jeans", "Men-Tops-Hoodies & Sweatshirts"],
"dryRun": false
}
| Field | Type | Required | Description |
|---|---|---|---|
includeOverrides | boolean | No | If true, recalculate even manually overridden scores. Default: false |
productTypes | string[] | No | Limit recalculation to specific product types. Default: all types |
dryRun | boolean | No | If true, return preview without saving. Default: false |
Response (200 OK):
{
"recalculated": 2413,
"skippedOverrides": 12,
"changes": {
"increased": 145,
"decreased": 89,
"unchanged": 2179
},
"distribution": {
"0": 630,
"1": 42,
"2": 375,
"3": 815,
"4": 345,
"5": 218
},
"calculatedAt": "2026-02-10T14:35:00Z"
}
Rules Endpoints
Category rules define how product types map to base priorities. Each rule matches a product type pattern and assigns a base priority, optionally scoped to a specific season. Rules also carry tag-modifier configuration in the modifiers JSON field.
GET /api/rules
Returns all category rules for the tenant, sorted by product type pattern.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/rules |
| Auth Required | Yes |
| Rate Limit | 100/minute |
Response (200 OK):
{
"rules": [
{
"id": "rule-uuid-001",
"productTypePattern": "outerwear-heavy",
"season": null,
"basePriority": 3,
"modifiers": {
"tagAdjustments": {
"NAME BRAND": { "adjustment": 1 },
"DEAD50": { "override": 0 }
}
},
"matchingProducts": 72,
"createdAt": "2026-01-15T10:00:00Z",
"updatedAt": "2026-01-15T10:00:00Z"
},
{
"id": "rule-uuid-002",
"productTypePattern": "outerwear-heavy",
"season": "winter",
"basePriority": 5,
"modifiers": {},
"matchingProducts": 72,
"createdAt": "2026-01-15T10:00:00Z",
"updatedAt": "2026-01-15T10:00:00Z"
}
],
"totalRules": 80
}
POST /api/rules
Creates a new category rule. If a rule with the same pattern and season already exists, the request is rejected with a 409 Conflict.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/rules |
| Auth Required | Yes |
| Rate Limit | 100/minute |
Request Body:
{
"productTypePattern": "outerwear-heavy",
"season": "winter",
"basePriority": 5,
"modifiers": {
"tagAdjustments": {
"NAME BRAND": { "adjustment": 1 },
"DEAD50": { "override": 0 }
}
}
}
| Field | Type | Required | Constraints |
|---|---|---|---|
productTypePattern | string | Yes | Max 255 chars |
season | string | No | winter, spring, summer, fall, or null for all-season default |
basePriority | int | Yes | 0-5 inclusive |
modifiers | object | No | Tag adjustments and inventory modifiers |
Response (201 Created):
{
"id": "rule-uuid-new",
"productTypePattern": "outerwear-heavy",
"season": "winter",
"basePriority": 5,
"modifiers": { "tagAdjustments": { "NAME BRAND": { "adjustment": 1 } } },
"createdAt": "2026-02-10T14:30:00Z"
}
Error Responses:
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Missing required fields or invalid priority range |
| 409 | DUPLICATE_RULE | A rule with this pattern and season already exists |
PUT /api/rules/:id
Updates an existing category rule. Any field not included in the request body is left unchanged.
| Property | Value |
|---|---|
| Method | PUT |
| Path | /api/rules/:id |
| Auth Required | Yes |
| Rate Limit | 100/minute |
Request Body:
{
"basePriority": 4,
"modifiers": {
"tagAdjustments": {
"NAME BRAND": { "adjustment": 1 },
"Sale": { "adjustment": -1 }
}
}
}
Response (200 OK):
{
"id": "rule-uuid-001",
"productTypePattern": "outerwear-heavy",
"season": null,
"basePriority": 4,
"modifiers": { "tagAdjustments": { "NAME BRAND": { "adjustment": 1 }, "Sale": { "adjustment": -1 } } },
"updatedAt": "2026-02-10T14:35:00Z"
}
DELETE /api/rules/:id
Deletes a category rule. Products previously scored by this rule will retain their current scores until the next recalculation.
| Property | Value |
|---|---|
| Method | DELETE |
| Path | /api/rules/:id |
| Auth Required | Yes |
| Rate Limit | 100/minute |
Response (200 OK):
{
"deleted": true,
"id": "rule-uuid-001",
"affectedProducts": 72,
"message": "Rule deleted. Run recalculation to update affected products."
}
Seasons Endpoints
Season endpoints manage the seasonal calendar. Each tenant has four seasons by default. The Growth tier and above can customize dates and add micro-seasons.
GET /api/seasons
Returns all seasonal calendars for the tenant, including which season is currently active.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/seasons |
| Auth Required | Yes |
| Rate Limit | 100/minute |
Response (200 OK):
{
"currentSeason": "winter",
"currentSeasonCalendar": {
"id": "cal-uuid-winter",
"name": "Winter",
"season": "winter",
"startMonth": 11,
"endMonth": 2
},
"nextTransition": {
"toSeason": "spring",
"date": "2026-03-01",
"daysUntil": 19
},
"seasons": [
{
"id": "cal-uuid-winter",
"name": "Winter",
"season": "winter",
"startMonth": 11,
"endMonth": 2,
"isCurrent": true,
"categoryOverrides": {
"outerwear-heavy": 5,
"hoodies-sweatshirts": 5,
"shorts": 0,
"swim-shorts": 0
}
},
{
"id": "cal-uuid-spring",
"name": "Spring",
"season": "spring",
"startMonth": 3,
"endMonth": 4,
"isCurrent": false,
"categoryOverrides": {
"jeans-pants": 4,
"t-shirts": 4,
"outerwear-heavy": 1
}
}
]
}
PUT /api/seasons/:id
Updates a seasonal calendar’s dates or category overrides.
| Property | Value |
|---|---|
| Method | PUT |
| Path | /api/seasons/:id |
| Auth Required | Yes (Growth tier+) |
| Rate Limit | 100/minute |
Request Body:
{
"startMonth": 12,
"endMonth": 2,
"categoryOverrides": {
"outerwear-heavy": 5,
"hoodies-sweatshirts": 5,
"headwear-cold": 5,
"jeans-pants": 4,
"shorts": 0,
"swim-shorts": 0,
"footwear-sandals": 0
}
}
Response (200 OK):
{
"id": "cal-uuid-winter",
"name": "Winter",
"season": "winter",
"startMonth": 12,
"endMonth": 2,
"categoryOverrides": { "outerwear-heavy": 5, "hoodies-sweatshirts": 5, "headwear-cold": 5 },
"updatedAt": "2026-02-10T14:40:00Z"
}
Error Responses:
| Status | Code | When |
|---|---|---|
| 403 | PLAN_LIMIT | Starter tier cannot modify seasonal calendars |
| 400 | VALIDATION_ERROR | Invalid month values |
Sync Endpoints
Sync endpoints manage the Google Sheets supplemental feed pipeline and provide visibility into sync history and status.
POST /api/sync/sheet
Pushes current priority scores to the configured Google Sheet. The Sheet is then automatically fetched by Google Merchant Center on its daily schedule.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/sync/sheet |
| Auth Required | Yes |
| Rate Limit | 5/minute |
Request Body:
{
"mode": "full",
"activeOnly": true
}
| Field | Type | Required | Description |
|---|---|---|---|
mode | string | No | full (rewrite entire sheet) or delta (update changed rows). Default: full |
activeOnly | boolean | No | Only sync variants with inventory_quantity > 0. Default: true |
Response (202 Accepted):
{
"syncLogId": "sync-uuid-sheet-001",
"status": "started",
"sheetId": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms",
"sheetUrl": "https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms",
"estimatedRows": 15000,
"message": "Google Sheet sync started"
}
GET /api/sync/status
Returns the current sync status including last successful sync, pending changes, and next scheduled sync.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/sync/status |
| Auth Required | Yes |
| Rate Limit | 100/minute |
Response (200 OK):
{
"lastSync": {
"id": "sync-uuid-001",
"syncType": "sheet",
"status": "completed",
"productsSynced": 14892,
"errors": [],
"startedAt": "2026-02-10T08:00:00Z",
"completedAt": "2026-02-10T08:02:30Z",
"durationSeconds": 150
},
"pendingChanges": 45,
"sheetConfigured": true,
"gmcMerchantId": "123456789",
"nextScheduledSync": "2026-02-10T20:00:00Z",
"syncFrequency": "daily"
}
GET /api/sync/logs
Returns paginated sync history with filtering by type and status.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/sync/logs |
| Auth Required | Yes |
| Rate Limit | 100/minute |
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
page | int | 1 | Page number |
limit | int | 20 | Items per page (max 100) |
syncType | string | – | Filter: shopify, gmc, sheet |
status | string | – | Filter: started, completed, failed |
Response (200 OK):
{
"logs": [
{
"id": "sync-uuid-001",
"syncType": "sheet",
"status": "completed",
"productsSynced": 14892,
"errors": [],
"startedAt": "2026-02-10T08:00:00Z",
"completedAt": "2026-02-10T08:02:30Z"
},
{
"id": "sync-uuid-002",
"syncType": "shopify",
"status": "completed",
"productsSynced": 2425,
"errors": [],
"startedAt": "2026-02-10T07:00:00Z",
"completedAt": "2026-02-10T07:05:15Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 156,
"totalPages": 8
}
}
Settings Endpoints
Settings endpoints provide read and write access to tenant configuration including GMC connection details, Google Sheet URL, and scoring defaults.
GET /api/settings
Returns the full tenant configuration.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/settings |
| Auth Required | Yes |
| Rate Limit | 100/minute |
Response (200 OK):
{
"store": {
"shopifyShopDomain": "nexus-clothes.myshopify.com",
"planTier": "growth",
"status": "active",
"installedAt": "2026-01-15T10:00:00Z"
},
"googleMerchantCenter": {
"merchantId": "123456789",
"connected": true
},
"googleSheet": {
"sheetId": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms",
"sheetUrl": "https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms",
"configured": true
},
"scoring": {
"defaultPriority": 3,
"newArrivalDays": 14,
"newArrivalPriority": 5
},
"sync": {
"frequency": "daily",
"lastSyncAt": "2026-02-10T08:00:00Z"
}
}
PUT /api/settings
Updates tenant configuration. Only the fields included in the request body are modified.
| Property | Value |
|---|---|
| Method | PUT |
| Path | /api/settings |
| Auth Required | Yes |
| Rate Limit | 100/minute |
Request Body:
{
"googleMerchantCenter": {
"merchantId": "123456789"
},
"googleSheet": {
"sheetUrl": "https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms"
},
"scoring": {
"newArrivalDays": 30
}
}
Response (200 OK):
{
"updated": true,
"settings": {
"googleMerchantCenter": { "merchantId": "123456789", "connected": true },
"googleSheet": { "sheetId": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms", "configured": true },
"scoring": { "defaultPriority": 3, "newArrivalDays": 30, "newArrivalPriority": 5 }
}
}
Billing Endpoints
Billing endpoints manage the Shopify recurring application charge lifecycle. All billing is handled through Shopify’s Billing API, so AdPriority never collects payment information directly.
POST /api/billing/subscribe
Creates or updates a Shopify recurring application charge. Returns a confirmation URL that the merchant must visit to approve the charge.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/billing/subscribe |
| Auth Required | Yes |
| Rate Limit | 10/minute |
Request Body:
{
"plan": "growth"
}
| Field | Type | Required | Constraints |
|---|---|---|---|
plan | string | Yes | starter, growth, pro |
Response (200 OK):
{
"confirmationUrl": "https://nexus-clothes.myshopify.com/admin/charges/confirm?id=123456&signature=abc",
"chargeId": 123456789,
"plan": "growth",
"price": "79.00",
"trialDays": 14
}
GET /api/billing/status
Returns the current billing status and plan details.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/billing/status |
| Auth Required | Yes |
| Rate Limit | 100/minute |
Response (200 OK):
{
"plan": "growth",
"status": "active",
"price": "79.00",
"billingCycle": "monthly",
"activatedAt": "2026-01-15T10:00:00Z",
"trialEndsAt": null,
"features": {
"maxProducts": null,
"seasonalAutomation": true,
"tagModifiers": true,
"newArrivalBoost": true,
"googleAdsIntegration": false,
"syncFrequency": "hourly"
}
}
GDPR Compliance Endpoints
Shopify requires all apps to implement three GDPR webhook endpoints. These are called by Shopify when a customer or shop requests data handling. They are verified using HMAC-SHA256 signatures.
POST /api/gdpr/customers-data-request
Called when a customer requests their data. AdPriority does not store customer-identifiable data (only product and variant data), so this endpoint acknowledges the request and returns an empty payload.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/gdpr/customers-data-request |
| Auth Required | No (HMAC verified) |
Request Body (from Shopify):
{
"shop_id": 123456789,
"shop_domain": "nexus-clothes.myshopify.com",
"customer": { "id": 987654321, "email": "customer@example.com" },
"orders_requested": [123, 456]
}
Response (200 OK):
{
"message": "AdPriority does not store customer-identifiable data. No data to report."
}
POST /api/gdpr/customers-redact
Called when a customer requests deletion of their data. Since AdPriority stores no customer data, this is a no-op acknowledgment.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/gdpr/customers-redact |
| Auth Required | No (HMAC verified) |
Response (200 OK):
{
"message": "No customer data stored. Nothing to redact."
}
POST /api/gdpr/shop-redact
Called 48 hours after a shop uninstalls the app. Triggers full deletion of the tenant and all associated data.
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/gdpr/shop-redact |
| Auth Required | No (HMAC verified) |
Request Body (from Shopify):
{
"shop_id": 123456789,
"shop_domain": "nexus-clothes.myshopify.com"
}
Response (200 OK):
{
"message": "Tenant data for nexus-clothes.myshopify.com has been permanently deleted.",
"deletedRecords": {
"products": 5582,
"variants": 19537,
"priorityScores": 19537,
"categoryRules": 80,
"seasonalCalendars": 4,
"syncLogs": 2340,
"billing": 1
}
}
Shopify Webhooks
Shopify webhooks notify AdPriority of product changes in real time. All webhooks are verified using HMAC-SHA256 signatures with the app’s client secret.
WEBHOOK VERIFICATION
====================
Shopify sends:
X-Shopify-Hmac-SHA256: base64_encoded_hmac
X-Shopify-Shop-Domain: nexus-clothes.myshopify.com
X-Shopify-Topic: products/update
AdPriority verifies:
1. Compute HMAC-SHA256 of raw body using SHOPIFY_API_SECRET
2. Compare with X-Shopify-Hmac-SHA256 header
3. Reject if mismatch (return 401)
4. Process if valid (return 200 immediately, queue async work)
Registered Webhooks
| Topic | Path | Behavior |
|---|---|---|
products/create | /webhooks/shopify/products/create | Create product + variants in DB, calculate priority |
products/update | /webhooks/shopify/products/update | Update cached data, recalculate if type/tags changed |
products/delete | /webhooks/shopify/products/delete | Soft-delete product and variants |
app/uninstalled | /webhooks/shopify/app/uninstalled | Mark tenant as uninstalled, schedule data deletion |
Error Response Format
All error responses follow a consistent JSON structure.
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Priority must be between 0 and 5",
"details": {
"field": "priority",
"value": 7,
"constraint": "range:0-5"
}
}
}
Error Code Reference
| Code | HTTP Status | Description |
|---|---|---|
UNAUTHORIZED | 401 | Invalid or expired session token |
FORBIDDEN | 403 | Insufficient permissions for this action |
TENANT_SUSPENDED | 403 | Tenant account is suspended |
PLAN_LIMIT | 403 | Feature not available on current plan tier |
NOT_FOUND | 404 | Requested resource does not exist |
VALIDATION_ERROR | 400 | Request body failed validation |
DUPLICATE_RULE | 409 | Rule with same pattern and season exists |
SYNC_IN_PROGRESS | 409 | Another sync operation is running |
RATE_LIMITED | 429 | Too many requests |
INTERNAL_ERROR | 500 | Unhandled server error |
Rate Limit Headers
Every response includes rate limit information.
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 94
X-RateLimit-Reset: 1707220860
Request Validation
All request bodies are validated using Zod schemas before reaching the route handler. Invalid requests are rejected with a 400 status and detailed error messages.
// src/api/validators/schemas.ts
import { z } from 'zod';
export const PriorityOverrideSchema = z.object({
priority: z.number().int().min(0).max(5),
override: z.boolean().optional().default(true),
overrideReason: z.string().max(500).optional(),
});
export const CategoryRuleSchema = z.object({
productTypePattern: z.string().min(1).max(255),
season: z.enum(['winter', 'spring', 'summer', 'fall']).nullable().optional(),
basePriority: z.number().int().min(0).max(5),
modifiers: z.record(z.unknown()).optional().default({}),
});
export const SyncTriggerSchema = z.object({
mode: z.enum(['full', 'delta']).optional().default('full'),
activeOnly: z.boolean().optional().default(true),
});
export const BillingSubscribeSchema = z.object({
plan: z.enum(['starter', 'growth', 'pro']),
});
export const RecalculateSchema = z.object({
includeOverrides: z.boolean().optional().default(false),
productTypes: z.array(z.string()).optional(),
dryRun: z.boolean().optional().default(false),
});
Endpoint Summary
| Method | Path | Auth | Rate Limit | Tier | Description |
|---|---|---|---|---|---|
| POST | /auth/callback | No | 10/min | All | Shopify OAuth callback |
| GET | /auth/session | Yes | 100/min | All | Validate session |
| GET | /api/products | Yes | 100/min | All | List products (paginated) |
| GET | /api/products/:id | Yes | 100/min | All | Product detail with variants |
| POST | /api/products/sync | Yes | 5/min | All | Trigger Shopify sync |
| GET | /api/priorities | Yes | 100/min | All | List priority scores |
| PUT | /api/priorities/:variantId | Yes | 100/min | All | Manual override |
| POST | /api/priorities/recalculate | Yes | 5/min | All | Bulk recalculation |
| GET | /api/rules | Yes | 100/min | All | List category rules |
| POST | /api/rules | Yes | 100/min | All | Create rule |
| PUT | /api/rules/:id | Yes | 100/min | All | Update rule |
| DELETE | /api/rules/:id | Yes | 100/min | All | Delete rule |
| GET | /api/seasons | Yes | 100/min | All | List seasonal calendars |
| PUT | /api/seasons/:id | Yes | 100/min | Growth+ | Update calendar |
| POST | /api/sync/sheet | Yes | 5/min | All | Push to Google Sheet |
| GET | /api/sync/status | Yes | 100/min | All | Current sync status |
| GET | /api/sync/logs | Yes | 100/min | All | Sync history |
| GET | /api/settings | Yes | 100/min | All | Tenant settings |
| PUT | /api/settings | Yes | 100/min | All | Update settings |
| POST | /api/billing/subscribe | Yes | 10/min | All | Create subscription |
| GET | /api/billing/status | Yes | 100/min | All | Billing status |
| POST | /api/gdpr/customers-data-request | HMAC | – | All | GDPR data request |
| POST | /api/gdpr/customers-redact | HMAC | – | All | GDPR customer redact |
| POST | /api/gdpr/shop-redact | HMAC | – | All | GDPR shop redact |