# Harmon Storefront API - Full Documentation > Build your own ecommerce storefront on Harmon - the public, machine-authenticated commerce API for merchants. This file concatenates every documentation page as Markdown, in reading order, so an assistant can ingest the entire portal in a single fetch. The machine-readable API contract lives at https://developers.joelxhenry.com/openapi.json. --- # Harmon Storefront API Source: https://developers.joelxhenry.com/ > New here? Start with the **[Quickstart](/quickstart)** - your first API call in five minutes - > then browse the **[Guides](/guides/)**. The interactive API reference and the recipes cookbook > arrive in later phases. --- # Quickstart Source: https://developers.joelxhenry.com/quickstart Your first API call in **five minutes**: get a key, confirm it's wired, then read the catalog. Every snippet is copy-paste-runnable - paste your sandbox key and go. You never send a `tenant_id`; the merchant is resolved from the key. ## 1. Get a sandbox key Ask your Harmon admin to issue a **publishable** key for your store, or grab one yourself from the admin **API keys** screen. For exploring, start with a **sandbox** key - it has the `hk_test_` prefix and can never touch live commercial data. > A **publishable** key (`hk_*_pub_…`) is browser-safe and authorizes anonymous > catalog reads. A **secret** key (`hk_*_sec_…`) is server-side only and unlocks > customer-scoped reads, checkout, and webhooks. See > [Authentication & keys](/guides/authentication) for the full split. ## 2. Confirm the key is wired - `GET /v1/ping` `GET /v1/ping` echoes the resolved merchant, the key kind, the granted scopes, and the mode **without touching any data** - the fastest way to prove your key works. Paste your key once and every snippet on this page rewrites to use it: A success looks like: ```json { "tenant_id": "t_demo", "key_kind": "PUBLISHABLE", "scopes": ["storefront:catalog"], "mode": "sandbox" } ``` Confirm `"mode": "sandbox"` before you experiment - see [Sandbox vs live](/guides/sandbox-vs-live). ## 3. List products - `GET /v1/products` Anonymous catalog reads work with the publishable key. Paginate with `page` / `page_size`; the response carries a strong `ETag` you can revalidate against (see [Pagination & caching](/guides/pagination-and-caching)): Each product carries a **coarse availability band** (`IN_STOCK` / `LOW` / `OUT`), never a raw on-hand count - see [Availability](/guides/availability). ## Next steps - **[Authentication & keys](/guides/authentication)** - the capability split and shopper tokens for customer-scoped reads and checkout. - **[Pagination & caching](/guides/pagination-and-caching)** - keep a local catalog fresh with `ETag` and `updated_since`. - **[API Reference](/reference)** - every operation, parameter, and schema, with a live Try-it console. ::: tip In the API Reference Open these operations in the interactive reference (with a **Try it** console): - [`GET /v1/ping`](/reference#tag/diagnostics/GET/v1/ping) - check your key + mode - [`GET /v1/products`](/reference#tag/catalog/GET/v1/products) - list the catalog ::: --- # Guides Source: https://developers.joelxhenry.com/guides/ The Harmon Storefront API is a public, machine-authenticated **read + commerce** API for building your own ecommerce site on Harmon. It is served on its own host (`https://api.harmon.example`) and is the only externally consumed Harmon surface. Its machine-readable contract is the published [OpenAPI document](/reference); these guides are the human companion to it. ## Start here - **[Quickstart](/quickstart)** - your first API call in five minutes: get a key, ping, list products. - **[Authentication & keys](/guides/authentication)** - publishable vs secret keys, the capability split, and shopper tokens. - **[Sandbox vs live](/guides/sandbox-vs-live)** - build against a sandbox key, then flip to live. ## Core concepts - **[Errors](/guides/errors)** - section-by-section degradation (`errors[]`) and the hard-failure status table. - **[Rate limits](/guides/rate-limits)** - per-key limits and `Retry-After`. - **[Pagination & caching](/guides/pagination-and-caching)** - `page`/`page_size`, `ETag` / `If-None-Match`, and `updated_since` deltas. - **[Availability](/guides/availability)** - coarse stock bands, never raw counts. ## What this API does not do (v1) - **No merchant-authored writes** to catalog, pricing, or stock - those stay admin-authored (the deliberate read-only boundary). The only writes are shopper account create/login and cart/checkout. - **No online card payments** - checkout reuses the existing COD / credit-terms path. A payment-gateway step is a planned follow-up. - No OAuth2/OIDC, no guest checkout, no GraphQL, and no multi-currency presentment beyond the merchant's default currency. --- # Authentication & keys Source: https://developers.joelxhenry.com/guides/authentication Every request carries a merchant-scoped API key as a bearer token: ``` Authorization: Bearer hk_live_pub_xxxxxxxxxxxx ``` Don't have a key yet? Mint one on the admin **API keys** screen: The merchant is resolved from the key - you **never** send a `tenant_id`. Any `tenant_id` or `customer_id` you put in a body or query is ignored. ## Publishable vs secret keys There are two kinds of key, with a hard capability split: | Kind | Prefix | Ship to a browser? | Authorizes | | --------------- | ------------- | ------------------ | -------------------------------------------------------------------------- | | **Publishable** | `hk_*_pub_…` | ✅ yes (low-priv) | Anonymous catalog reads + initiating shopper auth | | **Secret** | `hk_*_sec_…` | ❌ **never** | Everything publishable does **plus** customer-scoped reads, checkout, webhooks | The split is **law**: a publishable key presented against a customer-scoped or webhook route is always `403`, never a silent downgrade. Keep your secret key server-side only. Smoke-test a key against `GET /v1/ping` - it echoes the resolved `tenant_id`, `key_kind`, granted `scopes`, and `mode` without touching any data: ```bash curl -s https://api.harmon.example/v1/ping \ -H "Authorization: Bearer hk_live_pub_xxxxxxxxxxxx" # → {"tenant_id":"…","key_kind":"PUBLISHABLE","scopes":["storefront:catalog"],"mode":"live"} ``` ## Shopper tokens (customer-scoped routes) Customer-scoped reads and the cart → checkout path require an end-shopper to be logged in. The key authenticates the *integration*; the shopper token authenticates the *customer*. Obtain a shopper token via the auth login flow, then send it alongside the **secret** key: ``` Authorization: Bearer hk_live_sec_… X-Storefront-Shopper-Token: ``` The shopper token carries the `customer_id`; Harmon auto-scopes every order, invoice, and credit read to that customer. A shopper token whose merchant differs from the key's merchant is rejected (`403`). ## CORS for publishable keys A **publishable** key carries a CORS **origin allowlist**: a browser request from an `Origin` not on the key's allowlist is refused `403`. Register every origin your storefront serves from when you issue the key. **Secret** keys are server-to-server and aren't origin-checked. ## Next steps - **[Sandbox vs live](/guides/sandbox-vs-live)** - build against a test key, then flip to live. - **[Errors](/guides/errors)** - what `401` / `403` mean and how reads degrade. - **[Quickstart](/quickstart)** - get a key and make your first call. ::: tip In the API Reference - [`GET /v1/ping`](/reference#tag/diagnostics/GET/v1/ping) - your first authenticated call; echoes the key's mode - [`GET /v1/me`](/reference#tag/customer/GET/v1/me) - the customer a shopper token resolves to ::: --- # Availability Source: https://developers.joelxhenry.com/guides/availability Availability is always a **coarse band** - never a raw on-hand count. This holds everywhere stock is surfaced: catalog reads, the dedicated availability endpoints, and `stock.changed` webhook events. | Band | Meaning | | ---------- | -------------------------------------------------- | | `IN_STOCK` | Comfortably available | | `LOW` | Limited quantity - running short | | `OUT` | Currently unavailable | | lead-time | Out of stock now, with an expected availability ETA | Coarse bands keep a merchant's exact inventory position private while still telling a shopper what they need to know to buy. ## Reading availability Availability rides along on catalog reads, and there are dedicated endpoints for a single SKU or a batch: ```bash # A single SKU: curl -s "https://api.harmon.example/v1/products/SKU-123/availability" \ -H "Authorization: Bearer hk_test_pub_your_key_here" # → {"sku":"SKU-123","band":"LOW"} # A batch in one call: curl -s -X POST "https://api.harmon.example/v1/availability/batch" \ -H "Authorization: Bearer hk_test_pub_your_key_here" \ -H "Content-Type: application/json" \ -d '{"skus":["SKU-123","SKU-456"]}' ``` ## Next steps - **[Pagination & caching](/guides/pagination-and-caching)** - keep availability fresh with `updated_since`. - **[API Reference](/reference)** - the availability operations in full. ::: tip In the API Reference - [`POST /v1/availability/batch`](/reference#tag/availability/POST/v1/availability/batch) - coarse bands for many SKUs in one call ::: --- # Errors Source: https://developers.joelxhenry.com/guides/errors ## Section-by-section degradation Read endpoints degrade **section by section** rather than failing the whole response. A partial fan-out returns `200` with an `errors[]` array naming each degraded section while the healthy sections still populate: ```json { "data": [ /* … the sections that loaded … */ ], "errors": [ { "field": "pricing", "code": "downstream_timeout", "detail": "pricing temporarily unavailable" } ] } ``` Treat a non-empty `errors[]` as **"this part is temporarily stale,"** not a hard failure - render what loaded and retry the degraded section. An empty (or absent) `errors[]` means the whole response is fresh. ## Hard failures Hard failures use standard HTTP status codes: | Status | Meaning | | ------ | ----------------------------------------------------------------------- | | `401` | Missing / malformed / unknown / revoked / expired key or shopper token | | `403` | Capability split (publishable on a privileged route), wrong merchant, off-allowlist Origin | | `404` | Not found, or a cross-customer resource accessed by URL | | `429` | Rate limit exceeded - see [Rate limits](/guides/rate-limits) | Checkout over a credit limit returns a **structured** `over_credit_limit` state (with the shortfall), not a raw `409` - inspect the response body, don't just branch on the status code. ## Next steps - **[Rate limits](/guides/rate-limits)** - handling `429` and `Retry-After`. - **[Authentication & keys](/guides/authentication)** - what triggers `401` / `403`. ::: tip In the API Reference Every operation lists its error responses. A good one to inspect: - [`GET /v1/products`](/reference#tag/catalog/GET/v1/products) - the `errors[]` degradation envelope on a catalog read ::: --- # Pagination & caching Source: https://developers.joelxhenry.com/guides/pagination-and-caching Anonymous catalog reads (`GET /v1/products`, `/v1/products/{sku}`, `/v1/categories`, `/v1/tags`) are built to be paginated, cached, and synced cheaply. ## Pagination Paginate the product list with `page` / `page_size`, and filter with `q`, `category`, `tag`, and - for AUTO_PARTS merchants - `vehicle_id`: ```bash curl -s "https://api.harmon.example/v1/products?page=2&page_size=20&category=brakes" \ -H "Authorization: Bearer hk_test_pub_your_key_here" ``` ## ETag / `If-None-Match` Responses carry a strong **`ETag`** and a `Cache-Control: max-age` header. Send the ETag back as `If-None-Match` to revalidate - an unchanged resource returns `304 Not Modified` with **no body**, which doesn't count against your [rate limit](/guides/rate-limits): ```bash # First request - capture the ETag from the response headers: curl -s -D - "https://api.harmon.example/v1/products?page=1" \ -H "Authorization: Bearer hk_test_pub_your_key_here" # → ETag: "a1b2c3…" # Revalidate - unchanged returns 304, no body: curl -s -o /dev/null -w '%{http_code}\n' "https://api.harmon.example/v1/products?page=1" \ -H "Authorization: Bearer hk_test_pub_your_key_here" \ -H 'If-None-Match: "a1b2c3…"' # → 304 ``` ## `updated_since` - incremental sync Pass **`updated_since=`** to fetch only products changed since your last sync. This is the efficient way to keep a local cache - or a headless storefront's search index - fresh **between** webhook deliveries, without re-listing the whole catalog: ```bash curl -s "https://api.harmon.example/v1/products?updated_since=2026-06-01T00:00:00Z" \ -H "Authorization: Bearer hk_test_pub_your_key_here" ``` A typical sync loop: store the timestamp of your last successful pull, then on the next run pass it as `updated_since` and merge the returned deltas into your local copy. ## Next steps - **[Availability](/guides/availability)** - coarse stock bands on catalog reads. - **[Rate limits](/guides/rate-limits)** - why `304`s and deltas matter. ::: tip In the API Reference - [`GET /v1/products`](/reference#tag/catalog/GET/v1/products) - see the `limit`/`cursor` params, the `ETag` response header, and `updated_since` ::: --- # Rate limits Source: https://developers.joelxhenry.com/guides/rate-limits Each key is rate-limited **per minute**. Bursting past the limit returns `429` with a `Retry-After` header (in seconds) - back off for that long, then retry: ``` HTTP/1.1 429 Too Many Requests Retry-After: 12 ``` A simple, correct client honours `Retry-After` rather than guessing: ```python import time, requests def get_with_backoff(url, headers): while True: res = requests.get(url, headers=headers) if res.status_code != 429: return res time.sleep(int(res.headers.get("Retry-After", "1"))) ``` ## Tips - **Cache aggressively.** Anonymous catalog reads carry an `ETag` - revalidate with `If-None-Match` so a `304` costs you nothing against the limit. See [Pagination & caching](/guides/pagination-and-caching). - **Sync with deltas.** Use `updated_since` to pull only what changed instead of re-listing the whole catalog. - **Ask for a higher limit.** Each key's per-minute limit is set when it's issued - your Harmon admin can raise it for production traffic. ## Next steps - **[Pagination & caching](/guides/pagination-and-caching)** - spend fewer requests with `ETag` and `updated_since`. - **[Errors](/guides/errors)** - the full status-code table. ::: tip In the API Reference - [`GET /v1/products`](/reference#tag/catalog/GET/v1/products) - a rate-limited read; pair it with `If-None-Match` so a `304` costs nothing ::: --- # Sandbox vs live Source: https://developers.joelxhenry.com/guides/sandbox-vs-live A key minted with the **`hk_test_`** prefix runs in **sandbox** mode; every other key is **live**. Mode is derived from the key alone - there is no separate environment toggle to forget - and is echoed on every response: ``` X-Harmon-Mode: sandbox ``` `GET /v1/ping` also returns `"mode": "sandbox" | "live"`, so you can assert your test environment is wired to a test key **before** placing an order: ```bash curl -s https://api.harmon.example/v1/ping \ -H "Authorization: Bearer hk_test_pub_your_key_here" # → {"…":"…","mode":"sandbox"} ``` ## How to use it Use a sandbox key plus a sandbox merchant data set to build and test your integration end-to-end - read the catalog, build a cart, place an order - without ever touching live commercial data. When you're ready for production, swap in your **live** key; nothing else about your code changes. > **Sandbox by default when exploring.** The interactive Try-it console and the > runnable snippets in these docs default to a sandbox (`hk_test_…`) key and host > so you can never accidentally mutate live data while learning the API. ## Switching to live 1. Issue a **live** key pair (publishable + secret) from the admin **API keys** screen - see [Authentication & keys](/guides/authentication). 2. Set the live key in your production configuration (server-side for the secret key). 3. Confirm `GET /v1/ping` returns `"mode": "live"`. ## Next steps - **[Authentication & keys](/guides/authentication)** - the publishable/secret capability split. - **[Quickstart](/quickstart)** - your first sandbox call. ::: tip In the API Reference - [`GET /v1/ping`](/reference#tag/diagnostics/GET/v1/ping) - confirm whether a key resolves to `sandbox` or `live` ::: --- # Generate an SDK Source: https://developers.joelxhenry.com/guides/sdk-generation The OpenAPI contract is the **single source of truth** for this API, so you don't have to hand-write a client - generate a typed one from the live spec and regenerate whenever it changes. No published Harmon SDK required. ::: tip Always generate from the canonical spec Point your generator at the published contract - `{{ OPENAPI_URL }}` - not a copied-down file, so your client never drifts from what the API serves. ::: ## 1. Download the contract You can also grab it (and a ready-made Postman collection) from the [API Reference](/reference) download menu. ## 2. Generate a typed client Pick your stack - each command reads the canonical spec directly: Want the generated client to use **your** key automatically? Set it once with the [key control on the Quickstart](/quickstart), and every snippet on that page rewrites to your key. ## Next steps - **[Quickstart](/quickstart)** - your first call in five minutes. - **[Authentication & keys](/guides/authentication)** - the capability split. - **[API Reference](/reference)** - the interactive contract + downloads. --- # Verify webhook signatures Source: https://developers.joelxhenry.com/guides/webhook-signatures Every outbound webhook Harmon POSTs to your endpoint carries three headers: ``` X-Harmon-Event: order.confirmed X-Harmon-Delivery: X-Harmon-Signature: sha256= ``` The signature is an **HMAC-SHA256 of the exact request-body bytes**, keyed by the `signing_secret` Harmon returned when you created the subscription (revealed once). Verify it over the **raw bytes you received** - don't re-serialize the JSON first, or the bytes (and the HMAC) won't match. ::: warning Verify before you parse Compute the expected signature and reject the delivery on a mismatch **before** you `json.loads` the body. A request that fails verification is not from Harmon. ::: ## Try it Paste a signing secret and a body to see the signature Harmon would send, then verify a signature against them. Everything runs locally - your secret never leaves the browser. Tamper with a single character of the body (or the secret) after computing, then re-verify: the result flips to **Invalid**. That's exactly the check your receiver must perform. ## Verify in your stack ## Next steps - **[Errors](/guides/errors)** - status codes and how reads degrade. - **[Quickstart](/quickstart)** - your first authenticated call. - **[API Reference](/reference)** - the webhook subscription operations. --- # API Reference Source: https://developers.joelxhenry.com/reference The complete, interactive contract for the Harmon Storefront API - every operation, parameter, and schema, with a built-in **Try it** console. It is generated from the published OpenAPI document (the single source of truth), so it never drifts from what the API actually serves. New here? Start with the **[Quickstart](/quickstart)** for your first call in five minutes, then the **[Guides](/guides/)** for auth, sandbox vs live, errors, rate limits, caching, and availability. The **Try it** console below is pre-loaded with a **sandbox** key (`hk_test_…`), so you can fire `GET /v1/products` right now without setup - it can never touch live data. Ready for your own merchant? Mint a key and paste it in: ## Download & integrate Prefer to work from the raw contract or generate a client? Everything is downloadable - the spec is the single source of truth, so these never drift from what the API serves: - **OpenAPI** - [`openapi.json`](/openapi.json) · [`openapi.yaml`](/openapi.yaml) - **Postman** - [`postman.json`](/postman.json) (import straight into Postman) Building with an AI assistant? The whole portal is LLM-native: point it at [`llms.txt`](/llms.txt) (an index of every page) or [`llms-full.txt`](/llms-full.txt) (the entire docs as one Markdown file), and use the **Copy page as Markdown** button on any page. --- # Recipes Source: https://developers.joelxhenry.com/recipes/ Copy-paste cookbook for the most common storefront integrations. Each recipe is a runnable, end-to-end walkthrough in every language we support (cURL · JavaScript · Python · PHP/Laravel), with deep-links into the interactive [API Reference](/reference). New here? Start with the **[Quickstart](/quickstart)** for your first call in five minutes, then pick a recipe. ## The cookbook - **[Render a product grid](/recipes/product-grid)** - list the catalog with a publishable key and show coarse availability badges. - **[Cart → COD checkout](/recipes/cart-cod-checkout)** - the full commerce path: create a cart, add lines, price it, and place an order on the COD / credit-terms rails. - **[Verify a webhook](/recipes/verify-webhook)** - subscribe, then verify the HMAC signature over the raw bytes before parsing. - **[Keep a local catalog fresh with `updated_since`](/recipes/local-catalog-updated-since)** - seed a mirror once, then sync only what changed with `ETag` revalidation. - **[Customer-priced catalog with a shopper token](/recipes/customer-priced-catalog)** - show contract pricing and credit standing once a shopper signs in. ## Starter snippet The fastest path to a first `200` - confirm your key, then read the catalog. Paste your key once and it rewrites below: ## Building with an AI assistant? The whole portal is **LLM-native**. Point an assistant at the full docs and the OpenAPI contract and it can write a correct, current integration for you: - [`llms-full.txt`](/llms-full.txt) - the entire docs as one Markdown file - [`llms.txt`](/llms.txt) - an index of every page - [`openapi.json`](/openapi.json) · [`openapi.yaml`](/openapi.yaml) - the contract - Every page has a **Copy page as Markdown** button for pasting a single page ### Ready-to-paste prompt Drop this into your assistant, fill in the task, and go: ## Next steps - **[Guides](/guides/)** - auth, sandbox vs live, errors, rate limits, caching, availability - the building blocks every recipe draws on. - **[Generate an SDK](/guides/sdk-generation)** - a typed client from the live spec, no hand-written code. - **[API Reference](/reference)** - every operation with a live Try-it console. --- # Cart → COD checkout Source: https://developers.joelxhenry.com/recipes/cart-cod-checkout Place a real order: create a cart, add lines, re-price it, then check out on the existing **COD / credit-terms** rails (the only checkout path in v1 - there are no online card payments yet). Orders placed through the Storefront API are tagged `channel=ECOMMERCE`, server-side - you never set the channel yourself. ## What you need - A **secret** key (`hk_test_sec_…`) - this path is customer-scoped, so a publishable key is rejected `403`. - A **shopper token** for the signed-in customer (`X-Storefront-Shopper-Token`). The key authenticates your *integration*; the token authenticates the *customer*. See [Authentication & keys](/guides/authentication). ::: danger Secret keys are server-side only `hk_*_sec_…` keys unlock checkout, customer reads, and webhooks. **Never** ship one to a browser. Run every step on this page from your backend. ::: ## 1. Create a cart A cart is scoped to the shopper carried by the token - you never pass a `customer_id`. ## 2. Add lines Add each item by `sku` + `quantity`. (Products sold in alternate units can pass `order_uom` / `order_quantity`; everything settles in the product's base stock unit.) ## 3. Re-price the cart Pricing is computed server-side from the customer's contract prices plus tax/GCT. Always re-price before showing a total or checking out. ## 4. Check out Checkout converts the cart to an order on the COD / credit-terms rails. If the order would exceed the customer's credit limit, the API returns a **structured** `over_credit_limit` state (with the `shortfall`) - handle it, don't treat it as a generic error. Track the resulting order with `GET /v1/orders/{order_id}`, and reconcile against `GET /v1/invoices` once an invoice is issued. ## Next steps - **[Verify a webhook](/recipes/verify-webhook)** - react to `order.confirmed` and `invoice.issued` events instead of polling. - **[Customer-priced catalog with a shopper token](/recipes/customer-priced-catalog)** - show the same contract pricing while the shopper browses. - **[Errors](/guides/errors)** - the status table and the `over_credit_limit` state. ::: tip In the API Reference Open these operations in the interactive reference (with a **Try it** console): - [`POST /v1/carts`](/reference#tag/cart/POST/v1/carts) - create a cart - [`POST /v1/carts/{cart_id}/lines`](/reference#tag/cart/POST/v1/carts/{cart_id}/lines) - add a line - [`POST /v1/carts/{cart_id}/price`](/reference#tag/cart/POST/v1/carts/{cart_id}/price) - re-price - [`POST /v1/checkout`](/reference#tag/checkout/POST/v1/checkout) - place the order ::: --- # Customer-priced catalog with a shopper token Source: https://developers.joelxhenry.com/recipes/customer-priced-catalog Anonymous catalog reads show list content, but a B2B shopper expects **their** contract pricing and credit standing. Once a shopper signs in, attach their **shopper token** to a **secret**-key request and Harmon scopes everything - pricing, orders, invoices, credit - to that customer automatically. ## What you need - A **secret** key (`hk_test_sec_…`) - customer-scoped reads reject a publishable key with `403`. - A **shopper token** (`X-Storefront-Shopper-Token`) from the auth login flow. The token carries the `customer_id`; you never send it yourself. ::: danger Secret keys are server-side only Run every call here from your backend. A `hk_*_sec_…` key must never reach a browser. ::: ## 1. Identify the signed-in shopper - `GET /v1/me` Confirm the token resolves to the customer you expect before showing prices. A shopper token issued for a different merchant than the key's is rejected `403` - the merchant boundary holds even with a valid token. ## 2. Show contract pricing Catalog *content* is identical anonymous-or-not; the customer-specific **price** comes from pricing a cart of what they're viewing with the shopper token attached. Each priced line carries the customer's contract `unit_price` plus tax/GCT. ## 3. Surface credit standing Show available credit before checkout so an `over_credit_limit` block is never a surprise. ## Next steps - **[Cart → COD checkout](/recipes/cart-cod-checkout)** - turn the priced cart into an order. - **[Authentication & keys](/guides/authentication)** - how shopper tokens and the capability split fit together. ::: tip In the API Reference Open these operations in the interactive reference (with a **Try it** console): - [`GET /v1/me`](/reference#tag/customer/GET/v1/me) - the signed-in customer - [`GET /v1/me/credit-status`](/reference#tag/customer/GET/v1/me/credit-status) - credit standing - [`POST /v1/carts/{cart_id}/price`](/reference#tag/cart/POST/v1/carts/{cart_id}/price) - customer-priced lines ::: --- # Keep a local catalog fresh with `updated_since` Source: https://developers.joelxhenry.com/recipes/local-catalog-updated-since A headless storefront or a search index wants a **local mirror** of the catalog, not a live API call on every page view. Seed the mirror once, then keep it fresh cheaply: pull only what changed with `updated_since`, and skip unchanged pages with `ETag` revalidation. Pair it with [webhooks](/recipes/verify-webhook) for near-real-time updates between polls. ## 1. Seed the mirror (one full sync) Page through the whole catalog once and upsert into your local store. Record the newest `updated_at` you saw as your **watermark** - the next sync starts from there. ## 2. Pull deltas with `updated_since` `updated_since=` returns only products changed since that instant - the efficient way to keep the mirror current between webhook deliveries. Advance your watermark to the newest `updated_at` in each batch. ::: tip Belt and braces Subscribe to the `product.updated` and `stock.changed` [webhooks](/recipes/verify-webhook) for push updates, and run an `updated_since` poll on a timer as a backstop in case a delivery is ever missed. ::: ## 3. Revalidate with `ETag` / `If-None-Match` Every list response carries a strong `ETag`. Send it back as `If-None-Match` and an unchanged page returns `304 Not Modified` with no body - you keep your cached copy at almost zero cost. ## Next steps - **[Pagination & caching](/guides/pagination-and-caching)** - the full `ETag` / `If-None-Match` / `updated_since` model. - **[Verify a webhook](/recipes/verify-webhook)** - push updates to complement the poll. - **[Render a product grid](/recipes/product-grid)** - serve the mirror you just built. ::: tip In the API Reference Open this operation in the interactive reference (with a **Try it** console): - [`GET /v1/products`](/reference#tag/catalog/GET/v1/products) - supports `updated_since`, `ETag`, and `If-None-Match` ::: --- # Render a product grid Source: https://developers.joelxhenry.com/recipes/product-grid Build a browsable storefront grid from the public catalog - a page of products, each with a name, price, image, and a coarse **availability band** - using only a **publishable** key (browser-safe, anonymous catalog reads). No shopper login, no secret key. This is the most common first integration: a headless storefront's listing page. ## What you need - A **publishable** sandbox key (`hk_test_pub_…`). Grab one from the admin **API keys** screen. - Nothing else - catalog reads are anonymous. Paste your key once and every snippet below rewrites to use it: ## 1. List a page of products `GET /v1/products` returns a page of catalog items plus a `pagination` block. Paginate with `page` / `page_size`; narrow with `q`, `category`, or `tag`. Each item carries a **coarse availability band** (`IN_STOCK` / `LOW` / `OUT`), never a raw on-hand count - render it as a badge, not a quantity. See [Availability](/guides/availability). ::: tip Cache the grid The list response carries a strong `ETag`. Send it back as `If-None-Match` on the next load to get a `304 Not Modified` when nothing changed - see [Pagination & caching](/guides/pagination-and-caching). ::: ## 2. Link each card to its detail page The grid links each card to a product detail view backed by `GET /v1/products/{sku}`: ## Next steps - **[Keep a local catalog fresh with `updated_since`](/recipes/local-catalog-updated-since)** - sync only what changed instead of refetching every page. - **[Customer-priced catalog with a shopper token](/recipes/customer-priced-catalog)** - show contract pricing once a shopper signs in. - **[Availability](/guides/availability)** - the coarse-band model in full. ::: tip In the API Reference Open these operations in the interactive reference (with a **Try it** console): - [`GET /v1/products`](/reference#tag/catalog/GET/v1/products) - list the catalog - [`GET /v1/products/{sku}`](/reference#tag/catalog/GET/v1/products/{sku}) - a single product ::: --- # Verify a webhook Source: https://developers.joelxhenry.com/recipes/verify-webhook React to events - `order.confirmed`, `invoice.issued`, `stock.changed`, `order.shipped`, `product.updated` - instead of polling. Every delivery is **HMAC-signed**; verify the signature over the exact bytes you received **before** you parse the body, so a forged request never reaches your handler. ## What you need - A **secret** key with the `storefront:webhooks` scope (subscriptions are a privileged write). - A public HTTPS endpoint Harmon can POST to. ## 1. Subscribe Create a subscription with your endpoint and the events you care about. The response reveals a `signing_secret` **once** - store it immediately; you can't read it back. Test the wiring any time with `POST /v1/webhooks/{subscription_id}/ping` - it sends a signed test delivery to your endpoint. ## 2. Verify on receipt Each delivery carries three headers: ``` X-Harmon-Event: order.confirmed X-Harmon-Delivery: X-Harmon-Signature: sha256= ``` `X-Harmon-Signature` is an **HMAC-SHA256 of the exact request-body bytes**, keyed by your `signing_secret`. Recompute it over the **raw bytes** - don't re-serialize the JSON first, or the bytes (and the HMAC) won't match - and reject on a mismatch before doing anything else. ::: warning Verify before you parse A request that fails verification is not from Harmon. Compute the expected signature and reject the delivery **before** you decode the body. Use a constant-time compare (`timingSafeEqual` / `hmac.compare_digest` / `hash_equals`) so you leak no timing oracle on the secret. ::: Want to experiment without writing a receiver? The [Verify webhook signatures](/guides/webhook-signatures) guide has an interactive **playground**: paste a secret and a body, compute the signature, then tamper a byte and watch verification flip to **Invalid**. ## Next steps - **[Verify webhook signatures](/guides/webhook-signatures)** - the playground + the canonical signer in every language. - **[Cart → COD checkout](/recipes/cart-cod-checkout)** - produce the `order.confirmed` event this recipe consumes. ::: tip In the API Reference Open these operations in the interactive reference (with a **Try it** console): - [`POST /v1/webhooks`](/reference#tag/webhooks/POST/v1/webhooks) - create a subscription - [`POST /v1/webhooks/{subscription_id}/ping`](/reference#tag/webhooks/POST/v1/webhooks/{subscription_id}/ping) - send a signed test delivery ::: --- # Changelog Source: https://developers.joelxhenry.com/changelog What's new in the Harmon Storefront API and this developer portal. The API is at **v1** (`info.version 1.0.0`); the [API Reference](/reference) is generated from the live contract, so it always reflects what the API serves today. ::: tip Subscribe to changes Breaking changes ship under a new major API version (see [Versioning](#versioning) below). To track catalog/stock/order changes programmatically, use [webhooks](/recipes/verify-webhook) and the `updated_since` [delta sync](/recipes/local-catalog-updated-since). ::: ## API - v1 ### Additive - product media `GET /v1/products` and `GET /v1/products/{sku}` now carry product imagery: a single resolved `cover` (or `null`) plus a cover-first `media[]` gallery. Each entry (`MediaRefOut`) carries the presentation metadata and the resolved public WebP derivative URLs - `thumb_url` (small, for grids) and `large_url` (for viewing) - so you render images without a second request. A product with no imagery returns `cover: null` and `media: []`. This is an additive change (clients that ignore unknown fields are unaffected); adding, removing, or reordering a product's media changes its `ETag`, so `If-None-Match` revalidation picks up the new gallery automatically. ### v1.0.0 - initial public release The first public, machine-authenticated commerce API for building your own storefront on Harmon. - **Auth** - merchant-scoped API keys with a hard publishable/secret capability split, plus shopper tokens for customer-scoped reads and checkout. See [Authentication & keys](/guides/authentication). - **Sandbox & live** - `hk_test_` keys run in sandbox; mode is echoed on every response. See [Sandbox vs live](/guides/sandbox-vs-live). - **Catalog** - products, categories, tags, coarse availability bands, and vehicle fitment, with `ETag`/`If-None-Match` revalidation and `updated_since` deltas. See [Pagination & caching](/guides/pagination-and-caching). - **Commerce** - cart → COD/credit-terms checkout (`channel=ECOMMERCE`), customer-scoped orders, invoices, and credit status. - **Webhooks** - HMAC-signed delivery of `order.*`, `invoice.*`, `product.*`, and `stock.changed` events. See [Verify webhook signatures](/guides/webhook-signatures). ## Portal This branded developer portal ships alongside v1: - Quickstart, guides, and a [recipes cookbook](/recipes/) with copy-paste, multi-language snippets (cURL · JavaScript · Python · PHP/Laravel). - An embedded, OpenAPI-driven [API Reference](/reference) with a sandbox-default **Try it** console. - LLM-native artifacts - [`llms.txt`](/llms.txt), [`llms-full.txt`](/llms-full.txt), per-page "copy as Markdown," and one-click [OpenAPI](/openapi.json) / Postman downloads. ## Versioning The Storefront API is versioned in the **URL path** (`/v1/…`). The contract follows these rules: - **Additive changes are not breaking.** New endpoints, new optional request fields, and new response fields can appear within `v1` at any time - write clients that ignore unknown fields. - **Breaking changes ship under a new major version** (`/v2/…`). The previous major stays available through its announced deprecation window. - **`info.version`** (currently `1.0.0`) tracks the contract's semantic version; the [API Reference](/reference) and the downloadable [OpenAPI](/openapi.json) are always generated from the live spec, so they never drift from what the API serves. When a `v2` contract exists, the **version switcher** in the top navigation will let you read the `v1` and `v2` references side by side. Today only `v1` is published.