whale-code 6.5.4 → 6.5.5

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.
@@ -0,0 +1,1478 @@
1
+ // server/handlers/api-docs.ts — Static API documentation reference for agents
2
+ const GATEWAY_BASE_URL = "https://whale-gateway.fly.dev";
3
+ // ---------------------------------------------------------------------------
4
+ // Endpoint registry — every public REST API route
5
+ // ---------------------------------------------------------------------------
6
+ const SECTIONS = {
7
+ products: {
8
+ name: "Products",
9
+ description: "Product catalog CRUD, variations, and categories",
10
+ base: "/v1/stores/:storeId/products",
11
+ endpoints: [
12
+ {
13
+ method: "GET",
14
+ path: "/v1/stores/:storeId/products",
15
+ scope: "read:products",
16
+ description: "List products with pagination and filters",
17
+ params: {
18
+ limit: "Number (1-100, default 25)",
19
+ starting_after: "Product ID cursor for forward pagination",
20
+ ending_before: "Product ID cursor for backward pagination",
21
+ category_id: "Filter by category UUID",
22
+ status: "Filter by status: published, draft, archived",
23
+ type: "Filter by type: simple, variable, grouped",
24
+ search: "Full-text search on name and SKU",
25
+ },
26
+ response: '{ object: "list", data: Product[], has_more: boolean, url: string }',
27
+ },
28
+ {
29
+ method: "GET",
30
+ path: "/v1/stores/:storeId/products/:id",
31
+ scope: "read:products",
32
+ description: "Get a single product with variations and inventory",
33
+ response: "Product (includes pricing_data, field_values, inventory, variations)",
34
+ },
35
+ {
36
+ method: "POST",
37
+ path: "/v1/stores/:storeId/products",
38
+ scope: "write:products",
39
+ description: "Create a new product",
40
+ body: {
41
+ name: "Required. Product name",
42
+ sku: "SKU code",
43
+ category_id: "Category UUID",
44
+ status: "published | draft (default: draft)",
45
+ type: "simple | variable | grouped (default: simple)",
46
+ description: "Full description",
47
+ short_description: "Summary text",
48
+ cost_price: "Cost per unit",
49
+ pricing_data: "Pricing tiers JSON",
50
+ field_values: "Custom fields JSON (must match category field schema)",
51
+ featured_image: "Image URL",
52
+ image_gallery: "Array of image URLs",
53
+ manage_stock: "Boolean — track inventory",
54
+ stock_quantity: "Initial stock (if manage_stock)",
55
+ },
56
+ response: "Product",
57
+ },
58
+ {
59
+ method: "PATCH",
60
+ path: "/v1/stores/:storeId/products/:id",
61
+ scope: "write:products",
62
+ description: "Update an existing product (partial update)",
63
+ body: { "...": "Any product fields to update" },
64
+ response: "Product",
65
+ },
66
+ {
67
+ method: "DELETE",
68
+ path: "/v1/stores/:storeId/products/:id",
69
+ scope: "write:products",
70
+ description: "Archive a product (soft delete)",
71
+ response: "{ deleted: true }",
72
+ },
73
+ {
74
+ method: "GET",
75
+ path: "/v1/stores/:storeId/products/:id/variations",
76
+ scope: "read:products",
77
+ description: "List variations for a product",
78
+ response: '{ object: "list", data: Variation[] }',
79
+ },
80
+ {
81
+ method: "POST",
82
+ path: "/v1/stores/:storeId/products/:id/variations",
83
+ scope: "write:products",
84
+ description: "Create a product variation",
85
+ body: {
86
+ name: "Variation name (e.g. 'Small', '1oz')",
87
+ sku: "Variation SKU",
88
+ pricing_data: "Variation-specific pricing",
89
+ field_values: "Variation-specific field values",
90
+ stock_quantity: "Initial stock",
91
+ },
92
+ response: "Variation",
93
+ },
94
+ {
95
+ method: "PATCH",
96
+ path: "/v1/stores/:storeId/products/:id/variations/:variationId",
97
+ scope: "write:products",
98
+ description: "Update a variation",
99
+ body: { "...": "Any variation fields" },
100
+ response: "Variation",
101
+ },
102
+ {
103
+ method: "DELETE",
104
+ path: "/v1/stores/:storeId/products/:id/variations/:variationId",
105
+ scope: "write:products",
106
+ description: "Delete a variation",
107
+ response: "{ deleted: true }",
108
+ },
109
+ ],
110
+ },
111
+ orders: {
112
+ name: "Orders",
113
+ description: "Order management — list, view, update status, void, and refund",
114
+ base: "/v1/stores/:storeId/orders",
115
+ endpoints: [
116
+ {
117
+ method: "GET",
118
+ path: "/v1/stores/:storeId/orders",
119
+ scope: "read:orders",
120
+ description: "List orders with pagination",
121
+ params: {
122
+ limit: "Number (1-100, default 25)",
123
+ starting_after: "Order ID cursor",
124
+ ending_before: "Order ID cursor",
125
+ status: "Filter: pending, confirmed, processing, shipped, delivered, cancelled",
126
+ customer_id: "Filter by customer UUID",
127
+ },
128
+ response: '{ object: "list", data: Order[], has_more: boolean }',
129
+ },
130
+ {
131
+ method: "GET",
132
+ path: "/v1/stores/:storeId/orders/:id",
133
+ scope: "read:orders",
134
+ description: "Get a single order with line items",
135
+ response: "Order (includes line_items[], customer, payment_details)",
136
+ },
137
+ {
138
+ method: "PATCH",
139
+ path: "/v1/stores/:storeId/orders/:id",
140
+ scope: "write:orders",
141
+ description: "Update order status or fields",
142
+ body: { status: "New status", notes: "Optional order notes" },
143
+ response: "Order",
144
+ },
145
+ {
146
+ method: "POST",
147
+ path: "/v1/stores/:storeId/orders/:id/void",
148
+ scope: "write:orders",
149
+ description: "Void an unsettled order",
150
+ response: "Order (status: voided)",
151
+ notes: "Only works on orders where payment has not yet settled",
152
+ },
153
+ {
154
+ method: "POST",
155
+ path: "/v1/stores/:storeId/orders/:id/refund",
156
+ scope: "write:orders",
157
+ description: "Refund a settled order",
158
+ body: { amount: "Partial refund amount (optional — omit for full refund)", reason: "Refund reason" },
159
+ response: "Order (includes refund details)",
160
+ },
161
+ ],
162
+ },
163
+ cart: {
164
+ name: "Cart",
165
+ description: "Shopping cart — create, add/update/remove items, view",
166
+ base: "/v1/stores/:storeId/cart",
167
+ endpoints: [
168
+ {
169
+ method: "POST",
170
+ path: "/v1/stores/:storeId/cart",
171
+ scope: "write:cart",
172
+ description: "Create a new shopping cart",
173
+ body: {
174
+ customer_id: "Optional customer UUID",
175
+ location_id: "Optional location UUID (auto-resolves default)",
176
+ },
177
+ response: "Cart { id, items: [], totals }",
178
+ },
179
+ {
180
+ method: "GET",
181
+ path: "/v1/stores/:storeId/cart/:cartId",
182
+ scope: "read:cart",
183
+ description: "Get cart with all items and computed totals",
184
+ response: "Cart { id, items: CartItem[], totals: { subtotal, tax, total } }",
185
+ },
186
+ {
187
+ method: "POST",
188
+ path: "/v1/stores/:storeId/cart/:cartId/items",
189
+ scope: "write:cart",
190
+ description: "Add an item to the cart",
191
+ body: {
192
+ product_id: "Required. Product UUID",
193
+ quantity: "Required. Number of units",
194
+ variation_id: "Variation UUID (for variable products)",
195
+ tier: "Pricing tier name (e.g. 'retail', 'wholesale')",
196
+ unit_price: "Override price (optional — auto-resolved from pricing_data)",
197
+ },
198
+ response: "Cart (updated)",
199
+ },
200
+ {
201
+ method: "PATCH",
202
+ path: "/v1/stores/:storeId/cart/:cartId/items/:itemId",
203
+ scope: "write:cart",
204
+ description: "Update cart item quantity",
205
+ body: { quantity: "New quantity" },
206
+ response: "Cart (updated)",
207
+ },
208
+ {
209
+ method: "DELETE",
210
+ path: "/v1/stores/:storeId/cart/:cartId/items/:itemId",
211
+ scope: "write:cart",
212
+ description: "Remove an item from the cart",
213
+ response: "Cart (updated)",
214
+ },
215
+ ],
216
+ },
217
+ checkout: {
218
+ name: "Checkout",
219
+ description: "E-commerce checkout and POS payment intents",
220
+ base: "/v1/stores/:storeId/checkout",
221
+ endpoints: [
222
+ {
223
+ method: "POST",
224
+ path: "/v1/stores/:storeId/checkout",
225
+ scope: "write:checkout",
226
+ description: "Convert a cart into an order (e-commerce checkout)",
227
+ body: {
228
+ cart_id: "Required. Cart UUID",
229
+ payment_method: "Payment method identifier",
230
+ shipping_address: "Shipping address JSON",
231
+ billing_address: "Billing address JSON",
232
+ },
233
+ response: "Order",
234
+ notes: "Cart is consumed — cannot be reused after successful checkout",
235
+ },
236
+ {
237
+ method: "POST",
238
+ path: "/v1/stores/:storeId/checkout/intents",
239
+ scope: "write:checkout",
240
+ description: "Create a payment intent (POS terminal flow)",
241
+ body: {
242
+ cart_id: "Cart UUID",
243
+ amount: "Total amount in dollars",
244
+ payment_method: "Terminal payment method",
245
+ terminal_id: "POS terminal identifier",
246
+ },
247
+ response: "PaymentIntent { id, status: 'created', amount }",
248
+ },
249
+ {
250
+ method: "GET",
251
+ path: "/v1/stores/:storeId/checkout/intents/:id",
252
+ scope: "read:checkout",
253
+ description: "Get payment intent status",
254
+ response: "PaymentIntent { id, status, amount, created_at }",
255
+ },
256
+ {
257
+ method: "POST",
258
+ path: "/v1/stores/:storeId/checkout/intents/:id/capture",
259
+ scope: "write:checkout",
260
+ description: "Capture an authorized payment",
261
+ response: "PaymentIntent { status: 'captured' }",
262
+ },
263
+ {
264
+ method: "POST",
265
+ path: "/v1/stores/:storeId/checkout/intents/:id/cancel",
266
+ scope: "write:checkout",
267
+ description: "Cancel a payment intent",
268
+ response: "PaymentIntent { status: 'cancelled' }",
269
+ },
270
+ {
271
+ method: "POST",
272
+ path: "/v1/stores/:storeId/checkout/intents/:id/charge",
273
+ scope: "write:checkout",
274
+ description: "Charge via Dejavoo POS terminal",
275
+ body: { terminal_id: "Dejavoo terminal ID" },
276
+ response: "PaymentIntent (with terminal response)",
277
+ notes: "POS-specific — sends charge request to physical terminal",
278
+ },
279
+ {
280
+ method: "POST",
281
+ path: "/v1/stores/:storeId/checkout/intents/:id/abort",
282
+ scope: "write:checkout",
283
+ description: "Abort an in-progress terminal charge",
284
+ response: "PaymentIntent { status: 'aborted' }",
285
+ },
286
+ ],
287
+ },
288
+ customers: {
289
+ name: "Customers",
290
+ description: "Customer CRM — CRUD and search",
291
+ base: "/v1/stores/:storeId/customers",
292
+ endpoints: [
293
+ {
294
+ method: "GET",
295
+ path: "/v1/stores/:storeId/customers",
296
+ scope: "read:customers",
297
+ description: "List customers with pagination and search",
298
+ params: {
299
+ limit: "Number (1-100, default 25)",
300
+ starting_after: "Customer ID cursor",
301
+ search: "Search by name, email, or phone",
302
+ },
303
+ response: '{ object: "list", data: Customer[], has_more: boolean }',
304
+ },
305
+ {
306
+ method: "GET",
307
+ path: "/v1/stores/:storeId/customers/:id",
308
+ scope: "read:customers",
309
+ description: "Get a single customer with full profile",
310
+ response: "Customer (includes loyalty_points, total_spent, total_orders)",
311
+ },
312
+ {
313
+ method: "POST",
314
+ path: "/v1/stores/:storeId/customers",
315
+ scope: "write:customers",
316
+ description: "Create a new customer",
317
+ body: {
318
+ first_name: "Required",
319
+ last_name: "Required",
320
+ email: "Email address",
321
+ phone: "Phone number",
322
+ date_of_birth: "YYYY-MM-DD",
323
+ },
324
+ response: "Customer",
325
+ },
326
+ {
327
+ method: "PATCH",
328
+ path: "/v1/stores/:storeId/customers/:id",
329
+ scope: "write:customers",
330
+ description: "Update a customer",
331
+ body: { "...": "Any customer fields" },
332
+ response: "Customer",
333
+ },
334
+ ],
335
+ },
336
+ inventory: {
337
+ name: "Inventory",
338
+ description: "Inventory levels, summaries, adjustments, and transfers",
339
+ base: "/v1/stores/:storeId/inventory",
340
+ endpoints: [
341
+ {
342
+ method: "GET",
343
+ path: "/v1/stores/:storeId/inventory",
344
+ scope: "read:inventory",
345
+ description: "List inventory levels across locations",
346
+ params: {
347
+ limit: "Number (1-100, default 25)",
348
+ starting_after: "Cursor",
349
+ location_id: "Filter by location UUID",
350
+ product_id: "Filter by product UUID",
351
+ },
352
+ response: '{ object: "list", data: InventoryLevel[] }',
353
+ },
354
+ {
355
+ method: "GET",
356
+ path: "/v1/stores/:storeId/inventory/summary",
357
+ scope: "read:inventory",
358
+ description: "Inventory summary grouped by location",
359
+ response: "{ locations: [{ id, name, total_products, total_units, total_value }] }",
360
+ },
361
+ {
362
+ method: "POST",
363
+ path: "/v1/stores/:storeId/inventory/adjust",
364
+ scope: "write:inventory",
365
+ description: "Adjust inventory quantity for a product at a location",
366
+ body: {
367
+ product_id: "Required. Product UUID",
368
+ location_id: "Required. Location UUID",
369
+ adjustment: "Required. Number (+/-) to adjust by",
370
+ reason: "Reason for adjustment",
371
+ },
372
+ response: "InventoryLevel (updated)",
373
+ },
374
+ {
375
+ method: "POST",
376
+ path: "/v1/stores/:storeId/inventory/transfer",
377
+ scope: "write:inventory",
378
+ description: "Transfer inventory between locations",
379
+ body: {
380
+ product_id: "Required. Product UUID",
381
+ from_location_id: "Required. Source location UUID",
382
+ to_location_id: "Required. Destination location UUID",
383
+ quantity: "Required. Number of units to transfer",
384
+ },
385
+ response: "{ from: InventoryLevel, to: InventoryLevel }",
386
+ },
387
+ ],
388
+ },
389
+ locations: {
390
+ name: "Locations",
391
+ description: "Store locations — list and view",
392
+ base: "/v1/stores/:storeId/locations",
393
+ endpoints: [
394
+ {
395
+ method: "GET",
396
+ path: "/v1/stores/:storeId/locations",
397
+ scope: "read:locations",
398
+ description: "List all store locations",
399
+ response: '{ object: "list", data: Location[] }',
400
+ },
401
+ {
402
+ method: "GET",
403
+ path: "/v1/stores/:storeId/locations/:id",
404
+ scope: "read:locations",
405
+ description: "Get a single location",
406
+ response: "Location { id, name, address, city, state, zip, type, is_active }",
407
+ },
408
+ ],
409
+ },
410
+ analytics: {
411
+ name: "Analytics",
412
+ description: "Sales, inventory, traffic, customer, and product analytics",
413
+ base: "/v1/stores/:storeId/analytics",
414
+ endpoints: [
415
+ {
416
+ method: "GET",
417
+ path: "/v1/stores/:storeId/analytics/sales",
418
+ scope: "read:analytics",
419
+ description: "Sales analytics summary",
420
+ params: {
421
+ period: "today | yesterday | last_7 | last_30 | last_90 | last_365 | ytd | mtd",
422
+ start_date: "YYYY-MM-DD (custom range)",
423
+ end_date: "YYYY-MM-DD (custom range)",
424
+ location_id: "Filter by location",
425
+ },
426
+ response: "{ total_revenue, total_orders, average_order_value, top_products[] }",
427
+ },
428
+ {
429
+ method: "GET",
430
+ path: "/v1/stores/:storeId/analytics/sales/daily",
431
+ scope: "read:analytics",
432
+ description: "Daily sales breakdown",
433
+ params: { period: "Time period", location_id: "Optional location filter" },
434
+ response: "{ days: [{ date, revenue, orders, average_order_value }] }",
435
+ },
436
+ {
437
+ method: "GET",
438
+ path: "/v1/stores/:storeId/analytics/sales/weekly",
439
+ scope: "read:analytics",
440
+ description: "Weekly sales breakdown",
441
+ params: { period: "Time period", location_id: "Optional location filter" },
442
+ response: "{ weeks: [{ week_start, revenue, orders }] }",
443
+ },
444
+ {
445
+ method: "GET",
446
+ path: "/v1/stores/:storeId/analytics/inventory",
447
+ scope: "read:analytics",
448
+ description: "Inventory analytics — stock levels and velocity",
449
+ response: "{ total_value, low_stock[], out_of_stock[], velocity[] }",
450
+ },
451
+ {
452
+ method: "GET",
453
+ path: "/v1/stores/:storeId/analytics/traffic",
454
+ scope: "read:analytics",
455
+ description: "Storefront traffic analytics",
456
+ response: "{ sessions, page_views, bounce_rate, avg_session_duration }",
457
+ },
458
+ {
459
+ method: "GET",
460
+ path: "/v1/stores/:storeId/analytics/funnel",
461
+ scope: "read:analytics",
462
+ description: "Conversion funnel analytics",
463
+ response: "{ steps: [{ name, count, conversion_rate }] }",
464
+ },
465
+ {
466
+ method: "GET",
467
+ path: "/v1/stores/:storeId/analytics/customers",
468
+ scope: "read:analytics",
469
+ description: "Customer analytics — segments, retention, lifetime value",
470
+ response: "{ total_customers, new_customers, returning, avg_ltv, segments[] }",
471
+ },
472
+ {
473
+ method: "GET",
474
+ path: "/v1/stores/:storeId/analytics/products",
475
+ scope: "read:analytics",
476
+ description: "Per-product performance analytics",
477
+ params: { limit: "Number of products", category_id: "Filter by category" },
478
+ response: "{ products: [{ id, name, revenue, units_sold, avg_price }] }",
479
+ },
480
+ ],
481
+ },
482
+ storefront: {
483
+ name: "Storefront",
484
+ description: "Storefront sessions, event tracking, and customer OTP authentication",
485
+ base: "/v1/stores/:storeId/storefront",
486
+ endpoints: [
487
+ {
488
+ method: "GET",
489
+ path: "/v1/stores/:storeId/storefront/sessions",
490
+ scope: "read:storefront",
491
+ description: "List storefront sessions",
492
+ response: '{ object: "list", data: Session[] }',
493
+ },
494
+ {
495
+ method: "GET",
496
+ path: "/v1/stores/:storeId/storefront/sessions/:id",
497
+ scope: "read:storefront",
498
+ description: "Get a single session",
499
+ response: "Session { id, customer_id, events[], created_at }",
500
+ },
501
+ {
502
+ method: "POST",
503
+ path: "/v1/stores/:storeId/storefront/sessions",
504
+ scope: "write:storefront",
505
+ description: "Create a storefront session",
506
+ body: { customer_id: "Optional customer UUID", metadata: "Optional session metadata" },
507
+ response: "Session",
508
+ },
509
+ {
510
+ method: "PATCH",
511
+ path: "/v1/stores/:storeId/storefront/sessions/:id",
512
+ scope: "write:storefront",
513
+ description: "Update a session",
514
+ body: { metadata: "Updated metadata" },
515
+ response: "Session",
516
+ },
517
+ {
518
+ method: "POST",
519
+ path: "/v1/stores/:storeId/storefront/events",
520
+ scope: "write:storefront",
521
+ description: "Track a storefront event",
522
+ body: {
523
+ session_id: "Session UUID",
524
+ event_type: "Event name (e.g. page_view, add_to_cart, purchase)",
525
+ event_data: "Event payload JSON",
526
+ },
527
+ response: "{ tracked: true }",
528
+ },
529
+ {
530
+ method: "POST",
531
+ path: "/v1/stores/:storeId/storefront/auth/send-code",
532
+ scope: "write:storefront",
533
+ description: "Send passwordless OTP to a customer",
534
+ body: { email: "Customer email OR", phone: "Customer phone" },
535
+ response: "{ sent: true }",
536
+ notes: "Rate-limited to prevent abuse. Code expires in 10 minutes.",
537
+ },
538
+ {
539
+ method: "POST",
540
+ path: "/v1/stores/:storeId/storefront/auth/verify-code",
541
+ scope: "write:storefront",
542
+ description: "Verify OTP code and authenticate customer",
543
+ body: { email: "Customer email OR phone", code: "6-digit OTP code" },
544
+ response: "{ customer_id, session_token, expires_at }",
545
+ },
546
+ ],
547
+ },
548
+ coa: {
549
+ name: "COA (Certificates of Analysis)",
550
+ description: "Lab test results — list, verify, view, and embed",
551
+ base: "/v1/stores/:storeId/coa",
552
+ endpoints: [
553
+ {
554
+ method: "GET",
555
+ path: "/v1/stores/:storeId/coa",
556
+ scope: "read:coa",
557
+ description: "List COAs (scoped to client)",
558
+ params: { limit: "Number (1-100)", starting_after: "Cursor" },
559
+ response: '{ object: "list", data: COA[] }',
560
+ },
561
+ {
562
+ method: "GET",
563
+ path: "/v1/stores/:storeId/coa/verify",
564
+ scope: "read:coa",
565
+ description: "Verify a COA by sample ID",
566
+ params: { sample_id: "Lab sample identifier" },
567
+ response: "COA (with verification status)",
568
+ notes: "Public-facing verification — can be linked from product pages",
569
+ },
570
+ {
571
+ method: "GET",
572
+ path: "/v1/stores/:storeId/coa/:id",
573
+ scope: "read:coa",
574
+ description: "Get a single COA with full lab data",
575
+ response: "COA { id, product_name, batch_number, sample_id, cannabinoids, terpenes, ... }",
576
+ },
577
+ {
578
+ method: "GET",
579
+ path: "/v1/stores/:storeId/coa/:id/embed",
580
+ scope: "read:coa",
581
+ description: "Get embeddable COA data for iframes or widgets",
582
+ response: "{ html: string, data: COA }",
583
+ notes: "Use for embedding lab results on product detail pages",
584
+ },
585
+ ],
586
+ },
587
+ portal: {
588
+ name: "Customer Portal",
589
+ description: "B2B portal — authentication, profiles, and documents",
590
+ base: "/v1/stores/:storeId/portal",
591
+ endpoints: [
592
+ {
593
+ method: "POST",
594
+ path: "/v1/stores/:storeId/portal/auth/send-code",
595
+ scope: "write:portal",
596
+ description: "Send portal login OTP",
597
+ body: { email: "Customer email" },
598
+ response: "{ sent: true }",
599
+ },
600
+ {
601
+ method: "POST",
602
+ path: "/v1/stores/:storeId/portal/auth/verify",
603
+ scope: "write:portal",
604
+ description: "Verify OTP and get portal session",
605
+ body: { email: "Customer email", code: "OTP code" },
606
+ response: "{ customer_id, token, expires_at }",
607
+ },
608
+ {
609
+ method: "POST",
610
+ path: "/v1/stores/:storeId/portal/auth/refresh",
611
+ scope: "write:portal",
612
+ description: "Refresh portal session token",
613
+ body: { token: "Current session token" },
614
+ response: "{ token, expires_at }",
615
+ },
616
+ {
617
+ method: "GET",
618
+ path: "/v1/stores/:storeId/portal/stores",
619
+ scope: "read:portal",
620
+ description: "Get stores the customer belongs to",
621
+ response: "{ stores: Store[] }",
622
+ },
623
+ {
624
+ method: "GET",
625
+ path: "/v1/stores/:storeId/portal/profiles",
626
+ scope: "read:portal",
627
+ description: "List customer profiles",
628
+ response: "{ profiles: Profile[] }",
629
+ },
630
+ {
631
+ method: "GET",
632
+ path: "/v1/stores/:storeId/portal/documents",
633
+ scope: "read:portal",
634
+ description: "List customer documents",
635
+ response: '{ object: "list", data: Document[] }',
636
+ },
637
+ {
638
+ method: "GET",
639
+ path: "/v1/stores/:storeId/portal/documents/:id",
640
+ scope: "read:portal",
641
+ description: "Get a single document",
642
+ response: "Document { id, name, type, url, created_at }",
643
+ },
644
+ {
645
+ method: "GET",
646
+ path: "/v1/stores/:storeId/portal/api-key",
647
+ scope: "read:portal",
648
+ description: "Get customer's API key for portal access",
649
+ response: "{ api_key, scopes[], expires_at }",
650
+ },
651
+ ],
652
+ },
653
+ };
654
+ // ---------------------------------------------------------------------------
655
+ // Auth reference
656
+ // ---------------------------------------------------------------------------
657
+ const AUTH_GUIDE = {
658
+ base_url: GATEWAY_BASE_URL,
659
+ api_version: "2026-02-20",
660
+ api_version_header: "X-API-Version",
661
+ authentication: {
662
+ method: "API Key via header",
663
+ header: "x-api-key",
664
+ key_format: "wk_live_<32chars> (production) or wk_test_<32chars> (sandbox)",
665
+ how_to_get: "Use the api_keys tool (action: generate) or the WhaleTools dashboard under Settings → API Keys",
666
+ },
667
+ scopes: {
668
+ wildcard: "* (full access), read:* (all reads), write:* (all writes)",
669
+ per_resource: [
670
+ "read:products", "write:products",
671
+ "read:orders", "write:orders",
672
+ "read:customers", "write:customers",
673
+ "read:inventory", "write:inventory",
674
+ "read:locations",
675
+ "read:analytics",
676
+ "read:cart", "write:cart",
677
+ "read:checkout", "write:checkout",
678
+ "read:storefront", "write:storefront",
679
+ "read:portal", "write:portal",
680
+ "read:coa",
681
+ ],
682
+ internal_only: ["write:agent", "write:telemetry"],
683
+ internal_note: "Agent chat and telemetry scopes are used internally by WhaleTools SDKs. Not needed for storefront/website integrations.",
684
+ },
685
+ rate_limits: {
686
+ default_per_minute: 60,
687
+ headers: "X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset",
688
+ exceeded_status: 429,
689
+ retry_header: "Retry-After (seconds)",
690
+ },
691
+ pagination: {
692
+ style: "Cursor-based",
693
+ params: "limit (1-100, default 25), starting_after (ID), ending_before (ID)",
694
+ response: '{ object: "list", data: [...], has_more: true|false, url: "..." }',
695
+ },
696
+ error_format: {
697
+ shape: '{ error: { type, message, code, param?, request_id? } }',
698
+ types: {
699
+ authentication_error: "401 — invalid_api_key (also returned for missing key)",
700
+ permission_error: "403 — insufficient_scope, store_mismatch",
701
+ not_found_error: "404 — resource_not_found",
702
+ invalid_request_error: "400/405/409 — validation_error, method_not_allowed, conflict",
703
+ rate_limit_error: "429 — rate_limit_exceeded, auth_rate_limited (too many auth attempts)",
704
+ api_error: "500 — internal_error",
705
+ },
706
+ },
707
+ security_headers: {
708
+ cors: "Configurable per-store. Default: allow all origins",
709
+ idempotency: "Idempotency-Key header supported on all POST/PATCH requests",
710
+ versioning: "X-API-Version header (optional — latest version used if omitted)",
711
+ },
712
+ url_pattern: "All endpoints: /v1/stores/:storeId/<resource>",
713
+ note: "Replace :storeId with your store UUID. Get it from the WhaleTools dashboard or the store tool.",
714
+ };
715
+ // ---------------------------------------------------------------------------
716
+ // Code examples by section
717
+ // ---------------------------------------------------------------------------
718
+ function getExamples(section) {
719
+ const BASE = GATEWAY_BASE_URL;
720
+ const storeVar = "${STORE_ID}";
721
+ const keyVar = "${API_KEY}";
722
+ const templates = {
723
+ products: {
724
+ raw_fetch: `// List products
725
+ const res = await fetch("${BASE}/v1/stores/${storeVar}/products?limit=25&status=published", {
726
+ headers: { "x-api-key": "${keyVar}" }
727
+ });
728
+ const { data: products, has_more } = await res.json();
729
+
730
+ // Get single product
731
+ const product = await fetch("${BASE}/v1/stores/${storeVar}/products/\${productId}", {
732
+ headers: { "x-api-key": "${keyVar}" }
733
+ }).then(r => r.json());`,
734
+ nextjs_ssr: `// app/products/page.tsx (Next.js App Router — Server Component)
735
+ const API_KEY = process.env.WHALE_API_KEY!;
736
+ const STORE_ID = process.env.WHALE_STORE_ID!;
737
+ const BASE = "${BASE}";
738
+
739
+ async function getProducts(page?: string) {
740
+ const params = new URLSearchParams({ limit: "25", status: "published" });
741
+ if (page) params.set("starting_after", page);
742
+
743
+ const res = await fetch(\`\${BASE}/v1/stores/\${STORE_ID}/products?\${params}\`, {
744
+ headers: { "x-api-key": API_KEY },
745
+ next: { revalidate: 60 }, // ISR: revalidate every 60s
746
+ });
747
+
748
+ if (!res.ok) throw new Error("Failed to fetch products");
749
+ return res.json();
750
+ }
751
+
752
+ export default async function ProductsPage({ searchParams }: { searchParams: { after?: string } }) {
753
+ const { data: products, has_more } = await getProducts(searchParams.after);
754
+
755
+ return (
756
+ <div className="grid grid-cols-3 gap-4">
757
+ {products.map((p: any) => (
758
+ <div key={p.id} className="border rounded p-4">
759
+ {p.featured_image && <img src={p.featured_image} alt={p.name} />}
760
+ <h2>{p.name}</h2>
761
+ <p>{p.pricing_data?.tiers?.[0]?.price ? \`$\${p.pricing_data.tiers[0].price}\` : "Contact for pricing"}</p>
762
+ </div>
763
+ ))}
764
+ {has_more && <a href={\`?after=\${products.at(-1)?.id}\`}>Next page →</a>}
765
+ </div>
766
+ );
767
+ }`,
768
+ react_client: `// hooks/useProducts.ts (React client-side)
769
+ import { useState, useEffect } from "react";
770
+
771
+ const BASE = "${BASE}";
772
+
773
+ export function useProducts(storeId: string, apiKey: string) {
774
+ const [products, setProducts] = useState([]);
775
+ const [loading, setLoading] = useState(true);
776
+
777
+ useEffect(() => {
778
+ fetch(\`\${BASE}/v1/stores/\${storeId}/products?status=published&limit=50\`, {
779
+ headers: { "x-api-key": apiKey },
780
+ })
781
+ .then(r => r.json())
782
+ .then(({ data }) => setProducts(data))
783
+ .finally(() => setLoading(false));
784
+ }, [storeId, apiKey]);
785
+
786
+ return { products, loading };
787
+ }`,
788
+ },
789
+ cart: {
790
+ raw_fetch: `// Create cart
791
+ const cart = await fetch("${BASE}/v1/stores/${storeVar}/cart", {
792
+ method: "POST",
793
+ headers: { "x-api-key": "${keyVar}", "Content-Type": "application/json" },
794
+ body: JSON.stringify({ customer_id: customerId })
795
+ }).then(r => r.json());
796
+
797
+ // Add item
798
+ await fetch("${BASE}/v1/stores/${storeVar}/cart/\${cart.id}/items", {
799
+ method: "POST",
800
+ headers: { "x-api-key": "${keyVar}", "Content-Type": "application/json" },
801
+ body: JSON.stringify({ product_id: "...", quantity: 2 })
802
+ }).then(r => r.json());
803
+
804
+ // View cart
805
+ const fullCart = await fetch("${BASE}/v1/stores/${storeVar}/cart/\${cart.id}", {
806
+ headers: { "x-api-key": "${keyVar}" }
807
+ }).then(r => r.json());`,
808
+ nextjs_ssr: `// app/api/cart/route.ts (Next.js Route Handler — server-side proxy)
809
+ import { NextRequest, NextResponse } from "next/server";
810
+
811
+ const API_KEY = process.env.WHALE_API_KEY!;
812
+ const STORE_ID = process.env.WHALE_STORE_ID!;
813
+ const BASE = "${BASE}";
814
+
815
+ export async function POST(req: NextRequest) {
816
+ const body = await req.json();
817
+ const res = await fetch(\`\${BASE}/v1/stores/\${STORE_ID}/cart\`, {
818
+ method: "POST",
819
+ headers: { "x-api-key": API_KEY, "Content-Type": "application/json" },
820
+ body: JSON.stringify(body),
821
+ });
822
+ return NextResponse.json(await res.json());
823
+ }`,
824
+ react_client: `// hooks/useCart.ts
825
+ import { useState, useCallback } from "react";
826
+
827
+ export function useCart(storeId: string, apiKey: string) {
828
+ const [cart, setCart] = useState<any>(null);
829
+ const BASE = "${BASE}";
830
+ const headers = { "x-api-key": apiKey, "Content-Type": "application/json" };
831
+
832
+ const createCart = useCallback(async () => {
833
+ const res = await fetch(\`\${BASE}/v1/stores/\${storeId}/cart\`, {
834
+ method: "POST", headers,
835
+ });
836
+ const data = await res.json();
837
+ setCart(data);
838
+ return data;
839
+ }, [storeId]);
840
+
841
+ const addItem = useCallback(async (productId: string, quantity: number) => {
842
+ if (!cart) return;
843
+ const res = await fetch(\`\${BASE}/v1/stores/\${storeId}/cart/\${cart.id}/items\`, {
844
+ method: "POST", headers,
845
+ body: JSON.stringify({ product_id: productId, quantity }),
846
+ });
847
+ const data = await res.json();
848
+ setCart(data);
849
+ return data;
850
+ }, [storeId, cart]);
851
+
852
+ const removeItem = useCallback(async (itemId: string) => {
853
+ if (!cart) return;
854
+ const res = await fetch(\`\${BASE}/v1/stores/\${storeId}/cart/\${cart.id}/items/\${itemId}\`, {
855
+ method: "DELETE", headers,
856
+ });
857
+ const data = await res.json();
858
+ setCart(data);
859
+ return data;
860
+ }, [storeId, cart]);
861
+
862
+ return { cart, createCart, addItem, removeItem };
863
+ }`,
864
+ },
865
+ checkout: {
866
+ raw_fetch: `// E-commerce checkout (convert cart to order)
867
+ const order = await fetch("${BASE}/v1/stores/${storeVar}/checkout", {
868
+ method: "POST",
869
+ headers: { "x-api-key": "${keyVar}", "Content-Type": "application/json" },
870
+ body: JSON.stringify({
871
+ cart_id: cartId,
872
+ payment_method: "card",
873
+ shipping_address: { street: "123 Main St", city: "Charlotte", state: "NC", zip: "28202" }
874
+ })
875
+ }).then(r => r.json());
876
+
877
+ // POS payment intent flow
878
+ const intent = await fetch("${BASE}/v1/stores/${storeVar}/checkout/intents", {
879
+ method: "POST",
880
+ headers: { "x-api-key": "${keyVar}", "Content-Type": "application/json" },
881
+ body: JSON.stringify({ cart_id: cartId, amount: 49.99 })
882
+ }).then(r => r.json());
883
+
884
+ // Capture after authorization
885
+ await fetch("${BASE}/v1/stores/${storeVar}/checkout/intents/\${intent.id}/capture", {
886
+ method: "POST",
887
+ headers: { "x-api-key": "${keyVar}" }
888
+ });`,
889
+ nextjs_ssr: `// app/api/checkout/route.ts (Server-side checkout — keep API key secret)
890
+ import { NextRequest, NextResponse } from "next/server";
891
+
892
+ const API_KEY = process.env.WHALE_API_KEY!;
893
+ const STORE_ID = process.env.WHALE_STORE_ID!;
894
+ const BASE = "${BASE}";
895
+
896
+ export async function POST(req: NextRequest) {
897
+ const { cart_id, shipping_address } = await req.json();
898
+
899
+ const res = await fetch(\`\${BASE}/v1/stores/\${STORE_ID}/checkout\`, {
900
+ method: "POST",
901
+ headers: { "x-api-key": API_KEY, "Content-Type": "application/json" },
902
+ body: JSON.stringify({ cart_id, shipping_address }),
903
+ });
904
+
905
+ if (!res.ok) {
906
+ const err = await res.json();
907
+ return NextResponse.json(err, { status: res.status });
908
+ }
909
+
910
+ return NextResponse.json(await res.json());
911
+ }`,
912
+ react_client: `// components/CheckoutButton.tsx
913
+ "use client";
914
+ import { useState } from "react";
915
+
916
+ export function CheckoutButton({ cartId }: { cartId: string }) {
917
+ const [loading, setLoading] = useState(false);
918
+
919
+ const handleCheckout = async () => {
920
+ setLoading(true);
921
+ try {
922
+ // Call YOUR server-side route (never expose API key to client)
923
+ const res = await fetch("/api/checkout", {
924
+ method: "POST",
925
+ headers: { "Content-Type": "application/json" },
926
+ body: JSON.stringify({ cart_id: cartId }),
927
+ });
928
+ const order = await res.json();
929
+ if (order.id) window.location.href = \`/orders/\${order.id}/confirmation\`;
930
+ } finally {
931
+ setLoading(false);
932
+ }
933
+ };
934
+
935
+ return <button onClick={handleCheckout} disabled={loading}>{loading ? "Processing..." : "Checkout"}</button>;
936
+ }`,
937
+ },
938
+ orders: {
939
+ raw_fetch: `// List orders
940
+ const { data: orders } = await fetch("${BASE}/v1/stores/${storeVar}/orders?limit=25", {
941
+ headers: { "x-api-key": "${keyVar}" }
942
+ }).then(r => r.json());
943
+
944
+ // Get order details
945
+ const order = await fetch("${BASE}/v1/stores/${storeVar}/orders/\${orderId}", {
946
+ headers: { "x-api-key": "${keyVar}" }
947
+ }).then(r => r.json());
948
+
949
+ // Update order status
950
+ await fetch("${BASE}/v1/stores/${storeVar}/orders/\${orderId}", {
951
+ method: "PATCH",
952
+ headers: { "x-api-key": "${keyVar}", "Content-Type": "application/json" },
953
+ body: JSON.stringify({ status: "shipped" })
954
+ });`,
955
+ nextjs_ssr: `// app/orders/page.tsx (Server Component)
956
+ async function getOrders() {
957
+ const res = await fetch(
958
+ \`${BASE}/v1/stores/\${process.env.WHALE_STORE_ID}/orders?limit=50\`,
959
+ { headers: { "x-api-key": process.env.WHALE_API_KEY! }, next: { revalidate: 30 } }
960
+ );
961
+ return res.json();
962
+ }
963
+
964
+ export default async function OrdersPage() {
965
+ const { data: orders } = await getOrders();
966
+ return (
967
+ <table>
968
+ <thead><tr><th>Order</th><th>Status</th><th>Total</th><th>Date</th></tr></thead>
969
+ <tbody>
970
+ {orders.map((o: any) => (
971
+ <tr key={o.id}>
972
+ <td><a href={\`/orders/\${o.id}\`}>{o.order_number}</a></td>
973
+ <td>{o.status}</td>
974
+ <td>\${o.total}</td>
975
+ <td>{new Date(o.created_at).toLocaleDateString()}</td>
976
+ </tr>
977
+ ))}
978
+ </tbody>
979
+ </table>
980
+ );
981
+ }`,
982
+ react_client: `// hooks/useOrders.ts
983
+ import useSWR from "swr";
984
+
985
+ const fetcher = (url: string) => fetch(url).then(r => r.json());
986
+
987
+ export function useOrders() {
988
+ // Fetch via your own API route (keeps API key server-side)
989
+ const { data, error, isLoading } = useSWR("/api/orders", fetcher);
990
+ return { orders: data?.data ?? [], error, isLoading };
991
+ }`,
992
+ },
993
+ customers: {
994
+ raw_fetch: `// Search customers
995
+ const { data: customers } = await fetch("${BASE}/v1/stores/${storeVar}/customers?search=john", {
996
+ headers: { "x-api-key": "${keyVar}" }
997
+ }).then(r => r.json());
998
+
999
+ // Create customer
1000
+ const customer = await fetch("${BASE}/v1/stores/${storeVar}/customers", {
1001
+ method: "POST",
1002
+ headers: { "x-api-key": "${keyVar}", "Content-Type": "application/json" },
1003
+ body: JSON.stringify({ first_name: "John", last_name: "Doe", email: "john@example.com" })
1004
+ }).then(r => r.json());`,
1005
+ nextjs_ssr: `// app/api/customers/route.ts
1006
+ import { NextRequest, NextResponse } from "next/server";
1007
+
1008
+ const headers = { "x-api-key": process.env.WHALE_API_KEY! };
1009
+ const base = \`${BASE}/v1/stores/\${process.env.WHALE_STORE_ID}\`;
1010
+
1011
+ export async function GET(req: NextRequest) {
1012
+ const search = req.nextUrl.searchParams.get("q") || "";
1013
+ const res = await fetch(\`\${base}/customers?search=\${encodeURIComponent(search)}\`, { headers });
1014
+ return NextResponse.json(await res.json());
1015
+ }`,
1016
+ react_client: `// hooks/useCustomers.ts
1017
+ import useSWR from "swr";
1018
+
1019
+ export function useCustomers(search: string) {
1020
+ const { data, isLoading } = useSWR(
1021
+ search ? \`/api/customers?q=\${encodeURIComponent(search)}\` : null,
1022
+ (url) => fetch(url).then(r => r.json())
1023
+ );
1024
+ return { customers: data?.data ?? [], isLoading };
1025
+ }`,
1026
+ },
1027
+ inventory: {
1028
+ raw_fetch: `// Get inventory summary
1029
+ const summary = await fetch("${BASE}/v1/stores/${storeVar}/inventory/summary", {
1030
+ headers: { "x-api-key": "${keyVar}" }
1031
+ }).then(r => r.json());
1032
+
1033
+ // Adjust stock
1034
+ await fetch("${BASE}/v1/stores/${storeVar}/inventory/adjust", {
1035
+ method: "POST",
1036
+ headers: { "x-api-key": "${keyVar}", "Content-Type": "application/json" },
1037
+ body: JSON.stringify({ product_id: "...", location_id: "...", adjustment: -5, reason: "Sold offline" })
1038
+ });`,
1039
+ nextjs_ssr: `// app/inventory/page.tsx
1040
+ async function getInventory() {
1041
+ const res = await fetch(
1042
+ \`${BASE}/v1/stores/\${process.env.WHALE_STORE_ID}/inventory/summary\`,
1043
+ { headers: { "x-api-key": process.env.WHALE_API_KEY! }, next: { revalidate: 60 } }
1044
+ );
1045
+ return res.json();
1046
+ }
1047
+
1048
+ export default async function InventoryPage() {
1049
+ const summary = await getInventory();
1050
+ return (
1051
+ <div>
1052
+ {summary.locations.map((loc: any) => (
1053
+ <div key={loc.id}>
1054
+ <h3>{loc.name}</h3>
1055
+ <p>{loc.total_products} products, {loc.total_units} units</p>
1056
+ </div>
1057
+ ))}
1058
+ </div>
1059
+ );
1060
+ }`,
1061
+ react_client: `// hooks/useInventory.ts
1062
+ import useSWR from "swr";
1063
+
1064
+ export function useInventorySummary() {
1065
+ const { data, isLoading } = useSWR("/api/inventory/summary", (url) => fetch(url).then(r => r.json()));
1066
+ return { summary: data, isLoading };
1067
+ }`,
1068
+ },
1069
+ analytics: {
1070
+ raw_fetch: `// Sales analytics
1071
+ const sales = await fetch("${BASE}/v1/stores/${storeVar}/analytics/sales?period=last_30", {
1072
+ headers: { "x-api-key": "${keyVar}" }
1073
+ }).then(r => r.json());
1074
+
1075
+ // Daily breakdown
1076
+ const daily = await fetch("${BASE}/v1/stores/${storeVar}/analytics/sales/daily?period=last_7", {
1077
+ headers: { "x-api-key": "${keyVar}" }
1078
+ }).then(r => r.json());
1079
+
1080
+ // Product performance
1081
+ const topProducts = await fetch("${BASE}/v1/stores/${storeVar}/analytics/products?limit=10", {
1082
+ headers: { "x-api-key": "${keyVar}" }
1083
+ }).then(r => r.json());`,
1084
+ nextjs_ssr: `// app/dashboard/page.tsx
1085
+ async function getDashboard() {
1086
+ const base = \`${BASE}/v1/stores/\${process.env.WHALE_STORE_ID}/analytics\`;
1087
+ const headers = { "x-api-key": process.env.WHALE_API_KEY! };
1088
+ const opts = { headers, next: { revalidate: 300 } as any };
1089
+
1090
+ const [sales, daily, products] = await Promise.all([
1091
+ fetch(\`\${base}/sales?period=last_30\`, opts).then(r => r.json()),
1092
+ fetch(\`\${base}/sales/daily?period=last_7\`, opts).then(r => r.json()),
1093
+ fetch(\`\${base}/products?limit=5\`, opts).then(r => r.json()),
1094
+ ]);
1095
+
1096
+ return { sales, daily, products };
1097
+ }
1098
+
1099
+ export default async function DashboardPage() {
1100
+ const { sales, daily, products } = await getDashboard();
1101
+ return (
1102
+ <div>
1103
+ <h1>Dashboard</h1>
1104
+ <div className="grid grid-cols-3 gap-4">
1105
+ <div>Revenue: \${sales.total_revenue}</div>
1106
+ <div>Orders: {sales.total_orders}</div>
1107
+ <div>AOV: \${sales.average_order_value}</div>
1108
+ </div>
1109
+ </div>
1110
+ );
1111
+ }`,
1112
+ react_client: `// hooks/useAnalytics.ts
1113
+ import useSWR from "swr";
1114
+
1115
+ export function useSalesAnalytics(period = "last_30") {
1116
+ const { data, isLoading } = useSWR(
1117
+ \`/api/analytics/sales?period=\${period}\`,
1118
+ (url) => fetch(url).then(r => r.json())
1119
+ );
1120
+ return { sales: data, isLoading };
1121
+ }`,
1122
+ },
1123
+ locations: {
1124
+ raw_fetch: `// List all locations
1125
+ const { data: locations } = await fetch("${BASE}/v1/stores/${storeVar}/locations", {
1126
+ headers: { "x-api-key": "${keyVar}" }
1127
+ }).then(r => r.json());`,
1128
+ nextjs_ssr: `// lib/api.ts — shared helper
1129
+ export async function getLocations() {
1130
+ const res = await fetch(
1131
+ \`${BASE}/v1/stores/\${process.env.WHALE_STORE_ID}/locations\`,
1132
+ { headers: { "x-api-key": process.env.WHALE_API_KEY! }, next: { revalidate: 3600 } }
1133
+ );
1134
+ return res.json().then(r => r.data);
1135
+ }`,
1136
+ react_client: `// hooks/useLocations.ts
1137
+ import useSWR from "swr";
1138
+
1139
+ export function useLocations() {
1140
+ const { data } = useSWR("/api/locations", (url) => fetch(url).then(r => r.json()));
1141
+ return data?.data ?? [];
1142
+ }`,
1143
+ },
1144
+ storefront: {
1145
+ raw_fetch: `// Track page view event
1146
+ await fetch("${BASE}/v1/stores/${storeVar}/storefront/events", {
1147
+ method: "POST",
1148
+ headers: { "x-api-key": "${keyVar}", "Content-Type": "application/json" },
1149
+ body: JSON.stringify({
1150
+ session_id: sessionId,
1151
+ event_type: "page_view",
1152
+ event_data: { url: "/products/blue-dream", product_id: "..." }
1153
+ })
1154
+ });
1155
+
1156
+ // Send OTP for customer login
1157
+ await fetch("${BASE}/v1/stores/${storeVar}/storefront/auth/send-code", {
1158
+ method: "POST",
1159
+ headers: { "x-api-key": "${keyVar}", "Content-Type": "application/json" },
1160
+ body: JSON.stringify({ email: "customer@example.com" })
1161
+ });`,
1162
+ nextjs_ssr: `// app/api/auth/send-code/route.ts
1163
+ import { NextRequest, NextResponse } from "next/server";
1164
+
1165
+ export async function POST(req: NextRequest) {
1166
+ const { email } = await req.json();
1167
+ const res = await fetch(
1168
+ \`${BASE}/v1/stores/\${process.env.WHALE_STORE_ID}/storefront/auth/send-code\`,
1169
+ {
1170
+ method: "POST",
1171
+ headers: { "x-api-key": process.env.WHALE_API_KEY!, "Content-Type": "application/json" },
1172
+ body: JSON.stringify({ email }),
1173
+ }
1174
+ );
1175
+ return NextResponse.json(await res.json(), { status: res.status });
1176
+ }`,
1177
+ react_client: `// hooks/useAuth.ts
1178
+ import { useState } from "react";
1179
+
1180
+ export function useStorefrontAuth() {
1181
+ const [loading, setLoading] = useState(false);
1182
+
1183
+ const sendCode = async (email: string) => {
1184
+ setLoading(true);
1185
+ await fetch("/api/auth/send-code", {
1186
+ method: "POST",
1187
+ headers: { "Content-Type": "application/json" },
1188
+ body: JSON.stringify({ email }),
1189
+ });
1190
+ setLoading(false);
1191
+ };
1192
+
1193
+ const verifyCode = async (email: string, code: string) => {
1194
+ const res = await fetch("/api/auth/verify-code", {
1195
+ method: "POST",
1196
+ headers: { "Content-Type": "application/json" },
1197
+ body: JSON.stringify({ email, code }),
1198
+ });
1199
+ return res.json(); // { customer_id, session_token }
1200
+ };
1201
+
1202
+ return { sendCode, verifyCode, loading };
1203
+ }`,
1204
+ },
1205
+ coa: {
1206
+ raw_fetch: `// List COAs
1207
+ const { data: coas } = await fetch("${BASE}/v1/stores/${storeVar}/coa?limit=50", {
1208
+ headers: { "x-api-key": "${keyVar}" }
1209
+ }).then(r => r.json());
1210
+
1211
+ // Verify by sample ID
1212
+ const verified = await fetch("${BASE}/v1/stores/${storeVar}/coa/verify?sample_id=LAB-2026-001", {
1213
+ headers: { "x-api-key": "${keyVar}" }
1214
+ }).then(r => r.json());
1215
+
1216
+ // Get embeddable COA data
1217
+ const embed = await fetch("${BASE}/v1/stores/${storeVar}/coa/\${coaId}/embed", {
1218
+ headers: { "x-api-key": "${keyVar}" }
1219
+ }).then(r => r.json());`,
1220
+ nextjs_ssr: `// app/products/[id]/coa/page.tsx
1221
+ async function getCOAEmbed(coaId: string) {
1222
+ const res = await fetch(
1223
+ \`${BASE}/v1/stores/\${process.env.WHALE_STORE_ID}/coa/\${coaId}/embed\`,
1224
+ { headers: { "x-api-key": process.env.WHALE_API_KEY! }, next: { revalidate: 3600 } }
1225
+ );
1226
+ return res.json();
1227
+ }
1228
+
1229
+ export default async function COAPage({ params }: { params: { id: string } }) {
1230
+ const { html, data } = await getCOAEmbed(params.id);
1231
+ return (
1232
+ <div>
1233
+ <h1>Certificate of Analysis — {data.product_name}</h1>
1234
+ <div dangerouslySetInnerHTML={{ __html: html }} />
1235
+ </div>
1236
+ );
1237
+ }`,
1238
+ react_client: `// components/COAViewer.tsx
1239
+ "use client";
1240
+ import { useEffect, useState } from "react";
1241
+
1242
+ export function COAViewer({ coaId }: { coaId: string }) {
1243
+ const [html, setHtml] = useState("");
1244
+
1245
+ useEffect(() => {
1246
+ fetch(\`/api/coa/\${coaId}/embed\`).then(r => r.json()).then(d => setHtml(d.html));
1247
+ }, [coaId]);
1248
+
1249
+ return <div dangerouslySetInnerHTML={{ __html: html }} />;
1250
+ }`,
1251
+ },
1252
+ portal: {
1253
+ raw_fetch: `// B2B portal login
1254
+ await fetch("${BASE}/v1/stores/${storeVar}/portal/auth/send-code", {
1255
+ method: "POST",
1256
+ headers: { "x-api-key": "${keyVar}", "Content-Type": "application/json" },
1257
+ body: JSON.stringify({ email: "buyer@dispensary.com" })
1258
+ });
1259
+
1260
+ // Verify and get session
1261
+ const session = await fetch("${BASE}/v1/stores/${storeVar}/portal/auth/verify", {
1262
+ method: "POST",
1263
+ headers: { "x-api-key": "${keyVar}", "Content-Type": "application/json" },
1264
+ body: JSON.stringify({ email: "buyer@dispensary.com", code: "123456" })
1265
+ }).then(r => r.json());
1266
+
1267
+ // List documents with portal token
1268
+ const { data: docs } = await fetch("${BASE}/v1/stores/${storeVar}/portal/documents", {
1269
+ headers: { "x-api-key": "${keyVar}", "Authorization": "Bearer " + session.token }
1270
+ }).then(r => r.json());`,
1271
+ nextjs_ssr: `// app/portal/layout.tsx — server-side session check
1272
+ import { cookies } from "next/headers";
1273
+ import { redirect } from "next/navigation";
1274
+
1275
+ export default async function PortalLayout({ children }: { children: React.ReactNode }) {
1276
+ const token = cookies().get("portal_token")?.value;
1277
+ if (!token) redirect("/portal/login");
1278
+ return <div className="portal-layout">{children}</div>;
1279
+ }`,
1280
+ react_client: `// hooks/usePortal.ts
1281
+ import { useState } from "react";
1282
+
1283
+ export function usePortal() {
1284
+ const [token, setToken] = useState<string | null>(null);
1285
+
1286
+ const login = async (email: string, code: string) => {
1287
+ const res = await fetch("/api/portal/verify", {
1288
+ method: "POST",
1289
+ headers: { "Content-Type": "application/json" },
1290
+ body: JSON.stringify({ email, code }),
1291
+ });
1292
+ const data = await res.json();
1293
+ setToken(data.token);
1294
+ return data;
1295
+ };
1296
+
1297
+ const getDocuments = async () => {
1298
+ const res = await fetch("/api/portal/documents", {
1299
+ headers: { Authorization: \`Bearer \${token}\` },
1300
+ });
1301
+ return res.json();
1302
+ };
1303
+
1304
+ return { token, login, getDocuments };
1305
+ }`,
1306
+ },
1307
+ };
1308
+ return templates[section] ?? null;
1309
+ }
1310
+ // ---------------------------------------------------------------------------
1311
+ // Quick start guide
1312
+ // ---------------------------------------------------------------------------
1313
+ const QUICK_START = {
1314
+ title: "WhaleTools API — Quick Start Guide",
1315
+ steps: [
1316
+ {
1317
+ step: 1,
1318
+ title: "Get your API key",
1319
+ description: "Generate an API key from the WhaleTools dashboard (Settings → API Keys) or use the api_keys MCP tool.",
1320
+ code: `// Via dashboard: Settings → API Keys → Generate
1321
+ // Or programmatically:
1322
+ // api_keys tool: { action: "generate", name: "My Website", scopes: ["read:products", "read:cart", "write:cart", "write:checkout"] }`,
1323
+ },
1324
+ {
1325
+ step: 2,
1326
+ title: "Set up environment variables",
1327
+ description: "Store your API key and store ID securely. Never expose the API key in client-side code.",
1328
+ code: `# .env.local (Next.js) or .env
1329
+ WHALE_API_KEY=wk_live_abc123...
1330
+ WHALE_STORE_ID=your-store-uuid`,
1331
+ },
1332
+ {
1333
+ step: 3,
1334
+ title: "Fetch products",
1335
+ description: "Make your first API call to list published products.",
1336
+ code: `const res = await fetch(
1337
+ \`https://whale-gateway.fly.dev/v1/stores/\${process.env.WHALE_STORE_ID}/products?status=published&limit=25\`,
1338
+ { headers: { "x-api-key": process.env.WHALE_API_KEY } }
1339
+ );
1340
+ const { data: products, has_more } = await res.json();
1341
+ console.log(\`Loaded \${products.length} products\`);`,
1342
+ },
1343
+ {
1344
+ step: 4,
1345
+ title: "Create a cart",
1346
+ description: "Start a shopping session by creating a cart.",
1347
+ code: `const cart = await fetch(
1348
+ \`https://whale-gateway.fly.dev/v1/stores/\${STORE_ID}/cart\`,
1349
+ {
1350
+ method: "POST",
1351
+ headers: { "x-api-key": API_KEY, "Content-Type": "application/json" },
1352
+ body: JSON.stringify({})
1353
+ }
1354
+ ).then(r => r.json());
1355
+ console.log("Cart ID:", cart.id);`,
1356
+ },
1357
+ {
1358
+ step: 5,
1359
+ title: "Add items to cart",
1360
+ description: "Add products to the cart by product ID.",
1361
+ code: `await fetch(
1362
+ \`https://whale-gateway.fly.dev/v1/stores/\${STORE_ID}/cart/\${cart.id}/items\`,
1363
+ {
1364
+ method: "POST",
1365
+ headers: { "x-api-key": API_KEY, "Content-Type": "application/json" },
1366
+ body: JSON.stringify({ product_id: products[0].id, quantity: 1 })
1367
+ }
1368
+ );`,
1369
+ },
1370
+ {
1371
+ step: 6,
1372
+ title: "Checkout",
1373
+ description: "Convert the cart into an order.",
1374
+ code: `const order = await fetch(
1375
+ \`https://whale-gateway.fly.dev/v1/stores/\${STORE_ID}/checkout\`,
1376
+ {
1377
+ method: "POST",
1378
+ headers: { "x-api-key": API_KEY, "Content-Type": "application/json" },
1379
+ body: JSON.stringify({
1380
+ cart_id: cart.id,
1381
+ shipping_address: {
1382
+ street: "123 Main St",
1383
+ city: "Charlotte",
1384
+ state: "NC",
1385
+ zip: "28202"
1386
+ }
1387
+ })
1388
+ }
1389
+ ).then(r => r.json());
1390
+ console.log("Order created:", order.id, "Status:", order.status);`,
1391
+ },
1392
+ ],
1393
+ tips: [
1394
+ "Keep your API key server-side. Use Next.js Route Handlers or API routes as a proxy for client-side calls.",
1395
+ "Use cursor pagination (starting_after) instead of offset for consistent results.",
1396
+ "Set next: { revalidate: 60 } in Next.js fetch for automatic ISR (Incremental Static Regeneration).",
1397
+ "Idempotency-Key header prevents duplicate orders — always set it on checkout requests.",
1398
+ "All list endpoints return { object: 'list', data: [...], has_more: boolean }.",
1399
+ ],
1400
+ };
1401
+ // ---------------------------------------------------------------------------
1402
+ // Main handler
1403
+ // ---------------------------------------------------------------------------
1404
+ export async function handleApiDocs(_sb, args, _storeId) {
1405
+ const action = args.action;
1406
+ switch (action) {
1407
+ case "sections": {
1408
+ const sections = Object.entries(SECTIONS).map(([key, s]) => ({
1409
+ key,
1410
+ name: s.name,
1411
+ description: s.description,
1412
+ endpoint_count: s.endpoints.length,
1413
+ }));
1414
+ return {
1415
+ success: true,
1416
+ data: {
1417
+ total_endpoints: sections.reduce((sum, s) => sum + s.endpoint_count, 0),
1418
+ sections,
1419
+ usage: 'Use api_docs with action "endpoints" and section "<key>" to see full endpoint details.',
1420
+ },
1421
+ };
1422
+ }
1423
+ case "endpoints": {
1424
+ const section = args.section;
1425
+ if (!section) {
1426
+ return { success: false, error: 'Missing required param "section". Use action "sections" to see available sections.' };
1427
+ }
1428
+ const s = SECTIONS[section];
1429
+ if (!s) {
1430
+ return {
1431
+ success: false,
1432
+ error: `Unknown section "${section}". Available: ${Object.keys(SECTIONS).join(", ")}`,
1433
+ };
1434
+ }
1435
+ return {
1436
+ success: true,
1437
+ data: {
1438
+ section: s.name,
1439
+ description: s.description,
1440
+ base: s.base,
1441
+ endpoints: s.endpoints,
1442
+ },
1443
+ };
1444
+ }
1445
+ case "auth": {
1446
+ return { success: true, data: AUTH_GUIDE };
1447
+ }
1448
+ case "examples": {
1449
+ const section = args.section;
1450
+ if (!section) {
1451
+ return { success: false, error: 'Missing required param "section". Use action "sections" to see available sections.' };
1452
+ }
1453
+ const examples = getExamples(section);
1454
+ if (!examples) {
1455
+ return {
1456
+ success: false,
1457
+ error: `No examples for section "${section}". Available: ${Object.keys(SECTIONS).join(", ")}`,
1458
+ };
1459
+ }
1460
+ return {
1461
+ success: true,
1462
+ data: {
1463
+ section,
1464
+ examples,
1465
+ tip: "Always keep API keys server-side. Use Next.js Route Handlers or API routes as a proxy for client-side calls.",
1466
+ },
1467
+ };
1468
+ }
1469
+ case "quick_start": {
1470
+ return { success: true, data: QUICK_START };
1471
+ }
1472
+ default:
1473
+ return {
1474
+ success: false,
1475
+ error: `Unknown api_docs action: "${action}". Available: sections, endpoints, auth, examples, quick_start`,
1476
+ };
1477
+ }
1478
+ }