Using the Clean Size Charts API
The Clean Size Charts app gives you a way to make your published size charts accessible to other tools — your support help desk, a sizing chatbot, a custom storefront, a PIM, or anywhere else you'd like to surface the same charts shoppers see on your product pages. You generate an API key from inside the app, hand it to the system that needs it, and that system can then look up any of your published charts on demand.
This article walks you through getting set up, making your first request, and understanding what the API returns. If you're a developer reading this on behalf of a merchant, sections 5 onwards are written for you.
**Available on all paid plans.** The public API is included for any store with an active paid subscription. Free-tier stores will receive a `403 permission_error` response until they upgrade.
1. Generating an API key
Every system that connects to your charts needs an API key. The key is unique to your store, and you can generate as many keys as you need (up to 10 active at a time).
A. Finding API keys in the app
1. Open the Clean Size Charts admin
2. Click **Settings** in the left navigation
3. Scroll down to the **API Access** section
You'll see a panel for generating new keys and a list of any keys you've already created.
B. Generating your first key
1. In the **Generate a new key** input, enter a label that describes what the key will be used for — for example, *Intercom chatbot* or *Hydrogen storefront — production*
2. Click **Generate key**
3. A modal will appear showing the full key, which begins with `csc_live_`
Copy the key from the modal right away. This is the only time the full key is shown — once you close the modal, only the prefix remains visible.
C. Storing your key safely
Treat your API key like a password:
- Never paste it into a public GitHub repository, a chat message, or a screenshot you share publicly
- Never include it in client-side JavaScript bundles — if a browser needs to call the API, route the call through a server you control
- Store production keys in your hosting provider's secret manager (Heroku Config Vars, Vercel Environment Variables, AWS Secrets Manager, etc.)
- Use a separate key for each integration so revoking one doesn't break the others
D. Revoking a key
If a key is leaked, an integration is decommissioned, or a team member with access leaves, revoke the key:
1. Find the key in the **Your keys** table by its label
2. Click **Revoke** in its row
3. Confirm in the dialog
Revocation is immediate. Any system using the key will start receiving `401 authentication_error` responses on the very next request.
2. Making your first request
The API lives at a single base URL, and every request to it carries your key in a standard `Authorization` header.
A. The base URL
All endpoints described in this article are relative to that URL.
B. The Authorization header
Every request must include this header:
Authorization: Bearer csc_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Substitute your actual key in place of the `xxxx...` portion. Note the literal word `Bearer ` followed by a single space — this format is required.
C. A test call
To confirm your key works, fetch the shop endpoint, which returns identifying information about your store:
# Replace KEY with your actual API key
curl -H "Authorization: Bearer csc_live_..." \
https://app.cleansizecharts.com/api/public/v1/shop
A working key returns:
{
"data": {
"shop_domain": "your-store.myshopify.com",
"default_language": "en",
"available_languages": ["en", "fr"]
}
}
If you instead see a `401 authentication_error`, double-check the `Bearer ` prefix, that your key starts with `csc_live_`, and that the key has not been revoked.
3. Looking up the right chart for a product
The most common use of the API is "given a product, return the size chart that applies to it." The `match` endpoint exists for exactly this.
A. The match endpoint
curl -H "Authorization: Bearer csc_live_..." \ "https://app.cleansizecharts.com/api/public/v1/size_charts/match?product_handle=cotton-tee"
This returns the single best-matching published chart for the product whose handle is `cotton-tee`. If no chart applies, it returns a `404 not_found`.
B. What you can pass
You typically pass whatever you know about the product. Any combination is allowed:
- `product_id` — the product's Shopify ID. Either a numeric ID (`8765`) or a Shopify GID (`gid://shopify/Product/8765`) works.
- `product_handle` — the product's URL handle, like `cotton-tee`
- `product_title` — the product's title (used only as a last-resort fallback)
- `product_type` — the Shopify product type, like `T-Shirts`
- `vendor` — the product's vendor
- `tags[]` — repeat the parameter for each tag: `?tags[]=mens&tags[]=summer`
- `collections[]` — collection handles or titles, also repeated
Passing more signals does not boost the score — the matching engine uses the strongest match (see section 5). But supplying more signals does give the API more chances to find a relevant chart.
C. Reading the response
A successful match looks like:
{
"data": {
"id": 4217,
"name": "Men's T-Shirts",
"content": {
"table": "<table>...</table>",
"top_text": "How to measure",
"bottom_text": "Care instructions"
},
"size_recommender": { "enabled": true, "age_prompt": false }
},
"meta": {
"match_score": 100,
"match_reason": "product_id"
}
}
The `data.content.table` field contains the actual sizing table as HTML — that's what you typically render on a product page or pipe into a chatbot reply. The `meta` block tells you why this chart was picked (more on that in section 5).
4. Listing all your size charts
Sometimes you want every published chart, not just the one for a specific product. Help-desk integrations often want the full list so an agent can pick the right one; nightly PIM syncs want the list so they can mirror your charts elsewhere.
A. The list endpoint
curl -H "Authorization: Bearer csc_live_..." \ "https://app.cleansizecharts.com/api/public/v1/size_charts"
This returns up to 25 published charts per page, in the same display priority order the app's storefront popup uses.
B. Pagination
Use `page` and `per_page` to fetch more:
curl -H "Authorization: Bearer csc_live_..." \ "https://app.cleansizecharts.com/api/public/v1/size_charts?page=2&per_page=50"
- `page` is 1-indexed
- `per_page` can be up to 100
Each list response includes a `meta` block telling you the total count, current page, and whether there are more pages:
"meta": {
"total": 87,
"page": 1,
"per_page": 25,
"has_more": true
}
C. Filtering by product context
You can also pass any of the filter parameters from section 3 to the list endpoint. Doing so runs the matching engine and returns only charts that apply to the supplied product context, sorted by match strength:
# Every chart that applies to the cotton tee, not just the best one curl -H "Authorization: Bearer csc_live_..." \ "https://app.cleansizecharts.com/api/public/v1/size_charts?product_handle=cotton-tee"
To get a single specific chart by its ID:
curl -H "Authorization: Bearer csc_live_..." \ "https://app.cleansizecharts.com/api/public/v1/size_charts/3276"
5. How the matching engine picks a chart
When you pass filter parameters, the API doesn't just dump every chart at you. It scores each published chart against the supplied product context and returns the strongest matches first.
A. The scoring rubric
Each chart receives a score equal to the strongest signal it matches against:
- **100 points** — the chart's products list includes the supplied product ID
- **100 points** — the chart's products list includes the supplied product handle
- **60 points** — one of the supplied collections matches one of the chart's collections
- **40 points** — at least one of the supplied tags matches one of the chart's tags
- **30 points** — the supplied vendor matches one of the chart's vendors
- **30 points** — the supplied product type matches one of the chart's product types
- **20 points** — the supplied product title is a substring of one of the chart's configured product titles
A chart's score is the **maximum** of its matching signals, not the sum. This matters: it means a chart specifically scoped to a product (score 100) always outranks a chart that just happens to share a tag (score 40), no matter how many incidental tag matches there are.
B. How ties are broken
When two charts have the same score, they're sorted by the same display priority order the storefront popup uses:
- Charts that scope to specific products win over charts that scope only to collections
- Among product-scoped charts, ones without any collections rank higher (they're more specific)
- Otherwise, the most recently updated chart wins
C. Debugging unexpected matches
Every chart returned by a filtered query carries two fields telling you why it matched:
- `match_score` — the score that won
- `match_reason` — one of `product_id`, `product_handle`, `collection`, `tag`, `vendor`, `product_type`, or `product_title`
If a query returns the wrong chart, look at `match_reason`:
- If it's `tag` or `vendor` and you expected `product_id`, the chart's product list in the app doesn't include the product you queried — open the chart in the editor and add it
- If two charts both match by `product_id`, the tie-break order from section 5.B applies — adjust one chart to be more specific (or less)
6. Understanding the respons
Every chart returned by the API has the same shape. Here's what each field means.
A. Identity fields
- `id` — the chart's numeric ID inside Clean Size Charts. Stable.
- `name` — the name you gave the chart in the app
- `status` — always `published` (drafts are not exposed via the API)
- `created_at`, `updated_at` — ISO 8601 timestamps
B. Content fields
The `content` object holds what shoppers actually see:
- `content.table` — the main sizing table as HTML
- `content.top_text` — optional intro text shown above the table
- `content.bottom_text` — optional text shown below the table
- `content.unit_conversion` — `CM`, `INCHES`, or `Off`. Indicates whether a unit-converted alternate table is available.
- `content.converted_table` — the auto-converted version, if you enabled unit conversion in the chart editor
- `content.image_url` — if you uploaded a chart image, the full CDN URL; otherwise `null`
When you render the chart on a product page, the typical pattern is to insert `content.table` via `dangerouslySetInnerHTML` (or your framework's equivalent) and let your users toggle between it and `content.converted_table`.
C. Conditions fields
The `conditions` object tells you which products the chart is configured to apply to. It mirrors what you see in the chart editor's *Conditions* tab:
- `conditions.products` — array of `{ id, handle, title }` objects
- `conditions.collections` — array of `{ handle, title }` objects
- `conditions.tags` — array of strings
- `conditions.vendors` — array of strings
- `conditions.product_types` — array of strings
A help-desk agent looking at this object can quickly understand "this chart is for our men's tees collection" without having to switch tabs into the app.
D. Size recommender flags
- `size_recommender.enabled` — whether the size recommender quiz is turned on for this chart
- `size_recommender.age_prompt` — whether the quiz asks for the shopper's age
If `enabled` is `true`, a chatbot can route the conversation into a sizing flow instead of just dumping the table.
E. Match metadata (filtered queries only)
When you used filter parameters, each chart also carries `match_score` and `match_reason` — see section 5.C.
7. Languages and translations
If your charts are translated, you can request a specific language by passing the `language` parameter:
curl -H "Authorization: Bearer csc_live_..." \ "https://app.cleansizecharts.com/api/public/v1/size_charts/3276?language=fr"
The response's `language` field tells you which language version you actually got. The fallback chain is:
- The requested language if the chart has been translated to it
- The chart's default content if not
- The chart's root content as a last resort
The `available_languages` field lists every language the chart has been authored in, so you can offer a language picker in your interface if appropriate.
8. Handling errors
All API errors return a consistent JSON shape:
{
"error": {
"type": "authentication_error",
"message": "Missing or invalid API key.",
"documentation_url": "https://cleansizecharts.com/api/docs/errors#authentication_error"
}
}
A. The error types you'll see
- `authentication_error` (HTTP 401) — your key is missing, malformed, unknown, or revoked
- `permission_error` (HTTP 403) — your shop's plan does not include API access. Upgrade your plan or use a different store.
- 'not_found` (HTTP 404) — the chart doesn't exist, isn't published, or belongs to a different shop. Also returned by `/size_charts/match` when nothing matched.
- `validation_error` (HTTP 400) — a query parameter was malformed (for example, `per_page=999` exceeds the maximum)
- `rate_limit_exceeded` (HTTP 429) — too many requests; see section 9
- `internal_error` (HTTP 500) — something broke on our side. Captured automatically; retry with backoff, and contact support if it persists.
B. A note on 404 for cross-shop lookups
If you try to fetch a chart that belongs to a different store, the response is `404 not_found` — the same response you'd get if the chart didn't exist anywhere. This is deliberate. A `403` response would confirm that the chart exists in another store, which would be an information leak. Treat `404` consistently in your code: it means "you can't see that chart," regardless of why.
9. Rate limits and being a good citizen
To keep the API responsive for everyone, requests are capped.
A. The limits
- **600 requests per minute, per API key**
- **100 requests per minute, per IP address**, for unauthenticated traffic
The per-key limit covers the vast majority of legitimate traffic. The per-IP limit mainly prevents abuse — well-behaved clients with a valid key never hit it.
B. What happens at the limit
You receive a `429 rate_limit_exceeded` response with these headers:
- `Retry-After` — the number of seconds to wait before retrying
- `X-RateLimit-Limit` — the limit that was hit
- `X-RateLimit-Reset` — Unix timestamp when the window resets
Your client should read `Retry-After`, wait at least that long, then retry the original request.
C. Caching to stay under the limit
Charts change rarely. Every successful response includes an `ETag` header. Cache that ETag and send it back on your next request:
curl -H "Authorization: Bearer csc_live_..." \ -H 'If-None-Match: "abc123..."' \ "https://app.cleansizecharts.com/api/public/v1/size_charts/3276"
If the chart hasn't changed, you'll get a `304 Not Modified` with no body — fast, cheap, and still counts as one request rather than zero, but you avoid re-parsing the same chart over and over.
Each response also sets `Cache-Control: s-maxage=600`, meaning shared caches like Cloudflare will serve cached copies for up to 10 minutes. If you control a CDN in front of your own integration, you can lean on this.
D. Reducing your request volume
- Cache chart responses for at least a few minutes between fetches
- Don't poll — there's no need to hit the API every second
- Use separate keys for separate integrations — each key has its own 600/min budget
- If you genuinely need a higher limit, contact support with your use case
10. Code examples
A. JavaScript / Node.js
// Set CSC_API_KEY in your environment, never hardcode the key.
const KEY = process.env.CSC_API_KEY;
const BASE = "https://app.cleansizecharts.com/api/public/v1";
async function getChartFor(productHandle) {
const url = new URL(`${BASE}/size_charts/match`);
url.searchParams.set("product_handle", productHandle);
const res = await fetch(url, {
headers: { Authorization: `Bearer ${KEY}` }
});
if (res.status === 404) return null; // no chart for this product
if (!res.ok) {
const body = await res.json();
throw new Error(`CSC API ${res.status}: ${body.error?.message}`);
}
return (await res.json()).data;
}
B. Python
import os
import requests
KEY = os.environ["CSC_API_KEY"] # never hardcode the key
BASE = "https://app.cleansizecharts.com/api/public/v1"
def get_chart_for(product_handle: str):
res = requests.get(
f"{BASE}/size_charts/match",
params={"product_handle": product_handle},
headers={"Authorization": f"Bearer {KEY}"},
timeout=10,
)
if res.status_code == 404:
return None
res.raise_for_status()
return res.json()["data"]
C. Ruby
require "net/http"
require "json"
require "cgi"
KEY = ENV.fetch("CSC_API_KEY") # never hardcode the key
BASE = "https://app.cleansizecharts.com/api/public/v1"
def get_chart_for(product_handle)
uri = URI("#{BASE}/size_charts/match?product_handle=#{CGI.escape(product_handle)}")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{KEY}"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
return nil if res.code == "404"
raise "CSC API #{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)
JSON.parse(res.body)["data"]
end
D. curl
# Set this in your shell, don't paste the key into commands you share. export KEY="csc_live_..." # Confirm the key works curl -H "Authorization: Bearer $KEY" \ https://app.cleansizecharts.com/api/public/v1/shop # Best match for a product curl -H "Authorization: Bearer $KEY" \ "https://app.cleansizecharts.com/api/public/v1/size_charts/match?product_handle=cotton-tee"
11. Common integration patterns
A. A sizing chatbot
A shopper asks your chatbot, *"What size men's tee should I get for the Cotton Tee?"* The bot:
1. Extracts the product handle (`cotton-tee`) from session context
2. Calls `/size_charts/match?product_handle=cotton-tee`
3. Composes a reply from `data.content.table` and `data.size_recommender.enabled`
If the recommender is enabled, the bot can route the user into a sizing quiz; otherwise it just shows the table.
B. A help-desk macro
A support agent receives a question about sizing for product 8765. A macro calls the list endpoint with that product ID:
GET /size_charts?product_id=8765
The macro returns every applicable chart (typically one or two), and the agent picks the right one to send.
C. A headless storefront (Hydrogen, Next.js, etc.)
On a product page, server-render the chart at request time:
export async function loader({ params, request }) {
const lang = request.headers.get("accept-language")?.split(",")[0] || "en";
const res = await fetch(
`https://app.cleansizecharts.com/api/public/v1/size_charts/match?product_handle=${params.handle}&language=${lang}`,
{ headers: { Authorization: `Bearer ${process.env.CSC_API_KEY}` } }
);
if (res.status === 404) return { chart: null };
const { data } = await res.json();
return { chart: data };
The chart's `content.table` is HTML that the merchant authored, so it's safe to render directly in your template.
D. A PIM or external catalog sync
A nightly job mirrors your charts into your PIM:
1. Fetch all charts via `/size_charts?per_page=100`, paginate as needed
2. Compare each chart's `updated_at` to the last-seen timestamp in your PIM
3. Skip unchanged charts; rewrite the ones that changed
This pattern reads the entire catalog cheaply by using ETag revalidation between runs.
12. Troubleshooting common issues
A. "I get 401 even though my key was just generated"
- Confirm the header format: it must be `Authorization: Bearer csc_live_...`. A missing `Bearer ` word, extra whitespace, or a wrong header name all produce 401.
- Confirm you're using the full key including the `csc_live_` prefix
- Check the **API Access** section in **Settings** — has the key been revoked?
B. "My match returns no chart, but I have a chart for that product"
Open the chart in the app and check its **Conditions** tab. The matching engine only sees what's configured there:
- For a `product_handle` query, the chart must list that product in **Conditions → Products**
- For a `tags[]` query, the chart must list at least one of those tags in **Conditions → Tags**
- And so on for collections, vendors, and product types
If you expected the chart to apply but the conditions are empty for that signal, add the missing condition and try again.
C. "My match returns the wrong chart"
Look at the `match_reason` in the response. If it says `tag` or `vendor` but you expected `product_id`, no chart is specifically scoped to your product — a more general chart is winning. Either add the product to the more specific chart's conditions, or accept that the more general chart is the right one for that product.
D. "I'm getting 429s but I'm not making many requests"
You're probably sharing one key across many callers — a chatbot, a help desk, and a storefront, for example. Each call counts against the same per-key bucket. Generate one key per integration and rate limits won't collide.
E. "My ETag conditional request doesn't return 304"
- Send the ETag value back exactly as you received it, including the surrounding quotes
- The same chart in a different language has a different ETag — they're separate cached responses
- 304 responses have no body; your client must handle that case
Need more help?
If you run into something not covered here, or if you need a higher rate limit for a legitimate use case, email us at help@cleansizecharts.com with as much detail as you can include:
- The endpoint you were calling
- The full URL with query parameters (you can redact the key prefix portion, but include the timestamp of the request)
- The response status code and `error.type` field
- What you expected to happen instead
For security issues, please email help@cleansizecharts.com rather than filing a public issue.
*Last updated: 2026-05-25*