# Dingyue Product Ordering API Buy redemption codes for AI subscriptions from a prepaid balance. Codes are delivered synchronously in the order response. Base URL: https://api.dingyue.app/shop/v1 OpenAPI (full schemas): https://api.dingyue.app/shop/v1/openapi.json Human docs: https://dingyue.app/docs/api/shop ## Authentication Every request except GET /pricing needs: Authorization: Bearer dy_sk_... Keys are created by a human in the account center at https://dingyue.app/account (API 中心 tab). Two key kinds: - general: scopes "ai:proxy shop:read" - can read the catalog/balance but every order endpoint returns 403 insufficient_scope. - product_ordering: scopes "shop:read shop:order" - required for all /orders endpoints. Cannot call the AI proxy. A key may carry optional operator-set spend limits (see Errors: 402/403 below). ## Endpoints GET /pricing no auth All level prices + level thresholds GET /products shop:read Enabled products at YOUR level price GET /products/{id}/availability?qty=N shop:read Live availability + current unit price GET /balance shop:read balance / reserved / available + level POST /orders shop:order Buy codes (idempotent, see below) GET /orders?externalOrderId=...&limit=N shop:order List orders (never includes codes) GET /orders/{id} shop:order Order detail; delivered orders include codes ## Ordering contract (read carefully) POST /orders body: {"externalOrderId": "", "productId": "prod_...", "qty": 1, "expectedUnitPrice": "21.99"} - externalOrderId: generate ONE unique id per logical purchase and PERSIST IT BEFORE sending. It is the idempotency key for the whole lifecycle. - expectedUnitPrice (optional): the price you read from /products. If the server-side price differs you get 409 price_changed instead of a silent charge at the new price. - Success response (status "delivered") contains codes[] (length == qty) and redeemUrl. Give each code together with redeemUrl to the end user. - ANY uncertain outcome (timeout, network error, 5xx, order_in_progress, order_commit_pending): retry the SAME POST with the SAME externalOrderId, productId and qty. The server resumes or replays idempotently - you will never be double-charged and never receive a second supplier order. - NEVER reuse an externalOrderId with a different productId or qty (409 idempotency_conflict), and never invent a new externalOrderId to retry an uncertain purchase - that would buy twice. - Recovery without state: GET /orders?externalOrderId= finds the order; GET /orders/{orderId} returns codes once delivered. ## Errors Error body: {"error": "", "message": "...", "details": {...}} 400 invalid_input Fix the request; do not retry unchanged. 401 unauthorized Key missing/invalid/revoked or account disabled. 402 insufficient_balance Top up in the account center, then retry the SAME externalOrderId. 402 key_daily_limit_exceeded This key's operator-set daily spend cap is reached; retry after UTC midnight or raise the cap in the account center. 403 insufficient_scope Use a product_ordering key for order endpoints. 403 key_order_limit_exceeded Order exceeds this key's single-order cap; lower qty or raise the cap in the account center. 404 not_found Product/order does not exist for this account. 409 price_changed Re-read /products, confirm the new price, then send a NEW order id with the new expectedUnitPrice. 409 idempotency_conflict externalOrderId reused with different product/qty. 409 out_of_stock Retry availability later; safe to re-poll. 409 supplier_price_changed Platform cost guard tripped; wait for repricing. 429 rate_limited Account-level ~60 req/min; exponential backoff. 503 order_in_progress Another attempt holds the order; wait details.retryAfterSeconds, then retry SAME externalOrderId. 503 order_commit_pending Result is being finalized; retry SAME externalOrderId. 503 supplier_unavailable Transient; retry SAME externalOrderId with backoff. 503 supplier_not_configured Operator-side problem; stop and report, do not loop. ## Minimal agent flow 1. GET /products -> pick product, note id and price. 2. (optional, qty > 1) GET /products/{id}/availability?qty=N. 3. Generate + persist externalOrderId; POST /orders. 4. On any uncertainty, retry the same POST unchanged until status "delivered" or a non-retryable 4xx. 5. Deliver codes[] + redeemUrl to the end user.