# WooCommerce API contract Status: SPEC · 2026-09-18 · external dependency: WooCommerce REST API v3. Source: official WooCommerce REST API v3 documentation (https://woocommerce.github.io/woocommerce-rest-api-docs/). This contract pins the exact WooCommerce surface the app codes against. `docs/contracts/CONTENT-WOO.md` owns the listing copy and payload intent; this file owns the wire shape and the external constraints. ## Requirements impact | Requirement | Impact on this design | |---|---| | SETTINGS-5, SETTINGS-10, SETTINGS-15 | base URL, HTTPS, app-password auth | | SETTINGS-35, SETTINGS-50, SETTINGS-51, SETTINGS-65 | pricing, taxonomy sync, tag scope, create status | | METADATA-5, METADATA-10, METADATA-15, METADATA-30 | categories, attributes, tags shapes | | REVIEW-10, REVIEW-25, REVIEW-45, REVIEW-50 | name, price, SKU, field limits | | PUBLISH-5, PUBLISH-10, PUBLISH-15, PUBLISH-20, PUBLISH-25, PUBLISH-30, PUBLISH-35 | create sequence, draft rule, errors | | PUBLISH-40, PUBLISH-45, PUBLISH-55, PUBLISH-60 | images, payload fields, sold-individually | | APP-35, APP-50 | error envelope mapping | ## 1. Transport and auth - Base path: `${WOO_BASE_URL}/wp-json/wc/v3`. - `WOO_BASE_URL` is HTTPS only and injected from env. - Auth: WordPress application password over HTTP Basic. The consumer-key example in the Woo docs is **not** used. - Content type `application/json`. Dates ISO8601 (`YYYY-MM-DDTHH:MM:SS`). Monetary values are strings with two decimals. ## 2. Endpoints used | Purpose | Method and path | |---|---| | Sync categories | `GET /products/categories?per_page=100&page=N` | | Sync tags | `GET /products/tags?per_page=100&page=N` | | Sync attributes | `GET /products/attributes?per_page=100` | | Sync attribute terms | `GET /products/attributes/{id}/terms?per_page=100` | | SKU lookup | `GET /products?sku={sku}` | | Create product | `POST /products` | | Upload media | `POST /wp-json/wp/v2/media` (WordPress core) | ## 3. Product create field alignment | Our value | Woo field | Woo type | Enum / constraint | |---|---|---|---| | title | `name` | string | none documented | | — | `type` | string | `simple` (default) | | create status | `status` | string | `draft`, `pending`, `private`, `publish`; default `publish` | | sku | `sku` | string | unique; no documented format | | price | `regular_price` | string | monetary string, 2 decimals | | description | `description` | string | none documented | | short_description | `short_description` | string | none documented | | species + wood categories | `categories` | array of `{id: integer}` | leaf ids | | fig-* / feat-* tags | `tags` | array of `{id: integer}` | tag ids | | uploaded PNGs | `images` | array of `{id: integer}` | attachment ids, lineup order | | five locked attributes | `attributes` | array of attribute elements | see §4 | | — | `virtual` | boolean | default `false` | | — | `downloadable` | boolean | default `false` | | — | `catalog_visibility` | string | `visible`, `catalog`, `search`, `hidden`; default `visible` | | — | `sold_individually` | boolean | default `false`; set `true` | `sold_individually: true` is the Woo mechanism that enforces "one slab is one unit." ## 4. `attributes` element shape Global attributes are referenced by `id` (synced from Woo), never created by name. Each element: ```json { "id": 8, "position": 0, "visible": true, "variation": false, "options": ["Live Edge"] } ``` | Attribute | `options` | |---|---| | Edge Type | `[term name]` | | Figure | `[term name, ...]` | | Grade | `[term name]` | | Thickness | `[term name]` | | Moisture | `[term name]` | `options` carries term **names**, not ids. `visible` is `true`; `variation` is `false`. ## 5. Categories, tags, images shapes - `categories`: `[{"id": 9}, {"id": 14}]` - `tags`: `[{"id": 34}]` - `images`: `[{"id": 42}, {"id": 793}]`, in photos-ready lineup order. - Media upload: `POST /wp-json/wp/v2/media` as `multipart/form-data` with the PNG bytes; the response `id` is the attachment id used in `images`. Keep alpha; PNG only. ## 6. Error shape ```json { "code": "woocommerce_rest_term_invalid", "message": "Resource doesn't exist.", "data": { "status": 404 } } ``` `401` auth, `400` bad request, `404` missing, `500` server. These map into the app's uniform envelope and never reach the owner raw. ## 7. Pagination `per_page` max 100, `page` 1-based. Read `X-WP-Total` and `X-WP-TotalPages`; follow pages until exhausted. Taxonomy lists return in full to the client. ## 8. No documented maximum lengths The Woo v3 docs state no maximum length for `name`, `description`, `short_description`, `sku`, or `regular_price`. Do not invent caps. The app enforces non-empty required fields and its own local formats. ## 9. Field length limits Hard limits are verified from WordPress/WooCommerce source; the display caps are conventions (Google states no numeric limit). | Field | Storage | Hard limit | Recommended display cap | Rationale | |---|---|---|---|---| | `name` | `wp_posts.post_title` (`text`) | 65,535 | 60 | on-screen title-tag width | | `short_description` | `wp_posts.post_excerpt` (`text`) | 65,535 | 160 | search snippet / meta description width | | `description` | `wp_posts.post_content` (`longtext`) | ~16.7M | none | no word-count target; write for the page | | `sku` | `wc_product_meta_lookup.sku` (`varchar(100)`, index `sku(50)`) | 100 | 50 | index prefix length | Sources: WordPress `schema.php`, WooCommerce `class-wc-install.php`, `class-wc-product-data-store-cpt.php`, and Google Search Central (title links, snippets, SEO starter guide). ## 10. Media size The store exposes its upload cap read-only as `max_upload_size` on system status. Validate the processed PNG against it before upload and surface a plain, actionable error if exceeded. ## 11. Versioning Woo `wc/v3` requires WooCommerce 3.5+ and WordPress 4.4+ with pretty permalinks. Breaking Woo API changes are handled by the app's own `/api/v1` version, not by pinning the store.