whale-code 6.5.8 → 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.
- package/dist/cli/services/agent-loop.js +26 -2
- package/dist/cli/services/agent-loop.js.map +1 -1
- package/dist/cli/services/hooks.js +2 -1
- package/dist/cli/services/hooks.js.map +1 -1
- package/dist/cli/services/telemetry-spans.js +1 -0
- package/dist/cli/services/telemetry-spans.js.map +1 -1
- package/dist/cli/services/telemetry.d.ts +23 -0
- package/dist/cli/services/telemetry.js +45 -1
- package/dist/cli/services/telemetry.js.map +1 -1
- package/dist/server/handlers/__test-utils__/test-db.d.ts +17 -3
- package/dist/server/handlers/__test-utils__/test-db.js +113 -14
- package/dist/server/handlers/__test-utils__/test-db.js.map +1 -1
- package/dist/server/handlers/affiliates.d.ts +9 -0
- package/dist/server/handlers/affiliates.js +197 -0
- package/dist/server/handlers/affiliates.js.map +1 -0
- package/dist/server/handlers/api-docs.d.ts +4 -2
- package/dist/server/handlers/api-docs.js +204 -1681
- package/dist/server/handlers/api-docs.js.map +1 -1
- package/dist/server/handlers/campaigns.d.ts +9 -0
- package/dist/server/handlers/campaigns.js +237 -0
- package/dist/server/handlers/campaigns.js.map +1 -0
- package/dist/server/handlers/catalog-schemas.js +9 -9
- package/dist/server/handlers/catalog-schemas.js.map +1 -1
- package/dist/server/handlers/catalog.js +1 -1
- package/dist/server/handlers/catalog.js.map +1 -1
- package/dist/server/handlers/comms-documents.js +28 -2
- package/dist/server/handlers/comms-documents.js.map +1 -1
- package/dist/server/handlers/comms-pdf-generation.js +25 -3
- package/dist/server/handlers/comms-pdf-generation.js.map +1 -1
- package/dist/server/handlers/comms-pdf-helpers.js +4 -4
- package/dist/server/handlers/comms-pdf-helpers.js.map +1 -1
- package/dist/server/handlers/comms.d.ts +100 -0
- package/dist/server/handlers/comms.js +146 -12
- package/dist/server/handlers/comms.js.map +1 -1
- package/dist/server/handlers/coupons.d.ts +9 -0
- package/dist/server/handlers/coupons.js +220 -0
- package/dist/server/handlers/coupons.js.map +1 -0
- package/dist/server/handlers/embeddings.js +1 -1
- package/dist/server/handlers/embeddings.js.map +1 -1
- package/dist/server/handlers/enrichment.js +2 -622
- package/dist/server/handlers/enrichment.js.map +1 -1
- package/dist/server/handlers/fulfillment.d.ts +9 -0
- package/dist/server/handlers/fulfillment.js +209 -0
- package/dist/server/handlers/fulfillment.js.map +1 -0
- package/dist/server/handlers/google-ads.d.ts +24 -0
- package/dist/server/handlers/google-ads.js +2199 -0
- package/dist/server/handlers/google-ads.js.map +1 -0
- package/dist/server/handlers/invoices.d.ts +9 -0
- package/dist/server/handlers/invoices.js +252 -0
- package/dist/server/handlers/invoices.js.map +1 -0
- package/dist/server/handlers/loyalty.d.ts +9 -0
- package/dist/server/handlers/loyalty.js +197 -0
- package/dist/server/handlers/loyalty.js.map +1 -0
- package/dist/server/handlers/meta-ads-graph-api.js +18 -3
- package/dist/server/handlers/meta-ads-graph-api.js.map +1 -1
- package/dist/server/handlers/phone.d.ts +9 -0
- package/dist/server/handlers/phone.js +197 -0
- package/dist/server/handlers/phone.js.map +1 -0
- package/dist/server/handlers/pipeline.d.ts +9 -0
- package/dist/server/handlers/pipeline.js +277 -0
- package/dist/server/handlers/pipeline.js.map +1 -0
- package/dist/server/handlers/qr-codes.d.ts +9 -0
- package/dist/server/handlers/qr-codes.js +198 -0
- package/dist/server/handlers/qr-codes.js.map +1 -0
- package/dist/server/handlers/reviews.d.ts +9 -0
- package/dist/server/handlers/reviews.js +171 -0
- package/dist/server/handlers/reviews.js.map +1 -0
- package/dist/server/handlers/segments.d.ts +9 -0
- package/dist/server/handlers/segments.js +229 -0
- package/dist/server/handlers/segments.js.map +1 -0
- package/dist/server/handlers/social.d.ts +9 -0
- package/dist/server/handlers/social.js +81 -0
- package/dist/server/handlers/social.js.map +1 -0
- package/dist/server/handlers/tax.d.ts +9 -0
- package/dist/server/handlers/tax.js +182 -0
- package/dist/server/handlers/tax.js.map +1 -0
- package/dist/server/handlers/wallet.d.ts +9 -0
- package/dist/server/handlers/wallet.js +203 -0
- package/dist/server/handlers/wallet.js.map +1 -0
- package/dist/server/handlers/webhooks-mgmt.d.ts +9 -0
- package/dist/server/handlers/webhooks-mgmt.js +181 -0
- package/dist/server/handlers/webhooks-mgmt.js.map +1 -0
- package/dist/server/handlers/wholesale.d.ts +9 -0
- package/dist/server/handlers/wholesale.js +219 -0
- package/dist/server/handlers/wholesale.js.map +1 -0
- package/dist/server/index.js +20 -9
- package/dist/server/index.js.map +1 -1
- package/dist/server/lib/clickhouse-buffer.js +1 -0
- package/dist/server/lib/clickhouse-buffer.js.map +1 -1
- package/dist/server/lib/coa-renderer.d.ts +1 -1
- package/dist/server/lib/coa-renderer.js +32 -10
- package/dist/server/lib/coa-renderer.js.map +1 -1
- package/dist/server/server-worker.d.ts +1 -0
- package/dist/server/server-worker.js +464 -3
- package/dist/server/server-worker.js.map +1 -1
- package/dist/server/tool-router.js +118 -4
- package/dist/server/tool-router.js.map +1 -1
- package/package.json +26 -3
- package/vendor/ink/package.json +0 -2
|
@@ -1,1517 +1,153 @@
|
|
|
1
|
-
// server/handlers/api-docs.ts —
|
|
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
|
-
//
|
|
8
|
+
// Cached OpenAPI spec — fetched from gateway on demand
|
|
7
9
|
// ---------------------------------------------------------------------------
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1033
|
-
return NextResponse.json(err, { status: res.status });
|
|
23
|
+
throw new Error(`Gateway returned ${res.status} fetching OpenAPI spec`);
|
|
1034
24
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
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
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1226
|
-
const
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
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
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
|
|
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
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
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
|
-
|
|
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
|
|
131
|
+
title: "WhaleTools API Quick Start",
|
|
1443
132
|
steps: [{
|
|
1444
133
|
step: 1,
|
|
1445
134
|
title: "Get your API key",
|
|
1446
|
-
description: "Generate
|
|
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: "
|
|
1453
|
-
|
|
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: "
|
|
1460
|
-
|
|
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: "
|
|
1470
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
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
|
-
|
|
1551
|
-
|
|
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: `
|
|
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 "
|
|
218
|
+
case "search":
|
|
1575
219
|
{
|
|
1576
|
-
const
|
|
1577
|
-
if (!
|
|
220
|
+
const query = (args.query || "").toLowerCase();
|
|
221
|
+
if (!query) {
|
|
1578
222
|
return {
|
|
1579
223
|
success: false,
|
|
1580
|
-
error: 'Missing required param "
|
|
224
|
+
error: 'Missing required param "query". Provide a search term (e.g., "products", "checkout", "webhook").'
|
|
1581
225
|
};
|
|
1582
226
|
}
|
|
1583
|
-
|
|
1584
|
-
|
|
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: `
|
|
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,
|
|
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
|
|
285
|
+
// OpenAPI spec proxy — serves /openapi.json on the agent server
|
|
1616
286
|
// ---------------------------------------------------------------------------
|
|
1617
287
|
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
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
|
-
/**
|
|
1626
|
-
function
|
|
1627
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1751
|
-
|
|
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
|
-
|
|
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
|