Chapter 15: API Design
API Architecture
AdPriority exposes a RESTful JSON API served by Express.js with TypeScript. All endpoints except Shopify webhooks require a valid Shopify session token. The API is designed for consumption by the embedded Shopify Polaris frontend via App Bridge authenticated fetch. Authentication uses Token Exchange exclusively – there is no legacy OAuth callback endpoint.
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 |
+---------------------------+
|
+----> /api/v1/products/* Product listing, sync triggers
+----> /api/v1/priorities/* Score management, overrides
+----> /api/v1/rules/* Category rule CRUD
+----> /api/v1/seasons/* Seasonal calendar management
+----> /api/v1/sync/* Google Sheets push, sync status
+----> /api/v1/settings/* Tenant configuration
+----> /api/v1/billing/* Subscription management
+----> /api/v1/gdpr/* Mandatory GDPR compliance webhooks
+----> /api/v1/webhooks/* Shopify product webhooks (HMAC verified)
+----> /api/v1/health Liveness probe
+----> /api/v1/health/ready Readiness probe (DB + Redis)
Base Configuration
Base URL (Development): http://localhost:3010
Base URL (Production): https://app.adpriority.com
API Prefix: /api/v1
Content-Type: application/json
Authentication: Shopify App Bridge session token (Token Exchange)
Rate Limit: 100 requests/minute (general), 10/minute (sync)
Health Endpoints
Health endpoints are unauthenticated and used by load balancers and container orchestrators.
GET /api/v1/health
Liveness probe. Returns 200 if the process is running. Does not check downstream dependencies.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/health |
| Auth Required | No |
Response (200 OK):
{
"status": "ok",
"version": "2.0.0",
"uptime": 86400
}
GET /api/v1/health/ready
Readiness probe. Returns 200 only when the server can serve traffic (database and Redis connections are live).
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/health/ready |
| Auth Required | No |
Response (200 OK):
{
"status": "ready",
"checks": {
"database": "ok",
"redis": "ok"
}
}
Response (503 Service Unavailable):
{
"status": "not_ready",
"checks": {
"database": "ok",
"redis": "error: connection refused"
}
}
Session Validation
Session validation is handled implicitly by the Shopify session token middleware on every authenticated request. The frontend calls the session endpoint on page load to confirm the session is still valid and retrieve tenant information.
GET /api/v1/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 | /api/v1/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": "adpriority_pro",
"status": "active",
"gmcMerchantId": "REDACTED",
"googleSheetId": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms"
},
"billing": {
"plan": "adpriority_pro",
"status": "active",
"activatedAt": "2026-01-15T10:00:00Z"
},
"features": {
"seasonalAutomation": true,
"tagModifiers": true,
"newArrivalBoost": true,
"googleAdsIntegration": true,
"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/v1/products
Returns a cursor-paginated, filterable list of products with their current priority scores.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/products |
| Auth Required | Yes |
| Rate Limit | 100/minute |
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
cursor | string | – | Cursor for next page (from previous response) |
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": {
"nextCursor": "eyJpZCI6InByb2QtdXVpZC0wMDIifQ==",
"prevCursor": null,
"hasNext": true,
"hasPrev": false,
"total": 2425
}
}
GET /api/v1/products/:id
Returns full detail for a single product, including all variants and their individual priority scores.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/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/v1/products/sync
Triggers a Shopify product sync that fetches all products from the Shopify Admin GraphQL 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/v1/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/v1/priorities
Returns priority scores with filtering and aggregation options.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/priorities |
| Auth Required | Yes |
| Rate Limit | 100/minute |
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
cursor | string | – | Cursor for next page |
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": {
"nextCursor": "eyJpZCI6InZhci11dWlkLTAwMSJ9",
"prevCursor": null,
"hasNext": true,
"hasPrev": false,
"total": 2425
}
}
PUT /api/v1/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/v1/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/v1/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/v1/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/v1/rules
Returns all category rules for the tenant, sorted by product type pattern.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/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/v1/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/v1/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/v1/rules/:id
Updates an existing category rule. Any field not included in the request body is left unchanged.
| Property | Value |
|---|---|
| Method | PUT |
| Path | /api/v1/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/v1/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/v1/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 AdPriority Pro tier can customize dates and add micro-seasons.
GET /api/v1/seasons
Returns all seasonal calendars for the tenant, including which season is currently active.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/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/v1/seasons/:id
Updates a seasonal calendar’s dates or category overrides.
| Property | Value |
|---|---|
| Method | PUT |
| Path | /api/v1/seasons/:id |
| Auth Required | Yes (AdPriority Pro) |
| Rate Limit | 100/minute |
Request Body:
{
"startMonth": 12,
"endMonth": 2,
"categoryOverrides": {
"outerwear-heavy": 5,
"hoodies-sweatshirts": 5,
"headwear-cold-weather": 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-weather": 5 },
"updatedAt": "2026-02-10T14:40:00Z"
}
Error Responses:
| Status | Code | When |
|---|---|---|
| 403 | PLAN_LIMIT | AdPriority tier cannot modify seasonal calendars (AdPriority Pro required) |
| 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/v1/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/v1/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/v1/sync/status
Returns the current sync status including last successful sync, pending changes, and next scheduled sync.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/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": "REDACTED",
"nextScheduledSync": "2026-02-10T20:00:00Z",
"syncFrequency": "daily"
}
GET /api/v1/sync/logs
Returns paginated sync history with filtering by type and status.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/sync/logs |
| Auth Required | Yes |
| Rate Limit | 100/minute |
Query Parameters:
| Param | Type | Default | Description |
|---|---|---|---|
cursor | string | – | Cursor for next page |
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": {
"nextCursor": "eyJpZCI6InN5bmMtdXVpZC0wMDIifQ==",
"prevCursor": null,
"hasNext": true,
"hasPrev": false,
"total": 156
}
}
Settings Endpoints
Settings endpoints provide read and write access to tenant configuration including GMC connection details, Google Sheet URL, and scoring defaults.
GET /api/v1/settings
Returns the full tenant configuration.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/settings |
| Auth Required | Yes |
| Rate Limit | 100/minute |
Response (200 OK):
{
"store": {
"shopifyShopDomain": "nexus-clothes.myshopify.com",
"planTier": "adpriority_pro",
"status": "active",
"installedAt": "2026-01-15T10:00:00Z"
},
"googleMerchantCenter": {
"merchantId": "REDACTED",
"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/v1/settings
Updates tenant configuration. Only the fields included in the request body are modified.
| Property | Value |
|---|---|
| Method | PUT |
| Path | /api/v1/settings |
| Auth Required | Yes |
| Rate Limit | 100/minute |
Request Body:
{
"googleMerchantCenter": {
"merchantId": "REDACTED"
},
"googleSheet": {
"sheetUrl": "https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms"
},
"scoring": {
"newArrivalDays": 30
}
}
Response (200 OK):
{
"updated": true,
"settings": {
"googleMerchantCenter": { "merchantId": "REDACTED", "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/v1/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/v1/billing/subscribe |
| Auth Required | Yes |
| Rate Limit | 10/minute |
Request Body:
{
"plan": "adpriority_pro"
}
| Field | Type | Required | Constraints |
|---|---|---|---|
plan | string | Yes | adpriority, adpriority_pro |
Response (200 OK):
{
"confirmationUrl": "https://nexus-clothes.myshopify.com/admin/charges/confirm?id=123456&signature=abc",
"chargeId": 123456789,
"plan": "adpriority_pro",
"price": "149.00",
"trialDays": 14
}
GET /api/v1/billing/status
Returns the current billing status and plan details.
| Property | Value |
|---|---|
| Method | GET |
| Path | /api/v1/billing/status |
| Auth Required | Yes |
| Rate Limit | 100/minute |
Response (200 OK):
{
"plan": "adpriority_pro",
"status": "active",
"price": "149.00",
"billingCycle": "monthly",
"activatedAt": "2026-01-15T10:00:00Z",
"trialEndsAt": null,
"features": {
"maxProducts": null,
"seasonalAutomation": true,
"tagModifiers": true,
"newArrivalBoost": true,
"googleAdsIntegration": true,
"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/v1/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/v1/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/v1/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/v1/gdpr/customers-redact |
| Auth Required | No (HMAC verified) |
Response (200 OK):
{
"message": "No customer data stored. Nothing to redact."
}
POST /api/v1/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/v1/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 and deduplicated via the X-Shopify-Webhook-Id header.
WEBHOOK VERIFICATION
====================
Shopify sends:
X-Shopify-Hmac-SHA256: base64_encoded_hmac
X-Shopify-Shop-Domain: nexus-clothes.myshopify.com
X-Shopify-Topic: products/update
X-Shopify-Webhook-Id: unique-webhook-id
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. Check X-Shopify-Webhook-Id against dedup cache
5. Skip if duplicate; otherwise process (return 200 immediately, queue async work)
Registered Webhooks
| Topic | Path | Behavior |
|---|---|---|
PRODUCTS_CREATE | /api/v1/webhooks/shopify/products/create | Create product + variants in DB, calculate priority |
PRODUCTS_UPDATE | /api/v1/webhooks/shopify/products/update | Update cached data, recalculate if type/tags changed |
PRODUCTS_DELETE | /api/v1/webhooks/shopify/products/delete | Soft-delete product and variants |
INVENTORY_LEVELS_UPDATE | /api/v1/webhooks/shopify/inventory/update | Recalculate inventory status label |
COLLECTIONS_UPDATE | /api/v1/webhooks/shopify/collections/update | Re-evaluate category group |
APP_SUBSCRIPTIONS_UPDATE | /api/v1/webhooks/shopify/subscriptions/update | Update tenant plan tier |
APP_UNINSTALLED | /api/v1/webhooks/shopify/app/uninstalled | Mark tenant as uninstalled, schedule data deletion |
SHOP_UPDATE | /api/v1/webhooks/shopify/shop/update | Update tenant shop metadata |
BULK_OPERATIONS_FINISH | /api/v1/webhooks/shopify/bulk-operations/finish | Process bulk operation results |
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(['adpriority', 'adpriority_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 |
|---|---|---|---|---|---|
| GET | /api/v1/health | No | – | All | Liveness probe |
| GET | /api/v1/health/ready | No | – | All | Readiness probe (DB + Redis) |
| GET | /api/v1/session | Yes | 100/min | All | Validate session |
| GET | /api/v1/products | Yes | 100/min | All | List products (cursor-paginated) |
| GET | /api/v1/products/:id | Yes | 100/min | All | Product detail with variants |
| POST | /api/v1/products/sync | Yes | 5/min | All | Trigger Shopify sync |
| GET | /api/v1/priorities | Yes | 100/min | All | List priority scores |
| PUT | /api/v1/priorities/:variantId | Yes | 100/min | All | Manual override |
| POST | /api/v1/priorities/recalculate | Yes | 5/min | All | Bulk recalculation |
| GET | /api/v1/rules | Yes | 100/min | All | List category rules |
| POST | /api/v1/rules | Yes | 100/min | All | Create rule |
| PUT | /api/v1/rules/:id | Yes | 100/min | All | Update rule |
| DELETE | /api/v1/rules/:id | Yes | 100/min | All | Delete rule |
| GET | /api/v1/seasons | Yes | 100/min | All | List seasonal calendars |
| PUT | /api/v1/seasons/:id | Yes | 100/min | AdPriority Pro | Update calendar |
| POST | /api/v1/sync/sheet | Yes | 5/min | All | Push to Google Sheet |
| GET | /api/v1/sync/status | Yes | 100/min | All | Current sync status |
| GET | /api/v1/sync/logs | Yes | 100/min | All | Sync history |
| GET | /api/v1/settings | Yes | 100/min | All | Tenant settings |
| PUT | /api/v1/settings | Yes | 100/min | All | Update settings |
| POST | /api/v1/billing/subscribe | Yes | 10/min | All | Create subscription |
| GET | /api/v1/billing/status | Yes | 100/min | All | Billing status |
| POST | /api/v1/gdpr/customers-data-request | HMAC | – | All | GDPR data request |
| POST | /api/v1/gdpr/customers-redact | HMAC | – | All | GDPR customer redact |
| POST | /api/v1/gdpr/shop-redact | HMAC | – | All | GDPR shop redact |