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.

PropertyValue
MethodPOST
Path/auth/callback
Auth RequiredNo (this creates the session)
Rate Limit10/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:

StatusCodeWhen
400INVALID_HMACHMAC signature verification failed
400INVALID_NONCEState parameter does not match stored nonce
500TOKEN_EXCHANGE_FAILEDShopify 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.

PropertyValue
MethodGET
Path/auth/session
Auth RequiredYes (session token)
Rate Limit100/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:

StatusCodeWhen
401UNAUTHORIZEDSession token invalid or expired
403TENANT_SUSPENDEDTenant 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.

PropertyValue
MethodGET
Path/api/products
Auth RequiredYes
Rate Limit100/minute

Query Parameters:

ParamTypeDefaultDescription
pageint1Page number (1-indexed)
limitint50Items per page (max 250)
statusstringactiveFilter: active, archived, draft, all
productTypestringFilter by product type
vendorstringFilter by vendor
priorityintFilter by priority score (0-5)
searchstringFull-text search on title
sortBystringtitleSort field: title, priority, updatedAt, productType
sortOrderstringascSort 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.

PropertyValue
MethodGET
Path/api/products/:id
Auth RequiredYes
Rate Limit100/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:

StatusCodeWhen
404NOT_FOUNDProduct 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.

PropertyValue
MethodPOST
Path/api/products/sync
Auth RequiredYes
Rate Limit5/minute

Request Body:

{
  "mode": "incremental",
  "since": "2026-02-09T00:00:00Z"
}
FieldTypeRequiredDescription
modestringNofull (all products) or incremental (since date). Default: incremental
sinceISO 8601NoOnly 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:

StatusCodeWhen
409SYNC_IN_PROGRESSAnother sync is already running for this tenant
429RATE_LIMITEDToo 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.

PropertyValue
MethodGET
Path/api/priorities
Auth RequiredYes
Rate Limit100/minute

Query Parameters:

ParamTypeDefaultDescription
pageint1Page number
limitint50Items per page (max 250)
priorityintFilter by exact score (0-5)
minPriorityintFilter: score >= value
maxPriorityintFilter: score <= value
overridebooleanFilter: only manual overrides
productTypestringFilter by product type
categoryGroupstringFilter by category group (label_2)
needsSyncbooleanFilter: 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.

PropertyValue
MethodPUT
Path/api/priorities/:variantId
Auth RequiredYes
Rate Limit100/minute

Request Body:

{
  "priority": 5,
  "override": true,
  "overrideReason": "Bestseller - always push hard"
}
FieldTypeRequiredConstraints
priorityintYes0-5 inclusive
overridebooleanNoDefault: true when manually setting
overrideReasonstringNoFree 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:

StatusCodeWhen
400VALIDATION_ERRORPriority not in range 0-5
404NOT_FOUNDVariant 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.

PropertyValue
MethodPOST
Path/api/priorities/recalculate
Auth RequiredYes
Rate Limit5/minute

Request Body:

{
  "includeOverrides": false,
  "productTypes": ["Men-Bottoms-Stacked Jeans", "Men-Tops-Hoodies & Sweatshirts"],
  "dryRun": false
}
FieldTypeRequiredDescription
includeOverridesbooleanNoIf true, recalculate even manually overridden scores. Default: false
productTypesstring[]NoLimit recalculation to specific product types. Default: all types
dryRunbooleanNoIf 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.

PropertyValue
MethodGET
Path/api/rules
Auth RequiredYes
Rate Limit100/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.

PropertyValue
MethodPOST
Path/api/rules
Auth RequiredYes
Rate Limit100/minute

Request Body:

{
  "productTypePattern": "outerwear-heavy",
  "season": "winter",
  "basePriority": 5,
  "modifiers": {
    "tagAdjustments": {
      "NAME BRAND": { "adjustment": 1 },
      "DEAD50": { "override": 0 }
    }
  }
}
FieldTypeRequiredConstraints
productTypePatternstringYesMax 255 chars
seasonstringNowinter, spring, summer, fall, or null for all-season default
basePriorityintYes0-5 inclusive
modifiersobjectNoTag 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:

StatusCodeWhen
400VALIDATION_ERRORMissing required fields or invalid priority range
409DUPLICATE_RULEA 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.

PropertyValue
MethodPUT
Path/api/rules/:id
Auth RequiredYes
Rate Limit100/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.

PropertyValue
MethodDELETE
Path/api/rules/:id
Auth RequiredYes
Rate Limit100/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.

PropertyValue
MethodGET
Path/api/seasons
Auth RequiredYes
Rate Limit100/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.

PropertyValue
MethodPUT
Path/api/seasons/:id
Auth RequiredYes (Growth tier+)
Rate Limit100/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:

StatusCodeWhen
403PLAN_LIMITStarter tier cannot modify seasonal calendars
400VALIDATION_ERRORInvalid 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.

PropertyValue
MethodPOST
Path/api/sync/sheet
Auth RequiredYes
Rate Limit5/minute

Request Body:

{
  "mode": "full",
  "activeOnly": true
}
FieldTypeRequiredDescription
modestringNofull (rewrite entire sheet) or delta (update changed rows). Default: full
activeOnlybooleanNoOnly 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.

PropertyValue
MethodGET
Path/api/sync/status
Auth RequiredYes
Rate Limit100/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.

PropertyValue
MethodGET
Path/api/sync/logs
Auth RequiredYes
Rate Limit100/minute

Query Parameters:

ParamTypeDefaultDescription
pageint1Page number
limitint20Items per page (max 100)
syncTypestringFilter: shopify, gmc, sheet
statusstringFilter: 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.

PropertyValue
MethodGET
Path/api/settings
Auth RequiredYes
Rate Limit100/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.

PropertyValue
MethodPUT
Path/api/settings
Auth RequiredYes
Rate Limit100/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.

PropertyValue
MethodPOST
Path/api/billing/subscribe
Auth RequiredYes
Rate Limit10/minute

Request Body:

{
  "plan": "growth"
}
FieldTypeRequiredConstraints
planstringYesstarter, 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.

PropertyValue
MethodGET
Path/api/billing/status
Auth RequiredYes
Rate Limit100/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.

PropertyValue
MethodPOST
Path/api/gdpr/customers-data-request
Auth RequiredNo (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.

PropertyValue
MethodPOST
Path/api/gdpr/customers-redact
Auth RequiredNo (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.

PropertyValue
MethodPOST
Path/api/gdpr/shop-redact
Auth RequiredNo (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

TopicPathBehavior
products/create/webhooks/shopify/products/createCreate product + variants in DB, calculate priority
products/update/webhooks/shopify/products/updateUpdate cached data, recalculate if type/tags changed
products/delete/webhooks/shopify/products/deleteSoft-delete product and variants
app/uninstalled/webhooks/shopify/app/uninstalledMark 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

CodeHTTP StatusDescription
UNAUTHORIZED401Invalid or expired session token
FORBIDDEN403Insufficient permissions for this action
TENANT_SUSPENDED403Tenant account is suspended
PLAN_LIMIT403Feature not available on current plan tier
NOT_FOUND404Requested resource does not exist
VALIDATION_ERROR400Request body failed validation
DUPLICATE_RULE409Rule with same pattern and season exists
SYNC_IN_PROGRESS409Another sync operation is running
RATE_LIMITED429Too many requests
INTERNAL_ERROR500Unhandled 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

MethodPathAuthRate LimitTierDescription
POST/auth/callbackNo10/minAllShopify OAuth callback
GET/auth/sessionYes100/minAllValidate session
GET/api/productsYes100/minAllList products (paginated)
GET/api/products/:idYes100/minAllProduct detail with variants
POST/api/products/syncYes5/minAllTrigger Shopify sync
GET/api/prioritiesYes100/minAllList priority scores
PUT/api/priorities/:variantIdYes100/minAllManual override
POST/api/priorities/recalculateYes5/minAllBulk recalculation
GET/api/rulesYes100/minAllList category rules
POST/api/rulesYes100/minAllCreate rule
PUT/api/rules/:idYes100/minAllUpdate rule
DELETE/api/rules/:idYes100/minAllDelete rule
GET/api/seasonsYes100/minAllList seasonal calendars
PUT/api/seasons/:idYes100/minGrowth+Update calendar
POST/api/sync/sheetYes5/minAllPush to Google Sheet
GET/api/sync/statusYes100/minAllCurrent sync status
GET/api/sync/logsYes100/minAllSync history
GET/api/settingsYes100/minAllTenant settings
PUT/api/settingsYes100/minAllUpdate settings
POST/api/billing/subscribeYes10/minAllCreate subscription
GET/api/billing/statusYes100/minAllBilling status
POST/api/gdpr/customers-data-requestHMACAllGDPR data request
POST/api/gdpr/customers-redactHMACAllGDPR customer redact
POST/api/gdpr/shop-redactHMACAllGDPR shop redact