whale-code 6.5.7 → 6.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +14 -2
  2. package/dist/cli/services/agent-loop.js +26 -2
  3. package/dist/cli/services/agent-loop.js.map +1 -1
  4. package/dist/cli/services/hooks.js +2 -1
  5. package/dist/cli/services/hooks.js.map +1 -1
  6. package/dist/cli/services/telemetry-spans.js +1 -0
  7. package/dist/cli/services/telemetry-spans.js.map +1 -1
  8. package/dist/cli/services/telemetry.d.ts +23 -0
  9. package/dist/cli/services/telemetry.js +45 -1
  10. package/dist/cli/services/telemetry.js.map +1 -1
  11. package/dist/server/handlers/__test-utils__/test-db.d.ts +17 -3
  12. package/dist/server/handlers/__test-utils__/test-db.js +113 -14
  13. package/dist/server/handlers/__test-utils__/test-db.js.map +1 -1
  14. package/dist/server/handlers/affiliates.d.ts +9 -0
  15. package/dist/server/handlers/affiliates.js +197 -0
  16. package/dist/server/handlers/affiliates.js.map +1 -0
  17. package/dist/server/handlers/api-docs.d.ts +4 -2
  18. package/dist/server/handlers/api-docs.js +204 -1681
  19. package/dist/server/handlers/api-docs.js.map +1 -1
  20. package/dist/server/handlers/campaigns.d.ts +9 -0
  21. package/dist/server/handlers/campaigns.js +237 -0
  22. package/dist/server/handlers/campaigns.js.map +1 -0
  23. package/dist/server/handlers/catalog-schemas.js +9 -9
  24. package/dist/server/handlers/catalog-schemas.js.map +1 -1
  25. package/dist/server/handlers/catalog.js +1 -1
  26. package/dist/server/handlers/catalog.js.map +1 -1
  27. package/dist/server/handlers/comms-documents.js +28 -2
  28. package/dist/server/handlers/comms-documents.js.map +1 -1
  29. package/dist/server/handlers/comms-pdf-generation.js +25 -3
  30. package/dist/server/handlers/comms-pdf-generation.js.map +1 -1
  31. package/dist/server/handlers/comms-pdf-helpers.js +4 -4
  32. package/dist/server/handlers/comms-pdf-helpers.js.map +1 -1
  33. package/dist/server/handlers/comms.d.ts +100 -0
  34. package/dist/server/handlers/comms.js +146 -12
  35. package/dist/server/handlers/comms.js.map +1 -1
  36. package/dist/server/handlers/coupons.d.ts +9 -0
  37. package/dist/server/handlers/coupons.js +220 -0
  38. package/dist/server/handlers/coupons.js.map +1 -0
  39. package/dist/server/handlers/embeddings.js +1 -1
  40. package/dist/server/handlers/embeddings.js.map +1 -1
  41. package/dist/server/handlers/enrichment.js +2 -622
  42. package/dist/server/handlers/enrichment.js.map +1 -1
  43. package/dist/server/handlers/fulfillment.d.ts +9 -0
  44. package/dist/server/handlers/fulfillment.js +209 -0
  45. package/dist/server/handlers/fulfillment.js.map +1 -0
  46. package/dist/server/handlers/google-ads.d.ts +24 -0
  47. package/dist/server/handlers/google-ads.js +2199 -0
  48. package/dist/server/handlers/google-ads.js.map +1 -0
  49. package/dist/server/handlers/invoices.d.ts +9 -0
  50. package/dist/server/handlers/invoices.js +252 -0
  51. package/dist/server/handlers/invoices.js.map +1 -0
  52. package/dist/server/handlers/loyalty.d.ts +9 -0
  53. package/dist/server/handlers/loyalty.js +197 -0
  54. package/dist/server/handlers/loyalty.js.map +1 -0
  55. package/dist/server/handlers/meta-ads-graph-api.js +18 -3
  56. package/dist/server/handlers/meta-ads-graph-api.js.map +1 -1
  57. package/dist/server/handlers/phone.d.ts +9 -0
  58. package/dist/server/handlers/phone.js +197 -0
  59. package/dist/server/handlers/phone.js.map +1 -0
  60. package/dist/server/handlers/pipeline.d.ts +9 -0
  61. package/dist/server/handlers/pipeline.js +277 -0
  62. package/dist/server/handlers/pipeline.js.map +1 -0
  63. package/dist/server/handlers/qr-codes.d.ts +9 -0
  64. package/dist/server/handlers/qr-codes.js +198 -0
  65. package/dist/server/handlers/qr-codes.js.map +1 -0
  66. package/dist/server/handlers/reviews.d.ts +9 -0
  67. package/dist/server/handlers/reviews.js +171 -0
  68. package/dist/server/handlers/reviews.js.map +1 -0
  69. package/dist/server/handlers/segments.d.ts +9 -0
  70. package/dist/server/handlers/segments.js +229 -0
  71. package/dist/server/handlers/segments.js.map +1 -0
  72. package/dist/server/handlers/social.d.ts +9 -0
  73. package/dist/server/handlers/social.js +81 -0
  74. package/dist/server/handlers/social.js.map +1 -0
  75. package/dist/server/handlers/tax.d.ts +9 -0
  76. package/dist/server/handlers/tax.js +182 -0
  77. package/dist/server/handlers/tax.js.map +1 -0
  78. package/dist/server/handlers/wallet.d.ts +9 -0
  79. package/dist/server/handlers/wallet.js +203 -0
  80. package/dist/server/handlers/wallet.js.map +1 -0
  81. package/dist/server/handlers/webhooks-mgmt.d.ts +9 -0
  82. package/dist/server/handlers/webhooks-mgmt.js +181 -0
  83. package/dist/server/handlers/webhooks-mgmt.js.map +1 -0
  84. package/dist/server/handlers/wholesale.d.ts +9 -0
  85. package/dist/server/handlers/wholesale.js +219 -0
  86. package/dist/server/handlers/wholesale.js.map +1 -0
  87. package/dist/server/index.js +20 -9
  88. package/dist/server/index.js.map +1 -1
  89. package/dist/server/lib/clickhouse-buffer.js +1 -0
  90. package/dist/server/lib/clickhouse-buffer.js.map +1 -1
  91. package/dist/server/lib/coa-renderer.d.ts +1 -1
  92. package/dist/server/lib/coa-renderer.js +32 -10
  93. package/dist/server/lib/coa-renderer.js.map +1 -1
  94. package/dist/server/server-worker.d.ts +1 -0
  95. package/dist/server/server-worker.js +464 -3
  96. package/dist/server/server-worker.js.map +1 -1
  97. package/dist/server/tool-router.js +118 -4
  98. package/dist/server/tool-router.js.map +1 -1
  99. package/package.json +28 -4
  100. package/vendor/ink/package.json +0 -2
  101. package/whale-logo.png +0 -0
@@ -1,1517 +1,153 @@
1
- // server/handlers/api-docs.ts — Static API documentation reference for agents
1
+ // server/handlers/api-docs.ts — Dynamic API documentation from gateway OpenAPI spec
2
2
 
3
3
  const GATEWAY_BASE_URL = "https://whale-gateway.fly.dev";
4
+ const OPENAPI_URL = `${GATEWAY_BASE_URL}/openapi.json`;
5
+ const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
4
6
 
5
7
  // ---------------------------------------------------------------------------
6
- // Types
8
+ // Cached OpenAPI spec — fetched from gateway on demand
7
9
  // ---------------------------------------------------------------------------
8
10
 
9
- // ---------------------------------------------------------------------------
10
- // Endpoint registry — every public REST API route
11
- // ---------------------------------------------------------------------------
12
-
13
- const SECTIONS = {
14
- products: {
15
- name: "Products",
16
- description: "Product catalog CRUD, variations, and categories",
17
- base: "/v1/stores/:storeId/products",
18
- endpoints: [{
19
- method: "GET",
20
- path: "/v1/stores/:storeId/products",
21
- scope: "read:products",
22
- description: "List products with pagination and filters",
23
- params: {
24
- limit: "Number (1-100, default 25)",
25
- starting_after: "Product ID cursor for forward pagination",
26
- ending_before: "Product ID cursor for backward pagination",
27
- category_id: "Filter by category UUID",
28
- status: "Filter by status: published, draft, archived",
29
- type: "Filter by type: simple, variable, grouped",
30
- search: "Full-text search on name and SKU",
31
- include: "Set to 'none' to skip loading variations (faster for large catalogs)"
32
- },
33
- response: '{ object: "list", data: Product[], has_more: boolean, url: string }'
34
- }, {
35
- method: "GET",
36
- path: "/v1/stores/:storeId/products/:id",
37
- scope: "read:products",
38
- description: "Get a single product with variations and inventory",
39
- response: "Product (includes pricing_data, field_values, inventory, variations)"
40
- }, {
41
- method: "POST",
42
- path: "/v1/stores/:storeId/products",
43
- scope: "write:products",
44
- description: "Create a new product",
45
- body: {
46
- name: "Required. Product name",
47
- sku: "SKU code",
48
- category_id: "Category UUID",
49
- status: "published | draft (default: draft)",
50
- type: "simple | variable | grouped (default: simple)",
51
- description: "Full description",
52
- short_description: "Summary text",
53
- cost_price: "Cost per unit",
54
- pricing_data: "Pricing tiers JSON",
55
- field_values: "Custom fields JSON (must match category field schema)",
56
- featured_image: "Image URL",
57
- image_gallery: "Array of image URLs",
58
- manage_stock: "Boolean — track inventory",
59
- stock_quantity: "Initial stock (if manage_stock)"
60
- },
61
- response: "Product"
62
- }, {
63
- method: "PATCH",
64
- path: "/v1/stores/:storeId/products/:id",
65
- scope: "write:products",
66
- description: "Update an existing product (partial update)",
67
- body: {
68
- "...": "Any product fields to update"
69
- },
70
- response: "Product"
71
- }, {
72
- method: "DELETE",
73
- path: "/v1/stores/:storeId/products/:id",
74
- scope: "write:products",
75
- description: "Archive a product (soft delete)",
76
- response: "{ deleted: true }"
77
- }, {
78
- method: "GET",
79
- path: "/v1/stores/:storeId/products/:id/variations",
80
- scope: "read:products",
81
- description: "List variations for a product",
82
- response: '{ object: "list", data: Variation[] }'
83
- }, {
84
- method: "POST",
85
- path: "/v1/stores/:storeId/products/:id/variations",
86
- scope: "write:products",
87
- description: "Create a product variation",
88
- body: {
89
- name: "Variation name (e.g. 'Small', '1oz')",
90
- sku: "Variation SKU",
91
- pricing_data: "Variation-specific pricing",
92
- field_values: "Variation-specific field values",
93
- stock_quantity: "Initial stock"
94
- },
95
- response: "Variation"
96
- }, {
97
- method: "PATCH",
98
- path: "/v1/stores/:storeId/products/:id/variations/:variationId",
99
- scope: "write:products",
100
- description: "Update a variation",
101
- body: {
102
- "...": "Any variation fields"
103
- },
104
- response: "Variation"
105
- }, {
106
- method: "DELETE",
107
- path: "/v1/stores/:storeId/products/:id/variations/:variationId",
108
- scope: "write:products",
109
- description: "Delete a variation",
110
- response: "{ deleted: true }"
111
- }]
112
- },
113
- orders: {
114
- name: "Orders",
115
- description: "Order management — list, view, update status, void, and refund",
116
- base: "/v1/stores/:storeId/orders",
117
- endpoints: [{
118
- method: "GET",
119
- path: "/v1/stores/:storeId/orders",
120
- scope: "read:orders",
121
- description: "List orders with pagination",
122
- params: {
123
- limit: "Number (1-100, default 25)",
124
- starting_after: "Order ID cursor",
125
- ending_before: "Order ID cursor",
126
- status: "Filter: pending, confirmed, processing, shipped, delivered, cancelled",
127
- customer_id: "Filter by customer UUID",
128
- location_id: "Filter by location UUID",
129
- created_after: "ISO datetime — filter orders created on or after this date",
130
- created_before: "ISO datetime — filter orders created on or before this date"
131
- },
132
- response: '{ object: "list", data: Order[], has_more: boolean }'
133
- }, {
134
- method: "GET",
135
- path: "/v1/stores/:storeId/orders/:id",
136
- scope: "read:orders",
137
- description: "Get a single order with line items",
138
- response: "Order (includes line_items[], customer, payment_details)"
139
- }, {
140
- method: "PATCH",
141
- path: "/v1/stores/:storeId/orders/:id",
142
- scope: "write:orders",
143
- description: "Update order status or fields",
144
- body: {
145
- status: "Order status: pending, confirmed, processing, preparing, packed, ready, shipped, in_transit, delivered, completed, cancelled, refunded",
146
- fulfillment_status: "unfulfilled, partial, fulfilled, cancelled, shipped, in_transit, delivered, ready_for_pickup, ready",
147
- tracking_number: "Shipping tracking number",
148
- tracking_url: "Tracking URL",
149
- shipping_service: "Carrier name",
150
- shipping_method: "Shipping method",
151
- staff_notes: "Internal notes"
152
- },
153
- response: "Order"
154
- }, {
155
- method: "POST",
156
- path: "/v1/stores/:storeId/orders/:id/void",
157
- scope: "write:orders",
158
- description: "Void an unsettled order",
159
- response: "Order (status: voided)",
160
- notes: "Only works on orders where payment has not yet settled"
161
- }, {
162
- method: "POST",
163
- path: "/v1/stores/:storeId/orders/:id/refund",
164
- scope: "write:orders",
165
- description: "Refund a settled order",
166
- body: {
167
- amount: "Partial refund amount (optional — omit for full refund)",
168
- reason: "Refund reason"
169
- },
170
- response: "Order (includes refund details)"
171
- }]
172
- },
173
- cart: {
174
- name: "Cart",
175
- description: "Shopping cart — create, add/update/remove items, view",
176
- base: "/v1/stores/:storeId/cart",
177
- endpoints: [{
178
- method: "POST",
179
- path: "/v1/stores/:storeId/cart",
180
- scope: "write:cart",
181
- description: "Create a new shopping cart",
182
- body: {
183
- customer_id: "Optional customer UUID",
184
- location_id: "Optional location UUID (auto-resolves default)"
185
- },
186
- response: "Cart { id, items: [], totals }"
187
- }, {
188
- method: "GET",
189
- path: "/v1/stores/:storeId/cart/:cartId",
190
- scope: "read:cart",
191
- description: "Get cart with all items and computed totals",
192
- response: "Cart { id, items: CartItem[], totals: { subtotal, tax, total } }"
193
- }, {
194
- method: "POST",
195
- path: "/v1/stores/:storeId/cart/:cartId/items",
196
- scope: "write:cart",
197
- description: "Add an item to the cart",
198
- body: {
199
- product_id: "Required. Product UUID",
200
- quantity: "Required. Number of units",
201
- variation_id: "Variation UUID (for variable products)",
202
- tier: "Pricing tier name (e.g. 'retail', 'wholesale')",
203
- unit_price: "Override price (optional — auto-resolved from pricing_data)"
204
- },
205
- response: 'CartItem { object: "cart_item", id, product_id, product_name, quantity, unit_price, line_total }'
206
- }, {
207
- method: "PATCH",
208
- path: "/v1/stores/:storeId/cart/:cartId/items/:itemId",
209
- scope: "write:cart",
210
- description: "Update cart item quantity",
211
- body: {
212
- quantity: "New quantity (must be >= 1)"
213
- },
214
- response: 'CartItem { object: "cart_item", id, quantity, unit_price, line_total }'
215
- }, {
216
- method: "DELETE",
217
- path: "/v1/stores/:storeId/cart/:cartId/items/:itemId",
218
- scope: "write:cart",
219
- description: "Remove an item from the cart",
220
- response: '{ object: "cart_item", id, deleted: true }'
221
- }]
222
- },
223
- checkout: {
224
- name: "Checkout",
225
- description: "E-commerce checkout and POS payment intents",
226
- base: "/v1/stores/:storeId/checkout",
227
- endpoints: [{
228
- method: "POST",
229
- path: "/v1/stores/:storeId/checkout",
230
- scope: "write:checkout",
231
- description: "Convert a cart into an order (e-commerce checkout)",
232
- body: {
233
- cart_id: "Required. Cart UUID",
234
- customer_email: "Customer email (auto-creates customer if not found)",
235
- customer_name: "Customer name",
236
- customer_id: "Existing customer UUID",
237
- payment_method: "Payment method identifier",
238
- opaque_data: "Authorize.Net card token { dataDescriptor, dataValue }",
239
- bill_to: "Billing address { firstName, lastName, address, city, state, zip, country }",
240
- ship_to: "Shipping address (same shape as bill_to)",
241
- loyalty_code: "Loyalty/discount code",
242
- loyalty_points_redeemed: "Points to redeem (integer >= 0)",
243
- loyalty_discount_amount: "Discount amount from loyalty (number >= 0)"
244
- },
245
- response: 'Order { object: "order", id, order_number, status: "completed" }',
246
- notes: "Cart is consumed — cannot be reused after successful checkout. Supports idempotency (returns existing order on replay)."
247
- }, {
248
- method: "POST",
249
- path: "/v1/stores/:storeId/checkout/intents",
250
- scope: "write:checkout",
251
- description: "Create a payment intent (POS terminal flow)",
252
- body: {
253
- cartItems: "Required. Array of cart items",
254
- paymentMethod: "Payment method (card, cash, etc.)",
255
- locationId: "Location UUID",
256
- registerId: "POS register ID",
257
- sessionId: "POS session ID",
258
- customerId: "Customer UUID",
259
- customerName: "Customer name",
260
- userId: "Staff user UUID",
261
- tipAmount: "Tip amount",
262
- cashTendered: "Cash amount given (for cash payments)",
263
- changeGiven: "Change returned",
264
- idempotencyKey: "Idempotency key for safe retries"
265
- },
266
- response: "PaymentIntent { id, status: 'created', amount }"
267
- }, {
268
- method: "GET",
269
- path: "/v1/stores/:storeId/checkout/intents/:id",
270
- scope: "read:checkout",
271
- description: "Get payment intent status",
272
- response: "PaymentIntent { id, status, amount, created_at }"
273
- }, {
274
- method: "POST",
275
- path: "/v1/stores/:storeId/checkout/intents/:id/capture",
276
- scope: "write:checkout",
277
- description: "Capture an authorized payment",
278
- response: "PaymentIntent { status: 'captured' }"
279
- }, {
280
- method: "POST",
281
- path: "/v1/stores/:storeId/checkout/intents/:id/cancel",
282
- scope: "write:checkout",
283
- description: "Cancel a payment intent",
284
- response: "PaymentIntent { status: 'cancelled' }"
285
- }, {
286
- method: "POST",
287
- path: "/v1/stores/:storeId/checkout/intents/:id/charge",
288
- scope: "write:checkout",
289
- description: "Charge via Dejavoo POS terminal",
290
- body: {
291
- terminal_id: "Dejavoo terminal ID"
292
- },
293
- response: "PaymentIntent (with terminal response)",
294
- notes: "POS-specific — sends charge request to physical terminal"
295
- }, {
296
- method: "POST",
297
- path: "/v1/stores/:storeId/checkout/intents/:id/abort",
298
- scope: "write:checkout",
299
- description: "Abort an in-progress terminal charge",
300
- response: "PaymentIntent { status: 'aborted' }"
301
- }]
302
- },
303
- customers: {
304
- name: "Customers",
305
- description: "Customer CRM — CRUD and search",
306
- base: "/v1/stores/:storeId/customers",
307
- endpoints: [{
308
- method: "GET",
309
- path: "/v1/stores/:storeId/customers",
310
- scope: "read:customers",
311
- description: "List customers with pagination and search",
312
- params: {
313
- limit: "Number (1-100, default 25)",
314
- starting_after: "Customer ID cursor",
315
- search: "Search by name, email, or phone"
316
- },
317
- response: '{ object: "list", data: Customer[], has_more: boolean }'
318
- }, {
319
- method: "GET",
320
- path: "/v1/stores/:storeId/customers/:id",
321
- scope: "read:customers",
322
- description: "Get a single customer with full profile",
323
- response: "Customer (includes loyalty_points, total_spent, total_orders)"
324
- }, {
325
- method: "POST",
326
- path: "/v1/stores/:storeId/customers",
327
- scope: "write:customers",
328
- description: "Create a new customer",
329
- body: {
330
- first_name: "Required",
331
- last_name: "Required",
332
- email: "Email address",
333
- phone: "Phone number",
334
- date_of_birth: "YYYY-MM-DD"
335
- },
336
- response: "Customer"
337
- }, {
338
- method: "PATCH",
339
- path: "/v1/stores/:storeId/customers/:id",
340
- scope: "write:customers",
341
- description: "Update a customer",
342
- body: {
343
- "...": "Any customer fields"
344
- },
345
- response: "Customer"
346
- }]
347
- },
348
- inventory: {
349
- name: "Inventory",
350
- description: "Inventory levels, summaries, adjustments, and transfers",
351
- base: "/v1/stores/:storeId/inventory",
352
- endpoints: [{
353
- method: "GET",
354
- path: "/v1/stores/:storeId/inventory",
355
- scope: "read:inventory",
356
- description: "List inventory levels across locations",
357
- params: {
358
- limit: "Number (1-100, default 25)",
359
- starting_after: "Cursor",
360
- location_id: "Filter by location UUID",
361
- product_id: "Filter by product UUID",
362
- low_stock: "Set to 'true' to filter to low_stock or out_of_stock items only"
363
- },
364
- response: '{ object: "list", data: InventoryLevel[] }'
365
- }, {
366
- method: "GET",
367
- path: "/v1/stores/:storeId/inventory/summary",
368
- scope: "read:inventory",
369
- description: "Inventory summary grouped by location",
370
- response: "{ locations: [{ id, name, total_products, total_units, total_value }] }"
371
- }, {
372
- method: "POST",
373
- path: "/v1/stores/:storeId/inventory/adjust",
374
- scope: "write:inventory",
375
- description: "Adjust inventory quantity for a product at a location",
376
- body: {
377
- product_id: "Required. Product UUID",
378
- location_id: "Required. Location UUID",
379
- quantity_change: "Required. Number (+/-) to adjust by",
380
- reason: "Reason for adjustment (default: 'api_adjustment')"
381
- },
382
- response: '{ object: "inventory_adjustment", product_id, location_id, quantity_change, new_quantity, reason }'
383
- }, {
384
- method: "POST",
385
- path: "/v1/stores/:storeId/inventory/transfer",
386
- scope: "write:inventory",
387
- description: "Transfer inventory between locations",
388
- body: {
389
- product_id: "Required. Product UUID",
390
- from_location_id: "Required. Source location UUID",
391
- to_location_id: "Required. Destination location UUID",
392
- quantity: "Required. Number of units to transfer"
393
- },
394
- response: "{ from: InventoryLevel, to: InventoryLevel }"
395
- }]
396
- },
397
- locations: {
398
- name: "Locations",
399
- description: "Store locations — list and view",
400
- base: "/v1/stores/:storeId/locations",
401
- endpoints: [{
402
- method: "GET",
403
- path: "/v1/stores/:storeId/locations",
404
- scope: "read:locations",
405
- description: "List all store locations",
406
- response: '{ object: "list", data: Location[] }'
407
- }, {
408
- method: "GET",
409
- path: "/v1/stores/:storeId/locations/:id",
410
- scope: "read:locations",
411
- description: "Get a single location",
412
- response: "Location { id, name, address, city, state, zip, type, is_active }"
413
- }]
414
- },
415
- analytics: {
416
- name: "Analytics",
417
- description: "Sales, inventory, traffic, customer, and product analytics",
418
- base: "/v1/stores/:storeId/analytics",
419
- endpoints: [{
420
- method: "GET",
421
- path: "/v1/stores/:storeId/analytics/sales",
422
- scope: "read:analytics",
423
- description: "Sales analytics summary",
424
- params: {
425
- start_date: "YYYY-MM-DD (default: 30 days ago)",
426
- end_date: "YYYY-MM-DD (default: today)",
427
- location_id: "Filter by location UUID"
428
- },
429
- response: '{ object: "analytics.sales", period: { start_date, end_date }, total_orders, completed_orders, total_revenue, total_tax, total_discount, average_order_value, gross_sales, net_sales, total_profit, profit_margin, unique_customers }'
430
- }, {
431
- method: "GET",
432
- path: "/v1/stores/:storeId/analytics/sales/daily",
433
- scope: "read:analytics",
434
- description: "Daily sales breakdown",
435
- params: {
436
- start_date: "YYYY-MM-DD (default: 30 days ago)",
437
- end_date: "YYYY-MM-DD (default: today)",
438
- location_id: "Filter by location UUID"
439
- },
440
- response: "{ days: [{ date, revenue, orders, average_order_value }] }"
441
- }, {
442
- method: "GET",
443
- path: "/v1/stores/:storeId/analytics/sales/weekly",
444
- scope: "read:analytics",
445
- description: "Weekly sales breakdown",
446
- params: {
447
- start_date: "YYYY-MM-DD (default: 30 days ago)",
448
- end_date: "YYYY-MM-DD (default: today)",
449
- location_id: "Filter by location UUID"
450
- },
451
- response: "{ weeks: [{ week_start, revenue, orders }] }"
452
- }, {
453
- method: "GET",
454
- path: "/v1/stores/:storeId/analytics/inventory",
455
- scope: "read:analytics",
456
- description: "Inventory analytics — stock levels and velocity",
457
- response: "{ total_value, low_stock[], out_of_stock[], velocity[] }"
458
- }, {
459
- method: "GET",
460
- path: "/v1/stores/:storeId/analytics/traffic",
461
- scope: "read:analytics",
462
- description: "Storefront traffic analytics",
463
- response: "{ sessions, page_views, bounce_rate, avg_session_duration }"
464
- }, {
465
- method: "GET",
466
- path: "/v1/stores/:storeId/analytics/funnel",
467
- scope: "read:analytics",
468
- description: "Conversion funnel analytics",
469
- response: "{ steps: [{ name, count, conversion_rate }] }"
470
- }, {
471
- method: "GET",
472
- path: "/v1/stores/:storeId/analytics/customers",
473
- scope: "read:analytics",
474
- description: "Customer analytics — segments, retention, lifetime value",
475
- response: "{ total_customers, new_customers, returning, avg_ltv, segments[] }"
476
- }, {
477
- method: "GET",
478
- path: "/v1/stores/:storeId/analytics/products",
479
- scope: "read:analytics",
480
- description: "Per-product performance analytics",
481
- params: {
482
- limit: "Number of products",
483
- category_id: "Filter by category"
484
- },
485
- response: "{ products: [{ id, name, revenue, units_sold, avg_price }] }"
486
- }]
487
- },
488
- storefront: {
489
- name: "Storefront",
490
- description: "Storefront sessions, event tracking, and customer OTP authentication",
491
- base: "/v1/stores/:storeId/storefront",
492
- endpoints: [{
493
- method: "GET",
494
- path: "/v1/stores/:storeId/storefront/sessions",
495
- scope: "read:storefront",
496
- description: "List storefront sessions",
497
- response: '{ object: "list", data: Session[] }'
498
- }, {
499
- method: "GET",
500
- path: "/v1/stores/:storeId/storefront/sessions/:id",
501
- scope: "read:storefront",
502
- description: "Get a single session",
503
- response: "Session { id, customer_id, events[], created_at }"
504
- }, {
505
- method: "POST",
506
- path: "/v1/stores/:storeId/storefront/sessions",
507
- scope: "write:storefront",
508
- description: "Create a storefront session",
509
- body: {
510
- customer_id: "Optional customer UUID",
511
- metadata: "Optional session metadata"
512
- },
513
- response: "Session"
514
- }, {
515
- method: "PATCH",
516
- path: "/v1/stores/:storeId/storefront/sessions/:id",
517
- scope: "write:storefront",
518
- description: "Update a session",
519
- body: {
520
- metadata: "Updated metadata"
521
- },
522
- response: "Session"
523
- }, {
524
- method: "POST",
525
- path: "/v1/stores/:storeId/storefront/events",
526
- scope: "write:storefront",
527
- description: "Track a storefront event",
528
- body: {
529
- session_id: "Session UUID",
530
- event_type: "Event name (e.g. page_view, add_to_cart, purchase)",
531
- event_data: "Event payload JSON"
532
- },
533
- response: "{ tracked: true }"
534
- }, {
535
- method: "POST",
536
- path: "/v1/stores/:storeId/storefront/auth/send-code",
537
- scope: "write:storefront",
538
- description: "Send passwordless OTP to a customer",
539
- body: {
540
- email: "Customer email OR",
541
- phone: "Customer phone"
542
- },
543
- response: "{ sent: true }",
544
- notes: "Rate-limited to prevent abuse. Code expires in 10 minutes."
545
- }, {
546
- method: "POST",
547
- path: "/v1/stores/:storeId/storefront/auth/verify-code",
548
- scope: "write:storefront",
549
- description: "Verify OTP code and authenticate customer",
550
- body: {
551
- email: "Customer email OR phone",
552
- code: "6-digit OTP code"
553
- },
554
- response: "{ customer_id, session_token, expires_at }"
555
- }]
556
- },
557
- coa: {
558
- name: "COA (Certificates of Analysis)",
559
- description: "Lab test results — list, verify, view, and embed",
560
- base: "/v1/stores/:storeId/coa",
561
- endpoints: [{
562
- method: "GET",
563
- path: "/v1/stores/:storeId/coa",
564
- scope: "read:coa",
565
- description: "List COAs (scoped to client)",
566
- params: {
567
- limit: "Number (1-100)",
568
- starting_after: "Cursor"
569
- },
570
- response: '{ object: "list", data: COA[] }'
571
- }, {
572
- method: "GET",
573
- path: "/v1/stores/:storeId/coa/verify",
574
- scope: "read:coa",
575
- description: "Verify a COA by sample ID",
576
- params: {
577
- sample_id: "Lab sample identifier"
578
- },
579
- response: "COA (with verification status)",
580
- notes: "Public-facing verification — can be linked from product pages"
581
- }, {
582
- method: "GET",
583
- path: "/v1/stores/:storeId/coa/:id",
584
- scope: "read:coa",
585
- description: "Get a single COA with full lab data",
586
- response: "COA { id, product_name, batch_number, sample_id, cannabinoids, terpenes, ... }"
587
- }, {
588
- method: "GET",
589
- path: "/v1/stores/:storeId/coa/:id/embed",
590
- scope: "read:coa",
591
- description: "Get embeddable COA data for iframes or widgets",
592
- response: "{ html: string, data: COA }",
593
- notes: "Use for embedding lab results on product detail pages"
594
- }]
595
- },
596
- portal: {
597
- name: "Customer Portal",
598
- description: "B2B portal — authentication, profiles, and documents",
599
- base: "/v1/stores/:storeId/portal",
600
- endpoints: [{
601
- method: "POST",
602
- path: "/v1/stores/:storeId/portal/auth/send-code",
603
- scope: "write:portal",
604
- description: "Send portal login OTP",
605
- body: {
606
- email: "Customer email"
607
- },
608
- response: "{ sent: true }"
609
- }, {
610
- method: "POST",
611
- path: "/v1/stores/:storeId/portal/auth/verify",
612
- scope: "write:portal",
613
- description: "Verify OTP and get portal session",
614
- body: {
615
- email: "Customer email",
616
- code: "OTP code"
617
- },
618
- response: "{ customer_id, token, expires_at }"
619
- }, {
620
- method: "POST",
621
- path: "/v1/stores/:storeId/portal/auth/refresh",
622
- scope: "write:portal",
623
- description: "Refresh portal session token",
624
- body: {
625
- token: "Current session token"
626
- },
627
- response: "{ token, expires_at }"
628
- }, {
629
- method: "GET",
630
- path: "/v1/stores/:storeId/portal/stores",
631
- scope: "read:portal",
632
- description: "Get stores the customer belongs to",
633
- response: "{ stores: Store[] }"
634
- }, {
635
- method: "GET",
636
- path: "/v1/stores/:storeId/portal/profiles",
637
- scope: "read:portal",
638
- description: "List customer profiles",
639
- response: "{ profiles: Profile[] }"
640
- }, {
641
- method: "GET",
642
- path: "/v1/stores/:storeId/portal/documents",
643
- scope: "read:portal",
644
- description: "List customer documents",
645
- response: '{ object: "list", data: Document[] }'
646
- }, {
647
- method: "GET",
648
- path: "/v1/stores/:storeId/portal/documents/:id",
649
- scope: "read:portal",
650
- description: "Get a single document",
651
- response: "Document { id, name, type, url, created_at }"
652
- }, {
653
- method: "GET",
654
- path: "/v1/stores/:storeId/portal/api-key",
655
- scope: "read:portal",
656
- description: "Get customer's API key for portal access",
657
- response: "{ api_key, scopes[], expires_at }"
658
- }]
659
- },
660
- webhooks: {
661
- name: "Webhooks",
662
- description: "Inbound webhook ingestion — receives events from external services (HMAC-authenticated, no API key required)",
663
- base: "/v1/webhooks",
664
- endpoints: [{
665
- method: "POST",
666
- path: "/v1/webhooks/:slug",
667
- scope: "(none — HMAC signature)",
668
- description: "Receive an inbound webhook event",
669
- body: {
670
- timestamp: "Unix timestamp (optional — replay protection, rejects if > 5 min old)",
671
- event: "Event type identifier (or 'type' field)",
672
- "...": "All other fields pass through to webhook_events table"
673
- },
674
- response: '{ object: "webhook_event", received: true, event_type: string }',
675
- notes: "Authenticated via HMAC sha256 signature from the webhook endpoint's signing_secret. No API key required."
676
- }]
677
- },
678
- agent: {
679
- name: "Agent",
680
- description: "AI agent chat — SSE streaming proxy to the internal whale-agent service",
681
- base: "/v1/stores/:storeId/agent",
682
- endpoints: [{
683
- method: "POST",
684
- path: "/v1/stores/:storeId/agent/chat",
685
- scope: "write:agent",
686
- description: "Stream an AI agent response via Server-Sent Events",
687
- body: {
688
- message: "User message (string — either this or messages required)",
689
- messages: "Array of { role: 'user'|'assistant'|'system', content: string }",
690
- conversation_id: "Conversation ID for multi-turn context",
691
- context: "Additional context object passed to the agent"
692
- },
693
- response: "SSE stream (text/event-stream) — streamed tokens from the agent",
694
- notes: "Proxies to internal whale-agent service. 120s timeout (504 on timeout, 502 on connection failure)."
695
- }]
696
- },
697
- telemetry: {
698
- name: "Telemetry",
699
- description: "SDK telemetry ingestion — errors, analytics events, Web Vitals, and AI/LLM call tracking",
700
- base: "/v1/stores/:storeId/telemetry",
701
- endpoints: [{
702
- method: "POST",
703
- path: "/v1/stores/:storeId/telemetry/ingest",
704
- scope: "write:telemetry",
705
- description: "Ingest a batch of telemetry data (errors, events, vitals, AI calls)",
706
- body: {
707
- session: "Required. { session_id, visitor_id, started_at, page_url, referrer, user_agent, screen_width, screen_height, device, language, utm_* }",
708
- user: "Optional. { user_id, email?, name?, traits? }",
709
- errors: "Array (max 50). Each: { error_type, error_message, fingerprint, occurred_at, severity?, stack_trace?, breadcrumbs? }",
710
- events: "Array (max 100). Each: { event_name, properties, timestamp }",
711
- vitals: "Array (max 20). Each: { name: CLS|FID|LCP|INP|TTFB|FCP, value, rating, timestamp }",
712
- ai_calls: "Array (max 50). Each: { model, provider?, prompt_tokens?, completion_tokens?, cost?, duration_ms?, status?, agent_id? }"
713
- },
714
- response: '{ object: "telemetry_batch", accepted: { errors, events, vitals, ai_calls }, session_id }',
715
- notes: "Used by @neowhale/telemetry SDK. Errors go to ClickHouse, events to Supabase, vitals and AI calls to ClickHouse."
716
- }, {
717
- method: "POST",
718
- path: "/v1/native/telemetry",
719
- scope: "(JWT-authenticated)",
720
- description: "Ingest telemetry from native (macOS/iOS) apps",
721
- body: {
722
- "...": "Same batch format as /telemetry/ingest"
723
- },
724
- response: '{ object: "telemetry_batch", accepted: { ... } }',
725
- notes: "Authenticated via JWT token (not API key). Bypasses standard API key middleware."
726
- }]
727
- },
728
- media: {
729
- name: "Media",
730
- description: "Image and video proxy with on-the-fly transformations (HMAC-signed, no API key required)",
731
- base: "/v1/stores/:storeId",
732
- endpoints: [{
733
- method: "GET",
734
- path: "/v1/stores/:storeId/media",
735
- scope: "(HMAC signature via 's' query param)",
736
- description: "Proxy and transform an image (resize, format conversion, quality)",
737
- params: {
738
- url: "Required. Base64url-encoded source image URL",
739
- w: "Width: 64, 96, 128, 256, 384, 640, 828, 1080, 1280, or 1920",
740
- q: "Quality 1-100 (default: 80)",
741
- f: "Format: avif, webp, jpeg, png (default: webp)",
742
- s: "Required. HMAC signature"
743
- },
744
- response: "Binary image data with Cache-Control: public, max-age=86400, s-maxage=604800, immutable",
745
- notes: "Max 8 concurrent transforms, queues for 5s then returns 503 with Retry-After: 2. Use WhaleClient.signMedia() to generate signatures."
746
- }, {
747
- method: "GET",
748
- path: "/v1/stores/:storeId/video",
749
- scope: "(HMAC signature via 's' query param)",
750
- description: "Stream a video with HTTP range request support",
751
- params: {
752
- url: "Required. Base64url-encoded source video URL",
753
- s: "Required. HMAC signature"
754
- },
755
- response: "Streamed video (supports Accept-Ranges: bytes for seeking). Auto-resolves optimized -web.mp4 variants.",
756
- notes: "Cache-Control: public, max-age=86400, s-maxage=604800, immutable. X-Video-Source header indicates original vs optimized."
757
- }]
758
- },
759
- infrastructure: {
760
- name: "Infrastructure",
761
- description: "Health checks, OpenAPI spec, and interactive documentation",
762
- base: "",
763
- endpoints: [{
764
- method: "GET",
765
- path: "/health",
766
- scope: "(no auth)",
767
- description: "Health check — simple or detailed with dependency status",
768
- params: {
769
- detailed: "Set to 'true' to check Supabase and ClickHouse dependencies"
770
- },
771
- response: '{ status: "ok"|"degraded"|"down", version: "2026-02-20", uptime_s?, dependencies?: { supabase, clickhouse } }',
772
- notes: "Simple response: 200 always. Detailed: 503 if both dependencies are down. Cached for 10s."
773
- }, {
774
- method: "GET",
775
- path: "/openapi.json",
776
- scope: "(no auth)",
777
- description: "OpenAPI 3.0 specification",
778
- response: "OpenAPI JSON spec"
779
- }, {
780
- method: "GET",
781
- path: "/docs",
782
- scope: "(no auth)",
783
- description: "Interactive API documentation page",
784
- response: "HTML page"
785
- }]
11
+ let _cache = null;
12
+ async function fetchOpenApiSpec() {
13
+ if (_cache && Date.now() - _cache.fetchedAt < CACHE_TTL_MS) {
14
+ return _cache.spec;
786
15
  }
787
- };
788
-
789
- // ---------------------------------------------------------------------------
790
- // Auth reference
791
- // ---------------------------------------------------------------------------
792
-
793
- const AUTH_GUIDE = {
794
- base_url: GATEWAY_BASE_URL,
795
- api_version: "2026-02-20",
796
- api_version_header: "X-API-Version",
797
- authentication: {
798
- method: "API Key via header",
799
- header: "x-api-key",
800
- key_format: "wk_live_<32chars> (production) or wk_test_<32chars> (sandbox)",
801
- how_to_get: "Use the api_keys tool (action: generate) or the WhaleTools dashboard under Settings → API Keys"
802
- },
803
- scopes: {
804
- wildcard: "* (full access), read:* (all reads), write:* (all writes)",
805
- per_resource: ["read:products", "write:products", "read:orders", "write:orders", "read:customers", "write:customers", "read:inventory", "write:inventory", "read:locations", "read:analytics", "read:cart", "write:cart", "read:checkout", "write:checkout", "read:storefront", "write:storefront", "read:portal", "write:portal", "read:coa"],
806
- internal: ["write:agent", "write:telemetry"],
807
- internal_note: "Agent chat and telemetry scopes are used by WhaleTools SDKs and internal services. Media/video endpoints use HMAC signatures instead of API keys."
808
- },
809
- rate_limits: {
810
- default_per_minute: 60,
811
- headers: "X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset",
812
- exceeded_status: 429,
813
- retry_header: "Retry-After (seconds)"
814
- },
815
- pagination: {
816
- style: "Cursor-based",
817
- params: "limit (1-100, default 25), starting_after (ID), ending_before (ID)",
818
- response: '{ object: "list", data: [...], has_more: true|false, url: "..." }'
819
- },
820
- error_format: {
821
- shape: '{ error: { type, message, code, param?, request_id? } }',
822
- types: {
823
- authentication_error: "401 — invalid_api_key (also returned for missing key)",
824
- permission_error: "403 — insufficient_scope, store_mismatch",
825
- not_found_error: "404 — resource_not_found",
826
- invalid_request_error: "400/405/409 — validation_error, method_not_allowed, conflict",
827
- rate_limit_error: "429 — rate_limit_exceeded, auth_rate_limited (too many auth attempts)",
828
- api_error: "500 — internal_error"
829
- }
830
- },
831
- security_headers: {
832
- cors: "Configurable per-store. Default: allow all origins",
833
- idempotency: "Idempotency-Key header supported on all POST/PATCH requests",
834
- versioning: "X-API-Version header (optional — latest version used if omitted)"
835
- },
836
- url_pattern: "All endpoints: /v1/stores/:storeId/<resource>",
837
- note: "Replace :storeId with your store UUID. Get it from the WhaleTools dashboard or the store tool."
838
- };
839
-
840
- // ---------------------------------------------------------------------------
841
- // Code examples by section
842
- // ---------------------------------------------------------------------------
843
-
844
- function getExamples(section) {
845
- const BASE = GATEWAY_BASE_URL;
846
- const storeVar = "${STORE_ID}";
847
- const keyVar = "${API_KEY}";
848
- const templates = {
849
- products: {
850
- raw_fetch: `// List products
851
- const res = await fetch("${BASE}/v1/stores/${storeVar}/products?limit=25&status=published", {
852
- headers: { "x-api-key": "${keyVar}" }
853
- });
854
- const { data: products, has_more } = await res.json();
855
-
856
- // Get single product
857
- const product = await fetch("${BASE}/v1/stores/${storeVar}/products/\${productId}", {
858
- headers: { "x-api-key": "${keyVar}" }
859
- }).then(r => r.json());`,
860
- nextjs_ssr: `// app/products/page.tsx (Next.js App Router — Server Component)
861
- const API_KEY = process.env.WHALE_API_KEY!;
862
- const STORE_ID = process.env.WHALE_STORE_ID!;
863
- const BASE = "${BASE}";
864
-
865
- async function getProducts(page?: string) {
866
- const params = new URLSearchParams({ limit: "25", status: "published" });
867
- if (page) params.set("starting_after", page);
868
-
869
- const res = await fetch(\`\${BASE}/v1/stores/\${STORE_ID}/products?\${params}\`, {
870
- headers: { "x-api-key": API_KEY },
871
- next: { revalidate: 60 }, // ISR: revalidate every 60s
872
- });
873
-
874
- if (!res.ok) throw new Error("Failed to fetch products");
875
- return res.json();
876
- }
877
-
878
- export default async function ProductsPage({ searchParams }: { searchParams: { after?: string } }) {
879
- const { data: products, has_more } = await getProducts(searchParams.after);
880
-
881
- return (
882
- <div className="grid grid-cols-3 gap-4">
883
- {products.map((p: any) => (
884
- <div key={p.id} className="border rounded p-4">
885
- {p.featured_image && <img src={p.featured_image} alt={p.name} />}
886
- <h2>{p.name}</h2>
887
- <p>{p.pricing_data?.tiers?.[0]?.price ? \`$\${p.pricing_data.tiers[0].price}\` : "Contact for pricing"}</p>
888
- </div>
889
- ))}
890
- {has_more && <a href={\`?after=\${products.at(-1)?.id}\`}>Next page →</a>}
891
- </div>
892
- );
893
- }`,
894
- react_client: `// hooks/useProducts.ts (React client-side)
895
- import { useState, useEffect } from "react";
896
-
897
- const BASE = "${BASE}";
898
-
899
- export function useProducts(storeId: string, apiKey: string) {
900
- const [products, setProducts] = useState([]);
901
- const [loading, setLoading] = useState(true);
902
-
903
- useEffect(() => {
904
- fetch(\`\${BASE}/v1/stores/\${storeId}/products?status=published&limit=50\`, {
905
- headers: { "x-api-key": apiKey },
906
- })
907
- .then(r => r.json())
908
- .then(({ data }) => setProducts(data))
909
- .finally(() => setLoading(false));
910
- }, [storeId, apiKey]);
911
-
912
- return { products, loading };
913
- }`
16
+ const res = await fetch(OPENAPI_URL, {
17
+ headers: {
18
+ Accept: "application/json"
914
19
  },
915
- cart: {
916
- raw_fetch: `// Create cart
917
- const cart = await fetch("${BASE}/v1/stores/${storeVar}/cart", {
918
- method: "POST",
919
- headers: { "x-api-key": "${keyVar}", "Content-Type": "application/json" },
920
- body: JSON.stringify({ customer_id: customerId })
921
- }).then(r => r.json());
922
-
923
- // Add item
924
- await fetch("${BASE}/v1/stores/${storeVar}/cart/\${cart.id}/items", {
925
- method: "POST",
926
- headers: { "x-api-key": "${keyVar}", "Content-Type": "application/json" },
927
- body: JSON.stringify({ product_id: "...", quantity: 2 })
928
- }).then(r => r.json());
929
-
930
- // View cart
931
- const fullCart = await fetch("${BASE}/v1/stores/${storeVar}/cart/\${cart.id}", {
932
- headers: { "x-api-key": "${keyVar}" }
933
- }).then(r => r.json());`,
934
- nextjs_ssr: `// app/api/cart/route.ts (Next.js Route Handler — server-side proxy)
935
- import { NextRequest, NextResponse } from "next/server";
936
-
937
- const API_KEY = process.env.WHALE_API_KEY!;
938
- const STORE_ID = process.env.WHALE_STORE_ID!;
939
- const BASE = "${BASE}";
940
-
941
- export async function POST(req: NextRequest) {
942
- const body = await req.json();
943
- const res = await fetch(\`\${BASE}/v1/stores/\${STORE_ID}/cart\`, {
944
- method: "POST",
945
- headers: { "x-api-key": API_KEY, "Content-Type": "application/json" },
946
- body: JSON.stringify(body),
20
+ signal: AbortSignal.timeout(8000)
947
21
  });
948
- return NextResponse.json(await res.json());
949
- }`,
950
- react_client: `// hooks/useCart.ts
951
- import { useState, useCallback } from "react";
952
-
953
- export function useCart(storeId: string, apiKey: string) {
954
- const [cart, setCart] = useState<any>(null);
955
- const BASE = "${BASE}";
956
- const headers = { "x-api-key": apiKey, "Content-Type": "application/json" };
957
-
958
- const createCart = useCallback(async () => {
959
- const res = await fetch(\`\${BASE}/v1/stores/\${storeId}/cart\`, {
960
- method: "POST", headers,
961
- });
962
- const data = await res.json();
963
- setCart(data);
964
- return data;
965
- }, [storeId]);
966
-
967
- const addItem = useCallback(async (productId: string, quantity: number) => {
968
- if (!cart) return;
969
- const res = await fetch(\`\${BASE}/v1/stores/\${storeId}/cart/\${cart.id}/items\`, {
970
- method: "POST", headers,
971
- body: JSON.stringify({ product_id: productId, quantity }),
972
- });
973
- const data = await res.json();
974
- setCart(data);
975
- return data;
976
- }, [storeId, cart]);
977
-
978
- const removeItem = useCallback(async (itemId: string) => {
979
- if (!cart) return;
980
- const res = await fetch(\`\${BASE}/v1/stores/\${storeId}/cart/\${cart.id}/items/\${itemId}\`, {
981
- method: "DELETE", headers,
982
- });
983
- const data = await res.json();
984
- setCart(data);
985
- return data;
986
- }, [storeId, cart]);
987
-
988
- return { cart, createCart, addItem, removeItem };
989
- }`
990
- },
991
- checkout: {
992
- raw_fetch: `// E-commerce checkout (convert cart to order)
993
- const order = await fetch("${BASE}/v1/stores/${storeVar}/checkout", {
994
- method: "POST",
995
- headers: { "x-api-key": "${keyVar}", "Content-Type": "application/json" },
996
- body: JSON.stringify({
997
- cart_id: cartId,
998
- payment_method: "card",
999
- shipping_address: { street: "123 Main St", city: "Charlotte", state: "NC", zip: "28202" }
1000
- })
1001
- }).then(r => r.json());
1002
-
1003
- // POS payment intent flow
1004
- const intent = await fetch("${BASE}/v1/stores/${storeVar}/checkout/intents", {
1005
- method: "POST",
1006
- headers: { "x-api-key": "${keyVar}", "Content-Type": "application/json" },
1007
- body: JSON.stringify({ cart_id: cartId, amount: 49.99 })
1008
- }).then(r => r.json());
1009
-
1010
- // Capture after authorization
1011
- await fetch("${BASE}/v1/stores/${storeVar}/checkout/intents/\${intent.id}/capture", {
1012
- method: "POST",
1013
- headers: { "x-api-key": "${keyVar}" }
1014
- });`,
1015
- nextjs_ssr: `// app/api/checkout/route.ts (Server-side checkout — keep API key secret)
1016
- import { NextRequest, NextResponse } from "next/server";
1017
-
1018
- const API_KEY = process.env.WHALE_API_KEY!;
1019
- const STORE_ID = process.env.WHALE_STORE_ID!;
1020
- const BASE = "${BASE}";
1021
-
1022
- export async function POST(req: NextRequest) {
1023
- const { cart_id, shipping_address } = await req.json();
1024
-
1025
- const res = await fetch(\`\${BASE}/v1/stores/\${STORE_ID}/checkout\`, {
1026
- method: "POST",
1027
- headers: { "x-api-key": API_KEY, "Content-Type": "application/json" },
1028
- body: JSON.stringify({ cart_id, shipping_address }),
1029
- });
1030
-
1031
22
  if (!res.ok) {
1032
- const err = await res.json();
1033
- return NextResponse.json(err, { status: res.status });
23
+ throw new Error(`Gateway returned ${res.status} fetching OpenAPI spec`);
1034
24
  }
1035
-
1036
- return NextResponse.json(await res.json());
1037
- }`,
1038
- react_client: `// components/CheckoutButton.tsx
1039
- "use client";
1040
- import { useState } from "react";
1041
-
1042
- export function CheckoutButton({ cartId }: { cartId: string }) {
1043
- const [loading, setLoading] = useState(false);
1044
-
1045
- const handleCheckout = async () => {
1046
- setLoading(true);
1047
- try {
1048
- // Call YOUR server-side route (never expose API key to client)
1049
- const res = await fetch("/api/checkout", {
1050
- method: "POST",
1051
- headers: { "Content-Type": "application/json" },
1052
- body: JSON.stringify({ cart_id: cartId }),
1053
- });
1054
- const order = await res.json();
1055
- if (order.id) window.location.href = \`/orders/\${order.id}/confirmation\`;
1056
- } finally {
1057
- setLoading(false);
1058
- }
25
+ const spec = await res.json();
26
+ _cache = {
27
+ spec,
28
+ fetchedAt: Date.now()
1059
29
  };
1060
-
1061
- return <button onClick={handleCheckout} disabled={loading}>{loading ? "Processing..." : "Checkout"}</button>;
1062
- }`
1063
- },
1064
- orders: {
1065
- raw_fetch: `// List orders
1066
- const { data: orders } = await fetch("${BASE}/v1/stores/${storeVar}/orders?limit=25", {
1067
- headers: { "x-api-key": "${keyVar}" }
1068
- }).then(r => r.json());
1069
-
1070
- // Get order details
1071
- const order = await fetch("${BASE}/v1/stores/${storeVar}/orders/\${orderId}", {
1072
- headers: { "x-api-key": "${keyVar}" }
1073
- }).then(r => r.json());
1074
-
1075
- // Update order status
1076
- await fetch("${BASE}/v1/stores/${storeVar}/orders/\${orderId}", {
1077
- method: "PATCH",
1078
- headers: { "x-api-key": "${keyVar}", "Content-Type": "application/json" },
1079
- body: JSON.stringify({ status: "shipped" })
1080
- });`,
1081
- nextjs_ssr: `// app/orders/page.tsx (Server Component)
1082
- async function getOrders() {
1083
- const res = await fetch(
1084
- \`${BASE}/v1/stores/\${process.env.WHALE_STORE_ID}/orders?limit=50\`,
1085
- { headers: { "x-api-key": process.env.WHALE_API_KEY! }, next: { revalidate: 30 } }
1086
- );
1087
- return res.json();
30
+ return spec;
1088
31
  }
1089
32
 
1090
- export default async function OrdersPage() {
1091
- const { data: orders } = await getOrders();
1092
- return (
1093
- <table>
1094
- <thead><tr><th>Order</th><th>Status</th><th>Total</th><th>Date</th></tr></thead>
1095
- <tbody>
1096
- {orders.map((o: any) => (
1097
- <tr key={o.id}>
1098
- <td><a href={\`/orders/\${o.id}\`}>{o.order_number}</a></td>
1099
- <td>{o.status}</td>
1100
- <td>\${o.total}</td>
1101
- <td>{new Date(o.created_at).toLocaleDateString()}</td>
1102
- </tr>
1103
- ))}
1104
- </tbody>
1105
- </table>
1106
- );
1107
- }`,
1108
- react_client: `// hooks/useOrders.ts
1109
- import useSWR from "swr";
1110
-
1111
- const fetcher = (url: string) => fetch(url).then(r => r.json());
1112
-
1113
- export function useOrders() {
1114
- // Fetch via your own API route (keeps API key server-side)
1115
- const { data, error, isLoading } = useSWR("/api/orders", fetcher);
1116
- return { orders: data?.data ?? [], error, isLoading };
1117
- }`
1118
- },
1119
- customers: {
1120
- raw_fetch: `// Search customers
1121
- const { data: customers } = await fetch("${BASE}/v1/stores/${storeVar}/customers?search=john", {
1122
- headers: { "x-api-key": "${keyVar}" }
1123
- }).then(r => r.json());
1124
-
1125
- // Create customer
1126
- const customer = await fetch("${BASE}/v1/stores/${storeVar}/customers", {
1127
- method: "POST",
1128
- headers: { "x-api-key": "${keyVar}", "Content-Type": "application/json" },
1129
- body: JSON.stringify({ first_name: "John", last_name: "Doe", email: "john@example.com" })
1130
- }).then(r => r.json());`,
1131
- nextjs_ssr: `// app/api/customers/route.ts
1132
- import { NextRequest, NextResponse } from "next/server";
1133
-
1134
- const headers = { "x-api-key": process.env.WHALE_API_KEY! };
1135
- const base = \`${BASE}/v1/stores/\${process.env.WHALE_STORE_ID}\`;
1136
-
1137
- export async function GET(req: NextRequest) {
1138
- const search = req.nextUrl.searchParams.get("q") || "";
1139
- const res = await fetch(\`\${base}/customers?search=\${encodeURIComponent(search)}\`, { headers });
1140
- return NextResponse.json(await res.json());
1141
- }`,
1142
- react_client: `// hooks/useCustomers.ts
1143
- import useSWR from "swr";
1144
-
1145
- export function useCustomers(search: string) {
1146
- const { data, isLoading } = useSWR(
1147
- search ? \`/api/customers?q=\${encodeURIComponent(search)}\` : null,
1148
- (url) => fetch(url).then(r => r.json())
1149
- );
1150
- return { customers: data?.data ?? [], isLoading };
1151
- }`
1152
- },
1153
- inventory: {
1154
- raw_fetch: `// Get inventory summary
1155
- const summary = await fetch("${BASE}/v1/stores/${storeVar}/inventory/summary", {
1156
- headers: { "x-api-key": "${keyVar}" }
1157
- }).then(r => r.json());
1158
-
1159
- // Adjust stock
1160
- await fetch("${BASE}/v1/stores/${storeVar}/inventory/adjust", {
1161
- method: "POST",
1162
- headers: { "x-api-key": "${keyVar}", "Content-Type": "application/json" },
1163
- body: JSON.stringify({ product_id: "...", location_id: "...", adjustment: -5, reason: "Sold offline" })
1164
- });`,
1165
- nextjs_ssr: `// app/inventory/page.tsx
1166
- async function getInventory() {
1167
- const res = await fetch(
1168
- \`${BASE}/v1/stores/\${process.env.WHALE_STORE_ID}/inventory/summary\`,
1169
- { headers: { "x-api-key": process.env.WHALE_API_KEY! }, next: { revalidate: 60 } }
1170
- );
1171
- return res.json();
33
+ /** Get cached spec if available (for sync callers like /openapi.json endpoint) */
34
+ function getCachedSpecSync() {
35
+ if (_cache && Date.now() - _cache.fetchedAt < CACHE_TTL_MS) {
36
+ return _cache.spec;
37
+ }
38
+ return null;
1172
39
  }
1173
40
 
1174
- export default async function InventoryPage() {
1175
- const summary = await getInventory();
1176
- return (
1177
- <div>
1178
- {summary.locations.map((loc: any) => (
1179
- <div key={loc.id}>
1180
- <h3>{loc.name}</h3>
1181
- <p>{loc.total_products} products, {loc.total_units} units</p>
1182
- </div>
1183
- ))}
1184
- </div>
1185
- );
1186
- }`,
1187
- react_client: `// hooks/useInventory.ts
1188
- import useSWR from "swr";
1189
-
1190
- export function useInventorySummary() {
1191
- const { data, isLoading } = useSWR("/api/inventory/summary", (url) => fetch(url).then(r => r.json()));
1192
- return { summary: data, isLoading };
1193
- }`
1194
- },
1195
- analytics: {
1196
- raw_fetch: `// Sales analytics
1197
- const sales = await fetch("${BASE}/v1/stores/${storeVar}/analytics/sales?period=last_30", {
1198
- headers: { "x-api-key": "${keyVar}" }
1199
- }).then(r => r.json());
1200
-
1201
- // Daily breakdown
1202
- const daily = await fetch("${BASE}/v1/stores/${storeVar}/analytics/sales/daily?period=last_7", {
1203
- headers: { "x-api-key": "${keyVar}" }
1204
- }).then(r => r.json());
1205
-
1206
- // Product performance
1207
- const topProducts = await fetch("${BASE}/v1/stores/${storeVar}/analytics/products?limit=10", {
1208
- headers: { "x-api-key": "${keyVar}" }
1209
- }).then(r => r.json());`,
1210
- nextjs_ssr: `// app/dashboard/page.tsx
1211
- async function getDashboard() {
1212
- const base = \`${BASE}/v1/stores/\${process.env.WHALE_STORE_ID}/analytics\`;
1213
- const headers = { "x-api-key": process.env.WHALE_API_KEY! };
1214
- const opts = { headers, next: { revalidate: 300 } as any };
1215
-
1216
- const [sales, daily, products] = await Promise.all([
1217
- fetch(\`\${base}/sales?period=last_30\`, opts).then(r => r.json()),
1218
- fetch(\`\${base}/sales/daily?period=last_7\`, opts).then(r => r.json()),
1219
- fetch(\`\${base}/products?limit=5\`, opts).then(r => r.json()),
1220
- ]);
41
+ // ---------------------------------------------------------------------------
42
+ // Helpers derive section/endpoint data from the live OpenAPI spec
43
+ // ---------------------------------------------------------------------------
1221
44
 
1222
- return { sales, daily, products };
45
+ function deriveSections(spec) {
46
+ const tags = spec.tags || [];
47
+ const tagEndpointCounts = {};
48
+ for (const [, methods] of Object.entries(spec.paths)) {
49
+ for (const [, op] of Object.entries(methods)) {
50
+ const operation = op;
51
+ if (operation.tags) {
52
+ for (const tag of operation.tags) {
53
+ tagEndpointCounts[tag] = (tagEndpointCounts[tag] || 0) + 1;
54
+ }
55
+ }
56
+ }
57
+ }
58
+ return tags.map(t => ({
59
+ key: t.name.toLowerCase().replace(/\s+/g, "_"),
60
+ name: t.name,
61
+ description: t.description || "",
62
+ endpoint_count: tagEndpointCounts[t.name] || 0
63
+ }));
1223
64
  }
1224
-
1225
- export default async function DashboardPage() {
1226
- const { sales, daily, products } = await getDashboard();
1227
- return (
1228
- <div>
1229
- <h1>Dashboard</h1>
1230
- <div className="grid grid-cols-3 gap-4">
1231
- <div>Revenue: \${sales.total_revenue}</div>
1232
- <div>Orders: {sales.total_orders}</div>
1233
- <div>AOV: \${sales.average_order_value}</div>
1234
- </div>
1235
- </div>
1236
- );
1237
- }`,
1238
- react_client: `// hooks/useAnalytics.ts
1239
- import useSWR from "swr";
1240
-
1241
- export function useSalesAnalytics(period = "last_30") {
1242
- const { data, isLoading } = useSWR(
1243
- \`/api/analytics/sales?period=\${period}\`,
1244
- (url) => fetch(url).then(r => r.json())
1245
- );
1246
- return { sales: data, isLoading };
1247
- }`
1248
- },
1249
- locations: {
1250
- raw_fetch: `// List all locations
1251
- const { data: locations } = await fetch("${BASE}/v1/stores/${storeVar}/locations", {
1252
- headers: { "x-api-key": "${keyVar}" }
1253
- }).then(r => r.json());`,
1254
- nextjs_ssr: `// lib/api.ts — shared helper
1255
- export async function getLocations() {
1256
- const res = await fetch(
1257
- \`${BASE}/v1/stores/\${process.env.WHALE_STORE_ID}/locations\`,
1258
- { headers: { "x-api-key": process.env.WHALE_API_KEY! }, next: { revalidate: 3600 } }
1259
- );
1260
- return res.json().then(r => r.data);
1261
- }`,
1262
- react_client: `// hooks/useLocations.ts
1263
- import useSWR from "swr";
1264
-
1265
- export function useLocations() {
1266
- const { data } = useSWR("/api/locations", (url) => fetch(url).then(r => r.json()));
1267
- return data?.data ?? [];
1268
- }`
1269
- },
1270
- storefront: {
1271
- raw_fetch: `// Track page view event
1272
- await fetch("${BASE}/v1/stores/${storeVar}/storefront/events", {
1273
- method: "POST",
1274
- headers: { "x-api-key": "${keyVar}", "Content-Type": "application/json" },
1275
- body: JSON.stringify({
1276
- session_id: sessionId,
1277
- event_type: "page_view",
1278
- event_data: { url: "/products/blue-dream", product_id: "..." }
1279
- })
1280
- });
1281
-
1282
- // Send OTP for customer login
1283
- await fetch("${BASE}/v1/stores/${storeVar}/storefront/auth/send-code", {
1284
- method: "POST",
1285
- headers: { "x-api-key": "${keyVar}", "Content-Type": "application/json" },
1286
- body: JSON.stringify({ email: "customer@example.com" })
1287
- });`,
1288
- nextjs_ssr: `// app/api/auth/send-code/route.ts
1289
- import { NextRequest, NextResponse } from "next/server";
1290
-
1291
- export async function POST(req: NextRequest) {
1292
- const { email } = await req.json();
1293
- const res = await fetch(
1294
- \`${BASE}/v1/stores/\${process.env.WHALE_STORE_ID}/storefront/auth/send-code\`,
1295
- {
1296
- method: "POST",
1297
- headers: { "x-api-key": process.env.WHALE_API_KEY!, "Content-Type": "application/json" },
1298
- body: JSON.stringify({ email }),
65
+ function deriveEndpointsForSection(spec, sectionName) {
66
+ const endpoints = [];
67
+ for (const [path, methods] of Object.entries(spec.paths)) {
68
+ for (const [method, op] of Object.entries(methods)) {
69
+ const operation = op;
70
+ if (operation.tags?.some(t => t.toLowerCase().replace(/\s+/g, "_") === sectionName || t === sectionName)) {
71
+ endpoints.push({
72
+ method: method.toUpperCase(),
73
+ path,
74
+ summary: operation.summary || "",
75
+ description: operation.description,
76
+ parameters: operation.parameters,
77
+ requestBody: operation.requestBody,
78
+ responses: operation.responses
79
+ });
80
+ }
1299
81
  }
1300
- );
1301
- return NextResponse.json(await res.json(), { status: res.status });
1302
- }`,
1303
- react_client: `// hooks/useAuth.ts
1304
- import { useState } from "react";
1305
-
1306
- export function useStorefrontAuth() {
1307
- const [loading, setLoading] = useState(false);
1308
-
1309
- const sendCode = async (email: string) => {
1310
- setLoading(true);
1311
- await fetch("/api/auth/send-code", {
1312
- method: "POST",
1313
- headers: { "Content-Type": "application/json" },
1314
- body: JSON.stringify({ email }),
1315
- });
1316
- setLoading(false);
1317
- };
1318
-
1319
- const verifyCode = async (email: string, code: string) => {
1320
- const res = await fetch("/api/auth/verify-code", {
1321
- method: "POST",
1322
- headers: { "Content-Type": "application/json" },
1323
- body: JSON.stringify({ email, code }),
1324
- });
1325
- return res.json(); // { customer_id, session_token }
1326
- };
1327
-
1328
- return { sendCode, verifyCode, loading };
1329
- }`
1330
- },
1331
- coa: {
1332
- raw_fetch: `// List COAs
1333
- const { data: coas } = await fetch("${BASE}/v1/stores/${storeVar}/coa?limit=50", {
1334
- headers: { "x-api-key": "${keyVar}" }
1335
- }).then(r => r.json());
1336
-
1337
- // Verify by sample ID
1338
- const verified = await fetch("${BASE}/v1/stores/${storeVar}/coa/verify?sample_id=LAB-2026-001", {
1339
- headers: { "x-api-key": "${keyVar}" }
1340
- }).then(r => r.json());
1341
-
1342
- // Get embeddable COA data
1343
- const embed = await fetch("${BASE}/v1/stores/${storeVar}/coa/\${coaId}/embed", {
1344
- headers: { "x-api-key": "${keyVar}" }
1345
- }).then(r => r.json());`,
1346
- nextjs_ssr: `// app/products/[id]/coa/page.tsx
1347
- async function getCOAEmbed(coaId: string) {
1348
- const res = await fetch(
1349
- \`${BASE}/v1/stores/\${process.env.WHALE_STORE_ID}/coa/\${coaId}/embed\`,
1350
- { headers: { "x-api-key": process.env.WHALE_API_KEY! }, next: { revalidate: 3600 } }
1351
- );
1352
- return res.json();
82
+ }
83
+ return endpoints;
84
+ }
85
+ function findSectionTag(spec, sectionKey) {
86
+ return spec.tags?.find(t => t.name.toLowerCase().replace(/\s+/g, "_") === sectionKey || t.name === sectionKey);
1353
87
  }
1354
88
 
1355
- export default async function COAPage({ params }: { params: { id: string } }) {
1356
- const { html, data } = await getCOAEmbed(params.id);
1357
- return (
1358
- <div>
1359
- <h1>Certificate of Analysis — {data.product_name}</h1>
1360
- <div dangerouslySetInnerHTML={{ __html: html }} />
1361
- </div>
1362
- );
1363
- }`,
1364
- react_client: `// components/COAViewer.tsx
1365
- "use client";
1366
- import { useEffect, useState } from "react";
1367
-
1368
- export function COAViewer({ coaId }: { coaId: string }) {
1369
- const [html, setHtml] = useState("");
1370
-
1371
- useEffect(() => {
1372
- fetch(\`/api/coa/\${coaId}/embed\`).then(r => r.json()).then(d => setHtml(d.html));
1373
- }, [coaId]);
89
+ // ---------------------------------------------------------------------------
90
+ // Auth guide (static these are usage instructions, not endpoint data)
91
+ // ---------------------------------------------------------------------------
1374
92
 
1375
- return <div dangerouslySetInnerHTML={{ __html: html }} />;
1376
- }`
93
+ const AUTH_GUIDE = {
94
+ authentication: {
95
+ method: "API Key via x-api-key header",
96
+ key_format: {
97
+ live: "wk_live_<32 chars> — production data",
98
+ test: "wk_test_<32 chars> — sandbox data"
1377
99
  },
1378
- portal: {
1379
- raw_fetch: `// B2B portal login
1380
- await fetch("${BASE}/v1/stores/${storeVar}/portal/auth/send-code", {
1381
- method: "POST",
1382
- headers: { "x-api-key": "${keyVar}", "Content-Type": "application/json" },
1383
- body: JSON.stringify({ email: "buyer@dispensary.com" })
1384
- });
1385
-
1386
- // Verify and get session
1387
- const session = await fetch("${BASE}/v1/stores/${storeVar}/portal/auth/verify", {
1388
- method: "POST",
1389
- headers: { "x-api-key": "${keyVar}", "Content-Type": "application/json" },
1390
- body: JSON.stringify({ email: "buyer@dispensary.com", code: "123456" })
1391
- }).then(r => r.json());
1392
-
1393
- // List documents with portal token
1394
- const { data: docs } = await fetch("${BASE}/v1/stores/${storeVar}/portal/documents", {
1395
- headers: { "x-api-key": "${keyVar}", "Authorization": "Bearer " + session.token }
1396
- }).then(r => r.json());`,
1397
- nextjs_ssr: `// app/portal/layout.tsx — server-side session check
1398
- import { cookies } from "next/headers";
1399
- import { redirect } from "next/navigation";
1400
-
1401
- export default async function PortalLayout({ children }: { children: React.ReactNode }) {
1402
- const token = cookies().get("portal_token")?.value;
1403
- if (!token) redirect("/portal/login");
1404
- return <div className="portal-layout">{children}</div>;
1405
- }`,
1406
- react_client: `// hooks/usePortal.ts
1407
- import { useState } from "react";
1408
-
1409
- export function usePortal() {
1410
- const [token, setToken] = useState<string | null>(null);
1411
-
1412
- const login = async (email: string, code: string) => {
1413
- const res = await fetch("/api/portal/verify", {
1414
- method: "POST",
1415
- headers: { "Content-Type": "application/json" },
1416
- body: JSON.stringify({ email, code }),
1417
- });
1418
- const data = await res.json();
1419
- setToken(data.token);
1420
- return data;
1421
- };
1422
-
1423
- const getDocuments = async () => {
1424
- const res = await fetch("/api/portal/documents", {
1425
- headers: { Authorization: \`Bearer \${token}\` },
1426
- });
1427
- return res.json();
1428
- };
1429
-
1430
- return { token, login, getDocuments };
1431
- }`
100
+ example: `fetch("${GATEWAY_BASE_URL}/v1/stores/{store_id}/products", {\n headers: { "x-api-key": "wk_live_..." }\n})`
101
+ },
102
+ scopes: {
103
+ description: "Each API key has scopes controlling access",
104
+ wildcard: "* full access (all read + write)",
105
+ categories: ["read:products, write:products", "read:orders, write:orders", "read:customers, write:customers", "read:inventory, write:inventory", "read:analytics", "read:storefront, write:storefront", "read:agents, write:agents", "read:documents, write:documents", "read:telemetry, write:telemetry"]
106
+ },
107
+ rate_limiting: {
108
+ headers: ["X-RateLimit-Limit", "X-RateLimit-Remaining", "X-RateLimit-Reset", "Retry-After (on 429)"],
109
+ plans: {
110
+ free: "60/min",
111
+ starter: "300/min",
112
+ growth: "600/min",
113
+ enterprise: "3000/min"
1432
114
  }
1433
- };
1434
- return templates[section] ?? null;
1435
- }
115
+ },
116
+ pagination: {
117
+ type: "Cursor-based",
118
+ params: ["limit (1-500, default 25)", "starting_after (resource ID)", "ending_before (resource ID)"]
119
+ },
120
+ errors: {
121
+ format: '{ "error": { "type": "...", "message": "...", "code": "...", "request_id": "req_..." } }',
122
+ types: ["authentication_error (401)", "permission_error (403)", "not_found_error (404)", "invalid_request_error (400/422)", "rate_limit_error (429)", "conflict_error (409)", "api_error (500)"]
123
+ }
124
+ };
1436
125
 
1437
126
  // ---------------------------------------------------------------------------
1438
- // Quick start guide
127
+ // Quick start guide (static usage instructions)
1439
128
  // ---------------------------------------------------------------------------
1440
129
 
1441
130
  const QUICK_START = {
1442
- title: "WhaleTools API Quick Start Guide",
131
+ title: "WhaleTools API Quick Start",
1443
132
  steps: [{
1444
133
  step: 1,
1445
134
  title: "Get your API key",
1446
- description: "Generate an API key from the WhaleTools dashboard (Settings API Keys) or use the api_keys MCP tool.",
1447
- code: `// Via dashboard: Settings → API Keys → Generate
1448
- // Or programmatically:
1449
- // api_keys tool: { action: "generate", name: "My Website", scopes: ["read:products", "read:cart", "write:cart", "write:checkout"] }`
135
+ description: "Generate a key via the dashboard at whaletools.dev/dashboard/api-keys or use the api_keys MCP tool."
1450
136
  }, {
1451
137
  step: 2,
1452
- title: "Set up environment variables",
1453
- description: "Store your API key and store ID securely. Never expose the API key in client-side code.",
1454
- code: `# .env.local (Next.js) or .env
1455
- WHALE_API_KEY=wk_live_abc123...
1456
- WHALE_STORE_ID=your-store-uuid`
138
+ title: "Make your first request",
139
+ code: `curl -H "x-api-key: wk_live_..." ${GATEWAY_BASE_URL}/v1/stores/{store_id}/products`
1457
140
  }, {
1458
141
  step: 3,
1459
- title: "Fetch products",
1460
- description: "Make your first API call to list published products.",
1461
- code: `const res = await fetch(
1462
- \`https://whale-gateway.fly.dev/v1/stores/\${process.env.WHALE_STORE_ID}/products?status=published&limit=25\`,
1463
- { headers: { "x-api-key": process.env.WHALE_API_KEY } }
1464
- );
1465
- const { data: products, has_more } = await res.json();
1466
- console.log(\`Loaded \${products.length} products\`);`
142
+ title: "Use the TypeScript SDK",
143
+ code: `import { WhaleClient } from "@neowhale/api-client";\nconst whale = new WhaleClient({ apiKey: "wk_live_..." });\nconst products = await whale.products.list();`
1467
144
  }, {
1468
145
  step: 4,
1469
- title: "Create a cart",
1470
- description: "Start a shopping session by creating a cart.",
1471
- code: `const cart = await fetch(
1472
- \`https://whale-gateway.fly.dev/v1/stores/\${STORE_ID}/cart\`,
1473
- {
1474
- method: "POST",
1475
- headers: { "x-api-key": API_KEY, "Content-Type": "application/json" },
1476
- body: JSON.stringify({})
1477
- }
1478
- ).then(r => r.json());
1479
- console.log("Cart ID:", cart.id);`
1480
- }, {
1481
- step: 5,
1482
- title: "Add items to cart",
1483
- description: "Add products to the cart by product ID.",
1484
- code: `await fetch(
1485
- \`https://whale-gateway.fly.dev/v1/stores/\${STORE_ID}/cart/\${cart.id}/items\`,
1486
- {
1487
- method: "POST",
1488
- headers: { "x-api-key": API_KEY, "Content-Type": "application/json" },
1489
- body: JSON.stringify({ product_id: products[0].id, quantity: 1 })
1490
- }
1491
- );`
1492
- }, {
1493
- step: 6,
1494
- title: "Checkout",
1495
- description: "Convert the cart into an order.",
1496
- code: `const order = await fetch(
1497
- \`https://whale-gateway.fly.dev/v1/stores/\${STORE_ID}/checkout\`,
1498
- {
1499
- method: "POST",
1500
- headers: { "x-api-key": API_KEY, "Content-Type": "application/json" },
1501
- body: JSON.stringify({
1502
- cart_id: cart.id,
1503
- shipping_address: {
1504
- street: "123 Main St",
1505
- city: "Charlotte",
1506
- state: "NC",
1507
- zip: "28202"
1508
- }
1509
- })
1510
- }
1511
- ).then(r => r.json());
1512
- console.log("Order created:", order.id, "Status:", order.status);`
146
+ title: "Use the Python SDK",
147
+ code: `from whaletools import WhaleClient\nwhale = WhaleClient(api_key="wk_live_...")\nproducts = whale.products.list()`
1513
148
  }],
1514
- tips: ["Keep your API key server-side. Use Next.js Route Handlers or API routes as a proxy for client-side calls.", "Use cursor pagination (starting_after) instead of offset for consistent results.", "Set next: { revalidate: 60 } in Next.js fetch for automatic ISR (Incremental Static Regeneration).", "Idempotency-Key header prevents duplicate orders — always set it on checkout requests.", "All list endpoints return { object: 'list', data: [...], has_more: boolean }."]
149
+ api_reference: `${GATEWAY_BASE_URL}/docs`,
150
+ openapi_spec: `${GATEWAY_BASE_URL}/openapi.json`
1515
151
  };
1516
152
 
1517
153
  // ---------------------------------------------------------------------------
@@ -1523,20 +159,25 @@ export async function handleApiDocs(_sb, args, _storeId) {
1523
159
  switch (action) {
1524
160
  case "sections":
1525
161
  {
1526
- const sections = Object.entries(SECTIONS).map(([key, s]) => ({
1527
- key,
1528
- name: s.name,
1529
- description: s.description,
1530
- endpoint_count: s.endpoints.length
1531
- }));
1532
- return {
1533
- success: true,
1534
- data: {
1535
- total_endpoints: sections.reduce((sum, s) => sum + s.endpoint_count, 0),
1536
- sections,
1537
- usage: 'Use api_docs with action "endpoints" and section "<key>" to see full endpoint details.'
1538
- }
1539
- };
162
+ try {
163
+ const spec = await fetchOpenApiSpec();
164
+ const sections = deriveSections(spec);
165
+ return {
166
+ success: true,
167
+ data: {
168
+ api_version: spec.info.version,
169
+ total_endpoints: sections.reduce((sum, s) => sum + s.endpoint_count, 0),
170
+ total_sections: sections.length,
171
+ sections,
172
+ usage: 'Use api_docs with action "endpoints" and section "<key>" to see full endpoint details.'
173
+ }
174
+ };
175
+ } catch (err) {
176
+ return {
177
+ success: false,
178
+ error: `Failed to fetch API spec: ${err.message}`
179
+ };
180
+ }
1540
181
  }
1541
182
  case "endpoints":
1542
183
  {
@@ -1547,53 +188,82 @@ export async function handleApiDocs(_sb, args, _storeId) {
1547
188
  error: 'Missing required param "section". Use action "sections" to see available sections.'
1548
189
  };
1549
190
  }
1550
- const s = SECTIONS[section];
1551
- if (!s) {
191
+ try {
192
+ const spec = await fetchOpenApiSpec();
193
+ const tag = findSectionTag(spec, section);
194
+ if (!tag) {
195
+ const available = (spec.tags || []).map(t => t.name.toLowerCase().replace(/\s+/g, "_"));
196
+ return {
197
+ success: false,
198
+ error: `Unknown section "${section}". Available: ${available.join(", ")}`
199
+ };
200
+ }
201
+ const endpoints = deriveEndpointsForSection(spec, section);
202
+ return {
203
+ success: true,
204
+ data: {
205
+ section: tag.name,
206
+ description: tag.description || "",
207
+ endpoint_count: endpoints.length,
208
+ endpoints
209
+ }
210
+ };
211
+ } catch (err) {
1552
212
  return {
1553
213
  success: false,
1554
- error: `Unknown section "${section}". Available: ${Object.keys(SECTIONS).join(", ")}`
214
+ error: `Failed to fetch API spec: ${err.message}`
1555
215
  };
1556
216
  }
1557
- return {
1558
- success: true,
1559
- data: {
1560
- section: s.name,
1561
- description: s.description,
1562
- base: s.base,
1563
- endpoints: s.endpoints
1564
- }
1565
- };
1566
- }
1567
- case "auth":
1568
- {
1569
- return {
1570
- success: true,
1571
- data: AUTH_GUIDE
1572
- };
1573
217
  }
1574
- case "examples":
218
+ case "search":
1575
219
  {
1576
- const section = args.section;
1577
- if (!section) {
220
+ const query = (args.query || "").toLowerCase();
221
+ if (!query) {
1578
222
  return {
1579
223
  success: false,
1580
- error: 'Missing required param "section". Use action "sections" to see available sections.'
224
+ error: 'Missing required param "query". Provide a search term (e.g., "products", "checkout", "webhook").'
1581
225
  };
1582
226
  }
1583
- const examples = getExamples(section);
1584
- if (!examples) {
227
+ try {
228
+ const spec = await fetchOpenApiSpec();
229
+ const results = [];
230
+ for (const [path, methods] of Object.entries(spec.paths)) {
231
+ for (const [method, op] of Object.entries(methods)) {
232
+ const operation = op;
233
+ const text = `${path} ${operation.summary || ""} ${operation.description || ""} ${(operation.tags || []).join(" ")}`.toLowerCase();
234
+ if (text.includes(query)) {
235
+ results.push({
236
+ method: method.toUpperCase(),
237
+ path,
238
+ summary: operation.summary || "",
239
+ tags: operation.tags || []
240
+ });
241
+ }
242
+ }
243
+ }
244
+ return {
245
+ success: true,
246
+ data: {
247
+ query,
248
+ result_count: results.length,
249
+ results: results.slice(0, 50),
250
+ ...(results.length > 50 ? {
251
+ note: `Showing first 50 of ${results.length} results. Narrow your search.`
252
+ } : {})
253
+ }
254
+ };
255
+ } catch (err) {
1585
256
  return {
1586
257
  success: false,
1587
- error: `No examples for section "${section}". Available: ${Object.keys(SECTIONS).join(", ")}`
258
+ error: `Failed to fetch API spec: ${err.message}`
1588
259
  };
1589
260
  }
261
+ }
262
+ case "auth":
263
+ {
1590
264
  return {
1591
265
  success: true,
1592
- data: {
1593
- section,
1594
- examples,
1595
- tip: "Always keep API keys server-side. Use Next.js Route Handlers or API routes as a proxy for client-side calls."
1596
- }
266
+ data: AUTH_GUIDE
1597
267
  };
1598
268
  }
1599
269
  case "quick_start":
@@ -1606,185 +276,38 @@ export async function handleApiDocs(_sb, args, _storeId) {
1606
276
  default:
1607
277
  return {
1608
278
  success: false,
1609
- error: `Unknown api_docs action: "${action}". Available: sections, endpoints, auth, examples, quick_start`
279
+ error: `Unknown api_docs action: "${action}". Available: sections, endpoints, search, auth, quick_start`
1610
280
  };
1611
281
  }
1612
282
  }
1613
283
 
1614
284
  // ---------------------------------------------------------------------------
1615
- // OpenAPI 3.0 spec generation — serves /openapi.json for Scalar docs viewer
285
+ // OpenAPI spec proxy — serves /openapi.json on the agent server
1616
286
  // ---------------------------------------------------------------------------
1617
287
 
1618
- let _cachedSpec = null;
1619
-
1620
- /** Convert :param to {param} for OpenAPI path syntax */
1621
- function toOpenApiPath(path) {
1622
- return path.replace(/:([a-zA-Z_]+)/g, "{$1}");
288
+ /** Generate (fetch) the OpenAPI spec — async version for the handler */
289
+ export async function generateOpenApiSpec() {
290
+ return fetchOpenApiSpec();
1623
291
  }
1624
292
 
1625
- /** Build OpenAPI parameter objects from the endpoint definition */
1626
- function buildParameters(endpoint, path) {
1627
- const params = [];
1628
-
1629
- // Extract path parameters from the path itself
1630
- const pathParams = path.match(/:([a-zA-Z_]+)/g) || [];
1631
- for (const p of pathParams) {
1632
- const name = p.slice(1);
1633
- params.push({
1634
- name,
1635
- in: "path",
1636
- required: true,
1637
- schema: {
1638
- type: "string"
1639
- },
1640
- description: name === "storeId" ? "Store UUID" : name === "id" ? "Resource UUID" : `${name} identifier`
1641
- });
1642
- }
1643
-
1644
- // Add query parameters for GET endpoints
1645
- if (endpoint.method === "GET" && endpoint.params) {
1646
- for (const [name, desc] of Object.entries(endpoint.params)) {
1647
- params.push({
1648
- name,
1649
- in: "query",
1650
- required: desc.toLowerCase().startsWith("required"),
1651
- schema: {
1652
- type: "string"
1653
- },
1654
- description: desc
1655
- });
1656
- }
1657
- }
1658
- return params;
1659
- }
293
+ /** Sync fallback: return cached spec or a redirect hint */
294
+ export function generateOpenApiSpecSync() {
295
+ const cached = getCachedSpecSync();
296
+ if (cached) return cached;
1660
297
 
1661
- /** Build request body schema from endpoint body definition */
1662
- function buildRequestBody(endpoint) {
1663
- if (!endpoint.body || endpoint.method === "GET") return undefined;
1664
- const properties = {};
1665
- const required = [];
1666
- for (const [name, desc] of Object.entries(endpoint.body)) {
1667
- if (name === "...") {
1668
- properties["additionalProperties"] = {
1669
- type: "object",
1670
- description: desc
1671
- };
1672
- continue;
1673
- }
1674
- properties[name] = {
1675
- type: "string",
1676
- description: desc
1677
- };
1678
- if (desc.toLowerCase().startsWith("required")) {
1679
- required.push(name);
1680
- }
1681
- }
298
+ // Return a minimal spec pointing to the canonical source
1682
299
  return {
1683
- required: true,
1684
- content: {
1685
- "application/json": {
1686
- schema: {
1687
- type: "object",
1688
- properties,
1689
- ...(required.length > 0 ? {
1690
- required
1691
- } : {})
1692
- }
1693
- }
1694
- }
1695
- };
1696
- }
1697
-
1698
- /** Generate the full OpenAPI 3.0 spec from SECTIONS */
1699
- export function generateOpenApiSpec() {
1700
- if (_cachedSpec) return _cachedSpec;
1701
- const paths = {};
1702
- for (const [sectionKey, section] of Object.entries(SECTIONS)) {
1703
- for (const endpoint of section.endpoints) {
1704
- const openApiPath = toOpenApiPath(endpoint.path);
1705
- if (!paths[openApiPath]) paths[openApiPath] = {};
1706
- const method = endpoint.method.toLowerCase();
1707
- const operation = {
1708
- summary: endpoint.description,
1709
- operationId: `${method}_${sectionKey}_${endpoint.path.split("/").pop()?.replace(/:/g, "")}`,
1710
- tags: [section.name],
1711
- parameters: buildParameters(endpoint, endpoint.path),
1712
- responses: {
1713
- "200": {
1714
- description: endpoint.response || "Success",
1715
- content: {
1716
- "application/json": {
1717
- schema: {
1718
- type: "object"
1719
- }
1720
- }
1721
- }
1722
- },
1723
- "401": {
1724
- description: "Unauthorized — missing or invalid API key"
1725
- },
1726
- "404": {
1727
- description: "Resource not found"
1728
- }
1729
- },
1730
- security: [{
1731
- BearerAuth: []
1732
- }, {
1733
- ApiKeyAuth: []
1734
- }]
1735
- };
1736
- if (endpoint.notes) {
1737
- operation.description = endpoint.notes;
1738
- }
1739
- const requestBody = buildRequestBody(endpoint);
1740
- if (requestBody) {
1741
- operation.requestBody = requestBody;
1742
- }
1743
- paths[openApiPath][method] = operation;
1744
- }
1745
- }
1746
- _cachedSpec = {
1747
- openapi: "3.0.3",
300
+ openapi: "3.1.0",
1748
301
  info: {
1749
302
  title: "WhaleTools Platform API",
1750
- description: "Complete commerce, catalog, CRM, analytics, and agent API for the WhaleTools platform.",
1751
- version: "2026-03-07",
1752
- contact: {
1753
- name: "WhaleTools",
1754
- url: "https://whaletools.dev"
1755
- }
303
+ version: "latest",
304
+ description: `Full spec available at ${OPENAPI_URL}`
1756
305
  },
1757
306
  servers: [{
1758
307
  url: GATEWAY_BASE_URL,
1759
308
  description: "Production"
1760
309
  }],
1761
- security: [{
1762
- BearerAuth: []
1763
- }, {
1764
- ApiKeyAuth: []
1765
- }],
1766
- paths,
1767
- components: {
1768
- securitySchemes: {
1769
- BearerAuth: {
1770
- type: "http",
1771
- scheme: "bearer",
1772
- bearerFormat: "JWT",
1773
- description: "Supabase JWT from auth login"
1774
- },
1775
- ApiKeyAuth: {
1776
- type: "apiKey",
1777
- in: "header",
1778
- name: "x-api-key",
1779
- description: "Store API key generated via the api_keys tool"
1780
- }
1781
- }
1782
- },
1783
- tags: Object.entries(SECTIONS).map(([, s]) => ({
1784
- name: s.name,
1785
- description: s.description
1786
- }))
310
+ paths: {}
1787
311
  };
1788
- return _cachedSpec;
1789
312
  }
1790
313
  //# sourceMappingURL=api-docs.js.map