docs: update CannaiQ API docs and client to use correct endpoints

- Changed endpoints from /dispensaries to /stores
- Changed auth from Bearer token to X-API-Key header
- Added new endpoint methods: getStoreProducts, getStoreProductMetrics,
  getStoreCompetitorSnapshot, getProductHistory, healthCheck, ping
- Updated documentation with all available endpoints and example responses
This commit is contained in:
kelly
2025-12-09 12:39:47 -07:00
parent 0fbf99c005
commit defeeffa07
2 changed files with 439 additions and 125 deletions

View File

@@ -13,6 +13,9 @@ use Illuminate\Support\Facades\Log;
* - Store metrics (pricing position, market share, trends)
* - Product metrics (velocity, pricing history, competitor positioning)
* - Competitor snapshots (out-of-stock, pricing, promotions)
*
* API Base URL: https://cannaiq.co/api/v1
* Authentication: X-API-Key header (trusted origins and localhost bypass auth)
*/
class CannaiqClient
{
@@ -45,38 +48,69 @@ class CannaiqClient
'Content-Type' => 'application/json',
];
// Add API key if configured (not needed for trusted origins)
// Add API key if configured (not needed for trusted origins *.cannabrands.app)
if ($this->apiKey) {
$headers['Authorization'] = 'Bearer '.$this->apiKey;
$headers['X-API-Key'] = $this->apiKey;
}
return $headers;
}
/**
* Get store-level metrics (pricing position, market share, trends)
* List all stores (paginated)
*
* @param string $externalStoreId CannaiQ store ID or slug
* @param int $limit Number of stores to return
* @param int $offset Pagination offset
*/
public function getStoreMetrics(string $externalStoreId): array
public function listStores(int $limit = 50, int $offset = 0): array
{
try {
$response = $this->http->get("/dispensaries/{$externalStoreId}");
$response = $this->http->get('/stores', [
'limit' => $limit,
'offset' => $offset,
]);
if ($response->successful()) {
return $response->json();
}
Log::warning('CannaiQ: Failed to fetch store metrics', [
'store_id' => $externalStoreId,
Log::warning('CannaiQ: Failed to list stores', [
'status' => $response->status(),
'body' => $response->body(),
]);
return ['error' => true, 'message' => 'Failed to fetch store metrics'];
return ['error' => true, 'message' => 'Failed to list stores', 'stores' => []];
} catch (\Exception $e) {
Log::error('CannaiQ: Exception fetching store metrics', [
'store_id' => $externalStoreId,
Log::error('CannaiQ: Exception listing stores', [
'error' => $e->getMessage(),
]);
return ['error' => true, 'message' => $e->getMessage(), 'stores' => []];
}
}
/**
* Get store details
*
* @param string $storeId CannaiQ store ID
*/
public function getStore(string $storeId): array
{
try {
$response = $this->http->get("/stores/{$storeId}");
if ($response->successful()) {
return $response->json();
}
Log::warning('CannaiQ: Failed to fetch store', [
'store_id' => $storeId,
'status' => $response->status(),
]);
return ['error' => true, 'message' => 'Failed to fetch store'];
} catch (\Exception $e) {
Log::error('CannaiQ: Exception fetching store', [
'store_id' => $storeId,
'error' => $e->getMessage(),
]);
@@ -85,16 +119,83 @@ class CannaiqClient
}
/**
* Get product-level metrics for a store
* Get store performance metrics
* Returns: product counts, brands, categories, price stats, stock health
*
* @param string $externalStoreId CannaiQ store ID or slug
* @param string $storeId CannaiQ store ID
*/
public function getStoreMetrics(string $storeId): array
{
try {
$response = $this->http->get("/stores/{$storeId}/metrics");
if ($response->successful()) {
return $response->json();
}
Log::warning('CannaiQ: Failed to fetch store metrics', [
'store_id' => $storeId,
'status' => $response->status(),
'body' => $response->body(),
]);
return ['error' => true, 'message' => 'Failed to fetch store metrics'];
} catch (\Exception $e) {
Log::error('CannaiQ: Exception fetching store metrics', [
'store_id' => $storeId,
'error' => $e->getMessage(),
]);
return ['error' => true, 'message' => $e->getMessage()];
}
}
/**
* Get store's product catalog
*
* @param string $storeId CannaiQ store ID
* @param int $limit Number of products to return
* @param int $offset Pagination offset
*/
public function getStoreProductMetrics(string $externalStoreId, int $limit = 100, int $offset = 0): array
public function getStoreProducts(string $storeId, int $limit = 100, int $offset = 0): array
{
try {
$response = $this->http->get("/dispensaries/{$externalStoreId}/products", [
$response = $this->http->get("/stores/{$storeId}/products", [
'limit' => $limit,
'offset' => $offset,
]);
if ($response->successful()) {
return $response->json();
}
Log::warning('CannaiQ: Failed to fetch store products', [
'store_id' => $storeId,
'status' => $response->status(),
]);
return ['error' => true, 'message' => 'Failed to fetch store products', 'products' => []];
} catch (\Exception $e) {
Log::error('CannaiQ: Exception fetching store products', [
'store_id' => $storeId,
'error' => $e->getMessage(),
]);
return ['error' => true, 'message' => $e->getMessage(), 'products' => []];
}
}
/**
* Get product-level metrics with price changes since last crawl
*
* @param string $storeId CannaiQ store ID
* @param int $limit Number of products to return
* @param int $offset Pagination offset
*/
public function getStoreProductMetrics(string $storeId, int $limit = 100, int $offset = 0): array
{
try {
$response = $this->http->get("/stores/{$storeId}/product-metrics", [
'limit' => $limit,
'offset' => $offset,
]);
@@ -104,14 +205,14 @@ class CannaiqClient
}
Log::warning('CannaiQ: Failed to fetch product metrics', [
'store_id' => $externalStoreId,
'store_id' => $storeId,
'status' => $response->status(),
]);
return ['error' => true, 'message' => 'Failed to fetch product metrics', 'products' => []];
} catch (\Exception $e) {
Log::error('CannaiQ: Exception fetching product metrics', [
'store_id' => $externalStoreId,
'store_id' => $storeId,
'error' => $e->getMessage(),
]);
@@ -120,41 +221,29 @@ class CannaiqClient
}
/**
* Get competitor snapshot for a store's market area
* Returns competitor pricing, out-of-stock items, and promo activity
* Get competitor snapshot for a store
* Returns: nearby competitors, price comparisons, brand overlap
*
* @param string $externalStoreId CannaiQ store ID or slug
* @param string $storeId CannaiQ store ID
*/
public function getStoreCompetitorSnapshot(string $externalStoreId): array
public function getStoreCompetitorSnapshot(string $storeId): array
{
try {
// First get the store details to find competitors in the area
$storeData = $this->getStoreMetrics($externalStoreId);
if (isset($storeData['error'])) {
return $storeData;
}
// Get all dispensaries (we'll filter by proximity on our end for now)
// In future, CannaiQ may provide a dedicated competitor endpoint
$response = $this->http->get('/dispensaries', [
'limit' => 50,
]);
$response = $this->http->get("/stores/{$storeId}/competitor-snapshot");
if ($response->successful()) {
$allStores = $response->json();
return [
'target_store' => $storeData,
'competitors' => $allStores['dispensaries'] ?? [],
'snapshot_time' => now()->toIso8601String(),
];
return $response->json();
}
return ['error' => true, 'message' => 'Failed to fetch competitor data'];
Log::warning('CannaiQ: Failed to fetch competitor snapshot', [
'store_id' => $storeId,
'status' => $response->status(),
]);
return ['error' => true, 'message' => 'Failed to fetch competitor snapshot'];
} catch (\Exception $e) {
Log::error('CannaiQ: Exception fetching competitor snapshot', [
'store_id' => $externalStoreId,
'store_id' => $storeId,
'error' => $e->getMessage(),
]);
@@ -162,56 +251,6 @@ class CannaiqClient
}
}
/**
* Get product details with price history
*
* @param int|string $productId CannaiQ product ID
*/
public function getProduct(int|string $productId): array
{
try {
$response = $this->http->get("/products/{$productId}");
if ($response->successful()) {
return $response->json();
}
return ['error' => true, 'message' => 'Failed to fetch product'];
} catch (\Exception $e) {
Log::error('CannaiQ: Exception fetching product', [
'product_id' => $productId,
'error' => $e->getMessage(),
]);
return ['error' => true, 'message' => $e->getMessage()];
}
}
/**
* Get historical price/stock snapshots for a product
*
* @param int|string $productId CannaiQ product ID
*/
public function getProductSnapshots(int|string $productId): array
{
try {
$response = $this->http->get("/products/{$productId}/snapshots");
if ($response->successful()) {
return $response->json();
}
return ['error' => true, 'message' => 'Failed to fetch snapshots', 'snapshots' => []];
} catch (\Exception $e) {
Log::error('CannaiQ: Exception fetching product snapshots', [
'product_id' => $productId,
'error' => $e->getMessage(),
]);
return ['error' => true, 'message' => $e->getMessage(), 'snapshots' => []];
}
}
/**
* Search products across all stores
*
@@ -240,7 +279,126 @@ class CannaiqClient
}
/**
* Check API health
* Get product details
*
* @param int|string $productId CannaiQ product ID
*/
public function getProduct(int|string $productId): array
{
try {
$response = $this->http->get("/products/{$productId}");
if ($response->successful()) {
return $response->json();
}
return ['error' => true, 'message' => 'Failed to fetch product'];
} catch (\Exception $e) {
Log::error('CannaiQ: Exception fetching product', [
'product_id' => $productId,
'error' => $e->getMessage(),
]);
return ['error' => true, 'message' => $e->getMessage()];
}
}
/**
* Get product price/stock history
*
* @param int|string $productId CannaiQ product ID
*/
public function getProductHistory(int|string $productId): array
{
try {
$response = $this->http->get("/products/{$productId}/history");
if ($response->successful()) {
return $response->json();
}
return ['error' => true, 'message' => 'Failed to fetch product history', 'history' => []];
} catch (\Exception $e) {
Log::error('CannaiQ: Exception fetching product history', [
'product_id' => $productId,
'error' => $e->getMessage(),
]);
return ['error' => true, 'message' => $e->getMessage(), 'history' => []];
}
}
/**
* List all brands
*/
public function listBrands(): array
{
try {
$response = $this->http->get('/brands');
if ($response->successful()) {
return $response->json();
}
return ['error' => true, 'message' => 'Failed to list brands', 'brands' => []];
} catch (\Exception $e) {
Log::error('CannaiQ: Exception listing brands', [
'error' => $e->getMessage(),
]);
return ['error' => true, 'message' => $e->getMessage(), 'brands' => []];
}
}
/**
* Get brand details and products
*
* @param string $brandName Brand name/slug
*/
public function getBrand(string $brandName): array
{
try {
$response = $this->http->get("/brands/{$brandName}");
if ($response->successful()) {
return $response->json();
}
return ['error' => true, 'message' => 'Failed to fetch brand'];
} catch (\Exception $e) {
Log::error('CannaiQ: Exception fetching brand', [
'brand' => $brandName,
'error' => $e->getMessage(),
]);
return ['error' => true, 'message' => $e->getMessage()];
}
}
/**
* List all categories
*/
public function listCategories(): array
{
try {
$response = $this->http->get('/categories');
if ($response->successful()) {
return $response->json();
}
return ['error' => true, 'message' => 'Failed to list categories', 'categories' => []];
} catch (\Exception $e) {
Log::error('CannaiQ: Exception listing categories', [
'error' => $e->getMessage(),
]);
return ['error' => true, 'message' => $e->getMessage(), 'categories' => []];
}
}
/**
* Check API health (full system health)
*/
public function healthCheck(): array
{
@@ -259,4 +417,24 @@ class CannaiqClient
];
}
}
/**
* Check basic API health
*/
public function ping(): array
{
try {
$response = $this->http->get('/health');
return [
'healthy' => $response->successful(),
'data' => $response->json(),
];
} catch (\Exception $e) {
return [
'healthy' => false,
'error' => $e->getMessage(),
];
}
}
}

View File

@@ -6,7 +6,7 @@ CannaiQ is the Marketing Intelligence Engine that powers competitive analysis, p
| Method | Description |
|--------|-------------|
| **Bearer Token** | `Authorization: Bearer <api_key>` |
| **X-API-Key Header** | `X-API-Key: <api_key>` |
| **Trusted Origins** | No auth needed for whitelisted domains |
### Trusted Origins (No Auth Required)
@@ -19,27 +19,86 @@ CannaiQ is the Marketing Intelligence Engine that powers competitive analysis, p
## Base URL
```
https://cannaiq.co/api/v1/
https://cannaiq.co/api/v1
```
## Available Endpoints
### Store Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/dispensaries` | GET | List all stores |
| `/dispensaries/:id` | GET | Store details |
| `/dispensaries/:id/products` | GET | Products for a store |
| `/products` | GET | Search products across stores |
| `/products/:id` | GET | Product details with price history |
| `/products/:id/snapshots` | GET | Historical price/stock data |
| `/health/full` | GET | System health status |
| `/stores` | GET | List all stores (paginated) |
| `/stores/:id` | GET | Get store details |
| `/stores/:id/products` | GET | Get store's product catalog |
| `/stores/:id/metrics` | GET | Store performance metrics (product counts, brands, categories, price stats, stock health) |
| `/stores/:id/product-metrics` | GET | Product-level metrics with price changes since last crawl |
| `/stores/:id/competitor-snapshot` | GET | Competitive intelligence (nearby competitors, price comparisons, brand overlap) |
### Product Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/products` | GET | Search products across all stores |
| `/products/:id` | GET | Get product details |
| `/products/:id/history` | GET | Get product price/stock history |
### Brand Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/brands` | GET | List all brands |
| `/brands/:name` | GET | Get brand details and products |
### Category Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/categories` | GET | List all categories |
### Health Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/health` | GET | Basic health check |
| `/health/full` | GET | Full system health status |
## Example Requests
### List Products for a Dispensary
### List Stores
```bash
curl "https://cannaiq.co/api/v1/dispensaries/123/products?limit=50"
curl "https://cannaiq.co/api/v1/stores?limit=50&offset=0"
```
### Get Store Metrics
```bash
curl "https://cannaiq.co/api/v1/stores/123/metrics"
```
**Response:**
```json
{
"store_name": "Green Leaf Dispensary",
"products_count": 245,
"brands_count": 42,
"categories": ["flower", "concentrates", "edibles"],
"average_price": 38.50,
"pricing_position": "competitive",
"market_share": 12.5,
"stock_health": {
"in_stock": 220,
"out_of_stock": 25,
"low_stock": 15
}
}
```
### Get Store Product Metrics
```bash
curl "https://cannaiq.co/api/v1/stores/123/product-metrics?limit=100"
```
**Response:**
@@ -51,28 +110,75 @@ curl "https://cannaiq.co/api/v1/dispensaries/123/products?limit=50"
"name": "Blue Dream 1g",
"brand_name": "Select",
"category": "flower",
"price": 35.00,
"current_price": 35.00,
"original_price": 40.00,
"price_change": -5.00,
"in_stock": true,
"thc_percent": 24.5
}
],
"total": 150,
"limit": 50,
"offset": 0
"meta": {
"total": 245,
"limit": 100,
"offset": 0
}
}
```
### Get Product with Price History
### Get Competitor Snapshot
```bash
curl "https://cannaiq.co/api/v1/products/456"
curl "https://cannaiq.co/api/v1/stores/123/competitor-snapshot"
```
### Get Historical Snapshots
**Response:**
```json
{
"target_store": {
"id": 123,
"name": "Green Leaf Dispensary",
"average_price": 38.50
},
"competitors": [
{
"id": 456,
"name": "Competitor Store",
"slug": "competitor-store",
"distance": 2.5,
"products_count": 180,
"average_price": 42.00
}
],
"snapshot_time": "2025-01-15T10:30:00Z"
}
```
### Get Product with History
```bash
curl "https://cannaiq.co/api/v1/products/456/snapshots"
curl "https://cannaiq.co/api/v1/products/456/history"
```
**Response:**
```json
{
"product": {
"id": 456,
"name": "Blue Dream 1g"
},
"history": [
{
"date": "2025-01-15",
"price": 35.00,
"in_stock": true
},
{
"date": "2025-01-14",
"price": 40.00,
"in_stock": true
}
]
}
```
## Rate Limits
@@ -108,28 +214,58 @@ curl "https://cannaiq.co/api/v1/products/456/snapshots"
### Laravel Service Example
```php
// app/Services/CannaiQ/CannaiQService.php
// app/Services/Cannaiq/CannaiqClient.php
namespace App\Services\CannaiQ;
namespace App\Services\Cannaiq;
use Illuminate\Support\Facades\Http;
class CannaiQService
class CannaiqClient
{
protected string $baseUrl = 'https://cannaiq.co/api/v1';
protected ?string $apiKey;
public function getDispensaryProducts(int $dispensaryId, int $limit = 50): array
public function __construct()
{
$response = Http::get("{$this->baseUrl}/dispensaries/{$dispensaryId}/products", [
'limit' => $limit,
]);
$this->apiKey = config('services.cannaiq.api_key');
}
protected function getHeaders(): array
{
$headers = [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
];
if ($this->apiKey) {
$headers['X-API-Key'] = $this->apiKey;
}
return $headers;
}
public function getStoreMetrics(string $storeId): array
{
$response = Http::withHeaders($this->getHeaders())
->get("{$this->baseUrl}/stores/{$storeId}/metrics");
return $response->json();
}
public function getProductSnapshots(int $productId): array
public function getStoreProductMetrics(string $storeId, int $limit = 100): array
{
$response = Http::get("{$this->baseUrl}/products/{$productId}/snapshots");
$response = Http::withHeaders($this->getHeaders())
->get("{$this->baseUrl}/stores/{$storeId}/product-metrics", [
'limit' => $limit,
]);
return $response->json();
}
public function getStoreCompetitorSnapshot(string $storeId): array
{
$response = Http::withHeaders($this->getHeaders())
->get("{$this->baseUrl}/stores/{$storeId}/competitor-snapshot");
return $response->json();
}
@@ -143,10 +279,10 @@ Since data refreshes every 4 hours, cache responses for 1-2 hours:
```php
use Illuminate\Support\Facades\Cache;
$products = Cache::remember(
"cannaiq:dispensary:{$id}:products",
$metrics = Cache::remember(
"cannaiq:store:{$storeId}:metrics",
now()->addHours(2),
fn () => $this->cannaiQ->getDispensaryProducts($id)
fn () => $this->cannaiqClient->getStoreMetrics($storeId)
);
```