# 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.