# Harmon Storefront API - Full Documentation
> Build your own ecommerce storefront on Harmon - the public, machine-authenticated commerce API for your store.
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.harmoncommerce.com/openapi.json.
---
# Harmon Storefront API
Source: https://developers.harmoncommerce.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.harmoncommerce.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 store or account id; your key identifies your store.
## 1. Get a sandbox key
Create a **publishable** key on the **API keys** screen. For exploring, start
with a **sandbox** key - it has the `hk_test_` prefix and can never touch live
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 which store your key resolves to, 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 `limit` /
`offset`; 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.harmoncommerce.com/guides/
The Harmon Storefront API lets you build your own ecommerce store: read the
catalog, price a cart, and place orders. The base URL is `https://api.harmoncommerce.com`, and
every endpoint is described in the [API reference](/reference) - these guides are
the plain-language 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, and shopper tokens.
- **[Sandbox vs live](/guides/sandbox-vs-live)** - build against a sandbox key,
then flip to live.
## Core concepts
- **[Errors](/guides/errors)** - partial responses (`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 doesn't do
- **Read-only catalog, pricing, and stock.** You can't change products, prices,
or stock through this API - the only writes are shopper sign-up/login and
cart/checkout.
- **No online card payments yet.** Checkout uses cash on delivery or credit
terms; card payments are coming later.
---
# Authentication & keys
Source: https://developers.harmoncommerce.com/guides/authentication
Every request carries your store's API key as a bearer token:
```
Authorization: Bearer hk_live_pub_xxxxxxxxxxxx
```
Don't have a key yet? Create one on the **API keys** screen:
Your key identifies your store, so you **never** send a store or account 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 |
| **Secret** | `hk_*_sec_…` | ❌ **never** | Everything publishable does **plus** customer-scoped reads, checkout, webhooks |
A publishable key used on a customer-scoped or webhook route is always rejected
with `403` - never silently downgraded. Keep your secret key server-side only.
Check a key against `GET /v1/ping` - it echoes which store the key resolves to
(`tenant_id`), the `key_kind`, granted `scopes`, and `mode`, without touching any
data:
```bash
curl -s https://api.harmoncommerce.com/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`; every order, invoice, and credit
read is automatically scoped to that customer. A shopper token that belongs to a
different store than the key 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.harmoncommerce.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 |
| `UNKNOWN` | Stock state can't be determined right now |
Coarse bands keep exact stock counts 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.harmoncommerce.com/v1/products/SKU-123/availability" \
-H "Authorization: Bearer hk_test_pub_your_key_here"
# → {"sku":"SKU-123","availability":"LOW"}
# A batch in one call:
curl -s -X POST "https://api.harmoncommerce.com/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.harmoncommerce.com/guides/errors
## Partial responses
A read endpoint can return part of its data instead of failing the whole request.
When one section is temporarily unavailable, the response is still `200` with an
`errors[]` array naming each affected section, while the rest of the data loads
normally:
```json
{
"data": [ /* … the sections that loaded … */ ],
"errors": [
{ "field": "pricing", "code": "downstream_timeout", "detail": "pricing temporarily unavailable" }
]
}
```
Treat a non-empty `errors[]` as **this section is temporarily unavailable** - render
the rest and retry it, not a hard failure. 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` | A publishable key on a privileged route, the wrong store, or an origin not on the key's allowlist |
| `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[]` partial-response envelope on a catalog read
:::
---
# Pagination & caching
Source: https://developers.harmoncommerce.com/guides/pagination-and-caching
Anonymous catalog reads (`GET /v1/products`, `/v1/products/{sku}`,
`/v1/categories`) are built to be paginated, cached, and synced
cheaply.
## Pagination
Paginate the product list with `limit` / `offset`, and filter with `q` and
`category`:
```bash
curl -s "https://api.harmoncommerce.com/v1/products?limit=20&offset=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.harmoncommerce.com/v1/products?limit=20" \
-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.harmoncommerce.com/v1/products?limit=20" \
-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.harmoncommerce.com/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`/`offset` params, the `ETag` response header, and `updated_since`
:::
---
# Rate limits
Source: https://developers.harmoncommerce.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.harmoncommerce.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.harmoncommerce.com/v1/ping \
-H "Authorization: Bearer hk_test_pub_your_key_here"
# → {"…":"…","mode":"sandbox"}
```
## How to use it
Use a sandbox key to build and test your integration end-to-end - read the
catalog, build a cart, place an order - against sandbox data, without ever
touching live 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. Create a **live** key pair (publishable + secret) on the **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.harmoncommerce.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)** - publishable vs secret keys.
- **[API Reference](/reference)** - the interactive contract + downloads.
---
# Verify webhook signatures
Source: https://developers.harmoncommerce.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.harmoncommerce.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 to use your own key? 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.harmoncommerce.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 with cash on delivery or
credit terms.
- **[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.harmoncommerce.com/recipes/cart-cod-checkout
Place a real order: create a cart, add lines, re-price it, then check out with
**cash on delivery or credit terms** (the only checkout path today - there are no
online card payments yet).
## 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.harmoncommerce.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 that belongs to a different store than the key is rejected
`403`, even if the token itself is valid.
## 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
secret keys work 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.harmoncommerce.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 time of this sync 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 time of each sync (captured just before the request).
::: 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.harmoncommerce.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_…`). Create one on the
**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 `total` count.
Paginate with `limit` / `offset`; narrow with `q` or `category`.
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.harmoncommerce.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.harmoncommerce.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
### Removed - vehicle catalog & fitment
The vehicle catalog and fitment endpoints (`/v1/vehicles/years`,
`/v1/vehicles/makes`, `/v1/vehicles/models`,
`/v1/vehicles/{vehicle_id}/products`, and `/v1/products/{sku}/fitments`) have
been retired, along with the `vehicle_id` filter on `GET /v1/products` and the
`fitment_enabled` response flag. If you filtered your catalog by vehicle, list
products with `GET /v1/products` and filter on your own attributes instead.
### Additive - rich product detail
`GET /v1/products/{sku}` now returns the full authored product detail page: a
long-form `body` (CommonMark **Markdown** - render it client-side; `description`
stays the product's name/label), a plain-text `short_description` summary (handy
for cards and listings), and a
store-defined `specs` store
(`{ "groups": [ { "label", "rows": [ { "label", "value", "highlight" } ] } ] }` -
the `highlight` rows back a quick-glance grid). Both the list and detail also
carry derived marketing `badges` (`ON_SALE`, `LOW_STOCK`) and sale pricing -
`compare_at_unit_price` (the strike-through list price) plus a derived
`discount_percent`. The product gallery (`media[]`) now includes YouTube/Vimeo
video embeds: a `MediaRefOut` carries `kind` (`IMAGE` or `VIDEO`) and, for a
video, `provider` + `embed_url` + `poster_url` + `duration_seconds`. A product
hidden on the storefront is absent from the list and `404`s on detail. All of
this is additive (clients that ignore unknown fields are unaffected); authoring
any of it changes the product's `ETag`, so `If-None-Match` revalidation picks up
the change automatically.
### 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** - store-scoped API keys with a hard publishable/secret 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, 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 → cash-on-delivery / credit-terms checkout, customer-scoped
orders, invoices, and credit status.
- **Webhooks** - HMAC-signed delivery of `order.confirmed`, `order.shipped`,
`order.cancelled`, `invoice.issued`, `product.created`, `product.updated`, 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.