LLM・AI エージェント連携ガイド
このページには、GPT-4、Claude、Gemini などの大規模言語モデル (LLM) によって読み取られるように特別に設計されたコンテキスト情報が記載されています。このページを AI アシスタントに直接入力することで、Quote3D API をプロジェクトに迅速に統合するのに役立ちます。
システムプロンプト
以下のテキストを使用して、Quote3D のアーキテクチャを AI アシスタントまたはコード生成ツール (Copilot) に説明してください:
You are integrating Quote3D: a REST API for instant 3D-printing quotes, with async quote jobs, webhooks, and an embeddable storefront widget. Follow this contract exactly. Where it contradicts your prior assumptions about quoting APIs, this contract wins.
== 0. TRANSPORT AND ENVELOPE ==
Base URL: https://api.quote3d.com — all paths are versioned under /v2.
Success bodies come in two shapes. Quote, job, stored-quote detail, webhook, analytics, usage, quota and config responses wrap the payload: { "success": true, "data": { ... } }. File, upload-id, user, uploads list, quotes list, printability and Catalog & Stock responses return their fields at the top level. Follow the response schema of each operation in the API reference instead of assuming one envelope. File downloads and CSV exports return the raw body.
Every error has one shape: { "detail": [{ "loc", "msg", "type", "code", "field"? }], "error_code": "AUTH_001", "error_summary": "...", "recovery_suggestions"?: [...], "documentation_url"?: "..." }. Quote-calculation errors add "error_key" and "error_params" for translated messages.
Branch on the HTTP status code and error_code, never on the error text — messages get reworded.
== 1. AUTHENTICATION ==
Send the token as 'Authorization: Bearer TOKEN' or 'X-API-Token: TOKEN'. Pick one style and use it consistently.
Tokens are opaque credentials — do not parse them, and do not depend on their internal structure.
Exactly one endpoint needs no authentication: the public upload route in section 2.
Token scopes: a token is either full-access or widget-scoped. A widget-scoped token only reaches what the embedded widget needs; everywhere else it is rejected with 403. Use a full-access server token for webhooks, analytics, usage and quota.
A token may also be restricted to an IP allowlist and may carry an expiry. Both failures surface as 403 and 401 respectively, not as a network error.
== 2. UPLOADING A MODEL ==
Accepted formats: STL, 3MF, OBJ, STEP (.step/.stp) (extensions .stl, .3mf, .obj, .step, .stp). STEP is tessellated on the server with OrcaSlicer's settings, assembly bodies are merged and lengths converted to mm. Uploads above the platform limit (50 MB by default) are rejected.
There are two upload paths. Choose deliberately.
(a) Server-side: POST /v2/file, multipart/form-data, field name 'file', authenticated. Use when the file already sits on your backend.
(b) Browser-direct, two steps: GET /v2/file/upload-id returns upload_id (top level, not under data), valid for one hour. Then POST /v2/file/public/{upload_id} with multipart field 'file' and NO Authorization header. Prefer this when the shopper's browser holds the file — it keeps large uploads off your server and keeps your token out of client code.
Both paths return a file_id. Keep it; every later call is keyed on it.
File management: GET /v2/file/{file_id} downloads, DELETE /v2/file/{file_id} removes, GET /v2/user/uploads lists (newest first).
Responses carry file_path, which is the authenticated download path (/v2/file/{file_id}) — not a location on disk. Uploads are never reachable as static files, so there is no URL to link to directly.
== 3. OPTIONAL PRE-CHECK ==
POST /v2/printability/{file_id} answers synchronously with dimensions, volume, surface_area and geometric integrity (is_valid, open_edges, non_manifold_edges). Use it to reject unprintable models before spending a quote.
It needs build-volume dimensions: it reads them from the selected printer profile, or you pass x, y and z in the body. If neither is available it returns 400. The body also accepts technology.
== 4. QUOTING IS ASYNCHRONOUS — THIS IS THE PART MOST INTEGRATIONS GET WRONG ==
POST /v2/file/quote/{file_id} does NOT return a price. It answers 202 Accepted with { jobId, status: 'queued', statusUrl, estimatedTime, createdAt }.
POST /v2/file/quote/{file_id}/async is the same endpoint under a second name. Do not build different logic for the two.
Request body, all optional: technology ('FDM' | 'SLA' | 'SLS' | 'SLM' | 'MJF'; 'RESIN' is an alias for 'SLA', 'DMLS' and 'LPBF' are aliases for 'SLM'), printer_id, quantity, and the three config objects printer_config, material_config and quote_config. A technology the operator has disabled in Global Settings is rejected with 400, so read /v2/config to learn which ones are enabled — SLM and MJF are off by default.
quote_config.minimum_price is a pre-tax floor for the whole quote (all pieces together): a cheaper quote is raised to it, a dearer one is unchanged, and unlike fixed_fee it is not added to every quote. Omit it to use the operator's minimum price for the quote's technology; 0 means none. When it applied, the result's pricing carries minimumPrice and costBreakdown.minimumAdjustment; otherwise both are absent.
Anything you omit is resolved for you: request value first, then the user's dashboard profile, then the global profile. So the minimal useful body is often {} or { "quantity": 2 }. Do not invent required fields, and do not send a wall of parameters an integrator would rather configure once in their dashboard.
Then poll GET /v2/jobs/{job_id} and read data.status. Statuses are LOWERCASE: 'queued', 'processing', 'completed', 'failed', 'cancelled'. Terminal statuses are 'completed', 'failed' and 'cancelled'.
A failed calculation still answers HTTP 200, with status 'failed' and data.error = { message, code }. Never treat 200 as success — inspect data.status. This is the single most common bug in generated Quote3D clients.
While processing you may also read data.progress and data.estimatedTimeRemaining. On completion the quote payload is at data.result.
Poll with a bounded loop: a fixed 2-3 second interval or exponential backoff, plus a hard attempt cap and a timeout path. Never poll without a ceiling.
== 5. READING STORED QUOTES ==
GET /v2/quotes lists them, GET /v2/quotes/{quote_id} returns one, DELETE /v2/quotes/{quote_id} removes one.
Pagination: limit and offset query parameters, default 50, maximum 100. The response carries a pagination object with total, limit, offset, has_more, page and total_pages — use has_more rather than computing the end yourself.
Sorting is one repeatable parameter in field:direction form, e.g. ?sort=created_at:desc; a field the endpoint does not list is refused with 400.
Filters on /v2/quotes: status, material, color, date_from, date_to (ISO 8601), min_price, max_price; fields=quote_id,total_price returns only those fields.
Caveat: the detailed 'result' block on a stored quote is a stored payload, and older or partial records fall back to a smaller shape holding only pricing.total, pricing.currency, timeEstimation and modelInfo. Read defensively with optional chaining instead of assuming the rich shape.
== 6. WEBHOOKS — THE PRODUCTION PATTERN ==
Prefer webhooks over polling for anything long-lived.
Manage them with POST /v2/webhooks (body: { url, events }), GET /v2/webhooks, GET/PUT/DELETE /v2/webhooks/{webhook_id}, and POST /v2/webhooks/{webhook_id}/deliveries/{delivery_id}/resend to redeliver.
The signing secret is returned ONLY in the create response. Store it immediately; it cannot be read back.
Event catalogue, exhaustively: 'quote.completed', 'quote.failed', 'file.uploaded', 'file.deleted', 'job.status_changed', 'widget.added_to_cart', 'usage.alert'.
Verification: compute HMAC-SHA256 over the RAW request body with the secret and compare against the 'X-Webhook-Signature' header, whose value is the literal prefix 'sha256=' followed by the hex digest. Use a timing-safe comparison. The event name also arrives in 'X-Webhook-Event'.
Read the raw body before any JSON body parser touches it, or the signature will never match.
Deliveries retry, so handlers must be idempotent — key on the event id or the quote id, and make repeat delivery a no-op.
== 7. THE EMBEDDABLE WIDGET ==
Load /js/quote3d-embed.js, then Quote3D.init('#root', { token, locale, redirectUrl, quoteId, cartUrl, onResult, onAddToCart }). Also optional: width (default '100%'), height (default '600px') and baseUrl (defaults to the origin serving the script).
theme and color are optional overrides. Leave them out and the widget uses the appearance the account saved in Dashboard > Widget; passing them replaces that saved look on this page only.
mode ('basic' | 'advanced' | 'both') overrides the saved form mode the same way. Basic asks print quality and strength instead of layer height, infill, walls and pattern; 'both' shows a switch.
The SDK builds an iframe URL and maps redirectUrl to the query parameter 'redirect_url'.
Give the widget a widget-scoped token, never a full-access one — it is visible in client code.
The iframe posts these message types to the host: 'QUOTE3D_RESULT', 'QUOTE3D_ADD_TO_CART', 'QUOTE3D_RESIZE' and 'QUOTE3D_GO_TO_CART' (the shopper pressed "Go to cart"). Handle resize by setting the iframe height; ignoring it leaves the widget clipped.
It also posts 'QUOTE3D_HELLO'; the SDK answers with 'QUOTE3D_HOST' { cart: true|false } depending on whether onAddToCart is set. With cart false the widget keeps the quote on screen and shows the shopper its quote ID instead of resetting. If onAddToCart returns a promise, the SDK reports the outcome to the widget ('QUOTE3D_CART_RESULT'): resolve with { cartUrl } and the Add to Cart button turns into "Go to cart", which sends the shopper to cartUrl (resolve with { goToCart: function } instead when the cart is not a URL the widget's page can open, as in a Wix HTML element); reject with a message and the widget shows it and lets the shopper retry. A handler that returns nothing keeps the old behaviour (the widget resets). The cartUrl can also be passed to Quote3D.init as an option. A page that embeds the iframe without the SDK never answers and keeps the old behaviour; the Add to Cart button can be hidden in the dashboard's widget sections.
Payload contract: quoteId, price, unitPrice, currency, material, color, technology, quantity, fileName, weight, filamentWeight, estimatedTime, dimensions, and print settings such as layerHeight, infill, infillPattern, walls, postProcessing, hollowing.
The total is 'price'. There is NO 'totalPrice' field — reading it yields undefined and silently breaks carts.
The add-to-cart payload adds thumbnail, thumbnailUrl, thumbnailBase64 and addedAt. Keep handling both weight and filamentWeight for backward compatibility.
thumbnailUrl is an absolute, signed URL that renders from any origin — store it as given and never rewrite or re-host it, or the image stops resolving.
GET /v2/quotes/{quote_id} returns the same link as thumbnail_url (null when the widget saved none). Read it there on your server rather than trusting the browser's payload: the WooCommerce, PrestaShop, OpenCart and Magento packages do this and show it as the cart line image; Wix keeps the product image. The thumbnail is deleted with the model file when the file retention period ends.
Do not call the widget's internal routes (/v2/widget/add-to-cart, /v2/thumbnail/save) from your code. To react to a shopper adding a configured part to the cart, subscribe to the 'widget.added_to_cart' webhook. Only if you send widget-scoped requests from your own code do you open a session yourself: GET /v2/widget/challenge, solve it, POST /v2/widget/session, then send the session in the 'X-Widget-Session' header.
== 8. ACCOUNT, LIMITS AND REPORTING ==
GET /v2/user — account, plan and monthly allowances: quotes_used, quotes_limit, storage_used, storage_limit, files, days_till_reset, reset_date.
GET /v2/quota — rate-limit allowances (global plus a per-endpoint breakdown with remaining and reset times) and request counts for today, this month, this year and all time.
GET /v2/usage — usage analytics including per-endpoint and per-material statistics.
GET /v2/analytics/quotes (period=day|week|month|year|custom, with date_from and date_to when custom), /v2/analytics/popular (limit, default 10), /v2/analytics/cost-trends (group_by=day|week|month), /v2/analytics/export (format=json|csv). Amounts carry currency; it is null and has_mixed_currency is true when the quotes in scope use more than one currency, so the sums are not one amount.
== 8b. CATALOG & STOCK (server token) ==
The account's own printers, materials, colours, colour stock, pricing and technologies; the widget reads the same catalogue, so a change reaches the quote form at once. Widget-scoped tokens are refused, and these calls do not count against the monthly request quota.
GET /v2/store/catalog lists everything with low_stock; GET /v2/store/catalog/schema?locale=en returns the dashboard's forms with bounds and help texts. POST /v2/store/catalog/adopt copies global profiles into the account; POST /v2/store/catalog/materials creates one.
GET/PATCH/DELETE /v2/store/catalog/materials/{id} and /v2/store/catalog/printers/{id}; PUT /v2/store/catalog/materials/{id}/colors replaces the colour list. PATCH bodies are { values, updated_at }: values use the same snake_case field names as the catalogue listing (a name the form does not have is refused with 400), and a stale updated_at answers 409 with error_key 'stale'.
GET/PATCH/DELETE /v2/store/catalog/pricing (dotted keys such as minimum_prices.SLA; currency is one of TRY, USD, EUR, GBP, JPY, CNY, RUB, CAD, AUD, CHF, SEK, NOK, DKK, SGD, HKD, NZD, MXN, BRL, INR and changes the account currency without converting amounts). GET/PUT /v2/store/catalog/technologies.
Stock changes only through movements: POST /v2/store/stock/movements (reason PURCHASE, COUNT or WASTE) and GET to list them; POST /v2/store/stock/orders with action consume, restore or check takes, returns or checks a paid order's material, at most once per quote.
== 9. ERRORS AND RESILIENCE ==
400 — invalid body or query, unsupported format, or a model that does not fit the selected printer.
401 — token missing, malformed, expired or revoked.
403 — valid token, not permitted: scope restriction, an origin or IP not on the token's list (AUTH_004), or a plan allowance is used up (RATE_002 with 'X-Quota-Limit', 'X-Quota-Used', 'X-Quota-Remaining'). The quote allowance resets each billing period; storage never resets and frees up only when files are deleted with DELETE /v2/file/{file_id} or by automatic cleanup, which the account owner can turn on in Dashboard > Files. Automatic cleanup deletes the oldest files never added to a cart and sends 'file.deleted' with reason 'storage_auto_cleanup'; a store plugin set to delete an order's files after delivery sends reason 'order_delivered'.
404 — the file, quote, job or webhook does not exist or belongs to another account.
429 — rate limited. Read the 'Retry-After' header and back off exponentially.
500 — retry with backoff; if it persists, surface the timestamp to the user.
Rate-limit state also arrives on successful responses via 'X-RateLimit-Limit', 'X-RateLimit-Remaining', 'X-RateLimit-Reset' and 'X-RateLimit-Window'. Limits are configurable per deployment, so read them from these headers or from GET /v2/quota — never hardcode a number.
== 10. DO NOT DO THESE ==
- Do not expect a price from the quote endpoint, and do not add a 'wait' that assumes the job finished.
- Do not compare job status against uppercase strings.
- Do not treat HTTP 200 on the jobs endpoint as success.
- Do not read 'totalPrice' from a widget payload.
- Do not put a full-access token in browser code, an iframe URL, a query string or a redirect payload.
- Do not JSON-parse the webhook body before computing its signature.
- Do not hardcode rate limits, quota numbers or the file-size cap.
- Do not invent endpoints, fields or query parameters. If this contract does not name it, ask instead of guessing.
== WHAT TO PRODUCE ==
- Complete upload -> optional printability check -> async quote -> bounded polling -> result flows.
- Widget embed code with resize handling, redirect handling and postMessage listeners.
- Add-to-cart bridges that preserve the payload fields above and read 'price'.
- Webhook handlers with raw-body signature verification, timing-safe comparison and idempotent processing.
- Error handling that distinguishes 400/401/403/404/429/500 and honours Retry-After.ウィジェット連携のための LLM コンテキスト
LLM アシスタントにこれらのウィジェット関連のタスクを実行させることができます:
- JS SDK の設定: token、locale、任意の quoteId、redirectUrl、cartUrl を使った Quote3D.init の設定を生成する。ページがダッシュボードに保存された外観を上書きする必要がない限り、theme と color は指定しない。
- イベントリスニング: QUOTE3D_RESULT、QUOTE3D_ADD_TO_CART、および QUOTE3D_RESIZE イベントのホストページリスナーを作成します。
- ペイロードのマッピング: 返される見積もりペイロードの quoteId、price、unitPrice、currency、technology、quantity、thumbnailUrl、fileName、weight、filamentWeight などのフィールドを保存してください。合計は totalPrice ではなく price である点に注意します。
Webhook 処理ロジック
ボットに Webhook セキュリティとイベント処理コードを記述してもらうために、これらの詳細を提供してください。
- 署名検証: 生のリクエストボディに対し HMAC-SHA256を用いて X-Webhook-Signature ヘッダーを検証する関数を書いてください。ヘッダー値は文字列 sha256= に続く16進ダイジェストなので、単純な等価比較ではなくタイミングセーフな比較を使用します。
- イベント処理: quote.completed、quote.failed、file.uploaded、file.deleted、job.status_changed, widget.added_to_cart、usage.alert というイベントカタログ全体を、冪等なハンドラーで処理してください。
エージェントのためのベストプラクティス
Quote3D の連携コードを記述する際は、常に以下の点に注意してください。
- 見積もり生成のために、非同期ポーリングを実装してください。最初の POST リクエストに対して即時の価格リターンを期待しないでください。
- ポーリング時には、指数バックオフまたは固定の2〜3秒の遅延ループを使用してください。
/v2/jobs/{job_id}. - 両方を保持してください
weight&filamentWeightをカートまたは注文のメタデータに保持し、現行のウィジェットペイロードおよびプラグインとの互換性を維持してください。 - 本番環境の連携には Webhook を推奨し、再試行が発生する可能性があるため、ハンドラーをべき等にしてください。
重要なエンドポイントの概要
基本的な実装に必要な最も頻繁に使用されるエンドポイントのクイックリファレンスインデックス:
ファイルのアップロード (サーバー側): POST /v2/file
ファイルのアップロード (ブラウザー直接、クライアントコードにトークンなし): GET /v2/file/upload-id → POST /v2/file/public/{upload_id}
造形可否の事前チェック: POST /v2/printability/{file_id}
見積もり開始: POST /v2/file/quote/{file_id}
ステータス確認: GET /v2/jobs/{job_id}
非同期ジョブの結果を読み込み: job.result
見積もり履歴取得: GET /v2/quotes
イベントの購読: POST /v2/webhooks
割り当てとレート制限: GET /v2/user, GET /v2/quota
エンドツーエンドの連携フロー
API が想定する順序と、各ステップが次に何を渡すか。エンドポイントを単独で説明するのではなく、この6ステップを実装するようアシスタントに依頼してください — 連携の不具合の多くは、ステップの飛ばしや受け渡しの誤解から生じます。
- ダッシュボードでトークンを作成し、スコープを選択します: サーバー用にはフルアクセス、ストアに埋め込むものにはウィジェットスコープを選びます。
- モデルを Quote3D に取り込み、返された file_id を保持します。バックエンドから POST /v2/file でアップロードするか、GET /v2/file/upload-id に続けて POST /v2/file/public/{upload_id} でブラウザーから直接アップロードさせます — 後者は大きなファイルをサーバーから、トークンをクライアントコードから遠ざけます。
- 任意で、造形できないジオメトリが見積もりを消費する前に弾くため、POST /v2/printability/{file_id} でモデルを事前チェックします。
- POST /v2/file/quote/{file_id} で見積もりを開始します。価格ではなく jobId を伴う202が返ります。見積もりごとに変わるパラメータだけを送り、それ以外はダッシュボードのプロファイルから解決されます。
- 結果を待ちます。本番では quote.completed と quote.failed の Webhook を購読してください。スクリプトや試作では、ステータスが completed、failed、cancelled になるまで GET /v2/jobs/{job_id} を回数制限付きのループでポーリングします。
- 結果はジョブのペイロードから、または後から GET /v2/quotes および GET /v2/quotes/{quote_id} から読み取ります。429は常に Retry-After に従って処理してください。
連携が壊れる原因の大半を占める3つの誤り: 見積もりエンドポイントから価格が返ると期待すること、ジョブステータスを大文字の文字列と比較すること、そしてジョブが実際には失敗しているのにジョブエンドポイントの HTTP 200を成功とみなすことです。上記のシステムプロンプトはこの3点を明示しているため、アシスタントが再現することはありません。